diff --git a/src/main/java/dev/loki/objective/GeneratorRepair.java b/src/main/java/dev/loki/objective/GeneratorRepair.java index e51f5b4..49cd7c7 100644 --- a/src/main/java/dev/loki/objective/GeneratorRepair.java +++ b/src/main/java/dev/loki/objective/GeneratorRepair.java @@ -20,6 +20,8 @@ public class GeneratorRepair { private final GeneratorManager manager; private final Map sessions; private final GeneratorMinigame minigame; + private final Map compassTaskIds = new HashMap<>(); + private final Map compassPrevItems = new HashMap<>(); private static final double REPAIR_PER_TICK = 100.0 / 1600.0; private static final double REPAIR_RANGE = 3.0; @@ -201,14 +203,47 @@ public class GeneratorRepair { compass.setItemMeta(meta); } - ItemStack prev = online.getInventory().getItemInOffHand(); + UUID killerId = online.getUniqueId(); + + Integer prevTaskId = compassTaskIds.remove(killerId); + if (prevTaskId != null) { + Bukkit.getScheduler().cancelTask(prevTaskId); + } + + compassPrevItems.put(killerId, online.getInventory().getItemInOffHand()); online.getInventory().setItemInOffHand(compass); - plugin.getServer().getScheduler().runTaskLater(plugin, () -> { - ItemStack current = online.getInventory().getItemInOffHand(); - if (current.getType() == Material.COMPASS) { - online.getInventory().setItemInOffHand(prev); + + int taskId = Bukkit.getScheduler().runTaskLater(plugin, () -> { + if (!online.isOnline()) return; + + ItemStack offHand = online.getInventory().getItemInOffHand(); + if (offHand.getType() == Material.COMPASS) { + online.getInventory().setItemInOffHand(compassPrevItems.get(killerId)); + compassTaskIds.remove(killerId); + compassPrevItems.remove(killerId); + return; } - }, durationTicks); + + ItemStack mainHand = online.getInventory().getItemInMainHand(); + if (mainHand.getType() == Material.COMPASS) { + online.getInventory().setItemInMainHand(compassPrevItems.get(killerId)); + compassTaskIds.remove(killerId); + compassPrevItems.remove(killerId); + return; + } + + for (int i = 0; i < online.getInventory().getSize(); i++) { + ItemStack item = online.getInventory().getItem(i); + if (item != null && item.getType() == Material.COMPASS) { + online.getInventory().setItem(i, null); + break; + } + } + compassTaskIds.remove(killerId); + compassPrevItems.remove(killerId); + }, durationTicks).getTaskId(); + + compassTaskIds.put(killerId, taskId); } } diff --git a/src/main/java/dev/loki/phase/EndgamePhase.java b/src/main/java/dev/loki/phase/EndgamePhase.java index 65b8624..d131f39 100644 --- a/src/main/java/dev/loki/phase/EndgamePhase.java +++ b/src/main/java/dev/loki/phase/EndgamePhase.java @@ -31,6 +31,7 @@ public class EndgamePhase implements Phase { private HatchCondition hatchCondition; private HatchEscape hatchEscape; private Sidebar sidebar; + private int endgameTimer = -1; // секунд до конца эндгейма (-1 = не активно) public EndgamePhase() { this.openTicks = 20 * 20; @@ -100,6 +101,15 @@ public class EndgamePhase implements Phase { @Override public void tick(GameSession session) { + updateGates(session); + + if (endgameTimer > 0 && session.getTicksElapsed() % 20 == 0) { + endgameTimer--; + if (sidebar != null) { + sidebar.setEndgameTimer(endgameTimer); + } + } + // Update sidebar stats every 40 ticks if (session.getTicksElapsed() % 40 == 0 && sidebar != null) { for (BasePlayer bp : session.getPlayers().values()) { @@ -198,6 +208,12 @@ public class EndgamePhase implements Phase { // ------------------------------------------------------------------------- private void activateEndgame(GameSession session) { + if (endgameTimer < 0) { + endgameTimer = session.getConfig().getPostgameSeconds(); + } + if (sidebar != null) { + sidebar.setEndgameTimer(endgameTimer); + } if (gateManager != null && !gatesActivated) { gateManager.activateEndgame(); gatesActivated = true; @@ -268,26 +284,40 @@ public class EndgamePhase implements Phase { } if (survivorsAlive == 0) { complete = true; + announceEndgameEnd(session); + teleportAllToLobby(session); } } private void announceEndgameEnd(GameSession session) { List results = new ArrayList<>(); - results.add("§6§l=== РЕЗУЛЬТАТЫ ==="); - int escaped = 0, died = 0; + int escaped = 0, died = 0, sacrificed = 0, leftBehind = 0; for (BasePlayer player : session.getPlayers().values()) { if (player.isKiller()) continue; Player p = player.getPlayer(); String name = p != null ? p.getName() : "Unknown"; - if (player.getState() == PlayerState.ESCAPED) { - results.add("§a✔ " + name + " — Сбежал"); - escaped++; - } else { - results.add("§c✘ " + name + " — Погиб"); - died++; + switch (player.getState()) { + case ESCAPED: + results.add("§a✔ " + name + " — Сбежал"); + escaped++; + break; + case DEAD: + results.add("§c✘ " + name + " — Погиб"); + died++; + break; + case HOOKED: + case CARRIED: + results.add("§4✘ " + name + " — Принесён в жертву"); + sacrificed++; + break; + default: + results.add("§7✘ " + name + " — Оставлен"); + leftBehind++; + break; } } - results.add("§7Выживших: §a" + escaped + " §7/ Погибших: §c" + died); + results.add("§7Выживших: §a" + escaped + " §7/ Погибших: §c" + died + + " §7/ В жертву: §4" + sacrificed + " §7/ Оставлено: §7" + leftBehind); String msg = String.join("\n", results); for (BasePlayer player : session.getPlayers().values()) { Player p = player.getPlayer(); diff --git a/src/main/java/dev/loki/phase/GameplayPhase.java b/src/main/java/dev/loki/phase/GameplayPhase.java index 44ce48f..21c51a0 100644 --- a/src/main/java/dev/loki/phase/GameplayPhase.java +++ b/src/main/java/dev/loki/phase/GameplayPhase.java @@ -113,6 +113,9 @@ public class GameplayPhase implements Phase { if (generatorRepair != null) generatorRepair.tick(); if (interaction != null) interaction.tickSurvivors(); hookHandler.tick(); + if (ui.tickMatchTimer(session)) { + shouldEnd = true; + } // Particle effects around generators (every 10 ticks) if (tickCounter % 10 == 0 && generatorManager != null) { @@ -161,8 +164,6 @@ public class GameplayPhase implements Phase { ui.resetPlayers(session); damageSystem.clear(); damageSystem.clearManagers(); - - Bukkit.broadcast(session.getPlugin().getConfigManager().getMessages().get("game.end")); } @Override @@ -371,6 +372,9 @@ public class GameplayPhase implements Phase { p.playSound(p.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.2f); Bukkit.broadcast(Component.text( "§6[DBD] §a" + p.getName() + " §6сбежал через ворота!")); + p.setGameMode(GameMode.SPECTATOR); + p.getInventory().clear(); + session.getPlugin().getLobbyManager().teleportToLobby(p); } } } diff --git a/src/main/java/dev/loki/phase/GameplayWinConditions.java b/src/main/java/dev/loki/phase/GameplayWinConditions.java index 2f91504..a2d96c7 100644 --- a/src/main/java/dev/loki/phase/GameplayWinConditions.java +++ b/src/main/java/dev/loki/phase/GameplayWinConditions.java @@ -57,24 +57,24 @@ public class GameplayWinConditions { return; } - // Killer wins: all survivors are downed/hooked/carried/dead - if (allSurvivorsIncapacitated(session)) { - winReason = "killer_win"; - Bukkit.broadcast(session.getPlugin().getConfigManager().getMessages().get("game.all-incapacitated")); - announceKillerWin(session); - shouldEnd = true; - return; - } - // Survivors win: all generators done and at least one escaped if (hasEscapedSurvivor(session)) { winReason = "survivors_win"; announceSurvivorsWin(session); shouldEnd = true; + return; + } + + // Killer wins: all survivors are downed/hooked/carried/dead (ESCAPED excluded) + if (allSurvivorsIncapacitated(session)) { + winReason = "killer_win"; + Bukkit.broadcast(session.getPlugin().getConfigManager().getMessages().get("game.all-incapacitated")); + announceKillerWin(session); + shouldEnd = true; } } - /** True if NO survivor can act (all are DOWNED, HOOKED, CARRIED, DEAD, or ESCAPED). */ + /** True if NO survivor can act (all are DOWNED, HOOKED, CARRIED, DEAD). */ private boolean allSurvivorsIncapacitated(GameSession session) { boolean hasSurvivors = false; for (BasePlayer bp : session.getPlayers().values()) { @@ -82,8 +82,7 @@ public class GameplayWinConditions { hasSurvivors = true; PlayerState st = bp.getState(); if (st != PlayerState.DOWNED && st != PlayerState.HOOKED - && st != PlayerState.CARRIED && st != PlayerState.DEAD - && st != PlayerState.ESCAPED) { + && st != PlayerState.CARRIED && st != PlayerState.DEAD) { return false; // at least one survivor can still act } } @@ -121,7 +120,7 @@ public class GameplayWinConditions { for (BasePlayer bp : session.getPlayers().values()) { Player p = bp.getPlayer(); if (p == null || !p.isOnline()) continue; - if (!bp.isKiller() && bp.getState() == PlayerState.DEAD) { + if (!bp.isKiller() && (bp.getState() == PlayerState.DEAD || bp.getState() == PlayerState.ESCAPED)) { p.setGameMode(GameMode.SPECTATOR); } else { p.setGameMode(GameMode.SURVIVAL); diff --git a/src/test/java/dev/loki/gate/ExitGateTest.java b/src/test/java/dev/loki/gate/ExitGateTest.java index 55e4242..81c288e 100644 --- a/src/test/java/dev/loki/gate/ExitGateTest.java +++ b/src/test/java/dev/loki/gate/ExitGateTest.java @@ -26,18 +26,17 @@ class ExitGateTest { } @Test - void setPowered_shouldTransitionToOpening() { + void setPowered_shouldStayClosed() { gate.setPowered(true); - assertEquals(ExitGate.GateState.OPENING, gate.getState()); + assertEquals(ExitGate.GateState.CLOSED, gate.getState()); assertTrue(gate.isPowered()); - assertTrue(gate.isOpening()); - assertFalse(gate.isClosed()); - assertFalse(gate.isOpen()); + assertTrue(gate.isClosed()); } @Test void setProgressDirect_shouldIncreaseProgress() { gate.setPowered(true); + gate.startOpening(); gate.setProgressDirect(0.5); assertEquals(0.5, gate.getProgress()); assertEquals(50, gate.getProgressPercent()); @@ -47,6 +46,7 @@ class ExitGateTest { @Test void setProgressDirect_shouldCompleteWhenReachingOne() { gate.setPowered(true); + gate.startOpening(); gate.setProgressDirect(1.0); assertEquals(1.0, gate.getProgress()); assertEquals(ExitGate.GateState.OPEN, gate.getState()); @@ -56,6 +56,7 @@ class ExitGateTest { @Test void setProgressDirect_shouldClampToMax() { gate.setPowered(true); + gate.startOpening(); gate.setProgressDirect(2.0); assertEquals(1.0, gate.getProgress()); assertTrue(gate.isOpen()); @@ -64,6 +65,7 @@ class ExitGateTest { @Test void setProgressDirect_shouldNotExceedMin() { gate.setPowered(true); + gate.startOpening(); gate.setProgressDirect(-0.5); assertEquals(0.0, gate.getProgress()); } @@ -79,6 +81,7 @@ class ExitGateTest { @Test void updateProgress_shouldWorkIncrementally() { gate.setPowered(true); + gate.startOpening(); gate.updateProgress(0.3); assertEquals(0.3, gate.getProgress()); gate.updateProgress(0.3); @@ -101,6 +104,7 @@ class ExitGateTest { @Test void saveAndRestoreProgress_shouldPersist() { gate.setPowered(true); + gate.startOpening(); gate.setProgressDirect(0.4); assertEquals(0.4, gate.getProgress()); diff --git a/src/test/java/dev/loki/phase/GameplayWinConditionsTest.java b/src/test/java/dev/loki/phase/GameplayWinConditionsTest.java index 2b0cbac..bf89cf8 100644 --- a/src/test/java/dev/loki/phase/GameplayWinConditionsTest.java +++ b/src/test/java/dev/loki/phase/GameplayWinConditionsTest.java @@ -98,14 +98,14 @@ class GameplayWinConditionsTest { // ========================================================================= @Test - void incapacitated_withEscapedAndRestDisabled_shouldReturnTrue() { + void incapacitated_withEscapedAndRestDisabled_shouldReturnFalse() { Map players = new HashMap<>(); players.put(UUID.randomUUID(), onlineKiller()); players.put(UUID.randomUUID(), survivorWithState(PlayerState.ESCAPED)); players.put(UUID.randomUUID(), survivorWithState(PlayerState.DOWNED)); - assertTrue(allIncapacitated(sessionWith(players)), - "ESCAPED survivors should be counted as incapacitated (bug #3)"); + assertFalse(allIncapacitated(sessionWith(players)), + "ESCAPED survivors should NOT count as incapacitated for killer win"); } @Test @@ -120,13 +120,14 @@ class GameplayWinConditionsTest { } @Test - void incapacitated_withAllTerminalStates_shouldReturnTrue() { + void incapacitated_withEscapedAndDead_shouldReturnFalse() { Map players = new HashMap<>(); players.put(UUID.randomUUID(), onlineKiller()); players.put(UUID.randomUUID(), survivorWithState(PlayerState.DEAD)); players.put(UUID.randomUUID(), survivorWithState(PlayerState.ESCAPED)); - assertTrue(allIncapacitated(sessionWith(players))); + assertFalse(allIncapacitated(sessionWith(players)), + "ESCAPED should not count as incapacitated"); } // =========================================================================