diff --git a/src/main/java/dev/loki/core/GameSession.java b/src/main/java/dev/loki/core/GameSession.java index 3504639..85ed62e 100644 --- a/src/main/java/dev/loki/core/GameSession.java +++ b/src/main/java/dev/loki/core/GameSession.java @@ -1,8 +1,11 @@ package dev.loki.core; import dev.loki.config.GameConfig; +import dev.loki.gate.GateManager; +import dev.loki.hook.HookManager; import dev.loki.mdbd; import dev.loki.objective.GeneratorManager; +import dev.loki.pallet.PalletManager; import dev.loki.phase.EndgamePhase; import dev.loki.phase.GameplayPhase; import dev.loki.phase.LobbyPhase; @@ -27,6 +30,11 @@ public class GameSession { private RoleAssigner roleAssigner; private GeneratorManager generatorManager; + // Optional managers set by GameplayPhase + private PalletManager palletManager; + private HookManager hookManager; + private GateManager gateManager; + public GameSession(mdbd plugin) { this.plugin = plugin; this.eventBus = new GameEventBus(); @@ -146,6 +154,19 @@ public class GameSession { return generatorManager; } + // ------------------------------------------------------------------------- + // Gameplay-phase managers (set by GameplayPhase.enter()) + // ------------------------------------------------------------------------- + + public PalletManager getPalletManager() { return palletManager; } + public void setPalletManager(PalletManager pm) { this.palletManager = pm; } + + public HookManager getHookManager() { return hookManager; } + public void setHookManager(HookManager hm) { this.hookManager = hm; } + + public GateManager getGateManager() { return gateManager; } + public void setGateManager(GateManager gm) { this.gateManager = gm; } + public void forceStart() { if (currentPhase instanceof LobbyPhase lobby) { lobby.forceStart(); diff --git a/src/main/java/dev/loki/mdbd.java b/src/main/java/dev/loki/mdbd.java index 20033ba..6b4b9cd 100644 --- a/src/main/java/dev/loki/mdbd.java +++ b/src/main/java/dev/loki/mdbd.java @@ -8,6 +8,7 @@ import dev.loki.core.GameManager; import dev.loki.data.GeneratorDataManager; import dev.loki.listener.GeneratorSelectListener; import dev.loki.listener.PlayerInteractListener; +import dev.loki.listener.PlayerMoveListener; import dev.loki.listener.PlayerQuitListener; import dev.loki.map.MapBuilder; import dev.loki.map.TemplateManager; @@ -25,6 +26,8 @@ public final class mdbd extends JavaPlugin { private DBDAdminCommand dbdAdminCommand; private SetupCommand setupCommand; private LobbyManager lobbyManager; + private PlayerInteractListener interactListener; + private PlayerMoveListener moveListener; @Override public void onEnable() { @@ -62,12 +65,17 @@ public final class mdbd extends JavaPlugin { getServer().getPluginManager().registerEvents( new GeneratorSelectListener(dbdAdminCommand, generatorData), this); + getServer().getPluginManager().registerEvents( + new dev.loki.listener.SetupBlockBreakListener(setupCommand), this); getServer().getPluginManager().registerEvents( new PlayerQuitListener(this, gameManager), this); - var interactListener = new PlayerInteractListener(this, gameManager, null); + this.interactListener = new PlayerInteractListener(this, gameManager, null); interactListener.setSetupCommand(setupCommand); getServer().getPluginManager().registerEvents(interactListener, this); + this.moveListener = new PlayerMoveListener(this, gameManager); + getServer().getPluginManager().registerEvents(moveListener, this); + getLogger().info("Mdbd Plugin enabled successfully!"); getLogger().info("Loaded " + templateManager.getAllTemplates().size() + " templates"); getLogger().info("Loaded " + generatorData.getCount() + " generators from data"); @@ -120,6 +128,14 @@ public final class mdbd extends JavaPlugin { return lobbyManager; } + public PlayerInteractListener getInteractListener() { + return interactListener; + } + + public PlayerMoveListener getMoveListener() { + return moveListener; + } + public void reloadConfigs() { configManager.loadConfigs(); templateManager.loadAllTemplates(); diff --git a/src/main/java/dev/loki/pallet/Pallet.java b/src/main/java/dev/loki/pallet/Pallet.java index 66c8e46..c8bad92 100644 --- a/src/main/java/dev/loki/pallet/Pallet.java +++ b/src/main/java/dev/loki/pallet/Pallet.java @@ -2,13 +2,11 @@ package dev.loki.pallet; import org.bukkit.Location; import org.bukkit.Material; +import org.bukkit.Particle; +import org.bukkit.Sound; import org.bukkit.block.Block; public class Pallet { - private final Location location; - private PalletState state; - private Block topBlock; - private Block bottomBlock; public enum PalletState { STANDING, @@ -16,70 +14,111 @@ public class Pallet { BROKEN } - public Pallet(Location location) { - this.location = location; + private final Location center; // The OAK_LOG position saved in locations.yml + private Block leftBlock; + private Block centerBlock; + private Block rightBlock; + private PalletState state; + + public Pallet(Location center) { + this.center = center.clone(); this.state = PalletState.STANDING; - this.topBlock = location.clone().add(0, 1, 0).getBlock(); - this.bottomBlock = location.getBlock(); + detectStructure(); } + private boolean isPlanks(Block block) { + return block.getType() == Material.OAK_PLANKS; + } + + /** + * Detects which axis the PLP structure lies on (X or Z) by checking + * neighboring blocks. Falls back to X axis if neither looks right. + */ + private void detectStructure() { + Block c = center.getBlock(); + centerBlock = c; + + Block xLeft = center.clone().add(-1, 0, 0).getBlock(); + Block xRight = center.clone().add( 1, 0, 0).getBlock(); + Block zLeft = center.clone().add( 0, 0, -1).getBlock(); + Block zRight = center.clone().add( 0, 0, 1).getBlock(); + + // Prefer the axis where both neighbours are OAK_PLANKS + boolean xPlpValid = isPlanks(xLeft) && isPlanks(xRight); + boolean zPlpValid = isPlanks(zLeft) && isPlanks(zRight); + + if (xPlpValid) { + leftBlock = xLeft; + rightBlock = xRight; + } else if (zPlpValid) { + leftBlock = zLeft; + rightBlock = zRight; + } else { + // Fallback: use X axis regardless (blocks may have been loaded before + // the world is fully available; state ops will still work). + leftBlock = xLeft; + rightBlock = xRight; + } + } + + // ------------------------------------------------------------------------- + // State transitions + // ------------------------------------------------------------------------- + + public void setDropped() { + if (state != PalletState.STANDING) return; + state = PalletState.DROPPED; + + // Replace the OAK_LOG center with OAK_PLANKS (sides stay as-is) + centerBlock.setType(Material.OAK_PLANKS); + + // Sound + particles at center + Location loc = center.clone().add(0.5, 0.5, 0.5); + if (loc.getWorld() != null) { + loc.getWorld().playSound(loc, Sound.BLOCK_WOOD_BREAK, 1.0f, 0.7f); + loc.getWorld().spawnParticle(Particle.BLOCK, loc, 30, 0.5, 0.3, 0.5, 0.1, + Material.OAK_PLANKS.createBlockData()); + } + } + + public void setBroken() { + state = PalletState.BROKEN; + leftBlock.setType(Material.AIR); + centerBlock.setType(Material.AIR); + rightBlock.setType(Material.AIR); + } + + public void reset() { + state = PalletState.STANDING; + // Restore PLP structure + leftBlock.setType(Material.OAK_PLANKS); + centerBlock.setType(Material.OAK_LOG); + rightBlock.setType(Material.OAK_PLANKS); + } + + // ------------------------------------------------------------------------- + // Accessors + // ------------------------------------------------------------------------- + public Location getLocation() { - return location; + return center.clone(); + } + + /** Centre of the pallet block for distance calculations. */ + public Location getCenter() { + return center.clone().add(0.5, 0.5, 0.5); } public PalletState getState() { return state; } - public boolean isStanding() { - return state == PalletState.STANDING; - } - - public boolean isDropped() { - return state == PalletState.DROPPED; - } - - public boolean isBroken() { - return state == PalletState.BROKEN; - } - - public void setDropped() { - this.state = PalletState.DROPPED; - updateVisuals(); - } - - public void setBroken() { - this.state = PalletState.BROKEN; - clearBlocks(); - } - - public void reset() { - this.state = PalletState.STANDING; - restoreBlocks(); - } - - private void updateVisuals() { - if (state == PalletState.DROPPED) { - bottomBlock.setType(Material.OAK_PLANKS); - topBlock.setType(Material.OAK_PLANKS); - } - } - - private void clearBlocks() { - bottomBlock.setType(Material.AIR); - topBlock.setType(Material.AIR); - } - - private void restoreBlocks() { - bottomBlock.setType(Material.OAK_FENCE); - topBlock.setType(Material.AIR); - } - - public Location getCenter() { - return location.clone().add(0.5, 0.5, 0.5); - } + public boolean isStanding() { return state == PalletState.STANDING; } + public boolean isDropped() { return state == PalletState.DROPPED; } + public boolean isBroken() { return state == PalletState.BROKEN; } public double distanceTo(Location loc) { + if (!getCenter().getWorld().equals(loc.getWorld())) return Double.MAX_VALUE; return getCenter().distance(loc); } } diff --git a/src/main/java/dev/loki/phase/GameplayPhase.java b/src/main/java/dev/loki/phase/GameplayPhase.java index 5ae81ec..1c3f5be 100644 --- a/src/main/java/dev/loki/phase/GameplayPhase.java +++ b/src/main/java/dev/loki/phase/GameplayPhase.java @@ -1,9 +1,29 @@ package dev.loki.phase; +import dev.loki.chest.ChestManager; +import dev.loki.chest.ChestOpening; import dev.loki.core.GameSession; +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.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.PlayerState; +import dev.loki.player.Survivor; +import dev.loki.ui.ActionBar; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Bukkit; @@ -11,23 +31,71 @@ import org.bukkit.Sound; import org.bukkit.entity.Player; 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; + + // Listener references grabbed from plugin + private PlayerMoveListener moveListener; + private PlayerInteractListener interactListener; + public GameplayPhase(GeneratorManager generatorManager) { this.generatorManager = generatorManager; - this.tickCounter = 0; - this.shouldEnd = false; - this.allGensDone = false; } + // ------------------------------------------------------------------------- + // Phase lifecycle + // ------------------------------------------------------------------------- + @Override public void enter(GameSession session) { - Bukkit.broadcast(Component.text("[DBD] Игра началась! Выжившие, чините генераторы!", NamedTextColor.GREEN)); + mdbd plugin = session.getPlugin(); tickCounter = 0; - // Teleport is handled by RoleAssigner.assignRoles — no double-teleport needed here + 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); + + // --- Wire listeners --- + moveListener = plugin.getMoveListener(); + interactListener = plugin.getInteractListener(); + + wireMoveListener(session); + wireInteractListener(session); + + Bukkit.broadcast(Component.text("[DBD] Игра началась! Выжившие, чините генераторы!", + NamedTextColor.GREEN)); } @Override @@ -35,19 +103,92 @@ public class GameplayPhase implements Phase { 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(); + } + + Bukkit.broadcast(Component.text("[DBD] Игровая фаза завершена!", NamedTextColor.GOLD)); + } + + @Override + public boolean isComplete() { + return shouldEnd; + } + + // ------------------------------------------------------------------------- + // 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) { @@ -58,36 +199,127 @@ public class GameplayPhase implements Phase { } private void checkWinConditions(GameSession session) { - int aliveSurvivors = 0; + int aliveSurvivors = 0; int escapedSurvivors = 0; - int totalSurvivors = 0; + int totalSurvivors = 0; for (BasePlayer bp : session.getPlayers().values()) { if (bp.isKiller()) continue; totalSurvivors++; - PlayerState state = bp.getState(); - if (state == PlayerState.ESCAPED) { + PlayerState st = bp.getState(); + if (st == PlayerState.ESCAPED) { escapedSurvivors++; - } else if (state != PlayerState.DEAD && state != PlayerState.HOOKED) { + } else if (st != PlayerState.DEAD && st != PlayerState.HOOKED) { aliveSurvivors++; } } - boolean allSurvivorsDead = (aliveSurvivors == 0 && escapedSurvivors == 0 && totalSurvivors > 0); + boolean allDead = (aliveSurvivors == 0 && escapedSurvivors == 0 && totalSurvivors > 0); boolean allEscaped = (escapedSurvivors == totalSurvivors && totalSurvivors > 0); - if (allSurvivorsDead || allEscaped) { + if (allDead || allEscaped) { shouldEnd = true; } } - @Override - public void exit(GameSession session) { - Bukkit.broadcast(Component.text("[DBD] Игровая фаза завершена!", NamedTextColor.GOLD)); + // ------------------------------------------------------------------------- + // 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)); + } + } + }); } - @Override - public boolean isComplete() { - return shouldEnd; + 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); + } + }); } }