feat(gameplay): implement hook sabotage, generator kick, jump listener, and boss bar UI

- 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
This commit is contained in:
loki5512344 2026-06-04 12:11:04 +02:00
parent 3d6204cc83
commit 89dc9eb00d
78 changed files with 2293 additions and 1210 deletions

View file

@ -2,30 +2,45 @@ 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;
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;
double wiggleProgress; // 0..100
CarryData(Killer killer, Survivor survivor) {
this.killer = killer;
this.killer = killer;
this.survivor = survivor;
this.wiggleProgress = 0;
}
@ -34,22 +49,35 @@ public class KillerCarry {
public KillerCarry(Plugin plugin, GameConfig config) {
this.plugin = plugin;
this.config = config;
this.activeCarries = new HashMap<>();
}
public boolean tryPickup(Killer killer, Survivor survivor) {
if (!canPickup(killer, survivor)) {
return false;
// -------------------------------------------------------------------------
// 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;
}
if (!HitValidator.isValidCarryPosition(killerPlayer, survivorPlayer)) return false;
startCarry(killer, survivor);
return true;
@ -57,112 +85,25 @@ public class KillerCarry {
public boolean canPickup(Killer killer, Survivor survivor) {
if (killer.isCarrying()) return false;
if (!survivor.getState().name().equals("DOWNED")) return false;
Player killerPlayer = killer.getPlayer();
Player survivorPlayer = survivor.getPlayer();
return killerPlayer != null && survivorPlayer != null && survivorPlayer.isOnline();
}
private void startCarry(Killer killer, Survivor survivor) {
killer.setCarrying(true);
Player survivorPlayer = survivor.getPlayer();
if (survivorPlayer != null) {
survivorPlayer.setInvisible(true);
}
CarryData data = new CarryData(killer, survivor);
activeCarries.put(killer.getUuid(), data);
if (carryTask == null) {
startCarryTask();
}
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);
stopCarry(data, false);
return true;
}
private void stopCarry(CarryData data) {
data.killer.setCarrying(false);
Player survivorPlayer = data.survivor.getPlayer();
if (survivorPlayer != null) {
survivorPlayer.setInvisible(false);
survivorPlayer.teleport(data.killer.getPlayer().getLocation());
}
if (activeCarries.isEmpty() && carryTask != null) {
carryTask.cancel();
carryTask = null;
}
}
private void startCarryTask() {
carryTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
activeCarries.values().removeIf(data -> {
if (!isValidCarry(data)) {
stopCarry(data);
return true;
}
updateCarryPosition(data);
updateWiggle(data);
return false;
});
}, 1L, 1L);
}
private boolean isValidCarry(CarryData data) {
Player killerPlayer = data.killer.getPlayer();
Player survivorPlayer = data.survivor.getPlayer();
if (killerPlayer == null || survivorPlayer == null) return false;
if (!killerPlayer.isOnline() || !survivorPlayer.isOnline()) return false;
return true;
}
private void updateCarryPosition(CarryData data) {
Player killerPlayer = data.killer.getPlayer();
Player survivorPlayer = data.survivor.getPlayer();
Location carryLoc = killerPlayer.getLocation().clone().add(0, -0.5, 0);
survivorPlayer.teleport(carryLoc);
}
private void updateWiggle(CarryData data) {
Player survivorPlayer = data.survivor.getPlayer();
if (survivorPlayer == null) return;
if (survivorPlayer.isSneaking()) {
data.wiggleProgress += 1.5;
}
if (data.wiggleProgress >= 100) {
forceDrop(data.killer);
}
}
public void forceDrop(Killer killer) {
CarryData data = activeCarries.remove(killer.getUuid());
killerToSurvivor.remove(killer.getUuid());
if (data == null) return;
Player survivorPlayer = data.survivor.getPlayer();
if (survivorPlayer != null) {
Location dropLoc = survivorPlayer.getLocation();
dropLoc.add(killer.getPlayer().getLocation().getDirection().multiply(1.5));
survivorPlayer.teleport(dropLoc);
}
stopCarry(data);
stopCarry(data, true);
}
public boolean isCarrying(Killer killer) {
@ -180,10 +121,119 @@ public class KillerCarry {
}
public void shutdown() {
if (carryTask != null) {
carryTask.cancel();
if (carryTask != null) { carryTask.cancel(); carryTask = null; }
for (CarryData data : new HashMap<>(activeCarries).values()) {
stopCarry(data, false);
}
activeCarries.values().forEach(this::stopCarry);
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 + "%"));
}
}