From ee4eb7f897043b57a687e188f3cee140d5404445 Mon Sep 17 00:00:00 2001 From: SashegDev Date: Thu, 17 Sep 2026 08:41:14 +0000 Subject: [PATCH] feat: GRENADE parabolic nade (35/5.5, 25TP, 15cd) + DIVE sky strike (25dmg knockback, 87.5TP, 120cd) --- .../blackknife/BlackKnifeListener.java | 49 +++- .../blackknife/BlackKnifePlugin.java | 1 + .../sashegdev/blackknife/EntityTracker.java | 2 +- .../sashegdev/blackknife/GrenadeAttack.java | 211 +++++++++++++++++ .../blackknife/KillMessageHandler.java | 2 +- .../sashegdev/blackknife/SkyDiveAttack.java | 220 ++++++++++++++++++ 6 files changed, 482 insertions(+), 3 deletions(-) create mode 100644 src/main/java/me/sashegdev/blackknife/GrenadeAttack.java create mode 100644 src/main/java/me/sashegdev/blackknife/SkyDiveAttack.java diff --git a/src/main/java/me/sashegdev/blackknife/BlackKnifeListener.java b/src/main/java/me/sashegdev/blackknife/BlackKnifeListener.java index 6a54e2a..8a96d8f 100644 --- a/src/main/java/me/sashegdev/blackknife/BlackKnifeListener.java +++ b/src/main/java/me/sashegdev/blackknife/BlackKnifeListener.java @@ -25,7 +25,7 @@ public class BlackKnifeListener implements Listener { private final java.util.Map previewTasks = new java.util.HashMap<>(); private final java.util.Map lungeHighlightTasks = new java.util.HashMap<>(); private final java.util.Map swordCombo = new java.util.HashMap<>(); - private static final String[] MODES = {"STARS", "SWORD", "DIAMOND", "DIAMOND_TRI", "BARRAGE", "LUNGE", "SNIPE", "TRI", "OVERHEAD"}; + private static final String[] MODES = {"STARS", "SWORD", "DIAMOND", "DIAMOND_TRI", "BARRAGE", "LUNGE", "SNIPE", "TRI", "OVERHEAD", "GRENADE", "DIVE"}; public BlackKnifeListener(BlackKnifePlugin plugin) { this.plugin = plugin; } @@ -155,6 +155,15 @@ public class BlackKnifeListener implements Listener { if (LungeAttack.tryOverhead(p, tm, gaze)) { p.setCooldown(item.getType(), 50); } + } else if (m.equals("GRENADE")) { + if (GrenadeAttack.tryUse(p, tm)) { + p.setCooldown(item.getType(), 15); + } + } else if (m.equals("DIVE")) { + if (SkyDiveAttack.tryUse(p, tm)) { + p.setCooldown(item.getType(), 120); + clearHighlight(p); + } } e.setCancelled(true); } @@ -269,6 +278,44 @@ public class BlackKnifeListener implements Listener { }; r.runTaskTimer(plugin, 1L, 4L); lungeHighlightTasks.put(p.getUniqueId(), r); + } else if (m.equals("GRENADE")) { + BukkitRunnable r = new BukkitRunnable() { + @Override public void run() { + if (!p.isOnline() || !getMode(p).equals("GRENADE")) { cancel(); return; } + if (p.hasCooldown(Material.NETHERITE_SWORD)) return; + if (!BlackKnifeItem.is(p.getInventory().getItemInMainHand()) && !BlackKnifeItem.is(p.getInventory().getItemInOffHand())) return; + Location gp = getGazePoint(p); + for (int i = 0; i < 16; i++) { + double a = (double) i / 16 * Math.PI * 2; + Location ring = gp.clone().add(Math.cos(a) * 1.5, 0.1, Math.sin(a) * 1.5); + p.spawnParticle(Particle.DUST, ring, 1, 0, 0, 0, 0, new Particle.DustOptions(Color.fromRGB(255, 40, 40), 1.2f)); + } + for (int i = 0; i < 24; i++) { + double a = (double) i / 24 * Math.PI * 2; + Location ring = gp.clone().add(Math.cos(a) * 3.0, 0.1, Math.sin(a) * 3.0); + p.spawnParticle(Particle.DUST, ring, 1, 0, 0, 0, 0, new Particle.DustOptions(Color.fromRGB(255, 200, 60), 1.0f)); + } + } + }; + r.runTaskTimer(plugin, 1L, 4L); + lungeHighlightTasks.put(p.getUniqueId(), r); + } else if (m.equals("DIVE")) { + BukkitRunnable r = new BukkitRunnable() { + @Override public void run() { + if (!p.isOnline() || !getMode(p).equals("DIVE")) { cancel(); return; } + if (p.hasCooldown(Material.NETHERITE_SWORD)) return; + if (!BlackKnifeItem.is(p.getInventory().getItemInMainHand()) && !BlackKnifeItem.is(p.getInventory().getItemInOffHand())) return; + Location gp = getGazePoint(p); + for (int i = 0; i < 16; i++) { + double a = (double) i / 16 * Math.PI * 2; + Location ring = gp.clone().add(Math.cos(a) * 1.2, 0.1, Math.sin(a) * 1.2); + p.spawnParticle(Particle.DUST, ring, 1, 0, 0, 0, 0, new Particle.DustOptions(Color.fromRGB(80, 160, 255), 1.2f)); + } + p.spawnParticle(Particle.CLOUD, gp.clone().add(0, 0.2, 0), 1, 0.1, 0.1, 0.1, 0); + } + }; + r.runTaskTimer(plugin, 1L, 4L); + lungeHighlightTasks.put(p.getUniqueId(), r); } else if (m.equals("DIAMOND_TRI")) { BukkitRunnable r = new BukkitRunnable() { @Override public void run() { diff --git a/src/main/java/me/sashegdev/blackknife/BlackKnifePlugin.java b/src/main/java/me/sashegdev/blackknife/BlackKnifePlugin.java index b663fa8..c235bac 100644 --- a/src/main/java/me/sashegdev/blackknife/BlackKnifePlugin.java +++ b/src/main/java/me/sashegdev/blackknife/BlackKnifePlugin.java @@ -31,6 +31,7 @@ public class BlackKnifePlugin extends JavaPlugin { getServer().getPluginManager().registerEvents(knifeListener, this); getServer().getPluginManager().registerEvents(swoonHandler, this); getServer().getPluginManager().registerEvents(new AttackTpListener(tension), this); + getServer().getPluginManager().registerEvents(new SkyDiveAttack(), this); getServer().getPluginManager().registerEvents(new ParryHandler(this), this); getServer().getPluginManager().registerEvents(magicListener, this); getServer().getPluginManager().registerEvents(chatHandler, this); diff --git a/src/main/java/me/sashegdev/blackknife/EntityTracker.java b/src/main/java/me/sashegdev/blackknife/EntityTracker.java index 4abc4fb..9880090 100644 --- a/src/main/java/me/sashegdev/blackknife/EntityTracker.java +++ b/src/main/java/me/sashegdev/blackknife/EntityTracker.java @@ -25,7 +25,7 @@ public class EntityTracker { boolean isBk = e.getPersistentDataContainer().has(BK_MARK, PersistentDataType.BYTE) || tracked.contains(e.getUniqueId()); if (!isBk && e instanceof org.bukkit.entity.BlockDisplay bd) { var mat = bd.getBlock().getMaterial(); - if (mat == org.bukkit.Material.DEEPSLATE_TILES || mat == org.bukkit.Material.SCULK_CATALYST || mat == org.bukkit.Material.AMETHYST_BLOCK || mat == org.bukkit.Material.MEDIUM_AMETHYST_BUD) isBk = true; + if (mat == org.bukkit.Material.DEEPSLATE_TILES || mat == org.bukkit.Material.SCULK_CATALYST || mat == org.bukkit.Material.AMETHYST_BLOCK || mat == org.bukkit.Material.MEDIUM_AMETHYST_BUD || mat == org.bukkit.Material.TNT) isBk = true; } if (isBk) { untrack(e); e.remove(); count++; } } diff --git a/src/main/java/me/sashegdev/blackknife/GrenadeAttack.java b/src/main/java/me/sashegdev/blackknife/GrenadeAttack.java new file mode 100644 index 0000000..e92c14b --- /dev/null +++ b/src/main/java/me/sashegdev/blackknife/GrenadeAttack.java @@ -0,0 +1,211 @@ +package me.sashegdev.blackknife; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Color; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Particle; +import org.bukkit.Sound; +import org.bukkit.entity.BlockDisplay; +import org.bukkit.entity.Display; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.util.Vector; + +import java.util.ArrayDeque; +import java.util.Deque; + +public class GrenadeAttack { + private static final float COST = 25f; + private static final double LETHAL_RADIUS = 1.5; + private static final double CHIP_RADIUS = 3.0; + private static final double LETHAL_DAMAGE = 35.0; + private static final double CHIP_DAMAGE = 5.5; + private static final double MAX_RANGE = 30.0; + private static final double ARC_MIN = 1.5; + private static final double ARC_PER_BLOCK = 0.25; + private static final double ARC_MAX = 4.0; + + public static boolean tryUse(Player p, TensionManager tension) { + if (SwoonHandler.isSwooned(p)) return false; + if (tension.getTP(p) < COST) { + p.sendActionBar(Component.text("Need 10% TP").color(NamedTextColor.RED)); + return false; + } + tension.setTP(p, tension.getTP(p) - COST); + Location eye = p.getEyeLocation(); + Vector dir = eye.getDirection().normalize(); + Location target = computeTarget(p, eye, dir); + double dist = eye.distance(target); + int duration = (int) Math.max(10, Math.min(40, dist * 3)); + double arcH = Math.max(ARC_MIN, Math.min(ARC_MAX, dist * ARC_PER_BLOCK)); + Location spawn = eye.clone().add(dir.clone().multiply(1)).add(0, 0.2, 0); + Vector throwDir = target.clone().subtract(spawn).toVector().normalize(); + + BlockDisplay nade = p.getWorld().spawn(spawn, BlockDisplay.class, d -> { + d.setBlock(Material.TNT.createBlockData()); + d.setBrightness(new Display.Brightness(15, 15)); + d.setViewRange(64); + d.setTransformation(new org.bukkit.util.Transformation( + new org.joml.Vector3f(-0.25f, -0.25f, -0.25f), + new org.joml.AxisAngle4f(), + new org.joml.Vector3f(0.5f, 0.5f, 0.5f), + new org.joml.AxisAngle4f())); + }); + EntityTracker.mark(nade); + p.getWorld().playSound(spawn, Sound.ENTITY_SNOWBALL_THROW, 1f, 0.7f); + BlackKnifePlugin plugin = BlackKnifePlugin.getPlugin(BlackKnifePlugin.class); + + new BukkitRunnable() { + Location flightStart = spawn.clone(); + Location flightTarget = target.clone(); + Vector flightDir = throwDir.clone(); + int flightDuration = duration; + int elapsed = 0; + int tick = 0; + double arc = arcH; + boolean targetWasSolid = flightTarget.getBlock().isSolid(); + final Deque trail = new ArrayDeque<>(); + + @Override public void run() { + if (!p.isOnline() || p.isDead() || !nade.isValid()) { + EntityTracker.untrack(nade); + if (nade.isValid()) nade.remove(); + cancel(); + return; + } + tick++; + elapsed++; + + if (tick % 3 == 0 && needsRetarget(nade.getLocation())) { + retarget(nade.getLocation()); + } + + double t = Math.min(1.0, (double) elapsed / flightDuration); + Location base = flightStart.clone().add(flightTarget.clone().subtract(flightStart).toVector().multiply(t)); + base.add(0, Math.sin(Math.PI * t) * arc, 0); + nade.teleport(base); + base.getWorld().spawnParticle(Particle.SMOKE, base, 1, 0.05, 0.05, 0.05, 0); + + trail.addLast(base.clone()); + while (trail.size() > 12) trail.pollFirst(); + renderTrail(); + + if (tick % 2 == 0) renderRings(flightTarget); + + boolean landed = t >= 1.0 || base.getBlock().isSolid(); + if (landed || nade.getTicksLived() > 100) { + Location expl = landed ? flightTarget.clone() : base.clone(); + EntityTracker.untrack(nade); + nade.remove(); + cancel(); + explode(p, expl, plugin); + } + } + + private boolean needsRetarget(Location cur) { + boolean nowSolid = flightTarget.getBlock().isSolid(); + if (nowSolid != targetWasSolid) return true; + Vector toTarget = flightTarget.clone().subtract(cur).toVector(); + double remaining = toTarget.length(); + if (remaining < 0.5) return false; + var ray = cur.getWorld().rayTraceBlocks(cur, toTarget.normalize(), remaining); + if (ray != null && ray.getHitBlock() != null) { + double hitDist = ray.getHitBlock().getLocation().distance(cur); + return hitDist + 0.5 < remaining; + } + return false; + } + + private void retarget(Location cur) { + var ray = cur.getWorld().rayTraceBlocks(cur, flightDir, MAX_RANGE); + Location nt; + if (ray != null && ray.getHitPosition() != null) { + nt = new Location(cur.getWorld(), ray.getHitPosition().getX(), ray.getHitPosition().getY(), ray.getHitPosition().getZ()); + if (ray.getHitBlockFace() != null) nt.add(0, 0.1, 0); + } else { + nt = cur.clone().add(flightDir.clone().multiply(12)); + nt.setY(cur.getWorld().getHighestBlockYAt(nt) + 1); + } + flightStart = cur.clone(); + flightTarget = nt.clone(); + double remaining = Math.max(1.0, cur.distance(nt)); + flightDuration = (int) Math.max(6, Math.min(40, remaining * 3)); + elapsed = 0; + arc = Math.max(ARC_MIN, Math.min(ARC_MAX, remaining * ARC_PER_BLOCK)); + targetWasSolid = flightTarget.getBlock().isSolid(); + } + + private void renderTrail() { + int size = trail.size(); + int i = 0; + for (Location tp : trail) { + float frac = size <= 1 ? 1f : (float) i / (size - 1); + float dustSize = 0.3f + frac * 1.1f; + p.spawnParticle(Particle.DUST, tp, 1, 0.02, 0.02, 0.02, 0, + new Particle.DustOptions(Color.fromRGB(180, 120, 40), dustSize)); + i++; + } + } + + private void renderRings(Location ground) { + Location g = ground.clone().add(0, 0.15, 0); + for (int k = 0; k < 16; k++) { + double a = (double) k / 16 * Math.PI * 2; + Location ring = g.clone().add(Math.cos(a) * LETHAL_RADIUS, 0, Math.sin(a) * LETHAL_RADIUS); + p.spawnParticle(Particle.DUST, ring, 1, 0, 0, 0, 0, + new Particle.DustOptions(Color.fromRGB(255, 40, 40), 1.2f)); + } + for (int k = 0; k < 24; k++) { + double a = (double) k / 24 * Math.PI * 2; + Location ring = g.clone().add(Math.cos(a) * CHIP_RADIUS, 0, Math.sin(a) * CHIP_RADIUS); + p.spawnParticle(Particle.DUST, ring, 1, 0, 0, 0, 0, + new Particle.DustOptions(Color.fromRGB(255, 200, 60), 1.0f)); + } + } + }.runTaskTimer(plugin, 1L, 1L); + return true; + } + + private static Location computeTarget(Player p, Location eye, Vector dir) { + var ray = p.getWorld().rayTraceBlocks(eye, dir, MAX_RANGE); + if (ray != null && ray.getHitPosition() != null) { + Location loc = new Location(p.getWorld(), ray.getHitPosition().getX(), ray.getHitPosition().getY(), ray.getHitPosition().getZ()); + if (ray.getHitBlockFace() != null) loc.add(0, 0.1, 0); + return loc; + } + Location fallback = eye.clone().add(dir.clone().multiply(20)); + fallback.setY(p.getWorld().getHighestBlockYAt(fallback) + 1); + return fallback; + } + + private static void explode(Player p, Location expl, BlackKnifePlugin plugin) { + expl.getWorld().spawnParticle(Particle.EXPLOSION, expl, 2); + expl.getWorld().spawnParticle(Particle.SMOKE, expl, 12, 0.3, 0.3, 0.3, 0.05); + expl.getWorld().spawnParticle(Particle.DUST, expl, 10, 0.3, 0.2, 0.3, 0, + new Particle.DustOptions(Color.fromRGB(255, 120, 30), 1.2f)); + expl.getWorld().playSound(expl, Sound.ENTITY_GENERIC_EXPLODE, 1f, 1.1f); + TensionManager tm = plugin.getTension(); + for (LivingEntity le : expl.getWorld().getNearbyLivingEntities(expl, CHIP_RADIUS + 0.5)) { + if (le == p) continue; + if (!DamageUtil.canHurt(p, le)) continue; + double d = le.getLocation().add(0, 1, 0).distance(expl); + if (d > CHIP_RADIUS) continue; + if (d <= LETHAL_RADIUS) { + DamageUtil.apply(p, le, LETHAL_DAMAGE); + tm.startBattle(p, le); + } else { + DamageUtil.apply(p, le, CHIP_DAMAGE); + tm.startBattle(p, le); + } + le.getWorld().spawnParticle(Particle.CRIT, le.getLocation().add(0, 1, 0), 8, 0.3, 0.3, 0.3, 0.1); + } + for (Player pl : expl.getWorld().getNearbyPlayers(expl, 0.9)) { + if (pl == p) continue; + double d2 = pl.getLocation().add(0, 1, 0).distanceSquared(expl); + if (d2 > 0.25 && d2 < 0.81) tm.tryGiveDodge(pl); + } + } +} diff --git a/src/main/java/me/sashegdev/blackknife/KillMessageHandler.java b/src/main/java/me/sashegdev/blackknife/KillMessageHandler.java index 260d800..e68d7ee 100644 --- a/src/main/java/me/sashegdev/blackknife/KillMessageHandler.java +++ b/src/main/java/me/sashegdev/blackknife/KillMessageHandler.java @@ -65,7 +65,7 @@ public class KillMessageHandler implements Listener { private boolean isKnifeMagic(String magic) { if (magic == null) return false; String m = magic.toLowerCase(); - return m.contains("swoon") || m.contains("sword") || m.contains("diamond") || m.contains("lunge") || m.contains("snipe") || m.contains("tri") || m.contains("overhead") || m.contains("stars") || m.contains("black knife") || m.contains("knife"); + return m.contains("swoon") || m.contains("sword") || m.contains("diamond") || m.contains("lunge") || m.contains("snipe") || m.contains("tri") || m.contains("overhead") || m.contains("stars") || m.contains("black knife") || m.contains("knife") || m.contains("grenade") || m.contains("dive"); } private String obf(String name) { diff --git a/src/main/java/me/sashegdev/blackknife/SkyDiveAttack.java b/src/main/java/me/sashegdev/blackknife/SkyDiveAttack.java new file mode 100644 index 0000000..2782996 --- /dev/null +++ b/src/main/java/me/sashegdev/blackknife/SkyDiveAttack.java @@ -0,0 +1,220 @@ +package me.sashegdev.blackknife; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Color; +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.Particle; +import org.bukkit.Sound; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.entity.PlayerDeathEvent; +import org.bukkit.event.player.PlayerMoveEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.util.Vector; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class SkyDiveAttack implements Listener { + private static final float COST = 87.5f; + private static final double DAMAGE = 25.0; + private static final double LAUNCH_UP = 1.3; + private static final double LAUNCH_FORWARD = 0.4; + private static final double DASH_SPEED = 2.8; + private static final long FREEZE_TICKS = 30L; + private static final long FALL_IMMUNE_MS = 8000L; + + private static final Map frozen = new ConcurrentHashMap<>(); + private static final Map immuneUntil = new ConcurrentHashMap<>(); + private static final Map diving = new ConcurrentHashMap<>(); + + public static boolean tryUse(Player p, TensionManager tension) { + if (SwoonHandler.isSwooned(p)) return false; + if (p.getGameMode() == GameMode.CREATIVE || p.getGameMode() == GameMode.SPECTATOR) return false; + if (Boolean.TRUE.equals(diving.get(p.getUniqueId()))) { + p.sendActionBar(Component.text("Already diving!").color(NamedTextColor.RED)); + return false; + } + if (tension.getTP(p) < COST) { + p.sendActionBar(Component.text("Need 35% TP").color(NamedTextColor.RED)); + return false; + } + tension.setTP(p, tension.getTP(p) - COST); + BlackKnifePlugin plugin = BlackKnifePlugin.getPlugin(BlackKnifePlugin.class); + + Vector look = p.getEyeLocation().getDirection(); + Vector horizontal = new Vector(look.getX(), 0, look.getZ()); + if (horizontal.lengthSquared() > 0.0001) horizontal.normalize().multiply(LAUNCH_FORWARD); + else horizontal = new Vector(0, 0, 0); + p.setVelocity(new Vector(horizontal.getX(), LAUNCH_UP, horizontal.getZ())); + p.setFallDistance(0); + immuneUntil.put(p.getUniqueId(), System.currentTimeMillis() + FALL_IMMUNE_MS); + diving.put(p.getUniqueId(), true); + p.getWorld().playSound(p.getLocation(), Sound.ENTITY_FIREWORK_ROCKET_LAUNCH, 1f, 0.8f); + p.getWorld().spawnParticle(Particle.CLOUD, p.getLocation(), 10, 0.3, 0.2, 0.3, 0.05); + + new BukkitRunnable() { + final double startY = p.getLocation().getY(); + int ticks = 0; + + @Override public void run() { + if (!p.isOnline() || p.isDead()) { cleanup(p); cancel(); return; } + p.setFallDistance(0); + ticks++; + double gained = p.getLocation().getY() - startY; + Vector v = p.getVelocity(); + if (v.getY() <= 0.25 || gained >= 5.5 || ticks >= 14) { + cancel(); + freeze(p, plugin); + } + if (ticks > 40) { cancel(); freeze(p, plugin); } + } + }.runTaskTimer(plugin, 1L, 1L); + return true; + } + + private static void freeze(Player p, BlackKnifePlugin plugin) { + if (!p.isOnline() || p.isDead()) { cleanup(p); return; } + Location top = p.getLocation().clone(); + p.setVelocity(new Vector(0, 0, 0)); + p.setFallDistance(0); + frozen.put(p.getUniqueId(), top); + p.getWorld().playSound(top, Sound.BLOCK_BEACON_ACTIVATE, 0.8f, 1.4f); + + new BukkitRunnable() { + long left = FREEZE_TICKS; + + @Override public void run() { + if (!p.isOnline() || p.isDead()) { cleanup(p); cancel(); return; } + Location anchor = frozen.get(p.getUniqueId()); + if (anchor == null) { cancel(); return; } + Location cur = p.getLocation(); + anchor.setYaw(cur.getYaw()); + anchor.setPitch(cur.getPitch()); + p.teleport(anchor); + p.setVelocity(new Vector(0, 0, 0)); + p.setFallDistance(0); + for (int k = 0; k < 10; k++) { + double a = (double) k / 10 * Math.PI * 2; + Location ring = anchor.clone().add(Math.cos(a) * 1.2, 0.1, Math.sin(a) * 1.2); + p.getWorld().spawnParticle(Particle.DUST, ring, 1, 0, 0, 0, 0, + new Particle.DustOptions(Color.fromRGB(80, 160, 255), 1.0f)); + } + left--; + if (left <= 0) { + frozen.remove(p.getUniqueId()); + cancel(); + dash(p, plugin); + } + } + }.runTaskTimer(plugin, 1L, 1L); + } + + private static void dash(Player p, BlackKnifePlugin plugin) { + if (!p.isOnline() || p.isDead()) { cleanup(p); return; } + Vector dir = p.getEyeLocation().getDirection().normalize(); + p.setVelocity(dir.clone().multiply(DASH_SPEED)); + p.setFallDistance(0); + immuneUntil.put(p.getUniqueId(), System.currentTimeMillis() + FALL_IMMUNE_MS); + p.getWorld().playSound(p.getLocation(), Sound.ENTITY_ENDER_DRAGON_FLAP, 1f, 1.2f); + + new BukkitRunnable() { + int ticks = 0; + boolean hit = false; + + @Override public void run() { + if (!p.isOnline() || p.isDead()) { cleanup(p); cancel(); return; } + p.setFallDistance(0); + ticks++; + Location cur = p.getLocation().add(0, 1, 0); + cur.getWorld().spawnParticle(Particle.DUST, cur, 2, 0.15, 0.15, 0.15, 0, + new Particle.DustOptions(Color.fromRGB(80, 160, 255), 1.4f)); + cur.getWorld().spawnParticle(Particle.CLOUD, cur, 1, 0.1, 0.1, 0.1, 0.02); + + for (LivingEntity le : cur.getWorld().getNearbyLivingEntities(cur, 1.2)) { + if (le == p) continue; + if (!DamageUtil.canHurt(p, le)) continue; + DamageUtil.apply(p, le, DAMAGE); + Vector kb = dir.clone().multiply(1.2); + kb.setY(kb.getY() + 0.4); + le.setVelocity(kb); + le.getWorld().spawnParticle(Particle.CRIT, le.getLocation().add(0, 1, 0), 12, 0.4, 0.4, 0.4, 0.1); + le.getWorld().playSound(le.getLocation(), Sound.ENTITY_PLAYER_ATTACK_CRIT, 1f, 0.8f); + plugin.getTension().startBattle(p, le); + hit = true; + break; + } + if (hit) { finish(); return; } + Location ahead = p.getEyeLocation().add(dir.clone().multiply(1.0)); + if (ahead.getBlock().isSolid()) { finish(); return; } + if (ticks > 40) { finish(); return; } + } + + private void finish() { + immuneUntil.put(p.getUniqueId(), System.currentTimeMillis() + 3000L); + cleanup(p); + cancel(); + } + }.runTaskTimer(plugin, 1L, 1L); + } + + private static void cleanup(Player p) { + frozen.remove(p.getUniqueId()); + diving.remove(p.getUniqueId()); + } + + public static boolean isImmune(Player p) { + Long until = immuneUntil.get(p.getUniqueId()); + return until != null && System.currentTimeMillis() < until; + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onFall(EntityDamageEvent e) { + if (e.getCause() != EntityDamageEvent.DamageCause.FALL) return; + if (!(e.getEntity() instanceof Player p)) return; + if (isImmune(p) || frozen.containsKey(p.getUniqueId()) || Boolean.TRUE.equals(diving.get(p.getUniqueId()))) { + e.setCancelled(true); + p.setFallDistance(0); + } + } + + @EventHandler(priority = EventPriority.HIGH) + public void onMove(PlayerMoveEvent e) { + Player p = e.getPlayer(); + Location anchor = frozen.get(p.getUniqueId()); + if (anchor == null) return; + if (e.getTo() == null) return; + double dx = e.getTo().getX() - anchor.getX(); + double dy = e.getTo().getY() - anchor.getY(); + double dz = e.getTo().getZ() - anchor.getZ(); + if (Math.abs(dx) > 0.05 || Math.abs(dy) > 0.05 || Math.abs(dz) > 0.05) { + Location fixed = anchor.clone(); + fixed.setYaw(e.getTo().getYaw()); + fixed.setPitch(e.getTo().getPitch()); + e.setTo(fixed); + p.sendActionBar(Component.text("Aiming...").color(NamedTextColor.AQUA)); + } + } + + @EventHandler + public void onQuit(PlayerQuitEvent e) { + frozen.remove(e.getPlayer().getUniqueId()); + diving.remove(e.getPlayer().getUniqueId()); + immuneUntil.remove(e.getPlayer().getUniqueId()); + } + + @EventHandler + public void onDeath(PlayerDeathEvent e) { + frozen.remove(e.getEntity().getUniqueId()); + diving.remove(e.getEntity().getUniqueId()); + immuneUntil.remove(e.getEntity().getUniqueId()); + } +}