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:
parent
3d6204cc83
commit
89dc9eb00d
78 changed files with 2293 additions and 1210 deletions
|
|
@ -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 + "%"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue