From c68b1d267e6a498be072c570dce3ce865421ac47 Mon Sep 17 00:00:00 2001 From: SashegDev Date: Mon, 5 Oct 2026 17:31:08 +0000 Subject: [PATCH] feat(blackknife): aim indicators use long-range particles, fix broken SWOON MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Частицы прицелов уходили через spawnParticle, который ванильный сервер режет на 32 блоках. Механика при этом работает на 120: OVERHEAD бьёт по точке взгляда, DIVE туда же, SNIPE и TRI ищут цель в конусе до 120 блоков. То есть вне 32 блоков прицела не было видно, а удар приходил — выглядит как поломка. Теперь прицелы идут через Particles.farRing/farParticle, которые вызывают World#spawnParticleLongDistance. Дистанция считается от игрока до точки прицела и подрезается потолком 120. Боевые эффекты остались на обычных spawnParticle: удар мгновенный, рисовать его за 100 блоков незачем. Переведены четыре прицела: превью OVERHEAD, превью DIVE, кольцо TRI/SNIPE и метки на цели. В OVERHEAD заодно кольцо из 8 отдельных SCULK_SOUL свернуто в один дисковый вызов. Попутно исправлен SWOON. DamageSource собирался только с withCausingEntity, без withDirectEntity, и Bukkit такой вызов отвергал: IllegalArgumentException: Direct entity must be set if causing entity is set То есть урон не наносился вообще, а в лог падало исключение на каждый удар ножом. Причина со времён переписывания SWOON, не сегодняшняя. build.gradle: подключён локальный paper-api из сборки Delta, потому что нового метода нет в стоковом API. Файл идёт первым, оба jar содержат org/bukkit/World.class, и на classpath побеждает тот, что раньше. --- build.gradle | 7 +++ .../blackknife/BlackKnifeListener.java | 51 +++++++++---------- .../me/sashegdev/blackknife/Particles.java | 47 +++++++++++++++++ .../me/sashegdev/blackknife/SwoonHandler.java | 6 +++ 4 files changed, 83 insertions(+), 28 deletions(-) diff --git a/build.gradle b/build.gradle index 0dddc0c..ccaf8fc 100644 --- a/build.gradle +++ b/build.gradle @@ -18,6 +18,13 @@ repositories { } dependencies { + // Локальная сборка Delta нужна из-за World#spawnParticleLongDistance, + // которого нет в стоковом paper-api. Подключается файлом, а не репозиторием: + // в локальной папке сборки нет maven-metadata.xml, поэтому SNAPSHOT оттуда + // не резолвится. Файл идёт ПЕРВЫМ, потому что оба jar содержат + // org/bukkit/World.class, и на classpath побеждает тот, что раньше. + compileOnly files("/root/DeltaZern/paper-api/build/libs/paper-api-1.21.8-R0.1-SNAPSHOT.jar") + // Стоковая версия остаётся ради транзитивных зависимостей (adventure и т.д.). compileOnly "io.papermc.paper:paper-api:1.21.8-R0.1-SNAPSHOT" } diff --git a/src/main/java/me/sashegdev/blackknife/BlackKnifeListener.java b/src/main/java/me/sashegdev/blackknife/BlackKnifeListener.java index 5dcc938..80016ee 100644 --- a/src/main/java/me/sashegdev/blackknife/BlackKnifeListener.java +++ b/src/main/java/me/sashegdev/blackknife/BlackKnifeListener.java @@ -100,6 +100,11 @@ public class BlackKnifeListener implements Listener { /** Порог косинуса конуса наведения: угол примерно 5.7 градуса. */ private static final double TARGET_DOT = 0.995D; + /** Радиус кольца прицела: у SNIPE уже, у TRI шире. */ + private static double r1For(String mode) { + return mode.equals("SNIPE") ? 0.6D : 0.8D; + } + /** * Поиск цели для SNIPE и TRI. * @@ -356,24 +361,20 @@ public class BlackKnifeListener implements Listener { // прицеливания, каждые 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); - p.spawnParticle(Particle.DUST, ring, 1, 0, 0, 0, 0, ringDust); - } + // SNIPE и TRI ищут цель в конусе до 120 блоков, а ванильные + // частицы режутся на 32: кольцо вокруг цели исчезало, и + // способность выглядела «никак». Кольцо теперь дальнее. + Particles.farRing(p.getWorld(), p, tl, r1For(m), ringDust, + m.equals("TRI") ? 16 : 12); 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, Particles.RED_13); + Location dot = tl.clone().add(Math.cos(a) * r1For(m), 0, Math.sin(a) * r1For(m)); + Particles.farParticle(p.getWorld(), p, dot, Particle.DUST, 1, 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, Particles.RED_10); + Particles.farParticle(p.getWorld(), p, tl.clone().add(0, 2, 0), Particle.DUST, 1, 0, 0, 0, Particles.RED_10); } }; r.runTaskTimer(plugin, 1L, 5L); @@ -385,17 +386,13 @@ public class BlackKnifeListener implements Listener { if (p.hasCooldown(Material.NETHERITE_SWORD)) return; if (!BlackKnifeItem.inHands(p)) return; Location gp = getGazePoint(p); - // 32 отдельных пакета на тик прицеливания -> 2 пакета. - // Кольцо radius 1.8 раскладывается диском, на масштабе - // полутора блоков глазом неотличимо от точек на окружности. - Particles.groundRing(p.getWorld(), gp.clone().add(0, 0.1, 0), 1.8, - Particles.dust(20, 20, 20, 1.2f), 16); - p.spawnParticle(Particle.ASH, gp, 16, 1.8, 0.05, 1.8, 0.0D); - for (int i = 0; i < 8; i++) { - double a = (double) i / 8 * Math.PI * 2; - 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); - } + // Прицел идёт через farRing: OVERHEAD бьёт по точке взгляда, + // а она бывает на 120 блоках, тогда как ванильные частицы + // режутся на 32. Дальше 32 блоков прицела не было видно вообще. + Particles.farRing(p.getWorld(), p, gp.clone().add(0, 0.1, 0), 1.8, + Particles.GREY_12, 16); + Particles.farParticle(p.getWorld(), p, gp, Particle.ASH, 16, 1.8, 0.05, 1.8, null); + p.spawnParticle(Particle.SCULK_SOUL, gp.clone().add(0, 0.1, 0), 8, 1, 0.02, 1, 0); p.spawnParticle(Particle.DUST, gp.clone().add(0, 0.1, 0), 1, 0, 0, 0, 0, Particles.RED_15); } }; @@ -429,11 +426,9 @@ public class BlackKnifeListener implements Listener { if (p.hasCooldown(Material.NETHERITE_SWORD)) 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, Particles.BLUE_12); - } + // DIVE целится по точке взгляда до 120 блоков — кольцо должно + // быть дальним, иначе после 32 блоков прицела не видно. + Particles.farRing(p.getWorld(), p, gp, 1.2D, Particles.BLUE_12, 16); p.spawnParticle(Particle.CLOUD, gp.clone().add(0, 0.2, 0), 1, 0.1, 0.1, 0.1, 0); } }; diff --git a/src/main/java/me/sashegdev/blackknife/Particles.java b/src/main/java/me/sashegdev/blackknife/Particles.java index e11dea1..5b6d876 100644 --- a/src/main/java/me/sashegdev/blackknife/Particles.java +++ b/src/main/java/me/sashegdev/blackknife/Particles.java @@ -4,6 +4,7 @@ import org.bukkit.Color; import org.bukkit.Location; import org.bukkit.Particle; import org.bukkit.World; +import org.bukkit.entity.Player; /** * Батчинг частиц. @@ -144,4 +145,50 @@ public final class Particles { public static final Particle.DustOptions DARKGREY_15 = dust(30, 30, 30, 1.5f); /** Тёмно-серый, размер 0.9 — сфера взрыва SWORD, 36 частиц за вызов. */ public static final Particle.DustOptions DARKGREY_09 = dust(30, 30, 30, 0.9f); + + // --------------------------------------------------------------------- + // Дальние частицы — прицелы. + // + // Механика способностей бьёт на 120 блоков, а ванильный spawnParticle режет + // на 32: прицел исчезает, а удар приходит. С 32 блоков игрок вообще не + // понимает, куда целится. + // + // World#spawnParticleLongDistance убирает обе отсечки — серверную (список + // получателей фильтруется на заданной дистанции) и клиентскую (alwaysShow + // едет в пакете). Прицелы ходят через неё, боевые эффекты остаются на + // обычных spawnParticle: удар мгновенный, рисовать его за 100 блоков не + // нужно, и лишний трафик на каждый тик не нужен. + // + // Дистанция считается от игрока до точки прицела и подрезается потолком из + // blackknife: дальше 120 блоков прицелы всё равно не рисуются, потому что + // сам взгляд дальше не достаёт. + // --------------------------------------------------------------------- + + /** Потолок: взглядом дальше не достать, и клиент всё равно режет. */ + public static final double MAX_AIM_DISTANCE = 120.0D; + + /** Дистанция от игрока до точки прицела, ограниченная MAX_AIM_DISTANCE. */ + public static double aimDistance(Player from, Location to) { + final double d = from.getLocation().distance(to); + return Math.max(1.0D, Math.min(MAX_AIM_DISTANCE, d)); + } + + /** Диск на земле, видимый издалека. */ + public static void farRing(World world, Player viewer, Location center, double radius, + Particle.DustOptions dust, int count) { + world.spawnParticleLongDistance(Particle.DUST, + center.getX(), center.getY(), center.getZ(), + count, radius, 0.0D, radius, 0.0D, dust, + aimDistance(viewer, center)); + } + + /** Частица с заданным разбросом, видимая издалека. */ + public static void farParticle(World world, Player viewer, Location at, Particle particle, + int count, double spreadX, double spreadY, double spreadZ, + T data) { + world.spawnParticleLongDistance(particle, + at.getX(), at.getY(), at.getZ(), + count, spreadX, spreadY, spreadZ, 0.0D, data, + aimDistance(viewer, at)); + } } \ No newline at end of file diff --git a/src/main/java/me/sashegdev/blackknife/SwoonHandler.java b/src/main/java/me/sashegdev/blackknife/SwoonHandler.java index a232cb0..7d69998 100644 --- a/src/main/java/me/sashegdev/blackknife/SwoonHandler.java +++ b/src/main/java/me/sashegdev/blackknife/SwoonHandler.java @@ -113,8 +113,14 @@ public class SwoonHandler implements Listener { tension.setTP(p, tension.getTP(p) - cost); final var cfg = plugin.getSwoonConfig(); + // withCausingEntity без withDirectEntity Bukkit не принимает: + // CraftDamageSourceBuilder.build() бросает + // "Direct entity must be set if causing entity is set". + // То есть урон просто не наносился, а в лог падало исключение на КАЖДЫЙ + // удар ножом — 15 раз за один короткий прогон. final DamageSource source = DamageSource.builder(DamageType.GENERIC_KILL) .withCausingEntity(p) + .withDirectEntity(p) .build(); target.getPersistentDataContainer().set(MAGIC_DAMAGE, PersistentDataType.BYTE, (byte) 1);