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,6 +2,7 @@ package dev.loki.phase;
import dev.loki.chest.ChestManager;
import dev.loki.chest.ChestOpening;
import dev.loki.config.GameConfig;
import dev.loki.core.GameSession;
import dev.loki.damage.AttackListener;
import dev.loki.damage.DamageHandler;
@ -10,10 +11,13 @@ import dev.loki.gate.GateManager;
import dev.loki.hook.Hook;
import dev.loki.hook.HookManager;
import dev.loki.hook.HookRescue;
import dev.loki.hook.HookSabotage;
import dev.loki.hook.HookSacrifice;
import dev.loki.hook.HookStage;
import dev.loki.killer.GeneratorKick;
import dev.loki.killer.KillerController;
import dev.loki.listener.PlayerInteractListener;
import dev.loki.listener.PlayerJumpListener;
import dev.loki.listener.PlayerMoveListener;
import dev.loki.mdbd;
import dev.loki.objective.GeneratorManager;
@ -29,9 +33,14 @@ import dev.loki.player.PlayerState;
import dev.loki.player.Survivor;
import dev.loki.state.StateMachine;
import dev.loki.ui.ActionBar;
import dev.loki.ui.BossBarManager;
import dev.loki.ui.Sidebar;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.title.Title;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
@ -40,6 +49,10 @@ import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageEvent;
import java.time.Duration;
import java.util.Collection;
import java.util.stream.Collectors;
public class GameplayPhase implements Phase {
private final GeneratorManager generatorManager;
@ -55,9 +68,14 @@ public class GameplayPhase implements Phase {
private HookSacrifice hookSacrifice;
private PalletDrop palletDrop;
private PalletBreak palletBreak;
@SuppressWarnings("unused")
private PalletStun palletStun;
private ChestManager chestManager;
private ChestOpening chestOpening;
private GeneratorKick generatorKick;
private HookSabotage hookSabotage;
private BossBarManager bossBarManager;
private Sidebar sidebar;
// Damage system
private PlayerManager playerManager;
@ -68,9 +86,10 @@ public class GameplayPhase implements Phase {
private Listener vanillaDamageBlocker;
private dev.loki.item.ItemUseListener itemUseListener;
// Listener references grabbed from plugin
// Listeners
private PlayerMoveListener moveListener;
private PlayerInteractListener interactListener;
private PlayerJumpListener jumpListener;
public GameplayPhase(GeneratorManager generatorManager) {
this.generatorManager = generatorManager;
@ -83,113 +102,141 @@ public class GameplayPhase implements Phase {
@Override
public void enter(GameSession session) {
mdbd plugin = session.getPlugin();
GameConfig cfg = session.getConfig();
tickCounter = 0;
shouldEnd = false;
allGensDone = false;
// --- Initialise managers ---
// --- Managers ---
palletManager = new PalletManager(plugin.getConfigManager().getLocations());
hookManager = new HookManager(plugin);
hookManager.loadHooks(plugin.getConfigManager().getLocations());
hookManager.setMaxHooks(cfg.getMaxHooks());
gateManager = new GateManager(plugin.getConfigManager().getLocations());
ActionBar ab = new ActionBar();
hookRescue = new HookRescue(plugin, ab);
hookRescue = new HookRescue(plugin, ab, cfg);
hookSacrifice = new HookSacrifice(plugin, null);
palletDrop = new PalletDrop(palletManager);
palletBreak = new PalletBreak(session.getConfig());
palletStun = new PalletStun(session.getConfig());
palletBreak = new PalletBreak(cfg);
palletStun = new PalletStun(cfg);
chestManager = new ChestManager(plugin, plugin.getConfigManager().getLocations());
chestOpening = new ChestOpening(plugin);
// Store managers on session so other code can reach them
// GeneratorKick for the killer
generatorKick = new GeneratorKick(plugin, cfg, generatorManager);
// HookSabotage for survivors
hookSabotage = new HookSabotage(plugin, cfg, hookManager);
session.setPalletManager(palletManager);
session.setHookManager(hookManager);
session.setGateManager(gateManager);
// --- Damage system ---
// Build PlayerManager from current session players
playerManager = new PlayerManager();
for (BasePlayer bp : session.getPlayers().values()) {
playerManager.register(bp);
}
stateMachine = new StateMachine();
stateMachine = new StateMachine();
damageHandler = new DamageHandler(stateMachine);
// When survivor is injured — show title + sound
damageHandler.setOnSurvivorInjured(survivor -> {
Player p = survivor.getPlayer();
if (p != null && p.isOnline()) {
p.showTitle(net.kyori.adventure.title.Title.title(
Component.text("РАНЕН!", net.kyori.adventure.text.format.TextColor.color(0xFF8C00)),
Component.text("Найдите аптечку или союзника", NamedTextColor.YELLOW)
p.showTitle(Title.title(
Component.text("РАНЕН!").color(TextColor.color(0xFF8C00)),
Component.text("Найдите аптечку или союзника").color(NamedTextColor.YELLOW)
));
p.playSound(p.getLocation(), Sound.ENTITY_PLAYER_HURT, 1.0f, 0.8f);
// Keep HP at 20 always — damage is state-based not HP-based
p.setHealth(20.0);
}
});
// When survivor is downed — show title + sound
damageHandler.setOnSurvivorDowned(survivor -> {
Player p = survivor.getPlayer();
if (p != null && p.isOnline()) {
p.showTitle(net.kyori.adventure.title.Title.title(
Component.text("НОКДАУН!", net.kyori.adventure.text.format.TextColor.color(0xFF2222)),
Component.text("Ждите помощи...", NamedTextColor.RED)
p.showTitle(Title.title(
Component.text("НОКДАУН!").color(TextColor.color(0xFF2222)),
Component.text("Ждите помощи...").color(NamedTextColor.RED)
));
p.playSound(p.getLocation(), Sound.ENTITY_PLAYER_HURT, 1.0f, 0.5f);
p.setHealth(20.0);
}
});
killerController = new KillerController(plugin, playerManager, damageHandler, session.getConfig());
// AttackListener — intercepts EntityDamageByEntityEvent, cancels vanilla damage, applies custom
// KillerController — AttackListener зарегистрирован ТОЛЬКО здесь
killerController = new KillerController(plugin, playerManager, damageHandler, cfg);
attackListener = new AttackListener(playerManager, damageHandler,
(killer, survivor) -> killerController.tryAttack(killer, survivor));
attackListener.register(plugin);
// Block ALL vanilla damage for players in this session (fall, fire, mobs, etc.)
// Block vanilla damage
vanillaDamageBlocker = new VanillaDamageBlocker(session);
Bukkit.getPluginManager().registerEvents(vanillaDamageBlocker, plugin);
// KillerController.initialize() does NOT re-register AttackListener
killerController.initialize();
// --- Give starting items to survivors & register item use ---
// --- BossBar ---
Collection<Survivor> survivors = session.getPlayers().values().stream()
.filter(bp -> !bp.isKiller())
.map(bp -> (Survivor) bp)
.collect(Collectors.toList());
Killer killer = (Killer) session.getPlayers().values().stream()
.filter(BasePlayer::isKiller)
.findFirst().orElse(null);
bossBarManager = new BossBarManager(plugin, cfg);
if (killer != null) bossBarManager.setTargets(killer, survivors);
if (cfg.isBossBarEnabled()) bossBarManager.start();
// --- Sidebar ---
sidebar = new Sidebar();
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p != null && p.isOnline()) sidebar.setupPlayer(p);
}
// --- Jump listener for carry escape ---
jumpListener = new PlayerJumpListener(plugin);
jumpListener.setOnJump(player -> {
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null || bp.isKiller()) return;
killerController.getCarry().onSurvivorJump(player.getUniqueId());
});
jumpListener.register();
// --- Items ---
itemUseListener = new dev.loki.item.ItemUseListener(plugin, session);
itemUseListener.register();
giveStartingItems(session);
// Apply player scales via Attribute API
applyPlayerScales(session, cfg);
// --- Wire listeners ---
moveListener = plugin.getMoveListener();
interactListener = plugin.getInteractListener();
wireMoveListener(session);
wireInteractListener(session);
Bukkit.broadcast(Component.text("[DBD] Игра началась! Выжившие, чините генераторы!",
NamedTextColor.GREEN));
NamedTextColor.GREEN));
}
@Override
public void tick(GameSession session) {
tickCounter++;
generatorManager.tick();
// Tick hook timers every game tick
tickHooks(session);
if (tickCounter % 20 == 0) {
checkGeneratorProgress(session);
checkWinConditions(session);
updateSidebar(session);
}
}
@Override
public void exit(GameSession session) {
// Clean up listeners
if (moveListener != null) {
moveListener.setOnSurvivorMove(null);
}
@ -198,41 +245,34 @@ public class GameplayPhase implements Phase {
interactListener.setOnRescueAttempt(null);
interactListener.setOnChestInteract(null);
interactListener.setOnAttack(null);
interactListener.setOnKillerSneak(null);
}
// Stop hooks
if (hookManager != null) {
hookManager.clear();
}
if (hookRescue != null) {
hookRescue.clear();
}
if (chestOpening != null) {
chestOpening.cancelAll();
}
// Unregister damage listeners
if (attackListener != null) {
attackListener.unregister();
if (jumpListener != null) {
jumpListener.unregister();
jumpListener = null;
}
if (hookManager != null) hookManager.clear();
if (hookRescue != null) hookRescue.clear();
if (chestOpening != null) chestOpening.cancelAll();
if (attackListener != null) attackListener.unregister();
if (vanillaDamageBlocker != null) {
HandlerList.unregisterAll(vanillaDamageBlocker);
vanillaDamageBlocker = null;
}
if (killerController != null) {
killerController.shutdown();
}
if (itemUseListener != null) {
itemUseListener.unregister();
}
if (playerManager != null) {
playerManager.clear();
}
// Restore HP for all players
if (killerController != null) killerController.shutdown();
if (generatorKick != null) generatorKick.shutdown();
if (hookSabotage != null) hookSabotage.shutdown();
if (bossBarManager != null) bossBarManager.stop();
if (sidebar != null) { sidebar.clear(); sidebar = null; }
if (itemUseListener != null) itemUseListener.unregister();
if (playerManager != null) playerManager.clear();
// Restore modes and HP
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p != null && p.isOnline()) {
p.setHealth(20.0);
p.setGameMode(GameMode.ADVENTURE);
}
}
@ -244,6 +284,49 @@ public class GameplayPhase implements Phase {
return shouldEnd;
}
// -------------------------------------------------------------------------
// Attribute-based player scaling
// -------------------------------------------------------------------------
private void applyPlayerScales(GameSession session, GameConfig cfg) {
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p == null || !p.isOnline()) continue;
double scale = bp.isKiller() ? cfg.getKillerScale() : cfg.getSurvivorScale();
try {
var attr = p.getAttribute(org.bukkit.attribute.Attribute.GENERIC_SCALE);
if (attr != null) attr.setBaseValue(scale);
} catch (Exception ignored) {
// Attribute may not exist on older Paper builds — skip silently
}
}
}
private void resetPlayerScales(GameSession session) {
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p == null || !p.isOnline()) continue;
try {
var attr = p.getAttribute(org.bukkit.attribute.Attribute.GENERIC_SCALE);
if (attr != null) attr.setBaseValue(1.0);
} catch (Exception ignored) {}
}
}
// -------------------------------------------------------------------------
// Sidebar update
// -------------------------------------------------------------------------
private void updateSidebar(GameSession session) {
if (sidebar == null) return;
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p != null && p.isOnline()) {
sidebar.updateGameStats(p, session, generatorManager);
}
}
}
// -------------------------------------------------------------------------
// Starting items
// -------------------------------------------------------------------------
@ -254,7 +337,6 @@ public class GameplayPhase implements Phase {
if (bp.isKiller()) continue;
Player p = bp.getPlayer();
if (p == null || !p.isOnline()) continue;
// Clear hotbar slots 0-3, give a random item in slot 0
p.getInventory().clear();
p.getInventory().setItem(0, loot.rollLoot());
p.sendMessage(Component.text("[DBD] Вы получили стартовый предмет!", NamedTextColor.YELLOW));
@ -268,26 +350,45 @@ public class GameplayPhase implements Phase {
private void tickHooks(GameSession session) {
for (Hook hook : hookManager.getAllHooks()) {
if (!hook.isOccupied()) continue;
hook.tick();
Survivor victim = hook.getHookedSurvivor();
if (victim == null) continue;
// Show action bar progress to the hooked survivor
Player vp = victim.getPlayer();
if (vp != null && vp.isOnline()) {
// ActionBar shows stage progress
double pct = hook.getStageProgressPercent();
String label = hook.getCurrentStage() == HookStage.STAGE_2
? "§cStage 2 - struggle!"
: "§eOn hook";
? "§cStage 2 — вырывайтесь!" : "§eНа крюке";
vp.sendActionBar(Component.text(label + " " + (int) pct + "%"));
}
// Sacrifice when timer expires
if (hook.getCurrentStage() == HookStage.SACRIFICE) {
hookSacrifice.sacrificeIfExpired(victim, hook);
// Перевод в SPECTATOR после смерти обрабатывается через setState
}
}
}
// -------------------------------------------------------------------------
// Spectator mode on death / after sacrifice
// -------------------------------------------------------------------------
/**
* Converts a dead survivor to SPECTATOR so they can watch the game.
*/
private void onSurvivorDied(Survivor survivor, GameSession session) {
Player p = survivor.getPlayer();
if (p == null || !p.isOnline()) return;
p.setGameMode(GameMode.SPECTATOR);
p.sendMessage(Component.text("[DBD] Вы мертвы. Наблюдайте за матчем.", NamedTextColor.DARK_RED));
// Let spectator follow a random alive player
for (BasePlayer other : session.getPlayers().values()) {
if (other.getUuid().equals(survivor.getUuid())) continue;
Player op = other.getPlayer();
if (op != null && op.isOnline() && other.getState() != PlayerState.DEAD) {
p.setSpectatorTarget(op);
break;
}
}
}
@ -326,47 +427,51 @@ public class GameplayPhase implements Phase {
PlayerState st = bp.getState();
if (st == PlayerState.ESCAPED) {
escapedSurvivors++;
} else if (st == PlayerState.DEAD) {
// dead, counted below
} else {
// HEALTHY, INJURED, DOWNED, CARRIED, HOOKED — still in game
} else if (st != PlayerState.DEAD) {
aliveSurvivors++;
}
// Переводим мёртвых в SPECTATOR (если ещё не переведены)
if (st == PlayerState.DEAD) {
Player p = bp.getPlayer();
if (p != null && p.isOnline() && p.getGameMode() != GameMode.SPECTATOR) {
onSurvivorDied((Survivor) bp, session);
}
}
}
if (totalSurvivors == 0) return; // no survivors registered yet
if (totalSurvivors == 0) return;
boolean killerWin = (aliveSurvivors == 0 && escapedSurvivors == 0);
boolean survivorsWin = (escapedSurvivors == totalSurvivors);
// Маньяк побеждает — все мертвы и никто не сбежал
boolean killerWin = (aliveSurvivors == 0 && escapedSurvivors == 0);
// Выжившие побеждают — хотя бы один сбежал
boolean survivorsWin = (escapedSurvivors >= 1);
if (killerWin) {
announceKillerWin(session);
shouldEnd = true;
} else if (survivorsWin) {
} else if (survivorsWin && aliveSurvivors == 0) {
// Все либо сбежали либо мертвы — конец матча
announceSurvivorWin(session);
shouldEnd = true;
}
}
private void announceKillerWin(GameSession session) {
Bukkit.broadcast(Component.text("", NamedTextColor.RED));
Bukkit.broadcast(Component.text("☠ КИЛЛЕР ПОБЕДИЛ! ☠", NamedTextColor.DARK_RED));
Bukkit.broadcast(Component.text("Все выжившие уничтожены.", NamedTextColor.RED));
Bukkit.broadcast(Component.text("", NamedTextColor.RED));
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p == null || !p.isOnline()) continue;
if (bp.isKiller()) {
p.showTitle(net.kyori.adventure.title.Title.title(
Component.text("ПОБЕДА!", net.kyori.adventure.text.format.TextColor.color(0xFF2222)),
Component.text("Ты уничтожил всех выживших", NamedTextColor.RED)
p.showTitle(Title.title(
Component.text("ПОБЕДА!").color(TextColor.color(0xFF2222)),
Component.text("Ты уничтожил всех выживших").color(NamedTextColor.RED)
));
p.playSound(p.getLocation(), Sound.UI_TOAST_CHALLENGE_COMPLETE, 1.0f, 1.0f);
} else {
p.showTitle(net.kyori.adventure.title.Title.title(
Component.text("ПОРАЖЕНИЕ", net.kyori.adventure.text.format.TextColor.color(0x888888)),
Component.text("Киллер победил", NamedTextColor.GRAY)
p.showTitle(Title.title(
Component.text("ПОРАЖЕНИЕ").color(TextColor.color(0x888888)),
Component.text("Киллер победил").color(NamedTextColor.GRAY)
));
p.playSound(p.getLocation(), Sound.ENTITY_WITHER_DEATH, 0.5f, 0.8f);
}
@ -374,24 +479,21 @@ public class GameplayPhase implements Phase {
}
private void announceSurvivorWin(GameSession session) {
Bukkit.broadcast(Component.text("", NamedTextColor.GREEN));
Bukkit.broadcast(Component.text("✔ ВЫЖИВШИЕ ПОБЕДИЛИ! ✔", NamedTextColor.GREEN));
Bukkit.broadcast(Component.text("Все выжившие сбежали!", NamedTextColor.YELLOW));
Bukkit.broadcast(Component.text("", NamedTextColor.GREEN));
Bukkit.broadcast(Component.text("Выжившие сбежали!", NamedTextColor.YELLOW));
for (BasePlayer bp : session.getPlayers().values()) {
Player p = bp.getPlayer();
if (p == null || !p.isOnline()) continue;
if (!bp.isKiller()) {
p.showTitle(net.kyori.adventure.title.Title.title(
Component.text("ПОБЕГ!", net.kyori.adventure.text.format.TextColor.color(0x00FF00)),
Component.text("Вы выбрались живыми!", NamedTextColor.YELLOW)
p.showTitle(Title.title(
Component.text("ПОБЕГ!").color(TextColor.color(0x00FF00)),
Component.text("Вы выбрались живыми!").color(NamedTextColor.YELLOW)
));
p.playSound(p.getLocation(), Sound.UI_TOAST_CHALLENGE_COMPLETE, 1.0f, 1.2f);
} else {
p.showTitle(net.kyori.adventure.title.Title.title(
Component.text("ПОРАЖЕНИЕ", net.kyori.adventure.text.format.TextColor.color(0x888888)),
Component.text("Все выжившие сбежали", NamedTextColor.GRAY)
p.showTitle(Title.title(
Component.text("ПОРАЖЕНИЕ").color(TextColor.color(0x888888)),
Component.text("Выжившие сбежали").color(NamedTextColor.GRAY)
));
p.playSound(p.getLocation(), Sound.ENTITY_WITHER_DEATH, 0.5f, 0.8f);
}
@ -404,12 +506,9 @@ public class GameplayPhase implements Phase {
private void wireMoveListener(GameSession session) {
if (moveListener == null) return;
// Gate escape: survivor walks into open gate zone after all gens done
moveListener.setOnSurvivorMove((survivor, loc) -> {
if (!allGensDone) return;
if (survivor.getState() == PlayerState.ESCAPED) return;
ExitGate gate = gateManager.getGateAt(loc, 3.0);
if (gate != null && gate.isOpen()) {
survivor.setState(PlayerState.ESCAPED);
@ -417,8 +516,7 @@ public class GameplayPhase implements Phase {
if (p != null) {
p.playSound(loc, Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.2f);
Bukkit.broadcast(Component.text(
"[DBD] " + p.getName() + " сбежал через ворота!",
NamedTextColor.GOLD));
"[DBD] " + p.getName() + " сбежал через ворота!", NamedTextColor.GOLD));
}
}
});
@ -427,75 +525,78 @@ public class GameplayPhase implements Phase {
private void wireInteractListener(GameSession session) {
if (interactListener == null) return;
// Pallet interact: survivor right-click = drop, killer left-click = break
interactListener.setOnPalletInteract((player, isRightClick) -> {
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null) return;
if (isRightClick && !bp.isKiller()) {
palletDrop.tryDrop((Survivor) bp);
} else if (!isRightClick && bp.isKiller()) {
Killer killer = (Killer) bp;
Killer k = (Killer) bp;
Pallet nearby = palletManager.getPalletAt(player.getLocation(), 3.0);
if (nearby != null && nearby.isDropped()) {
palletBreak.startBreaking(killer, nearby, session.getPlugin());
palletBreak.startBreaking(k, nearby, session.getPlugin());
}
}
});
// Rescue attempt: survivor right-clicks near a hook
interactListener.setOnRescueAttempt((player, ignored) -> {
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null || bp.isKiller()) return;
Survivor rescuer = (Survivor) bp;
if (rescuer.getState() == PlayerState.HOOKED) return;
Hook hook = hookManager.findNearestOccupiedHook(player.getLocation(), 3.0);
if (hook != null) {
hookRescue.startRescue(rescuer, hook);
}
if (hook != null) hookRescue.startRescue(rescuer, hook);
});
// Killer right-click: hook a carried survivor
interactListener.setOnAttack((player, isRightClick) -> {
if (!isRightClick) return; // left-click handled by damage system
if (!isRightClick) return;
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null || !bp.isKiller()) return;
Killer killer = (Killer) bp;
if (killer.getState() == PlayerState.CARRIED || killer.isCarrying()) {
// Find the carried survivor
for (BasePlayer other : session.getPlayers().values()) {
if (!other.isKiller() && other.getState() == PlayerState.CARRIED) {
Survivor carried = (Survivor) other;
if (hookManager.tryHookSurvivor(carried, player.getLocation())) {
killer.setCarrying(false);
// Teleport survivor to hook
Hook nearestHook = hookManager.findNearestOccupiedHook(
player.getLocation(), 5.0);
if (nearestHook != null) {
Player vp = carried.getPlayer();
if (vp != null) {
vp.teleport(nearestHook.getLocation());
}
}
if (!killer.isCarrying()) return;
for (BasePlayer other : session.getPlayers().values()) {
if (!other.isKiller() && other.getState() == PlayerState.CARRIED) {
Survivor carried = (Survivor) other;
if (hookManager.tryHookSurvivor(carried, player.getLocation())) {
killer.setCarrying(false);
Hook nearestHook = hookManager.findNearestOccupiedHook(player.getLocation(), 5.0);
if (nearestHook != null) {
Player vp = carried.getPlayer();
if (vp != null) vp.teleport(nearestHook.getLocation());
}
break;
}
break;
}
}
});
// Sneak: killer → GeneratorKick, survivor → HookSabotage
interactListener.setOnKillerSneak((player, isSneaking) -> {
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null) return;
if (bp.isKiller()) {
Killer killer = (Killer) bp;
if (isSneaking) {
generatorKick.startCharging(killer);
} else {
generatorKick.stopCharging(killer.getUuid());
}
} else {
// Survivor sneak → hook sabotage
Survivor surv = (Survivor) bp;
if (isSneaking) {
hookSabotage.startSabotage(surv);
} else {
hookSabotage.stopSabotage(surv.getUuid());
}
}
});
// Chest interact
interactListener.setOnChestInteract((player, ignored) -> {
BasePlayer bp = session.getPlayer(player.getUniqueId());
if (bp == null || bp.isKiller()) return;
var chest = chestManager.getNearestChest(player.getLocation(), 3.0);
if (chest != null) {
chestOpening.startOpening(player, chest);
}
if (chest != null) chestOpening.startOpening(player, chest);
});
}
@ -505,10 +606,7 @@ public class GameplayPhase implements Phase {
private static class VanillaDamageBlocker implements Listener {
private final GameSession session;
VanillaDamageBlocker(GameSession session) {
this.session = session;
}
VanillaDamageBlocker(GameSession session) { this.session = session; }
@EventHandler(priority = EventPriority.HIGHEST)
public void onDamage(EntityDamageEvent event) {