package dev.loki.phase; import dev.loki.chest.ChestManager; import dev.loki.chest.ChestOpening; import dev.loki.core.GameSession; import dev.loki.damage.AttackListener; import dev.loki.damage.DamageHandler; import dev.loki.gate.ExitGate; import dev.loki.gate.GateManager; import dev.loki.hook.Hook; import dev.loki.hook.HookManager; import dev.loki.hook.HookRescue; import dev.loki.hook.HookSacrifice; import dev.loki.hook.HookStage; import dev.loki.killer.KillerController; import dev.loki.listener.PlayerInteractListener; import dev.loki.listener.PlayerMoveListener; import dev.loki.mdbd; import dev.loki.objective.GeneratorManager; import dev.loki.pallet.Pallet; import dev.loki.pallet.PalletBreak; import dev.loki.pallet.PalletDrop; import dev.loki.pallet.PalletManager; import dev.loki.pallet.PalletStun; import dev.loki.player.BasePlayer; import dev.loki.player.Killer; import dev.loki.player.PlayerManager; import dev.loki.player.PlayerState; import dev.loki.player.Survivor; import dev.loki.state.StateMachine; import dev.loki.ui.ActionBar; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Bukkit; import org.bukkit.Sound; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.HandlerList; import org.bukkit.event.Listener; import org.bukkit.event.entity.EntityDamageEvent; public class GameplayPhase implements Phase { private final GeneratorManager generatorManager; private int tickCounter; private boolean shouldEnd; private boolean allGensDone; // Managers initialised in enter() private PalletManager palletManager; private HookManager hookManager; private GateManager gateManager; private HookRescue hookRescue; private HookSacrifice hookSacrifice; private PalletDrop palletDrop; private PalletBreak palletBreak; private PalletStun palletStun; private ChestManager chestManager; private ChestOpening chestOpening; // Damage system private PlayerManager playerManager; private StateMachine stateMachine; private DamageHandler damageHandler; private KillerController killerController; private AttackListener attackListener; private Listener vanillaDamageBlocker; private dev.loki.item.ItemUseListener itemUseListener; // Listener references grabbed from plugin private PlayerMoveListener moveListener; private PlayerInteractListener interactListener; public GameplayPhase(GeneratorManager generatorManager) { this.generatorManager = generatorManager; } // ------------------------------------------------------------------------- // Phase lifecycle // ------------------------------------------------------------------------- @Override public void enter(GameSession session) { mdbd plugin = session.getPlugin(); tickCounter = 0; shouldEnd = false; allGensDone = false; // --- Initialise managers --- palletManager = new PalletManager(plugin.getConfigManager().getLocations()); hookManager = new HookManager(plugin); hookManager.loadHooks(plugin.getConfigManager().getLocations()); gateManager = new GateManager(plugin.getConfigManager().getLocations()); ActionBar ab = new ActionBar(); hookRescue = new HookRescue(plugin, ab); hookSacrifice = new HookSacrifice(plugin, null); palletDrop = new PalletDrop(palletManager); palletBreak = new PalletBreak(session.getConfig()); palletStun = new PalletStun(session.getConfig()); chestManager = new ChestManager(plugin, plugin.getConfigManager().getLocations()); chestOpening = new ChestOpening(plugin); // Store managers on session so other code can reach them 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(); 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.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.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 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.) vanillaDamageBlocker = new VanillaDamageBlocker(session); Bukkit.getPluginManager().registerEvents(vanillaDamageBlocker, plugin); killerController.initialize(); // --- Give starting items to survivors & register item use --- itemUseListener = new dev.loki.item.ItemUseListener(plugin, session); itemUseListener.register(); giveStartingItems(session); // --- Wire listeners --- moveListener = plugin.getMoveListener(); interactListener = plugin.getInteractListener(); wireMoveListener(session); wireInteractListener(session); Bukkit.broadcast(Component.text("[DBD] Игра началась! Выжившие, чините генераторы!", 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); } } @Override public void exit(GameSession session) { // Clean up listeners if (moveListener != null) { moveListener.setOnSurvivorMove(null); } if (interactListener != null) { interactListener.setOnPalletInteract(null); interactListener.setOnRescueAttempt(null); interactListener.setOnChestInteract(null); interactListener.setOnAttack(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 (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 for (BasePlayer bp : session.getPlayers().values()) { Player p = bp.getPlayer(); if (p != null && p.isOnline()) { p.setHealth(20.0); } } Bukkit.broadcast(Component.text("[DBD] Игровая фаза завершена!", NamedTextColor.GOLD)); } @Override public boolean isComplete() { return shouldEnd; } // ------------------------------------------------------------------------- // Starting items // ------------------------------------------------------------------------- private void giveStartingItems(GameSession session) { var loot = new dev.loki.item.ChestLoot(); for (BasePlayer bp : session.getPlayers().values()) { 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)); } } // ------------------------------------------------------------------------- // Hook ticking // ------------------------------------------------------------------------- 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"; vp.sendActionBar(Component.text(label + " " + (int) pct + "%")); } // Sacrifice when timer expires if (hook.getCurrentStage() == HookStage.SACRIFICE) { hookSacrifice.sacrificeIfExpired(victim, hook); } } } // ------------------------------------------------------------------------- // Generator / win-condition checks // ------------------------------------------------------------------------- private void checkGeneratorProgress(GameSession session) { if (allGensDone) return; if (generatorManager.areAllRequiredCompleted()) { allGensDone = true; gateManager.activateEndgame(); Bukkit.broadcast(Component.text( "[DBD] Все генераторы починены! Открывайте ворота!", NamedTextColor.GREEN)); for (BasePlayer bp : session.getPlayers().values()) { Player p = bp.getPlayer(); if (p != null) { p.playSound(p.getLocation(), Sound.ENTITY_FIREWORK_ROCKET_BLAST, 1.0f, 1.0f); } } } } private void checkWinConditions(GameSession session) { int aliveSurvivors = 0; int escapedSurvivors = 0; int totalSurvivors = 0; for (BasePlayer bp : session.getPlayers().values()) { if (bp.isKiller()) continue; totalSurvivors++; 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 aliveSurvivors++; } } if (totalSurvivors == 0) return; // no survivors registered yet boolean killerWin = (aliveSurvivors == 0 && escapedSurvivors == 0); boolean survivorsWin = (escapedSurvivors == totalSurvivors); if (killerWin) { announceKillerWin(session); shouldEnd = true; } else if (survivorsWin) { 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.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.playSound(p.getLocation(), Sound.ENTITY_WITHER_DEATH, 0.5f, 0.8f); } } } 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)); 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.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.playSound(p.getLocation(), Sound.ENTITY_WITHER_DEATH, 0.5f, 0.8f); } } } // ------------------------------------------------------------------------- // Listener wiring // ------------------------------------------------------------------------- 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); Player p = survivor.getPlayer(); if (p != null) { p.playSound(loc, Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.2f); Bukkit.broadcast(Component.text( "[DBD] " + p.getName() + " сбежал через ворота!", NamedTextColor.GOLD)); } } }); } 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; Pallet nearby = palletManager.getPalletAt(player.getLocation(), 3.0); if (nearby != null && nearby.isDropped()) { palletBreak.startBreaking(killer, 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); } }); // Killer right-click: hook a carried survivor interactListener.setOnAttack((player, isRightClick) -> { if (!isRightClick) return; // left-click handled by damage system 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()); } } } break; } } } }); // 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); } }); } // ------------------------------------------------------------------------- // Inner class: blocks all vanilla damage for players in the session // ------------------------------------------------------------------------- private static class VanillaDamageBlocker implements Listener { private final GameSession session; VanillaDamageBlocker(GameSession session) { this.session = session; } @EventHandler(priority = EventPriority.HIGHEST) public void onDamage(EntityDamageEvent event) { if (!(event.getEntity() instanceof Player p)) return; if (!session.getPlayers().containsKey(p.getUniqueId())) return; event.setCancelled(true); p.setHealth(20.0); } } }