package dev.loki.killer; import dev.loki.config.GameConfig; import dev.loki.player.Killer; import dev.loki.player.PlayerState; import dev.loki.player.Survivor; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; import org.bukkit.scheduler.BukkitTask; import java.util.HashMap; import java.util.Map; import java.util.UUID; /** * Manages the Killer carrying a downed Survivor. * * Escape mechanic: Survivor spams SPACE (jump) to fill the escape bar. * When the bar reaches 100% the Survivor breaks free, is teleported * slightly forward and receives Speed I for carry.escape-speed-seconds. */ public class KillerCarry { private final Plugin plugin; private final GameConfig config; private final Map activeCarries = new HashMap<>(); private BukkitTask carryTask; // Separate map: killerUUID -> carried survivor UUID (for jump listener) private final Map killerToSurvivor = new HashMap<>(); private static class CarryData { final Killer killer; final Survivor survivor; double wiggleProgress; // 0..100 CarryData(Killer killer, Survivor survivor) { this.killer = killer; this.survivor = survivor; this.wiggleProgress = 0; } } public KillerCarry(Plugin plugin, GameConfig config) { this.plugin = plugin; this.config = config; } // ------------------------------------------------------------------------- // Called by PlayerJumpListener when a survivor jumps // ------------------------------------------------------------------------- public void onSurvivorJump(UUID survivorUuid) { for (CarryData data : activeCarries.values()) { if (data.survivor.getUuid().equals(survivorUuid)) { data.wiggleProgress += config.getCarryJumpFillPercent(); showWiggleProgress(data); if (data.wiggleProgress >= 100) { Bukkit.getScheduler().runTask(plugin, () -> forceDrop(data.killer)); } return; } } } // ------------------------------------------------------------------------- // Public API // ------------------------------------------------------------------------- public boolean tryPickup(Killer killer, Survivor survivor) { if (!canPickup(killer, survivor)) return false; Player killerPlayer = killer.getPlayer(); Player survivorPlayer = survivor.getPlayer(); if (killerPlayer == null || survivorPlayer == null) return false; if (!HitValidator.isValidCarryPosition(killerPlayer, survivorPlayer)) return false; startCarry(killer, survivor); return true; } public boolean canPickup(Killer killer, Survivor survivor) { if (killer.isCarrying()) return false; if (survivor.getState() != PlayerState.DOWNED) return false; Player kp = killer.getPlayer(); Player sp = survivor.getPlayer(); return kp != null && sp != null && sp.isOnline(); } public boolean drop(Killer killer) { CarryData data = activeCarries.remove(killer.getUuid()); killerToSurvivor.remove(killer.getUuid()); if (data == null) return false; stopCarry(data, false); return true; } public void forceDrop(Killer killer) { CarryData data = activeCarries.remove(killer.getUuid()); killerToSurvivor.remove(killer.getUuid()); if (data == null) return; stopCarry(data, true); } public boolean isCarrying(Killer killer) { return activeCarries.containsKey(killer.getUuid()); } public Survivor getCarried(Killer killer) { CarryData data = activeCarries.get(killer.getUuid()); return data != null ? data.survivor : null; } public double getWiggleProgress(Killer killer) { CarryData data = activeCarries.get(killer.getUuid()); return data != null ? data.wiggleProgress : 0; } public void shutdown() { if (carryTask != null) { carryTask.cancel(); carryTask = null; } for (CarryData data : new HashMap<>(activeCarries).values()) { stopCarry(data, false); } activeCarries.clear(); killerToSurvivor.clear(); } // ------------------------------------------------------------------------- // Private // ------------------------------------------------------------------------- private void startCarry(Killer killer, Survivor survivor) { killer.setCarrying(true); survivor.setState(PlayerState.CARRIED); Player survivorPlayer = survivor.getPlayer(); if (survivorPlayer != null) { survivorPlayer.setInvisible(true); survivorPlayer.sendMessage(Component.text( "[DBD] Нажимайте ПРОБЕЛ чтобы вырваться!", NamedTextColor.YELLOW)); } CarryData data = new CarryData(killer, survivor); activeCarries.put(killer.getUuid(), data); killerToSurvivor.put(killer.getUuid(), survivor.getUuid()); if (carryTask == null) startCarryTask(); } private void stopCarry(CarryData data, boolean escaped) { data.killer.setCarrying(false); Player killerPlayer = data.killer.getPlayer(); Player survivorPlayer = data.survivor.getPlayer(); if (survivorPlayer != null && survivorPlayer.isOnline()) { survivorPlayer.setInvisible(false); if (escaped) { // Drop survivor slightly forward from killer Location dropLoc = survivorPlayer.getLocation(); if (killerPlayer != null) { dropLoc = killerPlayer.getLocation().clone() .add(killerPlayer.getLocation().getDirection().multiply(2.0)); } survivorPlayer.teleport(dropLoc); // Speed I int speedTicks = config.getCarryEscapeSpeedSeconds() * 20; survivorPlayer.addPotionEffect( new PotionEffect(PotionEffectType.SPEED, speedTicks, 0, false, true)); survivorPlayer.sendMessage(Component.text( "[DBD] Вы вырвались!", NamedTextColor.GREEN)); data.survivor.setState(PlayerState.DOWNED); // still downed, just free } else { survivorPlayer.teleport(data.killer.getPlayer() != null ? data.killer.getPlayer().getLocation() : survivorPlayer.getLocation()); data.survivor.setState(PlayerState.DOWNED); } // Clear action bar progress survivorPlayer.sendActionBar(Component.empty()); } if (activeCarries.isEmpty() && carryTask != null) { carryTask.cancel(); carryTask = null; } } private void startCarryTask() { carryTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> { if (activeCarries.isEmpty()) return; for (CarryData data : new HashMap<>(activeCarries).values()) { if (!isValidCarry(data)) { activeCarries.remove(data.killer.getUuid()); killerToSurvivor.remove(data.killer.getUuid()); stopCarry(data, false); continue; } updateCarryPosition(data); showWiggleProgress(data); // refresh bar every tick } }, 1L, 1L); } private boolean isValidCarry(CarryData data) { Player kp = data.killer.getPlayer(); Player sp = data.survivor.getPlayer(); return kp != null && sp != null && kp.isOnline() && sp.isOnline(); } private void updateCarryPosition(CarryData data) { Player killerPlayer = data.killer.getPlayer(); Player survivorPlayer = data.survivor.getPlayer(); if (killerPlayer == null || survivorPlayer == null) return; Location carryLoc = killerPlayer.getLocation().clone().add(0, -0.5, 0); survivorPlayer.teleport(carryLoc); } private void showWiggleProgress(CarryData data) { Player survivorPlayer = data.survivor.getPlayer(); if (survivorPlayer == null) return; double pct = Math.min(100, data.wiggleProgress); int filled = (int) (pct / 10); int empty = 10 - filled; String bar = "§e[" + "§a█".repeat(filled) + "§7░".repeat(empty) + "§e]"; survivorPlayer.sendActionBar(Component.text("§6Побег: " + bar + " " + (int) pct + "%")); } }