- Add HookSabotage system for survivors to disable hooks temporarily - Add GeneratorKick killer ability to regress generator progress - Add PlayerJumpListener to track survivor jump actions - Add BossBarManager for displaying game state and objectives - Update generator regression logic to support killer kick mechanic - Update KillerController to integrate generator kick ability - Update PlayerInteractListener to handle new interaction types - Refactor game phases to use BossBarManager for UI updates - Update .gitignore to exclude compiled bin/ directory - Simplify PLAN.md documentation for easier maintenance
239 lines
8.7 KiB
Java
239 lines
8.7 KiB
Java
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<UUID, CarryData> activeCarries = new HashMap<>();
|
||
private BukkitTask carryTask;
|
||
|
||
// Separate map: killerUUID -> carried survivor UUID (for jump listener)
|
||
private final Map<UUID, UUID> 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 + "%"));
|
||
}
|
||
}
|