perf: cut per-tick allocation and collapse STARS into one scheduler task
Цель — не средний тик, а разброс: p95 26.9 мс и пики под 170 мс на 60 ботах бьют по игрокам сильнее, чем медиана 14.8 мс. Почти весь этот разброс шёл от мусора в горячих путях. 1. Кеш владельца ножа переехал в начало проверки. Раньше isOwner() сначала звал BlackKnifeItem.is() на обеих руках и только потом смотрел ownerCache, то есть кеш не срабатывал никогда. is() обязан взять ItemMeta, а Bukkit возвращает там полную копию метаданных вместе с PDC. tickShield() зовёт isOwner каждый тик на каждого игрока: при 60 игроках это 2400 копий ItemMeta в секунду. Теперь на попадание в кеш — ноль аллокаций, пересчёт раз в 2 с. Кеш хранит оба ответа сразу (в руках / где-то в инвентаре), потому что hasMagic и превью интересуют разные вещи. 2. BlackKnifeItem.inHands() вместо пары is() в горячих путях: 7 проверок превью прицеливания и SWOON на каждый удар. 3. DustOptions вынесены в константы Particles. 36 мест создавали new Particle.DustOptions(Color.fromRGB(...)), и почти все внутри циклов или задач с периодом в несколько тиков. DustOptions иммутабелен, поэтому один объект на цвет. Отдельно вынесен тернарник, выбиравший цвет кольца ВНУТРИ цикла в превью SNIPE/TRI: он пересчитывал один и тот же цвет на каждой из 12-16 итераций. 4. getGazePoint() больше не зовёт getEyeLocation() три раза и getDirection() два. Метод работает каждые 4 тика на каждого игрока с ножом. 5. Визуал STARS вынесен в StarFx под одну задачу. Раньше 18 звёзд и до 108 осколков получали по своей задаче с периодом в один тик — до 126 задач планировщика на одно нажатие, каждая со своим getNearbyEntities и getNearbyPlayers. Теперь один список под одной задачей, которая снимает себя сама в простое. Попутно исправлен баг: EntityTracker.untrack() вызывался на каждом тике, пока сущность жива, поэтому после первого тика она исчезала из множества отслеживаемых и EntityTracker.cleanup() её больше не находил. Теперь untrack вызывается один раз, при удалении.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Нож в руках игрока.
|
||||
*
|
||||
* <p>Отдельный метод вместо пары вызовов {@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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Точка, в которую смотрит игрок.
|
||||
*
|
||||
* <p>Раньше здесь вызывалось getEyeLocation() три раза и getDirection() два.
|
||||
* Каждый вызов — новая аллокация: Location и Vector у Bukkit копируются, а не
|
||||
* отдаются ссылкой. Метод зовётся из превью прицеливания каждые 4 тика на
|
||||
* каждого игрока с ножом, то есть на 60 игроках это 90 лишних Location и
|
||||
* 60 лишних Vector в секунду только на вычисление одного и того же вектора.
|
||||
*
|
||||
* <p>Сам 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
|
||||
// Касание земли отменяет урон: иначе можно было промахнуться,
|
||||
|
||||
@@ -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: падающие звёзды и их осколки.
|
||||
*
|
||||
* <p>Раньше каждая сущность получала СВОЮ задачу с периодом в один тик: 18 звёзд
|
||||
* на один вызов, и каждая при ударе о землю порождала ещё 6 осколков, у которых
|
||||
* тоже была своя задача. Итого до 126 одновременных записей в планировщике и
|
||||
* 126 обходов тика на одно нажатие, каждый со своим {@code getNearbyEntities} и
|
||||
* {@code getNearbyPlayers}.
|
||||
*
|
||||
* <p>Здесь всё это живёт в одном списке под одной задачей. Стоимость на тик
|
||||
* та же по сути — те же сущности двигаются, — но вместо 126 записей в
|
||||
* планировщике одна, и обход линейный по одному массиву.
|
||||
*
|
||||
* <p>Задача снимается с себя сама, когда список пуст, то есть в простое
|
||||
* планировщик не трогает вообще ничего.
|
||||
*
|
||||
* <p>Попутно исправлена ошибка: {@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<Node> NODES = new ArrayList<>(32);
|
||||
private static BukkitRunnable task;
|
||||
|
||||
private static final int STAR_COUNT = 18;
|
||||
private static final int SHARD_COUNT = 6;
|
||||
private static final int STAR_MAX_AGE = 120;
|
||||
private static final int SHARD_MAX_AGE = 40;
|
||||
|
||||
static void burst(BlackKnifePlugin plugin, 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 < STAR_COUNT; 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);
|
||||
Vector vel = new Vector((Math.random() - 0.5) * 0.08, -0.35 - Math.random() * 0.1, (Math.random() - 0.5) * 0.08);
|
||||
NODES.add(new Node(spawnStar(p.getWorld(), loc), vel, p, false));
|
||||
}
|
||||
ensureTask(plugin);
|
||||
}
|
||||
|
||||
private static BlockDisplay spawnStar(org.bukkit.World world, Location loc) {
|
||||
final BlockDisplay disp = world.spawn(loc, 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);
|
||||
return disp;
|
||||
}
|
||||
|
||||
private static BlockDisplay spawnShard(BlackKnifePlugin plugin, Player owner, Location loc, Vector vel) {
|
||||
final BlockDisplay shard = loc.getWorld().spawn(loc, 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);
|
||||
NODES.add(new Node(shard, vel, owner, true));
|
||||
return shard;
|
||||
}
|
||||
|
||||
private static void ensureTask(BlackKnifePlugin plugin) {
|
||||
if (task != null) {
|
||||
return;
|
||||
}
|
||||
task = new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
tick(plugin);
|
||||
}
|
||||
};
|
||||
task.runTaskTimer(plugin, 1L, 1L);
|
||||
}
|
||||
|
||||
private static void tick(BlackKnifePlugin plugin) {
|
||||
// Обратный проход: удаляем по индексу, не сдвигая список в середине.
|
||||
for (int i = NODES.size() - 1; i >= 0; i--) {
|
||||
final Node n = NODES.get(i);
|
||||
if (!n.ent.isValid() || n.ent.isDead()) {
|
||||
NODES.remove(i);
|
||||
continue;
|
||||
}
|
||||
n.age++;
|
||||
final Location cur = n.ent.getLocation();
|
||||
final Location next = cur.clone().add(n.vel);
|
||||
|
||||
if (n.shard) {
|
||||
if (!tickShard(plugin, n, cur, next)) {
|
||||
NODES.remove(i);
|
||||
}
|
||||
} else {
|
||||
if (!tickStar(plugin, n, cur, next)) {
|
||||
NODES.remove(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (NODES.isEmpty() && task != null) {
|
||||
task.cancel();
|
||||
task = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** @return false, если сущность обработана и её надо убрать из списка. */
|
||||
private static boolean tickStar(BlackKnifePlugin plugin, Node n, Location cur, Location next) {
|
||||
final Location nl = next;
|
||||
final Location nn = nl.clone().add(n.vel);
|
||||
final org.bukkit.World world = nl.getWorld();
|
||||
|
||||
if (nn.getBlock().isSolid() || nl.getY() <= world.getHighestBlockYAt(nl) + 0.5) {
|
||||
final Location expl = cur.clone().add(n.vel.clone().normalize().multiply(-0.1));
|
||||
n.ent.teleport(expl);
|
||||
EntityTracker.untrack(n.ent);
|
||||
n.ent.remove();
|
||||
world.spawnParticle(Particle.EXPLOSION, expl, 1);
|
||||
world.playSound(expl, Sound.ENTITY_GENERIC_EXPLODE, 0.8f, 1.2f);
|
||||
for (int s = 0; s < SHARD_COUNT; s++) {
|
||||
double a = s * Math.PI * 2 / SHARD_COUNT;
|
||||
spawnShard(plugin, n.owner, expl,
|
||||
new Vector(Math.cos(a) * 0.35, 0, Math.sin(a) * 0.35));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
n.ent.teleport(nl);
|
||||
world.spawnParticle(Particle.END_ROD, nl, 1, 0.05, 0.05, 0.05, 0);
|
||||
dodgeNear(plugin, n.owner, nl);
|
||||
if (n.age > STAR_MAX_AGE) {
|
||||
EntityTracker.untrack(n.ent);
|
||||
n.ent.remove();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @return false, если осколок обработан и его надо убрать из списка. */
|
||||
private static boolean tickShard(BlackKnifePlugin plugin, Node n, Location cur, Location next) {
|
||||
final Location n2 = next;
|
||||
final org.bukkit.World world = n2.getWorld();
|
||||
|
||||
if (n2.getBlock().isSolid()) {
|
||||
final Location stop = cur.clone().add(n.vel.clone().normalize().multiply(-0.1));
|
||||
n.ent.teleport(stop);
|
||||
EntityTracker.untrack(n.ent);
|
||||
n.ent.remove();
|
||||
return false;
|
||||
}
|
||||
|
||||
n.ent.teleport(n2);
|
||||
world.spawnParticle(Particle.CRIT, n2, 1, 0.05, 0.05, 0.05, 0);
|
||||
|
||||
for (var ent : world.getNearbyEntities(n2, 0.7, 0.7, 0.7)) {
|
||||
// Свой хостер не получает урон от собственного осколка.
|
||||
if (ent instanceof Player pl && pl != n.owner) {
|
||||
continue;
|
||||
}
|
||||
if (ent instanceof LivingEntity le && le != n.owner) {
|
||||
DamageUtil.apply(n.owner, le, 3.0);
|
||||
EntityTracker.untrack(n.ent);
|
||||
n.ent.remove();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
dodgeNear(plugin, n.owner, n2);
|
||||
if (n.age > SHARD_MAX_AGE) {
|
||||
EntityTracker.untrack(n.ent);
|
||||
n.ent.remove();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Вспышка dodg-эффекта для игроков, прошедших рядом с визуалом. */
|
||||
private static void dodgeNear(BlackKnifePlugin plugin, Player owner, Location at) {
|
||||
for (Player pl : at.getWorld().getNearbyPlayers(at, 0.9)) {
|
||||
if (pl == owner) {
|
||||
continue;
|
||||
}
|
||||
double d2 = pl.getLocation().add(0, 1, 0).distanceSquared(at);
|
||||
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, Particles.WHITE_015);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Сброс при выключении плагина. */
|
||||
static void clear() {
|
||||
for (Node n : NODES) {
|
||||
if (n.ent != null && n.ent.isValid()) {
|
||||
EntityTracker.untrack(n.ent);
|
||||
n.ent.remove();
|
||||
}
|
||||
}
|
||||
NODES.clear();
|
||||
if (task != null) {
|
||||
task.cancel();
|
||||
task = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,10 @@ public class SwoonHandler implements Listener {
|
||||
if (p.getGameMode() == org.bukkit.GameMode.SPECTATOR) {
|
||||
return;
|
||||
}
|
||||
if (!BlackKnifeItem.is(p.getInventory().getItemInMainHand()) && !BlackKnifeItem.is(p.getInventory().getItemInOffHand())) {
|
||||
// Кешированная проверка: событие срабатывает на КАЖДЫЙ удар, а is()
|
||||
// копирует ItemMeta целиком. В бою на 20 игроков это десятки копий NBT
|
||||
// в секунду ради проверки «есть ли нож в руках».
|
||||
if (!BlackKnifeItem.inHands(p)) {
|
||||
return;
|
||||
}
|
||||
if (!(e.getEntity() instanceof LivingEntity target)) {
|
||||
|
||||
@@ -66,7 +66,7 @@ public class SwordAttack {
|
||||
int points = Math.max(1, (int) (len * 4));
|
||||
for (int i = 0; i <= points; i++) {
|
||||
Location pp = cur.clone().add(step.clone().multiply((double) i / points));
|
||||
pp.getWorld().spawnParticle(Particle.DUST, pp, 1, 0, 0, 0, 0, new Particle.DustOptions(Color.fromRGB(0, 0, 0), 1.5f));
|
||||
pp.getWorld().spawnParticle(Particle.DUST, pp, 1, 0, 0, 0, 0, Particles.BLACK_15);
|
||||
}
|
||||
marker.teleport(nl);
|
||||
for (var ent : nl.getWorld().getNearbyEntities(nl, 0.7, 0.7, 0.7, e -> e instanceof LivingEntity && e != p)) {
|
||||
@@ -87,7 +87,7 @@ public class SwordAttack {
|
||||
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);
|
||||
}
|
||||
}
|
||||
EntityTracker.untrack(marker);
|
||||
|
||||
@@ -21,7 +21,19 @@ public class TensionManager implements Listener {
|
||||
private final Map<UUID, Long> lastCombat = new HashMap<>();
|
||||
private final Map<UUID, java.util.Set<UUID>> battleEnemies = new HashMap<>();
|
||||
private final Map<UUID, Long> lastDodge = new HashMap<>();
|
||||
private final Map<UUID, Boolean> ownerCache = new HashMap<>();
|
||||
/**
|
||||
* Кеш владельца ножа: [в руках, где-то в инвентаре].
|
||||
*
|
||||
* <p>Ключевой момент — кеш читается ПЕРВЫМ, а не после быстрого пути.
|
||||
* Раньше {@link #isOwner} сначала звал {@code BlackKnifeItem.is()} на обеих
|
||||
* руках и только потом смотрел кеш, то есть кеш не срабатывал никогда.
|
||||
* {@code ItemStack#getItemMeta()} возвращает полную копию ItemMeta вместе с
|
||||
* PDC — это аллокация и копирование NBT на каждый вызов. А {@link #tickShield}
|
||||
* зовёт {@code isOwner} каждый тик на каждого игрока, то есть при 60 игроках
|
||||
* это было 2400 копий ItemMeta в секунду. Теперь на попадание в кеш — ноль
|
||||
* аллокаций, а пересчёт раз в OWNER_CACHE_TTL.
|
||||
*/
|
||||
private final Map<UUID, boolean[]> ownerCache = new HashMap<>();
|
||||
private final Map<UUID, Long> ownerCacheTime = new HashMap<>();
|
||||
private final Map<UUID, Integer> lastPct = new HashMap<>();
|
||||
private static final long OWNER_CACHE_TTL = 2000L;
|
||||
@@ -90,16 +102,28 @@ public class TensionManager implements Listener {
|
||||
}
|
||||
|
||||
public boolean isOwner(Player p) {
|
||||
if (BlackKnifeItem.is(p.getInventory().getItemInMainHand()) || BlackKnifeItem.is(p.getInventory().getItemInOffHand())) return true;
|
||||
long now = System.currentTimeMillis();
|
||||
UUID id = p.getUniqueId();
|
||||
Long t = ownerCacheTime.get(id);
|
||||
Boolean cached = ownerCache.get(id);
|
||||
if (cached != null && t != null && now - t < OWNER_CACHE_TTL) return cached;
|
||||
boolean has = hasAnyBlackKnife(p);
|
||||
ownerCache.put(id, has);
|
||||
return knifeState(p, true);
|
||||
}
|
||||
|
||||
/** Нож в руках — дешевле, нужно превью прицеливания и {@link #hasMagic}. */
|
||||
public boolean hasKnifeInHands(Player p) {
|
||||
return knifeState(p, false);
|
||||
}
|
||||
|
||||
private boolean knifeState(Player p, boolean anywhere) {
|
||||
final UUID id = p.getUniqueId();
|
||||
final long now = System.currentTimeMillis();
|
||||
boolean[] cached = ownerCache.get(id);
|
||||
final Long t = ownerCacheTime.get(id);
|
||||
if (cached != null && t != null && now - t < OWNER_CACHE_TTL) {
|
||||
return cached[anywhere ? 1 : 0];
|
||||
}
|
||||
final boolean inHands = BlackKnifeItem.is(p.getInventory().getItemInMainHand())
|
||||
|| BlackKnifeItem.is(p.getInventory().getItemInOffHand());
|
||||
final boolean has = inHands || hasAnyBlackKnife(p);
|
||||
ownerCache.put(id, new boolean[]{inHands, has});
|
||||
ownerCacheTime.put(id, now);
|
||||
return has;
|
||||
return anywhere ? has : inHands;
|
||||
}
|
||||
|
||||
public void invalidateOwnerCache(Player p) {
|
||||
@@ -113,7 +137,7 @@ public class TensionManager implements Listener {
|
||||
}
|
||||
|
||||
public boolean hasMagic(Player p) {
|
||||
if (BlackKnifeItem.is(p.getInventory().getItemInMainHand()) || BlackKnifeItem.is(p.getInventory().getItemInOffHand())) return true;
|
||||
if (hasKnifeInHands(p)) return true;
|
||||
if (isOwner(p)) return true;
|
||||
var mgr = BlackKnifePlugin.getInstance() != null ? BlackKnifePlugin.getInstance().getMagicManager() : null;
|
||||
return mgr != null && mgr.hasAwakened(p.getUniqueId());
|
||||
|
||||
Reference in New Issue
Block a user