feat(blackknife): aim indicators use long-range particles, fix broken SWOON
Частицы прицелов уходили через 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 побеждает тот, что раньше.
This commit is contained in:
@@ -18,6 +18,13 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
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"
|
compileOnly "io.papermc.paper:paper-api:1.21.8-R0.1-SNAPSHOT"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,11 @@ public class BlackKnifeListener implements Listener {
|
|||||||
/** Порог косинуса конуса наведения: угол примерно 5.7 градуса. */
|
/** Порог косинуса конуса наведения: угол примерно 5.7 градуса. */
|
||||||
private static final double TARGET_DOT = 0.995D;
|
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.
|
* Поиск цели для SNIPE и TRI.
|
||||||
*
|
*
|
||||||
@@ -356,24 +361,20 @@ public class BlackKnifeListener implements Listener {
|
|||||||
// прицеливания, каждые 5 тиков, на каждого игрока.
|
// прицеливания, каждые 5 тиков, на каждого игрока.
|
||||||
final Particle.DustOptions ringDust = m.equals("TRI") ? Particles.TRI_15
|
final Particle.DustOptions ringDust = m.equals("TRI") ? Particles.TRI_15
|
||||||
: m.equals("SNIPE") ? Particles.SNIPE_15 : Particles.AIM_15;
|
: m.equals("SNIPE") ? Particles.SNIPE_15 : Particles.AIM_15;
|
||||||
final int ringCount = m.equals("TRI") ? 16 : 12;
|
// SNIPE и TRI ищут цель в конусе до 120 блоков, а ванильные
|
||||||
final double r1 = m.equals("SNIPE") ? 0.6 : 0.8;
|
// частицы режутся на 32: кольцо вокруг цели исчезало, и
|
||||||
for (int i = 0; i < ringCount; i++) {
|
// способность выглядела «никак». Кольцо теперь дальнее.
|
||||||
double angle = (double) i / ringCount * Math.PI * 2;
|
Particles.farRing(p.getWorld(), p, tl, r1For(m), ringDust,
|
||||||
double x = Math.cos(angle) * r1;
|
m.equals("TRI") ? 16 : 12);
|
||||||
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);
|
|
||||||
}
|
|
||||||
if (m.equals("TRI")) {
|
if (m.equals("TRI")) {
|
||||||
for (int k = 0; k < 3; k++) {
|
for (int k = 0; k < 3; k++) {
|
||||||
double a = k * 120 * Math.PI / 180;
|
double a = k * 120 * Math.PI / 180;
|
||||||
Location dot = tl.clone().add(Math.cos(a) * r1, 0, Math.sin(a) * r1);
|
Location dot = tl.clone().add(Math.cos(a) * r1For(m), 0, Math.sin(a) * r1For(m));
|
||||||
p.spawnParticle(Particle.DUST, dot, 1, 0, 0, 0, 0, Particles.RED_13);
|
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.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);
|
r.runTaskTimer(plugin, 1L, 5L);
|
||||||
@@ -385,17 +386,13 @@ public class BlackKnifeListener implements Listener {
|
|||||||
if (p.hasCooldown(Material.NETHERITE_SWORD)) return;
|
if (p.hasCooldown(Material.NETHERITE_SWORD)) return;
|
||||||
if (!BlackKnifeItem.inHands(p)) return;
|
if (!BlackKnifeItem.inHands(p)) return;
|
||||||
Location gp = getGazePoint(p);
|
Location gp = getGazePoint(p);
|
||||||
// 32 отдельных пакета на тик прицеливания -> 2 пакета.
|
// Прицел идёт через farRing: OVERHEAD бьёт по точке взгляда,
|
||||||
// Кольцо radius 1.8 раскладывается диском, на масштабе
|
// а она бывает на 120 блоках, тогда как ванильные частицы
|
||||||
// полутора блоков глазом неотличимо от точек на окружности.
|
// режутся на 32. Дальше 32 блоков прицела не было видно вообще.
|
||||||
Particles.groundRing(p.getWorld(), gp.clone().add(0, 0.1, 0), 1.8,
|
Particles.farRing(p.getWorld(), p, gp.clone().add(0, 0.1, 0), 1.8,
|
||||||
Particles.dust(20, 20, 20, 1.2f), 16);
|
Particles.GREY_12, 16);
|
||||||
p.spawnParticle(Particle.ASH, gp, 16, 1.8, 0.05, 1.8, 0.0D);
|
Particles.farParticle(p.getWorld(), p, gp, Particle.ASH, 16, 1.8, 0.05, 1.8, null);
|
||||||
for (int i = 0; i < 8; i++) {
|
p.spawnParticle(Particle.SCULK_SOUL, gp.clone().add(0, 0.1, 0), 8, 1, 0.02, 1, 0);
|
||||||
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);
|
|
||||||
}
|
|
||||||
p.spawnParticle(Particle.DUST, gp.clone().add(0, 0.1, 0), 1, 0, 0, 0, 0, Particles.RED_15);
|
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 (p.hasCooldown(Material.NETHERITE_SWORD)) return;
|
||||||
if (!BlackKnifeItem.inHands(p)) return;
|
if (!BlackKnifeItem.inHands(p)) return;
|
||||||
Location gp = getGazePoint(p);
|
Location gp = getGazePoint(p);
|
||||||
for (int i = 0; i < 16; i++) {
|
// DIVE целится по точке взгляда до 120 блоков — кольцо должно
|
||||||
double a = (double) i / 16 * Math.PI * 2;
|
// быть дальним, иначе после 32 блоков прицела не видно.
|
||||||
Location ring = gp.clone().add(Math.cos(a) * 1.2, 0.1, Math.sin(a) * 1.2);
|
Particles.farRing(p.getWorld(), p, gp, 1.2D, Particles.BLUE_12, 16);
|
||||||
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);
|
p.spawnParticle(Particle.CLOUD, gp.clone().add(0, 0.2, 0), 1, 0.1, 0.1, 0.1, 0);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import org.bukkit.Color;
|
|||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.Particle;
|
import org.bukkit.Particle;
|
||||||
import org.bukkit.World;
|
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);
|
public static final Particle.DustOptions DARKGREY_15 = dust(30, 30, 30, 1.5f);
|
||||||
/** Тёмно-серый, размер 0.9 — сфера взрыва SWORD, 36 частиц за вызов. */
|
/** Тёмно-серый, размер 0.9 — сфера взрыва SWORD, 36 частиц за вызов. */
|
||||||
public static final Particle.DustOptions DARKGREY_09 = dust(30, 30, 30, 0.9f);
|
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 <T> 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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -113,8 +113,14 @@ public class SwoonHandler implements Listener {
|
|||||||
tension.setTP(p, tension.getTP(p) - cost);
|
tension.setTP(p, tension.getTP(p) - cost);
|
||||||
|
|
||||||
final var cfg = plugin.getSwoonConfig();
|
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)
|
final DamageSource source = DamageSource.builder(DamageType.GENERIC_KILL)
|
||||||
.withCausingEntity(p)
|
.withCausingEntity(p)
|
||||||
|
.withDirectEntity(p)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
target.getPersistentDataContainer().set(MAGIC_DAMAGE, PersistentDataType.BYTE, (byte) 1);
|
target.getPersistentDataContainer().set(MAGIC_DAMAGE, PersistentDataType.BYTE, (byte) 1);
|
||||||
|
|||||||
Reference in New Issue
Block a user