cf1ee08bb8
Цель — не средний тик, а разброс: 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 вызывается один раз, при удалении.
218 lines
10 KiB
Java
218 lines
10 KiB
Java
package me.sashegdev.blackknife;
|
|
|
|
import net.kyori.adventure.text.Component;
|
|
import net.kyori.adventure.text.format.NamedTextColor;
|
|
import org.bukkit.Color;
|
|
import org.bukkit.Location;
|
|
import org.bukkit.Material;
|
|
import org.bukkit.Particle;
|
|
import org.bukkit.Sound;
|
|
import org.bukkit.entity.BlockDisplay;
|
|
import org.bukkit.entity.Display;
|
|
import org.bukkit.entity.LivingEntity;
|
|
import org.bukkit.entity.Player;
|
|
import org.bukkit.scheduler.BukkitRunnable;
|
|
import org.bukkit.util.Vector;
|
|
|
|
import java.util.ArrayDeque;
|
|
import java.util.Deque;
|
|
|
|
public class GrenadeAttack {
|
|
private static final float COST = 25f;
|
|
private static final double LETHAL_RADIUS = 1.5;
|
|
private static final double CHIP_RADIUS = 3.0;
|
|
private static final double LETHAL_DAMAGE = 35.0;
|
|
private static final double CHIP_DAMAGE = 5.5;
|
|
private static final double MAX_RANGE = 15.0;
|
|
private static final double TICKS_PER_BLOCK = 2.0;
|
|
private static final int MIN_DURATION = 8;
|
|
private static final double ARC_MIN = 1.5;
|
|
private static final double ARC_PER_BLOCK = 0.25;
|
|
private static final double ARC_MAX = 4.0;
|
|
|
|
public static boolean tryUse(Player p, TensionManager tension) {
|
|
if (tension.getTP(p) < COST) {
|
|
p.sendActionBar(Component.text("Need 10% TP").color(NamedTextColor.RED));
|
|
return false;
|
|
}
|
|
tension.setTP(p, tension.getTP(p) - COST);
|
|
Location eye = p.getEyeLocation();
|
|
Vector dir = eye.getDirection().normalize();
|
|
Location target = computeTarget(p, eye, dir);
|
|
double dist = eye.distance(target);
|
|
int duration = Math.max(MIN_DURATION, (int) (dist * TICKS_PER_BLOCK));
|
|
double arcH = Math.max(ARC_MIN, Math.min(ARC_MAX, dist * ARC_PER_BLOCK));
|
|
Location spawn = eye.clone().add(dir.clone().multiply(1)).add(0, 0.2, 0);
|
|
Vector throwDir = target.clone().subtract(spawn).toVector().normalize();
|
|
|
|
BlockDisplay nade = p.getWorld().spawn(spawn, BlockDisplay.class, d -> {
|
|
d.setBlock(Material.TNT.createBlockData());
|
|
d.setBrightness(new Display.Brightness(15, 15));
|
|
d.setViewRange(64);
|
|
d.setTransformation(new org.bukkit.util.Transformation(
|
|
new org.joml.Vector3f(-0.25f, -0.25f, -0.25f),
|
|
new org.joml.AxisAngle4f(),
|
|
new org.joml.Vector3f(0.5f, 0.5f, 0.5f),
|
|
new org.joml.AxisAngle4f()));
|
|
});
|
|
EntityTracker.mark(nade);
|
|
EntityTracker.smooth(nade);
|
|
p.getWorld().playSound(spawn, Sound.ENTITY_SNOWBALL_THROW, 1f, 0.7f);
|
|
BlackKnifePlugin plugin = BlackKnifePlugin.getPlugin(BlackKnifePlugin.class);
|
|
|
|
new BukkitRunnable() {
|
|
Location flightStart = spawn.clone();
|
|
Location flightTarget = target.clone();
|
|
Vector flightDir = throwDir.clone();
|
|
int flightDuration = duration;
|
|
// Предсказание: кривая полёта параметризована временем, поэтому просто
|
|
// начинаем отсчёт чуть позже. Траектория та же, граната летит на lead
|
|
// тиков короче и клиент не отстаёт. Половина полёта — потолок,
|
|
// иначе короткий бросок при lead 10 приземлился бы мгновенно.
|
|
int elapsed = Math.min(EntityTracker.leadTicks(), Math.max(0, flightDuration / 2));
|
|
int tick = 0;
|
|
double arc = arcH;
|
|
boolean targetWasSolid = flightTarget.getBlock().isSolid();
|
|
final Deque<Location> trail = new ArrayDeque<>();
|
|
|
|
@Override public void run() {
|
|
if (!p.isOnline() || p.isDead() || !nade.isValid()) {
|
|
EntityTracker.untrack(nade);
|
|
if (nade.isValid()) nade.remove();
|
|
cancel();
|
|
return;
|
|
}
|
|
tick++;
|
|
elapsed++;
|
|
|
|
if (tick % 3 == 0 && needsRetarget(nade.getLocation())) {
|
|
retarget(nade.getLocation());
|
|
}
|
|
|
|
double t = Math.min(1.0, (double) elapsed / flightDuration);
|
|
Location base = flightStart.clone().add(flightTarget.clone().subtract(flightStart).toVector().multiply(t));
|
|
base.add(0, Math.sin(Math.PI * t) * arc, 0);
|
|
nade.teleport(base);
|
|
base.getWorld().spawnParticle(Particle.SMOKE, base, 1, 0.05, 0.05, 0.05, 0);
|
|
|
|
trail.addLast(base.clone());
|
|
while (trail.size() > 12) trail.pollFirst();
|
|
renderTrail();
|
|
|
|
if (tick % 2 == 0) renderRings(flightTarget);
|
|
|
|
boolean landed = t >= 1.0 || base.getBlock().isSolid();
|
|
if (landed || nade.getTicksLived() > 100) {
|
|
Location expl = landed ? flightTarget.clone() : base.clone();
|
|
EntityTracker.untrack(nade);
|
|
nade.remove();
|
|
cancel();
|
|
explode(p, expl, plugin);
|
|
}
|
|
}
|
|
|
|
private boolean needsRetarget(Location cur) {
|
|
boolean nowSolid = flightTarget.getBlock().isSolid();
|
|
if (nowSolid != targetWasSolid) return true;
|
|
Vector toTarget = flightTarget.clone().subtract(cur).toVector();
|
|
double remaining = toTarget.length();
|
|
if (remaining < 0.5) return false;
|
|
var ray = cur.getWorld().rayTraceBlocks(cur, toTarget.normalize(), remaining);
|
|
if (ray != null && ray.getHitBlock() != null) {
|
|
double hitDist = ray.getHitBlock().getLocation().distance(cur);
|
|
return hitDist + 0.5 < remaining;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private void retarget(Location cur) {
|
|
var ray = cur.getWorld().rayTraceBlocks(cur, flightDir, MAX_RANGE);
|
|
Location nt;
|
|
if (ray != null && ray.getHitPosition() != null) {
|
|
nt = new Location(cur.getWorld(), ray.getHitPosition().getX(), ray.getHitPosition().getY(), ray.getHitPosition().getZ());
|
|
if (ray.getHitBlockFace() != null) nt.add(0, 0.1, 0);
|
|
} else {
|
|
nt = cur.clone().add(flightDir.clone().multiply(12));
|
|
nt.setY(cur.getWorld().getHighestBlockYAt(nt) + 1);
|
|
}
|
|
flightStart = cur.clone();
|
|
flightTarget = nt.clone();
|
|
double remaining = Math.max(1.0, cur.distance(nt));
|
|
flightDuration = Math.max(6, (int) (remaining * TICKS_PER_BLOCK));
|
|
elapsed = 0;
|
|
arc = Math.max(ARC_MIN, Math.min(ARC_MAX, remaining * ARC_PER_BLOCK));
|
|
targetWasSolid = flightTarget.getBlock().isSolid();
|
|
}
|
|
|
|
private void renderTrail() {
|
|
int size = trail.size();
|
|
int i = 0;
|
|
for (Location tp : trail) {
|
|
float frac = size <= 1 ? 1f : (float) i / (size - 1);
|
|
float dustSize = 0.3f + frac * 1.1f;
|
|
p.spawnParticle(Particle.DUST, tp, 1, 0.02, 0.02, 0.02, 0,
|
|
new Particle.DustOptions(Color.fromRGB(180, 120, 40), dustSize));
|
|
i++;
|
|
}
|
|
}
|
|
|
|
private void renderRings(Location ground) {
|
|
Location g = ground.clone().add(0, 0.15, 0);
|
|
for (int k = 0; k < 16; k++) {
|
|
double a = (double) k / 16 * Math.PI * 2;
|
|
Location ring = g.clone().add(Math.cos(a) * LETHAL_RADIUS, 0, Math.sin(a) * LETHAL_RADIUS);
|
|
p.spawnParticle(Particle.DUST, ring, 1, 0, 0, 0, 0,
|
|
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,
|
|
Particles.GOLD_10);
|
|
}
|
|
}
|
|
}.runTaskTimer(plugin, 1L, 1L);
|
|
return true;
|
|
}
|
|
|
|
public static Location computeTarget(Player p, Location eye, Vector dir) {
|
|
var ray = p.getWorld().rayTraceBlocks(eye, dir, MAX_RANGE);
|
|
if (ray != null && ray.getHitPosition() != null) {
|
|
Location loc = new Location(p.getWorld(), ray.getHitPosition().getX(), ray.getHitPosition().getY(), ray.getHitPosition().getZ());
|
|
if (ray.getHitBlockFace() != null) loc.add(0, 0.1, 0);
|
|
return loc;
|
|
}
|
|
Location fallback = eye.clone().add(dir.clone().multiply(MAX_RANGE));
|
|
fallback.setY(p.getWorld().getHighestBlockYAt(fallback) + 1);
|
|
return fallback;
|
|
}
|
|
|
|
private static void explode(Player p, Location expl, BlackKnifePlugin plugin) {
|
|
expl.getWorld().spawnParticle(Particle.EXPLOSION, expl, 2);
|
|
expl.getWorld().spawnParticle(Particle.SMOKE, expl, 12, 0.3, 0.3, 0.3, 0.05);
|
|
expl.getWorld().spawnParticle(Particle.DUST, expl, 10, 0.3, 0.2, 0.3, 0,
|
|
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)) {
|
|
if (le == p) continue;
|
|
if (!DamageUtil.canHurt(p, le)) continue;
|
|
double d = le.getLocation().add(0, 1, 0).distance(expl);
|
|
if (d > CHIP_RADIUS) continue;
|
|
if (d <= LETHAL_RADIUS) {
|
|
DamageUtil.apply(p, le, LETHAL_DAMAGE);
|
|
tm.startBattle(p, le);
|
|
} else {
|
|
DamageUtil.apply(p, le, CHIP_DAMAGE);
|
|
tm.startBattle(p, le);
|
|
}
|
|
le.getWorld().spawnParticle(Particle.CRIT, le.getLocation().add(0, 1, 0), 8, 0.3, 0.3, 0.3, 0.1);
|
|
}
|
|
for (Player pl : expl.getWorld().getNearbyPlayers(expl, 0.9)) {
|
|
if (pl == p) continue;
|
|
double d2 = pl.getLocation().add(0, 1, 0).distanceSquared(expl);
|
|
if (d2 > 0.25 && d2 < 0.81) tm.tryGiveDodge(pl);
|
|
}
|
|
}
|
|
}
|