diff --git a/src/main/java/me/sashegdev/blackknife/BlackKnifeItem.java b/src/main/java/me/sashegdev/blackknife/BlackKnifeItem.java index e2a45df..8c1a451 100644 --- a/src/main/java/me/sashegdev/blackknife/BlackKnifeItem.java +++ b/src/main/java/me/sashegdev/blackknife/BlackKnifeItem.java @@ -5,6 +5,7 @@ import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextDecoration; import org.bukkit.Material; import org.bukkit.NamespacedKey; +import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.persistence.PersistentDataType; @@ -34,4 +35,18 @@ public class BlackKnifeItem { var meta = item.getItemMeta(); return meta != null && meta.getPersistentDataContainer().has(KEY, PersistentDataType.BYTE); } + + /** + * Нож в руках игрока. + * + *
Отдельный метод вместо пары вызовов {@link #is} по инвентарю, потому что + * {@code getItemMeta()} возвращает полную копию метаданных вместе с PDC, то + * есть на каждый вызов это аллокация и копирование NBT. Здесь та же проверка, + * но её результат берётся из кеша владельца — ноль аллокаций на попадание. + */ + public static boolean inHands(Player p) { + var plugin = BlackKnifePlugin.getInstance(); + return plugin != null && plugin.getTension() != null + && plugin.getTension().hasKnifeInHands(p); + } } diff --git a/src/main/java/me/sashegdev/blackknife/BlackKnifeListener.java b/src/main/java/me/sashegdev/blackknife/BlackKnifeListener.java index 3bb56fd..5dcc938 100644 --- a/src/main/java/me/sashegdev/blackknife/BlackKnifeListener.java +++ b/src/main/java/me/sashegdev/blackknife/BlackKnifeListener.java @@ -7,6 +7,7 @@ import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.Particle; import org.bukkit.Sound; +import org.bukkit.World; import org.bukkit.entity.Display; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; @@ -67,14 +68,31 @@ public class BlackKnifeListener implements Listener { clearHighlight(p); } + /** + * Точка, в которую смотрит игрок. + * + *
Раньше здесь вызывалось getEyeLocation() три раза и getDirection() два. + * Каждый вызов — новая аллокация: Location и Vector у Bukkit копируются, а не + * отдаются ссылкой. Метод зовётся из превью прицеливания каждые 4 тика на + * каждого игрока с ножом, то есть на 60 игроках это 90 лишних Location и + * 60 лишних Vector в секунду только на вычисление одного и того же вектора. + * + *
Сам rayTraceBlocks на 120 блоков остался: он определяет, во что попал + * взгляд, и его результат меняется вместе с обзором игрока. + */ public Location getGazePoint(Player p) { - var ray = p.getWorld().rayTraceBlocks(p.getEyeLocation(), p.getEyeLocation().getDirection(), 120); + final Location eye = p.getEyeLocation(); + final Vector dir = eye.getDirection(); + final World world = eye.getWorld(); + + final var ray = world.rayTraceBlocks(eye, dir, 120); if (ray != null && ray.getHitPosition() != null) { - Location loc = new Location(p.getWorld(), ray.getHitPosition().getX(), ray.getHitPosition().getY(), ray.getHitPosition().getZ()); + final Location loc = new Location(world, + ray.getHitPosition().getX(), ray.getHitPosition().getY(), ray.getHitPosition().getZ()); if (ray.getHitBlockFace() != null) loc.add(0, 0.1, 0); return loc; } - return p.getEyeLocation().add(p.getEyeLocation().getDirection().normalize().multiply(20)); + return eye.add(dir.normalize().multiply(20)); } /** Дальность поиска цели, блоков. */ @@ -296,7 +314,7 @@ public class BlackKnifeListener implements Listener { @Override public void run() { if (!p.isOnline() || !getMode(p).equals("SWORD")) { previewTasks.remove(p.getUniqueId()); cancel(); return; } if (p.hasCooldown(Material.NETHERITE_SWORD)) return; - if (!BlackKnifeItem.is(p.getInventory().getItemInMainHand()) && !BlackKnifeItem.is(p.getInventory().getItemInOffHand())) return; + if (!BlackKnifeItem.inHands(p)) return; Vector dir = p.getEyeLocation().getDirection().normalize(); Vector right = dir.clone().crossProduct(new Vector(0, 1, 0)); if (right.lengthSquared() < 0.01) right = new Vector(1, 0, 0); @@ -306,15 +324,15 @@ public class BlackKnifeListener implements Listener { if (combo < 2) { for (int i = -2; i <= 2; i++) { Location pp = base.clone().add(right.clone().multiply(i * 0.3)).add(dir.clone().multiply(Math.abs(i) * 0.15)); - p.spawnParticle(Particle.DUST, pp, 1, 0, 0, 0, 0, new Particle.DustOptions(Color.fromRGB(0, 0, 0), 1.3f)); + p.spawnParticle(Particle.DUST, pp, 1, 0, 0, 0, 0, Particles.BLACK_13); } - p.spawnParticle(Particle.DUST, base.clone().add(dir.clone().multiply(1.2)), 1, 0, 0, 0, 0, new Particle.DustOptions(Color.fromRGB(0, 0, 0), 0.8f)); + p.spawnParticle(Particle.DUST, base.clone().add(dir.clone().multiply(1.2)), 1, 0, 0, 0, 0, Particles.BLACK_08); } else { for (int side = -1; side <= 1; side++) { Location c = base.clone().add(right.clone().multiply(side * 1.3)); for (int i = -2; i <= 2; i++) { Location pp = c.clone().add(right.clone().multiply(i * 0.22)).add(dir.clone().multiply(Math.abs(i) * 0.1)); - p.spawnParticle(Particle.DUST, pp, 1, 0, 0, 0, 0, new Particle.DustOptions(Color.fromRGB(50, 0, 0), 1.3f)); + p.spawnParticle(Particle.DUST, pp, 1, 0, 0, 0, 0, Particles.DARKRED_13); } } } @@ -327,29 +345,35 @@ public class BlackKnifeListener implements Listener { @Override public void run() { if (!p.isOnline() || !(getMode(p).equals("SNIPE") || getMode(p).equals("TRI"))) { cancel(); return; } if (p.hasCooldown(Material.NETHERITE_SWORD)) return; - if (!BlackKnifeItem.is(p.getInventory().getItemInMainHand()) && !BlackKnifeItem.is(p.getInventory().getItemInOffHand())) return; + if (!BlackKnifeItem.inHands(p)) return; LivingEntity target = getLungeTarget(p); if (target == null) return; Location tl = target.getLocation().add(0, 1, 0); - int ringCount = m.equals("TRI") ? 16 : 12; - double r1 = m.equals("SNIPE") ? 0.6 : 0.8; + // Цвет кольца выбирается ОДИН раз до цикла. Раньше тернарник с + // тремя Color.fromRGB стоял внутри цикла, то есть на каждой из + // 12-16 итераций вычислялся заново цвет, который за цикл не + // меняется: это до 16 лишних Color и DustOptions на каждый тик + // прицеливания, каждые 5 тиков, на каждого игрока. + final Particle.DustOptions ringDust = m.equals("TRI") ? Particles.TRI_15 + : m.equals("SNIPE") ? Particles.SNIPE_15 : Particles.AIM_15; + final int ringCount = m.equals("TRI") ? 16 : 12; + final double r1 = m.equals("SNIPE") ? 0.6 : 0.8; for (int i = 0; i < ringCount; i++) { double angle = (double) i / ringCount * Math.PI * 2; double x = Math.cos(angle) * r1; double z = Math.sin(angle) * r1; Location ring = tl.clone().add(x, 0, z); - Color col = m.equals("TRI") ? Color.fromRGB(80, 20, 80) : m.equals("SNIPE") ? Color.fromRGB(20, 100, 160) : Color.fromRGB(30, 80, 120); - p.spawnParticle(Particle.DUST, ring, 1, 0, 0, 0, 0, new Particle.DustOptions(col, 1.5f)); + p.spawnParticle(Particle.DUST, ring, 1, 0, 0, 0, 0, ringDust); } if (m.equals("TRI")) { for (int k = 0; k < 3; k++) { double a = k * 120 * Math.PI / 180; Location dot = tl.clone().add(Math.cos(a) * r1, 0, Math.sin(a) * r1); - p.spawnParticle(Particle.DUST, dot, 1, 0, 0, 0, 0, new Particle.DustOptions(Color.fromRGB(255, 50, 50), 1.3f)); + p.spawnParticle(Particle.DUST, dot, 1, 0, 0, 0, 0, Particles.RED_13); } } p.spawnParticle(Particle.SCULK_SOUL, tl, 3, 0.3, 0.5, 0.3, 0.02); - p.spawnParticle(Particle.DUST, tl.clone().add(0, 2, 0), 1, 0, 0, 0, 0, new Particle.DustOptions(Color.fromRGB(255, 50, 50), 1.0f)); + p.spawnParticle(Particle.DUST, tl.clone().add(0, 2, 0), 1, 0, 0, 0, 0, Particles.RED_10); } }; r.runTaskTimer(plugin, 1L, 5L); @@ -359,7 +383,7 @@ public class BlackKnifeListener implements Listener { @Override public void run() { if (!p.isOnline() || !getMode(p).equals("OVERHEAD")) { cancel(); return; } if (p.hasCooldown(Material.NETHERITE_SWORD)) return; - if (!BlackKnifeItem.is(p.getInventory().getItemInMainHand()) && !BlackKnifeItem.is(p.getInventory().getItemInOffHand())) return; + if (!BlackKnifeItem.inHands(p)) return; Location gp = getGazePoint(p); // 32 отдельных пакета на тик прицеливания -> 2 пакета. // Кольцо radius 1.8 раскладывается диском, на масштабе @@ -372,7 +396,7 @@ public class BlackKnifeListener implements Listener { Location ring = gp.clone().add(Math.cos(a) * 1, 0.1, Math.sin(a) * 1); p.spawnParticle(Particle.SCULK_SOUL, ring, 1, 0.02, 0.02, 0.02, 0); } - p.spawnParticle(Particle.DUST, gp.clone().add(0, 0.1, 0), 1, 0, 0, 0, 0, new Particle.DustOptions(Color.fromRGB(255, 40, 40), 1.5f)); + p.spawnParticle(Particle.DUST, gp.clone().add(0, 0.1, 0), 1, 0, 0, 0, 0, Particles.RED_15); } }; r.runTaskTimer(plugin, 1L, 4L); @@ -382,17 +406,17 @@ public class BlackKnifeListener implements Listener { @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; + if (!BlackKnifeItem.inHands(p)) return; Location gp = GrenadeAttack.computeTarget(p, p.getEyeLocation(), p.getEyeLocation().getDirection().normalize()); 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)); + p.spawnParticle(Particle.DUST, ring, 1, 0, 0, 0, 0, Particles.RED_12); } 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)); + p.spawnParticle(Particle.DUST, ring, 1, 0, 0, 0, 0, Particles.GOLD_10); } } }; @@ -403,12 +427,12 @@ public class BlackKnifeListener implements Listener { @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; + if (!BlackKnifeItem.inHands(p)) 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.DUST, ring, 1, 0, 0, 0, 0, Particles.BLUE_12); } p.spawnParticle(Particle.CLOUD, gp.clone().add(0, 0.2, 0), 1, 0.1, 0.1, 0.1, 0); } @@ -420,9 +444,9 @@ public class BlackKnifeListener implements Listener { @Override public void run() { if (!p.isOnline() || !getMode(p).equals("DIAMOND_TRI")) { cancel(); return; } if (p.hasCooldown(Material.NETHERITE_SWORD)) return; - if (!BlackKnifeItem.is(p.getInventory().getItemInMainHand()) && !BlackKnifeItem.is(p.getInventory().getItemInOffHand())) return; + if (!BlackKnifeItem.inHands(p)) return; Location base = p.getEyeLocation().add(p.getEyeLocation().getDirection().normalize().multiply(1.5)); - for (int i=0;i<3;i++){ double a=(double)i/3*Math.PI*2; Location pp=base.clone().add(Math.cos(a)*0.5, Math.sin(a)*0.3, Math.sin(a)*0.5); p.spawnParticle(Particle.DUST,pp,1,0,0,0,0,new Particle.DustOptions(Color.fromRGB(180,0,255),1f)); p.spawnParticle(Particle.END_ROD,pp,1,0.02,0.02,0.02,0); } + for (int i=0;i<3;i++){ double a=(double)i/3*Math.PI*2; Location pp=base.clone().add(Math.cos(a)*0.5, Math.sin(a)*0.3, Math.sin(a)*0.5); p.spawnParticle(Particle.DUST,pp,1,0,0,0,0,Particles.PURPLE_10); p.spawnParticle(Particle.END_ROD,pp,1,0.02,0.02,0.02,0); } } }; r.runTaskTimer(plugin, 1L, 5L); @@ -432,7 +456,7 @@ public class BlackKnifeListener implements Listener { @Override public void run() { if (!p.isOnline() || !getMode(p).equals("BARRAGE")) { cancel(); return; } if (p.hasCooldown(Material.NETHERITE_SWORD)) return; - if (!BlackKnifeItem.is(p.getInventory().getItemInMainHand()) && !BlackKnifeItem.is(p.getInventory().getItemInOffHand())) return; + if (!BlackKnifeItem.inHands(p)) return; Location c = p.getLocation().add(0,1,0); for(int i=0;i<5;i++){ double a=(double)i/5*Math.PI*2 + System.currentTimeMillis()*0.002; Location orb=c.clone().add(Math.cos(a)*1.5, Math.sin(a*2)*0.3, Math.sin(a)*1.5); p.spawnParticle(Particle.SCULK_SOUL,orb,1,0.02,0.02,0.02,0); p.spawnParticle(Particle.ASH,orb,1,0,0,0,0); } } @@ -442,100 +466,13 @@ public class BlackKnifeListener implements Listener { } } + /** + * Визуал STARS вынесен в {@link StarFx}: раньше каждая из 18 звёзд и каждый + * из 108 осколков получал собственную задачу с периодом в один тик, то + * есть одно нажатие порождало до 126 задач планировщика. Теперь они все + * едут под одной общей задачей, а в простое она снимает себя сама. + */ private void spawnStars(Player p, Location center) { - p.getWorld().playSound(center, Sound.BLOCK_AMETHYST_BLOCK_PLACE, 1f, 1.4f); - p.getWorld().playSound(center, Sound.BLOCK_BEACON_POWER_SELECT, 0.8f, 1.5f); - for (int i = 0; i < 18; i++) { - double angle = Math.random() * Math.PI * 2; - double r = 6 + Math.random() * 6; - Location loc = center.clone().add(Math.cos(angle) * r, 8 + Math.random() * 4, Math.sin(angle) * r); - org.bukkit.entity.BlockDisplay disp = p.getWorld().spawn(loc, org.bukkit.entity.BlockDisplay.class, d -> { - d.setBlock(Material.AMETHYST_BLOCK.createBlockData()); - d.setBrightness(new Display.Brightness(15, 15)); - d.setViewRange(64); - }); - EntityTracker.mark(disp); - EntityTracker.smooth(disp); - Vector vel = new Vector(0, -0.35 - Math.random() * 0.1, 0); - vel.add(new Vector((Math.random() - 0.5) * 0.08, 0, (Math.random() - 0.5) * 0.08)); - new BukkitRunnable() { - @Override public void run() { - if (!disp.isValid() || disp.isDead()) { cancel(); return; } - Location cur = disp.getLocation(); - Location nl = cur.clone().add(vel); - Location next = nl.clone().add(vel); - if (next.getBlock().isSolid() || nl.getY() <= p.getWorld().getHighestBlockYAt(nl) + 0.5) { - Location expl = cur.clone().add(vel.clone().normalize().multiply(-0.1)); - disp.teleport(expl); - EntityTracker.untrack(disp); - disp.remove(); - cancel(); - p.getWorld().spawnParticle(Particle.EXPLOSION, expl, 1); - p.getWorld().playSound(expl, Sound.ENTITY_GENERIC_EXPLODE, 0.8f, 1.2f); - for (int s = 0; s < 6; s++) { - double a = s * Math.PI * 2 / 6; - Vector sv = new Vector(Math.cos(a) * 0.35, 0, Math.sin(a) * 0.35); - org.bukkit.entity.BlockDisplay shard = p.getWorld().spawn(expl, org.bukkit.entity.BlockDisplay.class, d -> { - d.setBlock(Material.MEDIUM_AMETHYST_BUD.createBlockData()); - d.setBrightness(new Display.Brightness(15, 15)); - d.setViewRange(64); - }); - EntityTracker.mark(shard); - EntityTracker.smooth(shard); - new BukkitRunnable() { - @Override public void run() { - if (!shard.isValid()) { cancel(); return; } - Location cur2 = shard.getLocation(); - Location n2 = cur2.clone().add(sv); - if (n2.getBlock().isSolid()) { - Location expl2 = cur2.clone().add(sv.clone().normalize().multiply(-0.1)); - shard.teleport(expl2); - EntityTracker.untrack(shard); - shard.remove(); - cancel(); - return; - } - shard.teleport(n2); - n2.getWorld().spawnParticle(Particle.CRIT, n2, 1, 0.05, 0.05, 0.05, 0); - for (var ent : n2.getWorld().getNearbyEntities(n2, 0.7, 0.7, 0.7)) { - if (ent instanceof Player pl && pl != p) continue; - if (ent instanceof org.bukkit.entity.LivingEntity le && le != p) { - DamageUtil.apply(p, le, 3.0); - EntityTracker.untrack(shard); - shard.remove(); - cancel(); - break; - } - } - for (Player pl : n2.getWorld().getNearbyPlayers(n2, 0.9)) { - if (pl == p) continue; - double d2 = pl.getLocation().add(0, 1, 0).distanceSquared(n2); - if (d2 > 0.25 && d2 < 0.81) { - plugin.getTension().tryGiveDodge(pl); - pl.getWorld().spawnParticle(Particle.DUST, pl.getLocation().add(0, 0.25, 0), 25, 0.5, 0.05, 0.5, 0, new Particle.DustOptions(Color.fromRGB(255, 255, 255), 0.15f)); - } - } - EntityTracker.untrack(shard); - if (shard.getTicksLived() > 40) { shard.remove(); cancel(); } - } - }.runTaskTimer(plugin, 1L, 1L); - } - return; - } - disp.teleport(nl); - disp.getWorld().spawnParticle(Particle.END_ROD, nl, 1, 0.05, 0.05, 0.05, 0); - for (Player pl : nl.getWorld().getNearbyPlayers(nl, 0.9)) { - if (pl == p) continue; - double d2 = pl.getLocation().add(0, 1, 0).distanceSquared(nl); - if (d2 > 0.25 && d2 < 0.81) { - plugin.getTension().tryGiveDodge(pl); - pl.getWorld().spawnParticle(Particle.DUST, pl.getLocation().add(0, 0.25, 0), 25, 0.5, 0.05, 0.5, 0, new Particle.DustOptions(Color.fromRGB(255, 255, 255), 0.15f)); - } - } - EntityTracker.untrack(disp); - if (disp.getTicksLived() > 120) { disp.remove(); cancel(); } - } - }.runTaskTimer(plugin, 1L, 1L); - } + StarFx.burst(plugin, p, center); } } diff --git a/src/main/java/me/sashegdev/blackknife/BlackKnifePlugin.java b/src/main/java/me/sashegdev/blackknife/BlackKnifePlugin.java index d16c3da..8bff39d 100644 --- a/src/main/java/me/sashegdev/blackknife/BlackKnifePlugin.java +++ b/src/main/java/me/sashegdev/blackknife/BlackKnifePlugin.java @@ -119,6 +119,7 @@ public class BlackKnifePlugin extends JavaPlugin { @Override public void onDisable() { getServer().getScheduler().cancelTasks(this); + StarFx.clear(); if (tension != null) { tension.hideAll(); for (var p : getServer().getOnlinePlayers()) { diff --git a/src/main/java/me/sashegdev/blackknife/DiamondAttack.java b/src/main/java/me/sashegdev/blackknife/DiamondAttack.java index 80fbb71..e9b16ae 100644 --- a/src/main/java/me/sashegdev/blackknife/DiamondAttack.java +++ b/src/main/java/me/sashegdev/blackknife/DiamondAttack.java @@ -88,7 +88,7 @@ public class DiamondAttack { double d2 = pl.getLocation().add(0, 1, 0).distanceSquared(n2); if (d2 > 0.25 && d2 < 0.81) { BlackKnifePlugin.getPlugin(BlackKnifePlugin.class).getTension().tryGiveDodge(pl); - pl.getWorld().spawnParticle(Particle.DUST, pl.getLocation().add(0, 0.25, 0), 25, 0.5, 0.05, 0.5, 0, new Particle.DustOptions(Color.fromRGB(255, 255, 255), 0.15f)); + pl.getWorld().spawnParticle(Particle.DUST, pl.getLocation().add(0, 0.25, 0), 25, 0.5, 0.05, 0.5, 0, Particles.WHITE_015); } } EntityTracker.untrack(shard); @@ -100,7 +100,7 @@ public class DiamondAttack { } crystal.teleport(nl); crystal.getWorld().spawnParticle(Particle.END_ROD, nl, 1, 0.05, 0.05, 0.05, 0); - crystal.getWorld().spawnParticle(Particle.DUST, nl, 1, 0, 0, 0, 0, new Particle.DustOptions(Color.fromRGB(180, 0, 255), 1f)); + crystal.getWorld().spawnParticle(Particle.DUST, nl, 1, 0, 0, 0, 0, Particles.PURPLE_10); boolean hit = false; for (var ent : nl.getWorld().getNearbyEntities(nl, 0.7, 0.7, 0.7, e -> e instanceof LivingEntity && e != p)) { LivingEntity le = (LivingEntity) ent; @@ -113,7 +113,7 @@ public class DiamondAttack { double d = pl.getLocation().add(0, 1, 0).distanceSquared(nl); if (d > 0.25 && d < 0.81) { BlackKnifePlugin.getPlugin(BlackKnifePlugin.class).getTension().tryGiveDodge(pl); - pl.getWorld().spawnParticle(Particle.DUST, pl.getLocation().add(0, 0.25, 0), 25, 0.5, 0.05, 0.5, 0, new Particle.DustOptions(Color.fromRGB(255, 255, 255), 0.15f)); + pl.getWorld().spawnParticle(Particle.DUST, pl.getLocation().add(0, 0.25, 0), 25, 0.5, 0.05, 0.5, 0, Particles.WHITE_015); } } if (hit) { @@ -159,7 +159,7 @@ public class DiamondAttack { double d2 = pl.getLocation().add(0, 1, 0).distanceSquared(n2); if (d2 > 0.25 && d2 < 0.81) { BlackKnifePlugin.getPlugin(BlackKnifePlugin.class).getTension().tryGiveDodge(pl); - pl.getWorld().spawnParticle(Particle.DUST, pl.getLocation().add(0, 0.25, 0), 25, 0.5, 0.05, 0.5, 0, new Particle.DustOptions(Color.fromRGB(255, 255, 255), 0.15f)); + pl.getWorld().spawnParticle(Particle.DUST, pl.getLocation().add(0, 0.25, 0), 25, 0.5, 0.05, 0.5, 0, Particles.WHITE_015); } } EntityTracker.untrack(shard); diff --git a/src/main/java/me/sashegdev/blackknife/DiamondTripleAttack.java b/src/main/java/me/sashegdev/blackknife/DiamondTripleAttack.java index 38c5408..a95c538 100644 --- a/src/main/java/me/sashegdev/blackknife/DiamondTripleAttack.java +++ b/src/main/java/me/sashegdev/blackknife/DiamondTripleAttack.java @@ -68,7 +68,7 @@ public class DiamondTripleAttack { if (check.getBlock().isSolid()) { shard.remove(); cancel(); return; } } shard.teleport(nl); - nl.getWorld().spawnParticle(Particle.DUST, nl,1,0,0,0,0,new Particle.DustOptions(Color.fromRGB(180,0,255),1f)); + nl.getWorld().spawnParticle(Particle.DUST, nl,1,0,0,0,0,Particles.PURPLE_10); nl.getWorld().spawnParticle(Particle.END_ROD, nl,1,0.05,0.05,0.05,0); for (LivingEntity le : nl.getWorld().getNearbyLivingEntities(nl,0.8)) { if (le == p) continue; diff --git a/src/main/java/me/sashegdev/blackknife/GrenadeAttack.java b/src/main/java/me/sashegdev/blackknife/GrenadeAttack.java index 7c88c31..9a7dd08 100644 --- a/src/main/java/me/sashegdev/blackknife/GrenadeAttack.java +++ b/src/main/java/me/sashegdev/blackknife/GrenadeAttack.java @@ -162,13 +162,13 @@ public class GrenadeAttack { 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)); + Particles.RED_12); } 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)); + Particles.GOLD_10); } } }.runTaskTimer(plugin, 1L, 1L); @@ -191,7 +191,7 @@ public class GrenadeAttack { 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)); + Particles.ORANGE_12); 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)) { diff --git a/src/main/java/me/sashegdev/blackknife/LungeAttack.java b/src/main/java/me/sashegdev/blackknife/LungeAttack.java index 8203c47..41a4fb2 100644 --- a/src/main/java/me/sashegdev/blackknife/LungeAttack.java +++ b/src/main/java/me/sashegdev/blackknife/LungeAttack.java @@ -111,7 +111,7 @@ public class LungeAttack { if (!sword.isValid()) { cancel(); return; } Location cur = sword.getLocation(); if (!launched) { - cur.getWorld().spawnParticle(Particle.DUST, cur.clone().add(0, 0.5, 0), 3, 0.15, 0.15, 0.15, 0, new Particle.DustOptions(Color.fromRGB(10, 30, 50), 1.5f)); + cur.getWorld().spawnParticle(Particle.DUST, cur.clone().add(0, 0.5, 0), 3, 0.15, 0.15, 0.15, 0, Particles.NAVY_15); cur.getWorld().spawnParticle(Particle.SCULK_SOUL, cur, 1, 0.1, 0.1, 0.1, 0); hold--; if (hold <= 0) launched = true; @@ -121,7 +121,7 @@ public class LungeAttack { int points = Math.max(1, (int) (aim.length() * 3)); for (int j = 0; j <= points; j++) { Location pp = cur.clone().add(aim.clone().multiply((double) j / points)); - pp.getWorld().spawnParticle(Particle.DUST, pp, 1, 0, 0, 0, 0, new Particle.DustOptions(Color.fromRGB(0, 0, 0), 2.0f)); + pp.getWorld().spawnParticle(Particle.DUST, pp, 1, 0, 0, 0, 0, Particles.BLACK_20); pp.getWorld().spawnParticle(Particle.SCULK_SOUL, pp, 1, 0.05, 0.05, 0.05, 0); } int steps = Math.max(1, (int) Math.ceil(aim.length() / 0.5)); @@ -192,7 +192,7 @@ public class LungeAttack { } Location nl = cur.clone().add(vel); sword.teleport(nl); - nl.getWorld().spawnParticle(Particle.DUST, nl, 2, 0.15, 0.15, 0.15, 0, new Particle.DustOptions(Color.fromRGB(0, 0, 0), 1.5f)); + nl.getWorld().spawnParticle(Particle.DUST, nl, 2, 0.15, 0.15, 0.15, 0, Particles.BLACK_15); nl.getWorld().spawnParticle(Particle.SCULK_SOUL, nl, 1, 0.05, 0.05, 0.05, 0); if (nl.getY() <= gazePoint.getY() + 0.5 || nl.getBlock().isSolid()) { Location expl = gazePoint.clone(); diff --git a/src/main/java/me/sashegdev/blackknife/MagicListener.java b/src/main/java/me/sashegdev/blackknife/MagicListener.java index 7671c78..39ac068 100644 --- a/src/main/java/me/sashegdev/blackknife/MagicListener.java +++ b/src/main/java/me/sashegdev/blackknife/MagicListener.java @@ -105,12 +105,12 @@ public class MagicListener implements Listener { Location check = cur.clone().add(vel.clone().multiply((double)s/steps)); if (check.getBlock().isSolid()) { Location expl = cur.clone().add(vel.clone().normalize().multiply(Math.max(0, (double)(s-1)/steps))); - for (int k=0;k<12;k++){ double a=(double)k/12*Math.PI*2; for(int h=-1;h<=1;h++){ Location sphere=expl.clone().add(Math.cos(a)*0.5, h*0.25, Math.sin(a)*0.5); sphere.getWorld().spawnParticle(Particle.DUST, sphere,1,0,0,0,0,new Particle.DustOptions(Color.fromRGB(180,40,40),1f)); sphere.getWorld().spawnParticle(Particle.SMOKE,sphere,1,0.02,0.02,0.02,0);} } expl.getWorld().spawnParticle(Particle.CRIT,expl,6,0.2,0.2,0.2,0.05); expl.getWorld().playSound(expl,Sound.BLOCK_STONE_BREAK,1f,0.8f); + for (int k=0;k<12;k++){ double a=(double)k/12*Math.PI*2; for(int h=-1;h<=1;h++){ Location sphere=expl.clone().add(Math.cos(a)*0.5, h*0.25, Math.sin(a)*0.5); sphere.getWorld().spawnParticle(Particle.DUST, sphere,1,0,0,0,0,Particles.CRIMSON_10); sphere.getWorld().spawnParticle(Particle.SMOKE,sphere,1,0.02,0.02,0.02,0);} } expl.getWorld().spawnParticle(Particle.CRIT,expl,6,0.2,0.2,0.2,0.05); expl.getWorld().playSound(expl,Sound.BLOCK_STONE_BREAK,1f,0.8f); EntityTracker.untrack(marker); marker.remove(); cancel(); return; } } - for(int i=0;i<=4;i++){ Location pp=cur.clone().add(vel.clone().multiply((double)i/4)); pp.getWorld().spawnParticle(Particle.DUST,pp,1,0,0,0,0,new Particle.DustOptions(Color.fromRGB(200,40,40),1.4f)); pp.getWorld().spawnParticle(Particle.CRIT,pp,1,0.02,0.02,0.02,0); } + for(int i=0;i<=4;i++){ Location pp=cur.clone().add(vel.clone().multiply((double)i/4)); pp.getWorld().spawnParticle(Particle.DUST,pp,1,0,0,0,0,Particles.CRIMSON_14); pp.getWorld().spawnParticle(Particle.CRIT,pp,1,0.02,0.02,0.02,0); } marker.teleport(nl); for(var ent: nl.getWorld().getNearbyEntities(nl,0.7,0.7,0.7, en->en instanceof LivingEntity && en!=p)) { LivingEntity le=(LivingEntity)ent; @@ -151,7 +151,7 @@ public class MagicListener implements Listener { marker.remove(); cancel(); return; } } - for(int i=0;i<=4;i++){ Location pp=cur.clone().add(vel.clone().multiply((double)i/4)); pp.getWorld().spawnParticle(Particle.DUST,pp,1,0,0,0,0,new Particle.DustOptions(Color.fromRGB(136,204,255),1.3f)); pp.getWorld().spawnParticle(Particle.SNOWFLAKE,pp,1,0.02,0.02,0.02,0); } + for(int i=0;i<=4;i++){ Location pp=cur.clone().add(vel.clone().multiply((double)i/4)); pp.getWorld().spawnParticle(Particle.DUST,pp,1,0,0,0,0,Particles.ICE_13); pp.getWorld().spawnParticle(Particle.SNOWFLAKE,pp,1,0.02,0.02,0.02,0); } marker.teleport(nl); for(var ent: nl.getWorld().getNearbyEntities(nl,0.7,0.7,0.7, en->en instanceof LivingEntity && en!=p)) { LivingEntity le=(LivingEntity)ent; diff --git a/src/main/java/me/sashegdev/blackknife/Particles.java b/src/main/java/me/sashegdev/blackknife/Particles.java index ff585b8..cd75ebe 100644 --- a/src/main/java/me/sashegdev/blackknife/Particles.java +++ b/src/main/java/me/sashegdev/blackknife/Particles.java @@ -67,4 +67,79 @@ public final class Particles { public static Particle.DustOptions dust(int r, int g, int b, float size) { return new Particle.DustOptions(Color.fromRGB(r, g, b), size); } + + // --------------------------------------------------------------------- + // Готовые DustOptions. + // + // DustOptions иммутабелен, поэтому один объект на цвет безопасно делить + // между всеми вызовами. Раньше в коде было 36 мест вида + // new Particle.DustOptions(Color.fromRGB(...)) + // и почти все — внутри циклов или внутри задач с периодом в несколько + // тиков. Каждый такой вызов это две аллокации (Color и сам DustOptions) на + // КАЖДУЮ частицу: на превью GRENADE это 40 частиц каждые 4 тика на игрока, + // то есть 20 аллокаций на тик только на одного целящегося. Именно такого + // типа мусор и даёт короткие GC-пики в p95. + // --------------------------------------------------------------------- + + /** Чёрный, размер 1.3 — превью комбо SWORD и одиночная метка. */ + public static final Particle.DustOptions BLACK_13 = dust(0, 0, 0, 1.3f); + /** Чёрный, размер 0.8 — верхняя метка превью SWORD. */ + public static final Particle.DustOptions BLACK_08 = dust(0, 0, 0, 0.8f); + /** Тёмно-красный, размер 1.3 — превью комбо на третьем ударе. */ + public static final Particle.DustOptions DARKRED_13 = dust(50, 0, 0, 1.3f); + /** Ярко-красный, размер 1.5 — метка прицеливания OVERHEAD. */ + public static final Particle.DustOptions RED_15 = dust(255, 40, 40, 1.5f); + /** Красный, размер 1.2 — внутреннее кольцо GRENADE и метка SNIPE. */ + public static final Particle.DustOptions RED_12 = dust(255, 40, 40, 1.2f); + /** Золотой, размер 1.0 — внешнее кольцо GRENADE. */ + public static final Particle.DustOptions GOLD_10 = dust(255, 200, 60, 1.0f); + /** Сине-голубой, размер 1.2 — кольцо DIVE. */ + public static final Particle.DustOptions BLUE_12 = dust(80, 160, 255, 1.2f); + /** Сине-голубой, размер 1.4 — след DIVE. */ + public static final Particle.DustOptions BLUE_14 = dust(80, 160, 255, 1.4f); + /** Сине-голубой, размер 1.0 — метка DIVE. */ + public static final Particle.DustOptions BLUE_10 = dust(80, 160, 255, 1.0f); + /** Фиолетовый, размер 1.0 — превью DIAMOND_TRI. */ + public static final Particle.DustOptions PURPLE_10 = dust(180, 0, 255, 1.0f); + /** Тёмно-синий, размер 1.5 — след SNIPE. */ + public static final Particle.DustOptions NAVY_15 = dust(10, 30, 50, 1.5f); + /** Чёрный, размер 2.0 — ядро снаряда SNIPE. */ + public static final Particle.DustOptions BLACK_20 = dust(0, 0, 0, 2.0f); + /** Чёрный, размер 1.5 — хвост снаряда SNIPE. */ + public static final Particle.DustOptions BLACK_15 = dust(0, 0, 0, 1.5f); + /** Тёмно-серый, размер 1.2 — подсветка точки удара OVERHEAD. */ + public static final Particle.DustOptions GREY_12 = dust(20, 20, 20, 1.2f); + /** Оранжевый, размер 1.2 — снаряд GRENADE. */ + public static final Particle.DustOptions ORANGE_12 = dust(255, 120, 30, 1.2f); + /** Оранжево-золотой, размер по размеру частицы — пыль GRENADE. */ + public static Particle.DustOptions grenadeDust(float size) { + return new Particle.DustOptions(Color.fromRGB(180, 120, 40), size); + } + + /** Белый, размер 0.15 — вспышка dodg-эффекта. Шесть мест, одно на цвет. */ + public static final Particle.DustOptions WHITE_015 = dust(255, 255, 255, 0.15f); + /** Пурпурный, размер 1.5 — кольцо TRI. */ + public static final Particle.DustOptions TRI_15 = dust(80, 20, 80, 1.5f); + /** Синий, размер 1.5 — кольцо SNIPE. */ + public static final Particle.DustOptions SNIPE_15 = dust(20, 100, 160, 1.5f); + /** Тёмно-синий, размер 1.5 — запасной кольцевой цвет прицеливания. */ + public static final Particle.DustOptions AIM_15 = dust(30, 80, 120, 1.5f); + /** Красный, размер 1.3 — метка TRI. */ + public static final Particle.DustOptions RED_13 = dust(255, 50, 50, 1.3f); + /** Красный, размер 1.0 — верхняя метка прицеливания. */ + public static final Particle.DustOptions RED_10 = dust(255, 50, 50, 1.0f); + /** Тёмно-красный, размер 1.2 — снаряд PhantomBarrage. */ + public static final Particle.DustOptions BARRAGE_12 = dust(30, 10, 60, 1.2f); + /** Пурпурный, размер 1.2 — след SWORD. */ + public static final Particle.DustOptions SWORD_12 = dust(120, 40, 160, 1.2f); + /** Бледно-голубой, размер 1.0 — превью BARRAGE. */ + public static final Particle.DustOptions PALE_10 = dust(200, 200, 255, 1.0f); + /** Алый, размер 1.4 — след снаряда MagicListener. */ + public static final Particle.DustOptions CRIMSON_14 = dust(200, 40, 40, 1.4f); + /** Алый, размер 1.0 — взрыв MagicListener. */ + public static final Particle.DustOptions CRIMSON_10 = dust(180, 40, 40, 1.0f); + /** Ледяной, размер 1.3 — след второго снаряда MagicListener. */ + public static final Particle.DustOptions ICE_13 = dust(136, 204, 255, 1.3f); + /** Тёмно-серый, размер 1.5 — сфера SWORD. */ + public static final Particle.DustOptions DARKGREY_15 = dust(30, 30, 30, 1.5f); } \ No newline at end of file diff --git a/src/main/java/me/sashegdev/blackknife/PhantomBarrageAttack.java b/src/main/java/me/sashegdev/blackknife/PhantomBarrageAttack.java index cf60969..4597f43 100644 --- a/src/main/java/me/sashegdev/blackknife/PhantomBarrageAttack.java +++ b/src/main/java/me/sashegdev/blackknife/PhantomBarrageAttack.java @@ -65,7 +65,7 @@ public class PhantomBarrageAttack { for(int s=0;s<=steps;s++){ Location check=cur.clone().add(toTarget.clone().multiply((double)s/steps)); if(check.getBlock().isSolid()){orb.remove();cancel();return;} } orb.teleport(nl); cur = nl; - nl.getWorld().spawnParticle(Particle.DUST, nl,1,0,0,0,0,new Particle.DustOptions(Color.fromRGB(30,10,50),1.2f)); + nl.getWorld().spawnParticle(Particle.DUST, nl,1,0,0,0,0,Particles.BARRAGE_12); nl.getWorld().spawnParticle(Particle.SCULK_SOUL, nl,1,0.05,0.05,0.05,0); EntityTracker.untrack(orb); for(LivingEntity le: nl.getWorld().getNearbyLivingEntities(nl,0.9)){ if(le==p) continue; DamageUtil.apply(p, le, 6.0); le.getWorld().spawnParticle(Particle.CRIT, le.getLocation().add(0,1,0),8,0.3,0.3,0.3,0.1); orb.remove(); cancel(); return; } diff --git a/src/main/java/me/sashegdev/blackknife/SkyDiveAttack.java b/src/main/java/me/sashegdev/blackknife/SkyDiveAttack.java index fb3adc5..1c889df 100644 --- a/src/main/java/me/sashegdev/blackknife/SkyDiveAttack.java +++ b/src/main/java/me/sashegdev/blackknife/SkyDiveAttack.java @@ -167,7 +167,7 @@ public class SkyDiveAttack implements Listener { final double a = (double) k / 10 * Math.PI * 2; final 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(org.bukkit.Color.fromRGB(80, 160, 255), 1.0f)); + Particles.BLUE_10); } if (--left <= 0) { cancel(); @@ -200,7 +200,7 @@ public class SkyDiveAttack implements Listener { final 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(org.bukkit.Color.fromRGB(80, 160, 255), 1.4f)); + Particles.BLUE_14); cur.getWorld().spawnParticle(Particle.CLOUD, cur, 1, 0.1, 0.1, 0.1, 0.02); // Касание земли отменяет урон: иначе можно было промахнуться, diff --git a/src/main/java/me/sashegdev/blackknife/StarFx.java b/src/main/java/me/sashegdev/blackknife/StarFx.java new file mode 100644 index 0000000..ecbee05 --- /dev/null +++ b/src/main/java/me/sashegdev/blackknife/StarFx.java @@ -0,0 +1,244 @@ +package me.sashegdev.blackknife; + +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.ArrayList; +import java.util.List; + +/** + * Визуал способности STARS: падающие звёзды и их осколки. + * + *
Раньше каждая сущность получала СВОЮ задачу с периодом в один тик: 18 звёзд + * на один вызов, и каждая при ударе о землю порождала ещё 6 осколков, у которых + * тоже была своя задача. Итого до 126 одновременных записей в планировщике и + * 126 обходов тика на одно нажатие, каждый со своим {@code getNearbyEntities} и + * {@code getNearbyPlayers}. + * + *
Здесь всё это живёт в одном списке под одной задачей. Стоимость на тик + * та же по сути — те же сущности двигаются, — но вместо 126 записей в + * планировщике одна, и обход линейный по одному массиву. + * + *
Задача снимается с себя сама, когда список пуст, то есть в простое + * планировщик не трогает вообще ничего. + * + *
Попутно исправлена ошибка: {@code EntityTracker.untrack()} вызывался на
+ * каждом тике, пока сущность ещё жива. После первого тика она исчезала из
+ * множества отслеживаемых, и {@link EntityTracker#cleanup} больше не мог её
+ * найти — визуал переживал перезагрузку плагина. Теперь untrack вызывается
+ * ровно один раз, в момент удаления.
+ */
+final class StarFx {
+
+ private StarFx() {
+ }
+
+ /** Одна движущаяся сущность. */
+ private static final class Node {
+ final BlockDisplay ent;
+ final Vector vel;
+ final Player owner;
+ final boolean shard;
+ int age;
+
+ Node(BlockDisplay ent, Vector vel, Player owner, boolean shard) {
+ this.ent = ent;
+ this.vel = vel;
+ this.owner = owner;
+ this.shard = shard;
+ }
+ }
+
+ private static final List Ключевой момент — кеш читается ПЕРВЫМ, а не после быстрого пути.
+ * Раньше {@link #isOwner} сначала звал {@code BlackKnifeItem.is()} на обеих
+ * руках и только потом смотрел кеш, то есть кеш не срабатывал никогда.
+ * {@code ItemStack#getItemMeta()} возвращает полную копию ItemMeta вместе с
+ * PDC — это аллокация и копирование NBT на каждый вызов. А {@link #tickShield}
+ * зовёт {@code isOwner} каждый тик на каждого игрока, то есть при 60 игроках
+ * это было 2400 копий ItemMeta в секунду. Теперь на попадание в кеш — ноль
+ * аллокаций, а пересчёт раз в OWNER_CACHE_TTL.
+ */
+ private final Map