From d2b174b94c77d8b122523cbffc2e2f6a427b2d62 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Mon, 15 Jun 2026 10:22:02 +0200 Subject: [PATCH] =?UTF-8?q?fix:=2010=20bug=20fixes=20=E2=80=94=20healing?= =?UTF-8?q?=20dead=20players,=20kick=20bypass,=20gate=20race,=20swimming?= =?UTF-8?q?=20ghost,=20regression=20config,=20dead=20code=20cleanup,=20gat?= =?UTF-8?q?e=20zone=20detection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/dev/loki/chest/Chest.java | 19 ++ .../java/dev/loki/chest/ChestHologram.java | 10 +- .../java/dev/loki/chest/ChestManager.java | 35 +-- .../java/dev/loki/chest/ChestOpening.java | 14 +- .../java/dev/loki/command/SetupCommand.java | 157 ++++++++++- src/main/java/dev/loki/config/GameConfig.java | 6 + .../java/dev/loki/config/LocationsConfig.java | 15 ++ src/main/java/dev/loki/core/GameSession.java | 3 +- src/main/java/dev/loki/gate/ExitGate.java | 22 +- src/main/java/dev/loki/gate/GateManager.java | 6 + src/main/java/dev/loki/gate/GateOpening.java | 22 +- src/main/java/dev/loki/hook/Hook.java | 2 + src/main/java/dev/loki/hook/HookManager.java | 5 +- src/main/java/dev/loki/item/ItemMedkit.java | 6 +- src/main/java/dev/loki/item/ItemType.java | 4 +- .../java/dev/loki/killer/GeneratorKick.java | 10 +- .../loki/listener/PlayerInteractListener.java | 38 +-- .../dev/loki/objective/GeneratorManager.java | 11 +- .../loki/objective/GeneratorRegression.java | 52 ---- .../dev/loki/objective/GeneratorRepair.java | 16 +- .../java/dev/loki/objective/SkillCheck.java | 142 ---------- .../loki/objective/SkillCheckInventory.java | 245 ------------------ .../java/dev/loki/objective/SkillCheckUI.java | 120 --------- .../java/dev/loki/phase/EndgamePhase.java | 14 +- .../dev/loki/phase/GameplayDamageSystem.java | 40 ++- .../dev/loki/phase/GameplayHookHandler.java | 52 +++- .../dev/loki/phase/GameplayInteraction.java | 37 +-- .../java/dev/loki/phase/GameplayPhase.java | 144 +++++++--- .../dev/loki/survivor/SurvivorHealing.java | 8 +- .../dev/loki/survivor/SurvivorMovement.java | 2 +- src/main/java/dev/loki/ui/ActionBar.java | 33 +-- src/main/java/dev/loki/ui/Sidebar.java | 18 +- src/main/java/dev/loki/ui/TitleUI.java | 20 +- src/main/resources/game.yml | 9 + src/main/resources/messages.yml | 23 +- 35 files changed, 587 insertions(+), 773 deletions(-) delete mode 100644 src/main/java/dev/loki/objective/GeneratorRegression.java delete mode 100644 src/main/java/dev/loki/objective/SkillCheck.java delete mode 100644 src/main/java/dev/loki/objective/SkillCheckInventory.java delete mode 100644 src/main/java/dev/loki/objective/SkillCheckUI.java diff --git a/src/main/java/dev/loki/chest/Chest.java b/src/main/java/dev/loki/chest/Chest.java index 9e15fad..69de308 100644 --- a/src/main/java/dev/loki/chest/Chest.java +++ b/src/main/java/dev/loki/chest/Chest.java @@ -15,6 +15,9 @@ public class Chest { private boolean opened; private boolean empty; private ItemStack loot; + private long lastEmptiedTime; + + private static final long COOLDOWN_MILLIS = 3 * 60 * 1000; // 3 minutes public Chest(Location location) { this.id = UUID.randomUUID(); @@ -22,6 +25,7 @@ public class Chest { this.opened = false; this.empty = false; this.loot = null; + this.lastEmptiedTime = 0; } public UUID getId() { @@ -52,6 +56,21 @@ public class Chest { return !opened && !empty && hasLoot(); } + public void onLooted() { + this.empty = true; + this.lastEmptiedTime = System.currentTimeMillis(); + } + + public long getSecondsUntilReset() { + long elapsed = (System.currentTimeMillis() - lastEmptiedTime) / 1000; + long remaining = (COOLDOWN_MILLIS / 1000) - elapsed; + return Math.max(0, remaining); + } + + public boolean isOnCooldown() { + return getSecondsUntilReset() > 0; + } + public ItemStack getLoot() { return loot; } diff --git a/src/main/java/dev/loki/chest/ChestHologram.java b/src/main/java/dev/loki/chest/ChestHologram.java index 5a1c0d2..a0975ba 100644 --- a/src/main/java/dev/loki/chest/ChestHologram.java +++ b/src/main/java/dev/loki/chest/ChestHologram.java @@ -118,14 +118,8 @@ public class ChestHologram { String status; if (chest.isEmpty()) { - long secs = manager.getSecondsUntilNextReset(); - if (secs > 60) { - status = "§7Пуст §8(" + (secs / 60) + "м)"; - } else if (secs > 0) { - status = "§7Пуст §8(" + secs + "с)"; - } else { - status = "§7Пуст"; - } + long secs = chest.getSecondsUntilReset(); + status = secs > 0 ? "§7Пусто §8(" + secs + "с)" : "§7Пусто"; } else if (chest.isOpened()) { status = "§6Открывается..."; } else if (chest.hasLoot()) { diff --git a/src/main/java/dev/loki/chest/ChestManager.java b/src/main/java/dev/loki/chest/ChestManager.java index f38b4e7..ec8866d 100644 --- a/src/main/java/dev/loki/chest/ChestManager.java +++ b/src/main/java/dev/loki/chest/ChestManager.java @@ -21,7 +21,6 @@ public class ChestManager { private final GameConfig gameConfig; private ChestLoot chestLoot; private BukkitTask lootResetTask; - private long lastResetTime = System.currentTimeMillis(); public ChestManager(JavaPlugin plugin, LocationsConfig locationsConfig, GameConfig gameConfig) { this.plugin = plugin; @@ -36,7 +35,6 @@ public class ChestManager { List locations = locationsConfig.getChests(); for (Location loc : locations) { Chest chest = new Chest(loc); - // Populate loot immediately so it's available chest.setLoot(chestLoot.rollLoot()); chests.put(chest.getId(), chest); } @@ -44,14 +42,12 @@ public class ChestManager { } /** - * Start periodic loot reset timer. - * Every N minutes (from config), all empty chests get new loot. + * Start per-chest cooldown timer. Each chest refills individually 3 min after being looted. */ public void startLootResetTimer() { if (lootResetTask != null) lootResetTask.cancel(); - long intervalTicks = (long) (gameConfig.getChestLootResetMinutes() * 60 * 20); - lootResetTask = plugin.getServer().getScheduler().runTaskTimer(plugin, this::resetLoot, intervalTicks, intervalTicks); - plugin.getLogger().info("Chest loot reset timer started (every " + gameConfig.getChestLootResetMinutes() + " min)"); + lootResetTask = plugin.getServer().getScheduler().runTaskTimer(plugin, this::tickCooldowns, 20L, 20L); + plugin.getLogger().info("Per-chest cooldown timer started (3 min per chest)"); } public void stopLootResetTimer() { @@ -62,20 +58,16 @@ public class ChestManager { } /** - * Reset loot: refill ALL chests with new random loot. + * Every second, check each chest — if its cooldown expired, refill it. */ - private void resetLoot() { - int refilled = 0; + private void tickCooldowns() { for (Chest chest : chests.values()) { - // Skip chests currently being opened to avoid race condition if (chest.isOpened()) continue; - chest.setLoot(chestLoot.rollLoot()); - chest.setOpened(false); - chest.setEmpty(false); - refilled++; + if (chest.isEmpty() && !chest.isOnCooldown()) { + chest.setLoot(chestLoot.rollLoot()); + chest.setEmpty(false); + } } - this.lastResetTime = System.currentTimeMillis(); - plugin.getLogger().info("[Chest] Loot reset — " + refilled + " chests refilled"); } public Chest getChest(UUID id) { @@ -117,7 +109,6 @@ public class ChestManager { chest.setEmpty(false); chest.setLoot(chestLoot.rollLoot()); } - this.lastResetTime = System.currentTimeMillis(); plugin.getLogger().info("Reset all chests"); } @@ -140,13 +131,7 @@ public class ChestManager { return (int) chests.values().stream().filter(Chest::isAvailable).count(); } - /** Seconds until the next automatic loot reset. */ - public long getSecondsUntilNextReset() { - long elapsed = (System.currentTimeMillis() - lastResetTime) / 1000; - long total = (long) (gameConfig.getChestLootResetMinutes() * 60); - long remaining = total - elapsed; - return Math.max(0, remaining); - } + public ChestLoot getChestLoot() { return chestLoot; diff --git a/src/main/java/dev/loki/chest/ChestOpening.java b/src/main/java/dev/loki/chest/ChestOpening.java index 32a4122..1618e92 100644 --- a/src/main/java/dev/loki/chest/ChestOpening.java +++ b/src/main/java/dev/loki/chest/ChestOpening.java @@ -47,15 +47,9 @@ public class ChestOpening { return false; } - // Ensure chest has loot; roll if missing - if (!chest.hasLoot()) { - ItemStack rolled = chestLoot.rollLoot(); - if (rolled == null) { - player.sendMessage(messages().get("chest.empty")); - return false; - } - chest.setLoot(rolled); - chest.setEmpty(false); + if (!chest.hasLoot() || chest.isEmpty()) { + player.sendMessage(messages().get("chest.empty")); + return false; } if (!chest.isAvailable()) { @@ -152,7 +146,7 @@ public class ChestOpening { } } chest.setOpened(false); - chest.setEmpty(true); + chest.onLooted(); chest.setLoot(null); // Clear loot after giving chest.closeVisual(); diff --git a/src/main/java/dev/loki/command/SetupCommand.java b/src/main/java/dev/loki/command/SetupCommand.java index c9be15c..92c09dc 100644 --- a/src/main/java/dev/loki/command/SetupCommand.java +++ b/src/main/java/dev/loki/command/SetupCommand.java @@ -3,6 +3,7 @@ package dev.loki.command; import dev.loki.mdbd; import dev.loki.util.SpawnUtil; import dev.loki.util.TextUtil; +import dev.loki.config.LocationsConfig; import dev.loki.config.MessagesConfig; import org.bukkit.Location; import org.bukkit.Material; @@ -70,7 +71,8 @@ public class SetupCommand implements TabExecutor { return switch (args[0].toLowerCase()) { case "wand" -> giveWand(player); case "set" -> handleSet(player, args); - case "clear" -> clearMap(player); + case "clear" -> handleClear(player, args); + case "delete" -> handleDelete(player, args); case "clear-spawns" -> clearSpawns(player); case "lobby" -> setLobby(player); default -> { sendUsage(sender); yield true; } @@ -331,6 +333,147 @@ public class SetupCommand implements TabExecutor { return true; } + // ── Clear ──────────────────────────────────────────────────────── + + private boolean handleClear(Player player, String[] args) { + var locations = plugin.getConfigManager().getLocations(); + + if (args.length >= 2) { + String type = args[1].toLowerCase(); + return clearObjectsByType(player, locations, type); + } + + // Clear ALL object data from config (no block destruction) + locations.clearLocationList("generators"); + locations.clearLocationList("hooks"); + locations.clearLocationList("chests"); + locations.clearLocationList("killer-spawn"); + for (String id : List.of("a", "b")) { + locations.getConfig().set("gate-" + id, null); + locations.getConfig().set("gate-" + id + "-corner1", null); + locations.getConfig().set("gate-" + id + "-corner2", null); + } + locations.clearSurvivorSpawns(); + plugin.getGeneratorData().clearGenerators(); + plugin.getConfigManager().saveConfig("locations.yml", locations.getConfig()); + player.sendMessage(messages().get("setup.cleared")); + return true; + } + + private boolean clearObjectsByType(Player player, LocationsConfig locations, String type) { + String path = switch (type) { + case "generator" -> "generators"; + case "hook" -> "hooks"; + case "chest" -> "chests"; + case "gate" -> null; + case "killer-spawn" -> "killer-spawn"; + case "survivor-spawn" -> null; + default -> { player.sendMessage(messages().get("setup.unknown-type", type)); yield null; } + }; + if (path == null) { + if (type.equals("gate")) { + for (String id : List.of("a", "b")) { + locations.getConfig().set("gate-" + id, null); + locations.getConfig().set("gate-" + id + "-corner1", null); + locations.getConfig().set("gate-" + id + "-corner2", null); + } + } else if (type.equals("survivor-spawn")) { + locations.clearSurvivorSpawns(); + } else { + return true; + } + } else { + locations.clearLocationList(path); + if (type.equals("generator")) { + plugin.getGeneratorData().clearGenerators(); + } + } + plugin.getConfigManager().saveConfig("locations.yml", locations.getConfig()); + player.sendMessage(messages().get("setup.cleared-type", type)); + return true; + } + + // ── Delete ─────────────────────────────────────────────────────── + + private boolean handleDelete(Player player, String[] args) { + if (args.length < 3) { + player.sendMessage(messages().get("setup.delete-usage")); + return true; + } + + String type = args[1].toLowerCase(); + int index; + try { + index = Integer.parseInt(args[2]); + } catch (NumberFormatException e) { + player.sendMessage(messages().get("setup.invalid-number", args[2])); + return true; + } + + var locations = plugin.getConfigManager().getLocations(); + + switch (type) { + case "generator" -> { + var gens = locations.getGenerators(); + if (index < 0 || index >= gens.size()) { + player.sendMessage(messages().get("setup.out-of-range", index)); + return true; + } + locations.removeLocationAtIndex("generators", index); + plugin.getGeneratorData().removeGenerator(index); + player.sendMessage(messages().get("setup.deleted", "generator #" + index)); + } + case "hook" -> { + var hooks = locations.getHooks(); + if (index < 0 || index >= hooks.size()) { + player.sendMessage(messages().get("setup.out-of-range", index)); + return true; + } + locations.removeLocationAtIndex("hooks", index); + player.sendMessage(messages().get("setup.deleted", "hook #" + index)); + } + case "chest" -> { + var chests = locations.getChests(); + if (index < 0 || index >= chests.size()) { + player.sendMessage(messages().get("setup.out-of-range", index)); + return true; + } + locations.removeLocationAtIndex("chests", index); + player.sendMessage(messages().get("setup.deleted", "chest #" + index)); + } + case "killer-spawn" -> { + locations.clearLocationList("killer-spawn"); + player.sendMessage(messages().get("setup.deleted", "killer-spawn")); + } + case "survivor-spawn" -> { + if (index < 1 || index > 4) { + player.sendMessage(messages().get("setup.out-of-range", index)); + return true; + } + locations.clearSurvivorSpawn(index); + player.sendMessage(messages().get("setup.deleted", "survivor-spawn #" + index)); + } + case "gate" -> { + String id = index == 1 ? "a" : index == 2 ? "b" : null; + if (id == null) { + player.sendMessage(messages().get("setup.out-of-range", index)); + return true; + } + locations.getConfig().set("gate-" + id, null); + locations.getConfig().set("gate-" + id + "-corner1", null); + locations.getConfig().set("gate-" + id + "-corner2", null); + player.sendMessage(messages().get("setup.deleted", "gate-" + id)); + } + default -> { + player.sendMessage(messages().get("setup.unknown-type", type)); + return true; + } + } + + plugin.getConfigManager().saveConfig("locations.yml", locations.getConfig()); + return true; + } + private boolean clearSpawns(Player player) { var locations = plugin.getConfigManager().getLocations(); locations.clearLocationList("killer-spawn"); @@ -359,6 +502,8 @@ public class SetupCommand implements TabExecutor { sender.sendMessage(messages().get("usage.setup-killer-spawn")); sender.sendMessage(messages().get("usage.setup-survivor-spawn")); sender.sendMessage(messages().get("usage.setup-gate")); + sender.sendMessage(messages().get("usage.setup-clear")); + sender.sendMessage(messages().get("usage.setup-delete")); sender.sendMessage(messages().get("usage.setup-clear-spawns")); sender.sendMessage(messages().get("usage.setup-lobby")); } @@ -373,12 +518,14 @@ public class SetupCommand implements TabExecutor { @NotNull String label, @NotNull String[] args) { if (!sender.hasPermission("mdbd.admin")) return List.of(); if (args.length == 1) { - return List.of("wand", "set", "clear", "clear-spawns", "lobby").stream() + return List.of("wand", "set", "clear", "delete", "clear-spawns", "lobby").stream() .filter(s -> s.startsWith(args[0].toLowerCase())).toList(); } - if (args.length == 2 && args[0].equalsIgnoreCase("set")) { - return List.of("hook", "chest", "generator", "killer-spawn", "survivor-spawn", "gate").stream() - .filter(s -> s.startsWith(args[1].toLowerCase())).toList(); + if (args.length == 2) { + if (args[0].equalsIgnoreCase("set") || args[0].equalsIgnoreCase("clear") || args[0].equalsIgnoreCase("delete")) { + return List.of("generator", "hook", "chest", "killer-spawn", "survivor-spawn", "gate").stream() + .filter(s -> s.startsWith(args[1].toLowerCase())).toList(); + } } return List.of(); } diff --git a/src/main/java/dev/loki/config/GameConfig.java b/src/main/java/dev/loki/config/GameConfig.java index 9187ba4..3fa6634 100644 --- a/src/main/java/dev/loki/config/GameConfig.java +++ b/src/main/java/dev/loki/config/GameConfig.java @@ -84,8 +84,11 @@ public class GameConfig { // Endgame setDefault("endgame.gate-open-seconds", 20); + setDefault("endgame.pregame-seconds", 30); + setDefault("endgame.postgame-seconds", 60); setDefault("endgame.chest-open-seconds", 3); setDefault("endgame.gate-interaction-range", 3.0); + setDefault("endgame.escape-range", 2.0); // Killer notification setDefault("killer-notification.compass-duration-seconds", 5); @@ -263,8 +266,11 @@ public class GameConfig { // Endgame // ------------------------------------------------------------------------- public int getGateOpenSeconds() { return config.getInt("endgame.gate-open-seconds", 20); } + public int getPregameSeconds() { return config.getInt("endgame.pregame-seconds", 30); } + public int getPostgameSeconds() { return config.getInt("endgame.postgame-seconds", 60); } public int getChestOpenSeconds() { return config.getInt("endgame.chest-open-seconds", 3); } public double getGateInteractionRange() { return config.getDouble("endgame.gate-interaction-range", 3.0); } + public double getEscapeRange() { return config.getDouble("endgame.escape-range", 2.0); } // ------------------------------------------------------------------------- // Killer notification diff --git a/src/main/java/dev/loki/config/LocationsConfig.java b/src/main/java/dev/loki/config/LocationsConfig.java index 463ea4a..6cc3176 100644 --- a/src/main/java/dev/loki/config/LocationsConfig.java +++ b/src/main/java/dev/loki/config/LocationsConfig.java @@ -130,6 +130,12 @@ public class LocationsConfig { return -1; } + /** Clear a specific survivor spawn slot (index 1-4). */ + public void clearSurvivorSpawn(int index) { + if (index < 1 || index > 4) throw new IllegalArgumentException("Index must be 1-4"); + config.set("survivor-spawn-" + index, null); + } + /** Clear all survivor spawn slots. */ public void clearSurvivorSpawns() { for (int i = 1; i <= 4; i++) { @@ -219,6 +225,15 @@ public class LocationsConfig { public void clearLocationList(String path) { config.set(path, new ArrayList<>()); } + /** Remove a location at a specific index from a list. */ + public void removeLocationAtIndex(String path, int index) { + List list = safeGetStringList(path); + if (index >= 0 && index < list.size()) { + list.remove(index); + config.set(path, list); + } + } + public void setSingleLocation(String path, Location loc) { List list = new ArrayList<>(); list.add(locationToString(loc)); diff --git a/src/main/java/dev/loki/core/GameSession.java b/src/main/java/dev/loki/core/GameSession.java index 4a2b524..dfaed5c 100644 --- a/src/main/java/dev/loki/core/GameSession.java +++ b/src/main/java/dev/loki/core/GameSession.java @@ -42,7 +42,8 @@ public class GameSession { this.ticksElapsed = 0; this.roleAssigner = new RoleAssigner(plugin.getConfigManager().getLocations()); this.generatorManager = new GeneratorManager( - plugin.getGeneratorData(), plugin.getConfigManager().getGame().getGeneratorRequired()); + plugin.getGeneratorData(), plugin.getConfigManager().getGame().getGeneratorRequired(), + plugin.getConfigManager().getGame()); } public void start() { diff --git a/src/main/java/dev/loki/gate/ExitGate.java b/src/main/java/dev/loki/gate/ExitGate.java index 7d7989b..9df9e1e 100644 --- a/src/main/java/dev/loki/gate/ExitGate.java +++ b/src/main/java/dev/loki/gate/ExitGate.java @@ -150,9 +150,7 @@ public class ExitGate { public void setPowered(boolean powered) { this.powered = powered; - if (powered && state == GateState.CLOSED) { - state = GateState.OPENING; - } + // НЕ меняем статус на OPENING — ворота открываются только через взаимодействие игрока } public void startOpening() { @@ -174,6 +172,16 @@ public class ExitGate { } } + /** Принудительно открыть ворота (вызывается после таймера эндгейма). */ + public void forceOpen() { + this.state = GateState.OPEN; + this.progress = 1.0; + this.savedProgress = 1.0; + removeGateStructure(); + spawnOpenParticles(); + playOpenedSound(); + } + /** Прямая установка прогресса (для линейного расчёта в GateOpening). */ public void setProgressDirect(double value) { if (state != GateState.OPENING) return; @@ -204,6 +212,14 @@ public class ExitGate { } public boolean isInRange(Location playerLoc, double radius) { + if (hasZone) { + int px = playerLoc.getBlockX(), py = playerLoc.getBlockY(), pz = playerLoc.getBlockZ(); + int closestX = Math.max(zoneMinX, Math.min(px, zoneMaxX)); + int closestY = Math.max(zoneMinY, Math.min(py, zoneMaxY)); + int closestZ = Math.max(zoneMinZ, Math.min(pz, zoneMaxZ)); + double dx = px - closestX, dy = py - closestY, dz = pz - closestZ; + return Math.sqrt(dx * dx + dy * dy + dz * dz) <= radius; + } return location.distance(playerLoc) <= radius; } diff --git a/src/main/java/dev/loki/gate/GateManager.java b/src/main/java/dev/loki/gate/GateManager.java index bf28d40..69f3671 100644 --- a/src/main/java/dev/loki/gate/GateManager.java +++ b/src/main/java/dev/loki/gate/GateManager.java @@ -110,6 +110,12 @@ public class GateManager { return (int) gates.stream().filter(ExitGate::isOpen).count(); } + public void openAllGates() { + for (ExitGate gate : gates) { + gate.forceOpen(); + } + } + public void resetAll() { endgameActive = false; for (ExitGate gate : gates) { diff --git a/src/main/java/dev/loki/gate/GateOpening.java b/src/main/java/dev/loki/gate/GateOpening.java index 5e2da93..673969d 100644 --- a/src/main/java/dev/loki/gate/GateOpening.java +++ b/src/main/java/dev/loki/gate/GateOpening.java @@ -38,17 +38,22 @@ public class GateOpening { ExitGate gate = gateManager.getGateAt(player.getLocation(), INTERACTION_RADIUS); if (gate == null) { - player.sendMessage(messages.get("chest.door-nearby")); + player.sendMessage(messages.get("gate.door-nearby")); return false; } if (!gate.isPowered()) { - player.sendMessage(messages.get("chest.door-no-power")); + player.sendMessage(messages.get("gate.door-no-power")); return false; } - if (!gate.isClosed() && !gate.isOpening()) { - player.sendMessage(messages.get("chest.door-already")); + if (gate.isOpening()) { + player.sendMessage(messages.get("gate.door-already")); + return false; + } + + if (gate.isOpen()) { + player.sendMessage(messages.get("gate.door-already")); return false; } @@ -61,7 +66,7 @@ public class GateOpening { if (progress != null) { Player player = survivor.getPlayer(); if (player != null) { - player.sendMessage(messages.get("chest.door-interrupted")); + player.sendMessage(messages.get("gate.door-interrupted")); } } } @@ -74,11 +79,14 @@ public class GateOpening { Player player = survivor.getPlayer(); if (player == null) return; + // Перевести ворота в режим открытия + gate.startOpening(); + // Восстановить сохранённый прогресс gate.restoreProgress(); openingPlayers.put(survivor.getUuid(), new OpeningProgress(gate, 0)); - player.sendMessage(messages.get("chest.door-open")); + player.sendMessage(messages.get("gate.door-open")); new OpeningTask(survivor, gate, plugin).runTaskTimer(plugin, 0L, 1L); } @@ -89,7 +97,7 @@ public class GateOpening { World world = loc.getWorld(); if (player != null) { - player.sendMessage(messages.get("chest.door-opened")); + player.sendMessage(messages.get("gate.door-opened")); if (world != null) { world.playSound(loc, Sound.BLOCK_IRON_DOOR_OPEN, 1.0f, 1.0f); // Big particle burst when opened diff --git a/src/main/java/dev/loki/hook/Hook.java b/src/main/java/dev/loki/hook/Hook.java index 7c60d85..d3f40a0 100644 --- a/src/main/java/dev/loki/hook/Hook.java +++ b/src/main/java/dev/loki/hook/Hook.java @@ -76,4 +76,6 @@ public class Hook { public boolean isBasement() { return basement; } public boolean isDisabled() { return disabled; } public void setDisabled(boolean disabled) { this.disabled = disabled; } + public int getStageProgress() { return stageProgress; } + public void addStageProgress(int delta) { stageProgress = Math.max(0, stageProgress + delta); } } diff --git a/src/main/java/dev/loki/hook/HookManager.java b/src/main/java/dev/loki/hook/HookManager.java index 2da3ff5..21828b5 100644 --- a/src/main/java/dev/loki/hook/HookManager.java +++ b/src/main/java/dev/loki/hook/HookManager.java @@ -111,10 +111,7 @@ public class HookManager { return true; // "повешен" = завершён } - Hook nearest = findNearestUnoccupiedHook(killerLoc, 5.0); - if (nearest == null) { - nearest = findAvailableHook().orElse(null); - } + Hook nearest = findNearestUnoccupiedHook(killerLoc, 4.0); if (nearest != null) { nearest.hookSurvivor(survivor); survivor.setState(PlayerState.HOOKED); // Set state to HOOKED diff --git a/src/main/java/dev/loki/item/ItemMedkit.java b/src/main/java/dev/loki/item/ItemMedkit.java index 46ad11d..fefe9be 100644 --- a/src/main/java/dev/loki/item/ItemMedkit.java +++ b/src/main/java/dev/loki/item/ItemMedkit.java @@ -39,8 +39,8 @@ public class ItemMedkit implements BaseItem { meta.setDisplayName("§a" + getType().getDisplayName()); meta.setLore(Arrays.asList( "§7" + getType().getDescription(), - "§eCharges: §f" + charges + "/" + getType().getDefaultCharges(), - "§7Right-click to heal" + "§eЗаряды: §f" + charges + "/" + getType().getDefaultCharges(), + "§7ПКМ — лечиться" )); PersistentDataContainer pdc = meta.getPersistentDataContainer(); @@ -60,7 +60,7 @@ public class ItemMedkit implements BaseItem { @Override public void onUse(Player player) { if (charges > 0) { - player.sendMessage("§aYou start healing..."); + player.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.medkit.start")); } } diff --git a/src/main/java/dev/loki/item/ItemType.java b/src/main/java/dev/loki/item/ItemType.java index dd8ed48..1257c47 100644 --- a/src/main/java/dev/loki/item/ItemType.java +++ b/src/main/java/dev/loki/item/ItemType.java @@ -1,8 +1,8 @@ package dev.loki.item; public enum ItemType { - MEDKIT("Medkit", "Heals one health state", 16), - HOOK_CUTTER("Hook Cutter", "Cuts a survivor off a hook", 1); + MEDKIT("Медкит", "Лечит одно состояние здоровья", 16), + HOOK_CUTTER("Крюкорез", "Срезает выжившего с крюка", 1); private final String displayName; private final String description; diff --git a/src/main/java/dev/loki/killer/GeneratorKick.java b/src/main/java/dev/loki/killer/GeneratorKick.java index 110bf7c..82adc53 100644 --- a/src/main/java/dev/loki/killer/GeneratorKick.java +++ b/src/main/java/dev/loki/killer/GeneratorKick.java @@ -14,7 +14,9 @@ import org.bukkit.plugin.Plugin; import org.bukkit.scheduler.BukkitRunnable; import org.bukkit.scheduler.BukkitTask; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -39,6 +41,9 @@ public class GeneratorKick { private int globalCooldownTicks = 0; private BukkitTask cooldownTask; + /** Pending unblock tasks — cancelled on shutdown to avoid ghost tasks */ + private final List pendingUnblockTasks = new ArrayList<>(); + public GeneratorKick(Plugin plugin, GameConfig config, GeneratorManager generatorManager) { this.plugin = plugin; this.config = config; @@ -135,6 +140,8 @@ public class GeneratorKick { for (BukkitTask t : chargingTasks.values()) t.cancel(); chargingTasks.clear(); if (cooldownTask != null) { cooldownTask.cancel(); cooldownTask = null; } + for (BukkitTask t : pendingUnblockTasks) t.cancel(); + pendingUnblockTasks.clear(); } // ------------------------------------------------------------------------- @@ -164,9 +171,10 @@ public class GeneratorKick { // Unblock generator after kick-duration (separate from global cooldown) int unblockTicks = config.getKickDuration() * 20; - plugin.getServer().getScheduler().runTaskLater(plugin, () -> { + BukkitTask unblockTask = plugin.getServer().getScheduler().runTaskLater(plugin, () -> { gen.setBlocked(false); }, unblockTicks); + pendingUnblockTasks.add(unblockTask); // Start global kick cooldown startGlobalCooldown(); diff --git a/src/main/java/dev/loki/listener/PlayerInteractListener.java b/src/main/java/dev/loki/listener/PlayerInteractListener.java index 5b3b1ec..27f81b0 100644 --- a/src/main/java/dev/loki/listener/PlayerInteractListener.java +++ b/src/main/java/dev/loki/listener/PlayerInteractListener.java @@ -228,10 +228,32 @@ public class PlayerInteractListener implements Listener { // #13: Запрет входа в печки для всех (и маньяка тоже) — MUST be FIRST before any other checks if (event.getClickedBlock() != null) { String type = event.getClickedBlock().getType().toString(); + if (type.contains("FURNACE") || type.contains("SMOKER") || type.contains("BLAST_FURNACE")) { event.setCancelled(true); return; } + + // Chest interaction - cancel vanilla GUI for ALL players (including killer) + if (type.contains("CHEST")) { + event.setCancelled(true); + if (onChestInteract != null && !bp.isKiller()) { + onChestInteract.accept(player, true); + } + return; + } + } + + // Gate opening — проверяем ДО генератора, чтобы ворота можно было открыть рядом с геной + if (!bp.isKiller() && onHatchInteract != null) { + var s = gameManager.getPlayerSession(player.getUniqueId()); + if (s != null && s.getGateManager() != null) { + var gate = s.getGateManager().getGateAt(player.getLocation(), 3.0); + if (gate != null && !gate.isOpen()) { + onHatchInteract.accept(player, true); + return; + } + } } if (!bp.isKiller() && tryGeneratorInteract(player)) return; @@ -242,29 +264,15 @@ public class PlayerInteractListener implements Listener { return; } - // Gate opening via proximity for survivors — works on air clicks too - if (!bp.isKiller() && onHatchInteract != null) { - onHatchInteract.accept(player, true); - } - if (event.getClickedBlock() == null) return; String type = event.getClickedBlock().getType().toString(); - // Chest interaction - cancel vanilla GUI and use plugin chest system - if (type.contains("CHEST")) { - event.setCancelled(true); - if (onChestInteract != null && !bp.isKiller()) { - onChestInteract.accept(player, true); - } - return; - } // Hook rescue: if survivor clicks on a hook block, attempt rescue + // Hook rescue / block protection if (!bp.isKiller() && onRescueAttempt != null) { onRescueAttempt.accept(player, true); - if (type.contains("IRON_BARS") || type.contains("IRON")) event.setCancelled(true); } - // Cancel interaction with hook blocks to prevent block manipulation if (type.contains("IRON_BARS") || type.contains("IRON")) event.setCancelled(true); } diff --git a/src/main/java/dev/loki/objective/GeneratorManager.java b/src/main/java/dev/loki/objective/GeneratorManager.java index f8ac492..de128b0 100644 --- a/src/main/java/dev/loki/objective/GeneratorManager.java +++ b/src/main/java/dev/loki/objective/GeneratorManager.java @@ -1,6 +1,7 @@ package dev.loki.objective; import dev.loki.data.GeneratorDataManager; +import dev.loki.config.GameConfig; import org.bukkit.Location; import org.bukkit.entity.Player; @@ -12,11 +13,15 @@ import java.util.Map; public class GeneratorManager { private final Map generators; private final int requiredCount; + private final double normalRegressionRate; + private final double kickRegressionRate; - public GeneratorManager(GeneratorDataManager dataManager, int requiredCount) { + public GeneratorManager(GeneratorDataManager dataManager, int requiredCount, GameConfig config) { this.generators = new HashMap<>(); - this.requiredCount = requiredCount; loadGenerators(dataManager); + this.requiredCount = Math.min(requiredCount, generators.size()); + this.normalRegressionRate = config.getRegressionRate() / 20.0; + this.kickRegressionRate = config.getKickRegressionRate() / 20.0; } private void loadGenerators(GeneratorDataManager dataManager) { @@ -103,7 +108,7 @@ public class GeneratorManager { public void tick() { for (Generator gen : generators.values()) { if (gen.isUnderRegression()) { - double rate = gen.wasKicked() ? 0.125 : 0.0125; + double rate = gen.wasKicked() ? kickRegressionRate : normalRegressionRate; gen.removeProgress(rate); } gen.decrementRegression(); diff --git a/src/main/java/dev/loki/objective/GeneratorRegression.java b/src/main/java/dev/loki/objective/GeneratorRegression.java deleted file mode 100644 index 012a28d..0000000 --- a/src/main/java/dev/loki/objective/GeneratorRegression.java +++ /dev/null @@ -1,52 +0,0 @@ -package dev.loki.objective; - -import dev.loki.config.GameConfig; - -public class GeneratorRegression { - private final GameConfig config; - - public GeneratorRegression(GameConfig config) { - this.config = config; - } - - public void applyRegression(Generator generator) { - if (generator.isCompleted() || generator.isBeingRepaired()) return; - double rate = generator.wasKicked() ? getKickRegressionRate() : getNormalRegressionRate(); - generator.removeProgress(rate); - } - - public void onGeneratorKicked(Generator generator) { - generator.setUnderRegression(true, config.getKickDuration() * 20); - } - - public void tick(Generator generator) { - if (!generator.isUnderRegression()) return; - applyRegression(generator); - generator.decrementRegression(); - if (generator.getRegressionTicks() <= 0) { - generator.setKicked(false); - } - } - - public void tickAll(GeneratorManager manager) { - for (Generator gen : manager.getAllGenerators()) { - if (!gen.isCompleted() && !gen.isBeingRepaired()) { - tick(gen); - } - } - } - - private double getNormalRegressionRate() { - return config.getRegressionRate() / 20.0; - } - - private double getKickRegressionRate() { - return config.getKickRegressionRate() / 20.0; - } - - public static boolean shouldStartRegression(Generator generator) { - return !generator.isCompleted() - && !generator.isBeingRepaired() - && generator.getProgress() > 0; - } -} diff --git a/src/main/java/dev/loki/objective/GeneratorRepair.java b/src/main/java/dev/loki/objective/GeneratorRepair.java index 18c75d9..e51f5b4 100644 --- a/src/main/java/dev/loki/objective/GeneratorRepair.java +++ b/src/main/java/dev/loki/objective/GeneratorRepair.java @@ -2,6 +2,7 @@ package dev.loki.objective; import dev.loki.mdbd; import dev.loki.minigames.GeneratorMinigame; +import dev.loki.player.BasePlayer; import net.kyori.adventure.text.Component; import org.bukkit.Bukkit; import org.bukkit.Location; @@ -71,8 +72,8 @@ public class GeneratorRepair { if (session != null) { session.generator.setCurrentRepairer(null); // Progress is PRESERVED - generator keeps its progress value - // Only unblock if it was blocked due to an ongoing repair (not a kick) - if (!session.generator.isUnderRegression()) { + // Only unblock if it was blocked due to an ongoing repair (not a kick or block) + if (!session.generator.isUnderRegression() && !session.generator.isBlocked()) { session.generator.setBlocked(false); } player.sendActionBar(Component.empty()); @@ -176,12 +177,18 @@ public class GeneratorRepair { } } - /** Give every online killer a compass pointing to the failed generator for compass-duration-seconds. */ + /** Give only killers a compass pointing to the failed generator for compass-duration-seconds. */ private void notifyKillerWithCompass(Player failedPlayer, Generator generator) { + var session = plugin.getGameManager().getCurrentSession(); + if (session == null) return; + int durationTicks = plugin.getConfigManager().getGame().getCompassDurationSeconds() * 20; Location compassTarget = generator.getLocation(); - for (Player online : Bukkit.getOnlinePlayers()) { + for (BasePlayer bp : session.getPlayers().values()) { + if (!bp.isKiller()) continue; + Player online = bp.getPlayer(); + if (online == null || !online.isOnline()) continue; if (online.getUniqueId().equals(failedPlayer.getUniqueId())) continue; online.sendMessage(plugin.getConfigManager().getMessages().get("generator.kicked.notification")); @@ -196,7 +203,6 @@ public class GeneratorRepair { ItemStack prev = online.getInventory().getItemInOffHand(); online.getInventory().setItemInOffHand(compass); - // Восстанавливаем старый предмет только если в offhand всё ещё компас plugin.getServer().getScheduler().runTaskLater(plugin, () -> { ItemStack current = online.getInventory().getItemInOffHand(); if (current.getType() == Material.COMPASS) { diff --git a/src/main/java/dev/loki/objective/SkillCheck.java b/src/main/java/dev/loki/objective/SkillCheck.java deleted file mode 100644 index eff6f52..0000000 --- a/src/main/java/dev/loki/objective/SkillCheck.java +++ /dev/null @@ -1,142 +0,0 @@ -package dev.loki.objective; - -import dev.loki.mdbd; -import org.bukkit.Bukkit; - -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; - -public class SkillCheck { - private final mdbd plugin; - private final GeneratorRepair generatorRepair; - private final Map activeChecks; - - private static final double ZONE_SIZE = 0.15; - private static final double ROTATION_SPEED = 3.0; - - public SkillCheck(mdbd plugin, GeneratorRepair generatorRepair) { - this.plugin = plugin; - this.generatorRepair = generatorRepair; - this.activeChecks = new HashMap<>(); - } - - public void start(UUID playerId, Generator generator) { - double zoneStart = Math.random() * 0.7; - double zoneEnd = zoneStart + ZONE_SIZE; - - CheckState state = new CheckState( - playerId, - generator, - zoneStart, - zoneEnd, - 0.0, - ROTATION_SPEED - ); - activeChecks.put(playerId, state); - } - - public void tick() { - for (CheckState state : new HashMap<>(activeChecks).values()) { - state.update(); - - if (state.isExpired()) { - onFail(state); - } - } - } - - public boolean tryComplete(UUID playerId, boolean isSuccessZone) { - CheckState state = activeChecks.get(playerId); - if (state == null) return false; - - if (isSuccessZone) { - onSuccess(state); - } else { - onFail(state); - } - return true; - } - - private void onSuccess(CheckState state) { - activeChecks.remove(state.playerId); - Bukkit.getScheduler().runTask(plugin, () -> - generatorRepair.onSkillCheckResult(state.playerId, true, state.generator) - ); - } - - private void onFail(CheckState state) { - activeChecks.remove(state.playerId); - Bukkit.getScheduler().runTask(plugin, () -> - generatorRepair.onSkillCheckResult(state.playerId, false, state.generator) - ); - } - - public boolean isActive(UUID playerId) { - return activeChecks.containsKey(playerId); - } - - public CheckState getState(UUID playerId) { - return activeChecks.get(playerId); - } - - public static class CheckState { - final UUID playerId; - final Generator generator; - final double zoneStart; - final double zoneEnd; - double pointerPosition; - final double rotationSpeed; - int ticks; - - CheckState(UUID playerId, Generator generator, double zoneStart, double zoneEnd, - double pointerPosition, double rotationSpeed) { - this.playerId = playerId; - this.generator = generator; - this.zoneStart = zoneStart; - this.zoneEnd = zoneEnd; - this.pointerPosition = pointerPosition; - this.rotationSpeed = rotationSpeed; - this.ticks = 0; - } - - void update() { - pointerPosition += rotationSpeed * 0.01; - if (pointerPosition >= 1.0) { - pointerPosition = 0.0; - } - ticks++; - } - - boolean isExpired() { - return ticks > 100; - } - - boolean isInSuccessZone() { - return pointerPosition >= zoneStart && pointerPosition <= zoneEnd; - } - - public String getVisualRepresentation() { - int zoneStartPos = (int) (zoneStart * 20); - int zoneEndPos = (int) (zoneEnd * 20); - int pointerPos = (int) (pointerPosition * 20); - - StringBuilder sb = new StringBuilder("["); - for (int i = 0; i < 20; i++) { - if (i == pointerPos) { - sb.append("◆"); - } else if (i >= zoneStartPos && i <= zoneEndPos) { - sb.append("█"); - } else { - sb.append("░"); - } - } - sb.append("]"); - return sb.toString(); - } - - public double getPointerPosition() { - return pointerPosition; - } - } -} diff --git a/src/main/java/dev/loki/objective/SkillCheckInventory.java b/src/main/java/dev/loki/objective/SkillCheckInventory.java deleted file mode 100644 index 3ffe8c6..0000000 --- a/src/main/java/dev/loki/objective/SkillCheckInventory.java +++ /dev/null @@ -1,245 +0,0 @@ -package dev.loki.objective; - -import dev.loki.config.GameConfig; -import dev.loki.mdbd; -import dev.loki.objective.Generator; -import dev.loki.objective.GeneratorRepair; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.Sound; -import org.bukkit.entity.Player; -import org.bukkit.event.inventory.InventoryClickEvent; -import org.bukkit.event.inventory.InventoryCloseEvent; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; - -import java.util.*; - -/** - * GUI-based Skill Check minigame — ROULETTE style. - * - * Green block moves sequentially left-to-right (0→1→2→...→8→0→...). - * Starts FAST (2-3 ticks), gradually slows down (8-12 ticks). - * ~12-18 switches, then stops on final slot. - * Player must click the GREEN block. Clicking gray = fail. Timeout = fail. - */ -public class SkillCheckInventory { - private final mdbd plugin; - private final Map activeSessions = new HashMap<>(); - private static final int TIMEOUT_TICKS = 60; // 3 seconds — не выбрал = проигрыш -10% - private static final int MIN_SWITCHES = 5; - private static final int MAX_SWITCHES = 8; - - private final Material successMaterial; - private final Material failMaterial; - private final Material neutralMaterial; - - public SkillCheckInventory(mdbd plugin) { - this.plugin = plugin; - GameConfig cfg = plugin.getConfigManager().getGame(); - this.successMaterial = getMaterial(cfg.getSkillCheckSuccessBlock(), Material.GREEN_STAINED_GLASS_PANE); - this.failMaterial = getMaterial(cfg.getSkillCheckFailBlock(), Material.RED_STAINED_GLASS_PANE); - this.neutralMaterial = getMaterial(cfg.getSkillCheckNeutralBlock(), Material.GRAY_STAINED_GLASS_PANE); - } - - private static Material getMaterial(String name, Material fallback) { - Material m = Material.getMaterial(name); - return m != null ? m : fallback; - } - - public void startSkillCheck(Player player, UUID playerId, SkillCheck skillCheck, Generator generator, GeneratorRepair generatorRepair) { - SkillCheckSession session = new SkillCheckSession(player, playerId, skillCheck, generator, generatorRepair); - activeSessions.put(playerId, session); - - Inventory inv = Bukkit.createInventory(null, 9, Component.text("⚡ Skill Check!").color(NamedTextColor.YELLOW)); - fillInventory(inv, session.successSlot); - player.openInventory(inv); - - // Schedule timeout - plugin.getServer().getScheduler().runTaskLater(plugin, () -> { - if (activeSessions.containsKey(playerId)) { - onTimeout(playerId); - } - }, TIMEOUT_TICKS); - - // Start roulette animation - runRoulette(playerId); - } - - /** - * Roulette animation: green block moves left-to-right sequentially, - * slowing down over time like a real roulette wheel. - */ - private void runRoulette(UUID playerId) { - SkillCheckSession session = activeSessions.get(playerId); - if (session == null || !session.player.isOnline()) { - activeSessions.remove(playerId); - return; - } - - // Check if timeout happened - if (session.totalTicks >= TIMEOUT_TICKS) { - onTimeout(playerId); - return; - } - - // Move green sequentially to next slot - session.lastSuccessSlot = session.successSlot; - session.successSlot = (session.successSlot + 1) % 9; - session.switchesDone++; - - // Update inventory visually - Inventory inv = session.player.getOpenInventory().getTopInventory(); - if (inv != null && inv.getSize() == 9) { - inv.setItem(session.lastSuccessSlot, createNeutralGlass()); - inv.setItem(session.successSlot, createSuccessGlass()); - // Play tick sound for roulette feel - session.player.playSound(session.player.getLocation(), Sound.BLOCK_NOTE_BLOCK_HAT, 0.5f, 1.5f); - } - - // Calculate next delay — roulette slows down - int nextDelay; - int done = session.switchesDone; - int total = session.totalSwitches; - - if (done < total * 0.4) { - // Fast phase: 40% of switches - nextDelay = 2 + (int)(Math.random() * 2); // 2-3 ticks - } else if (done < total * 0.8) { - // Medium phase: 40% of switches - nextDelay = 4 + (int)(Math.random() * 3); // 4-6 ticks - } else { - // Slow phase: final 20% of switches - nextDelay = 8 + (int)(Math.random() * 5); // 8-12 ticks - } - - session.totalTicks += nextDelay; - - // Stop roulette after totalSwitches — green stays on final slot - if (done >= total) { - // Roulette stopped — player must click now (timeout still ticking) - return; - } - - // Continue roulette - plugin.getServer().getScheduler().runTaskLater(plugin, () -> runRoulette(playerId), nextDelay); - } - - /** - * Called when the skill check inventory is closed (e.g. player presses ESC). - * Auto-fail: treats close as timeout. - */ - public void onInventoryClose(InventoryCloseEvent event) { - if (!(event.getPlayer() instanceof Player player)) return; - UUID playerId = player.getUniqueId(); - if (activeSessions.containsKey(playerId)) { - onTimeout(playerId); - } - } - - public void handleClick(InventoryClickEvent event, UUID playerId) { - // event.setCancelled(true) is already called in SkillCheckListener before this - SkillCheckSession session = activeSessions.remove(playerId); - if (session == null) return; - - Player player = event.getWhoClicked() instanceof Player ? (Player) event.getWhoClicked() : null; - if (player == null) return; - - player.closeInventory(); - - int clickedSlot = event.getRawSlot(); - boolean success = (clickedSlot == session.successSlot); - - if (success) { - player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.5f); - } else { - player.playSound(player.getLocation(), Sound.ENTITY_BLAZE_HURT, 1.0f, 0.5f); - } - - session.generatorRepair.onSkillCheckResult(playerId, success, session.generator); - } - - private void onTimeout(UUID playerId) { - SkillCheckSession session = activeSessions.remove(playerId); - if (session == null) return; - - Player player = session.player; - if (player != null && player.isOnline()) { - player.closeInventory(); - player.playSound(player.getLocation(), Sound.ENTITY_BLAZE_HURT, 1.0f, 0.5f); - } - - // Timeout = failure - session.generatorRepair.onSkillCheckResult(playerId, false, session.generator); - } - - private void fillInventory(Inventory inv, int greenSlot) { - for (int i = 0; i < 9; i++) { - inv.setItem(i, i == greenSlot ? createSuccessGlass() : createNeutralGlass()); - } - } - - private ItemStack createSuccessGlass() { - ItemStack glass = new ItemStack(successMaterial); - ItemMeta meta = glass.getItemMeta(); - if (meta != null) { - meta.displayName(Component.text("✓ Нажми!").color(NamedTextColor.GREEN)); - glass.setItemMeta(meta); - } - return glass; - } - - private ItemStack createFailGlass() { - ItemStack glass = new ItemStack(failMaterial); - ItemMeta meta = glass.getItemMeta(); - if (meta != null) { - meta.displayName(Component.text("✗").color(NamedTextColor.RED)); - glass.setItemMeta(meta); - } - return glass; - } - - private ItemStack createNeutralGlass() { - ItemStack glass = new ItemStack(neutralMaterial); - ItemMeta meta = glass.getItemMeta(); - if (meta != null) { - meta.displayName(Component.text("-").color(NamedTextColor.GRAY)); - glass.setItemMeta(meta); - } - return glass; - } - - public void clear() { - for (UUID uuid : new HashSet<>(activeSessions.keySet())) { - SkillCheckSession session = activeSessions.remove(uuid); - if (session.player != null && session.player.isOnline()) { - session.player.closeInventory(); - } - } - activeSessions.clear(); - } - - private static class SkillCheckSession { - final Player player; - final UUID playerId; - final SkillCheck skillCheck; - final Generator generator; - final GeneratorRepair generatorRepair; - int successSlot = 0; - int lastSuccessSlot = -1; - int switchesDone = 0; - final int totalSwitches = MIN_SWITCHES + new Random().nextInt(MAX_SWITCHES - MIN_SWITCHES + 1); - int totalTicks = 0; - - SkillCheckSession(Player player, UUID playerId, SkillCheck skillCheck, Generator generator, GeneratorRepair generatorRepair) { - this.player = player; - this.playerId = playerId; - this.skillCheck = skillCheck; - this.generator = generator; - this.generatorRepair = generatorRepair; - } - } -} diff --git a/src/main/java/dev/loki/objective/SkillCheckUI.java b/src/main/java/dev/loki/objective/SkillCheckUI.java deleted file mode 100644 index 468caf1..0000000 --- a/src/main/java/dev/loki/objective/SkillCheckUI.java +++ /dev/null @@ -1,120 +0,0 @@ -package dev.loki.objective; - -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.minimessage.MiniMessage; -import net.kyori.adventure.title.Title; -import org.bukkit.entity.Player; - -import java.util.UUID; - -public class SkillCheckUI { - private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage(); - - public static void showSkillCheck(Player player, SkillCheck.CheckState state) { - String visual = buildVisualDisplay(state); - Component title = MINI_MESSAGE.deserialize(visual); - Component subtitle = MINI_MESSAGE.deserialize("Нажмите ПКМ в зеленой зоне!"); - - player.showTitle(Title.title(title, subtitle)); - } - - public static void showActionBarSkillCheck(Player player, SkillCheck.CheckState state) { - String visual = buildCompactVisual(state); - player.sendActionBar(MiniMessage.miniMessage().deserialize(visual)); - } - - public static void showSuccess(Player player) { - player.showTitle(Title.title( - MINI_MESSAGE.deserialize("✓ Отлично!"), - Component.empty(), - Title.Times.times(java.time.Duration.ZERO, java.time.Duration.ofMillis(500), java.time.Duration.ZERO) - )); - } - - public static void showFail(Player player) { - player.showTitle(Title.title( - MINI_MESSAGE.deserialize("✗ Провал!"), - MINI_MESSAGE.deserialize("Потеря прогресса..."), - Title.Times.times(java.time.Duration.ZERO, java.time.Duration.ofMillis(800), java.time.Duration.ZERO) - )); - } - - public static void clear(Player player) { - player.clearTitle(); - player.sendActionBar(Component.empty()); - } - - private static String buildVisualDisplay(SkillCheck.CheckState state) { - double zoneStart = state.zoneStart; - double zoneEnd = state.zoneEnd; - double pointer = state.getPointerPosition(); - - int displayWidth = 15; - int zoneStartPos = (int) (zoneStart * displayWidth); - int zoneEndPos = (int) (zoneEnd * displayWidth); - int pointerPos = (int) (pointer * displayWidth); - - StringBuilder sb = new StringBuilder(); - sb.append(""); - - for (int i = 0; i < displayWidth; i++) { - if (i == pointerPos) { - sb.append("◆"); - } else if (i >= zoneStartPos && i <= zoneEndPos) { - sb.append("█"); - } else { - sb.append("▒"); - } - } - - sb.append(""); - return sb.toString(); - } - - private static String buildCompactVisual(SkillCheck.CheckState state) { - double zoneStart = state.zoneStart; - double zoneEnd = state.zoneEnd; - double pointer = state.getPointerPosition(); - - int displayWidth = 10; - int zoneStartPos = (int) (zoneStart * displayWidth); - int zoneEndPos = (int) (zoneEnd * displayWidth); - int pointerPos = (int) (pointer * displayWidth); - - StringBuilder sb = new StringBuilder(); - sb.append("QTE: ["); - - for (int i = 0; i < displayWidth; i++) { - if (i == pointerPos) { - sb.append("♦"); - } else if (i >= zoneStartPos && i <= zoneEndPos) { - sb.append("█"); - } else { - sb.append("░"); - } - } - - sb.append("]"); - return sb.toString(); - } - - public static String getSkillCheckText(UUID playerId, double pointer, double zoneStart, double zoneEnd) { - int width = 12; - int pPos = (int) (pointer * width); - int zStart = (int) (zoneStart * width); - int zEnd = (int) (zoneEnd * width); - - StringBuilder sb = new StringBuilder("["); - for (int i = 0; i < width; i++) { - if (i == pPos) { - sb.append("◆"); - } else if (i >= zStart && i <= zEnd) { - sb.append("█"); - } else { - sb.append("░"); - } - } - sb.append("]"); - return sb.toString(); - } -} diff --git a/src/main/java/dev/loki/phase/EndgamePhase.java b/src/main/java/dev/loki/phase/EndgamePhase.java index 3b1d45a..65b8624 100644 --- a/src/main/java/dev/loki/phase/EndgamePhase.java +++ b/src/main/java/dev/loki/phase/EndgamePhase.java @@ -78,6 +78,15 @@ public class EndgamePhase implements Phase { gateManager = session.getGateManager(); } + // Если ворота уже открыты — игра завершилась по таймеру эндгейма, + // показываем результаты сразу + if (gateManager != null && gateManager.getOpenGateCount() > 0) { + this.complete = true; + announceEndgameEnd(session); + teleportAllToLobby(session); + return; + } + // Set up sidebar sidebar = new Sidebar(); for (BasePlayer bp : session.getPlayers().values()) { @@ -101,7 +110,6 @@ public class EndgamePhase implements Phase { } } - updateGates(session); updateHatch(session); checkWinConditions(session); } @@ -123,6 +131,10 @@ public class EndgamePhase implements Phase { hatch.reset(); } + if (gateManager != null) { + gateManager.resetAll(); + } + // exit() НЕ вызывает session.stop() — это делает transitionToNextPhase() // Телепорт и результаты уже обработаны в enter() или будут в teleportAllToLobby } diff --git a/src/main/java/dev/loki/phase/GameplayDamageSystem.java b/src/main/java/dev/loki/phase/GameplayDamageSystem.java index dcc226a..0ff4519 100644 --- a/src/main/java/dev/loki/phase/GameplayDamageSystem.java +++ b/src/main/java/dev/loki/phase/GameplayDamageSystem.java @@ -42,6 +42,7 @@ public class GameplayDamageSystem { private Listener vanillaDamageBlocker; private ItemUseListener itemUseListener; private PlayerJumpListener jumpListener; + private org.bukkit.scheduler.BukkitTask swimmingTask; /** * Initialize damage and combat system. @@ -83,18 +84,21 @@ public class GameplayDamageSystem { p.playSound(p.getLocation(), Sound.ENTITY_PLAYER_HURT, 1.0f, 0.5f); p.setHealth(20.0); p.setSwimming(true); - Bukkit.getScheduler().runTaskTimer(plugin, task -> { - if (!p.isOnline()) { - task.cancel(); - return; + swimmingTask = new org.bukkit.scheduler.BukkitRunnable() { + @Override + public void run() { + if (!p.isOnline()) { + cancel(); + return; + } + if (survivor.getState() != PlayerState.DOWNED) { + p.setSwimming(false); + cancel(); + return; + } + if (!p.isSwimming()) p.setSwimming(true); } - if (survivor.getState() != PlayerState.DOWNED) { - p.setSwimming(false); - task.cancel(); - return; - } - if (!p.isSwimming()) p.setSwimming(true); - }, 1L, 5L); + }.runTaskTimer(plugin, 1L, 5L); }); killerController = new KillerController(plugin, playerManager, damageHandler, cfg); @@ -138,6 +142,10 @@ public class GameplayDamageSystem { * Clear all damage system resources. */ public void clear() { + if (swimmingTask != null) { + swimmingTask.cancel(); + swimmingTask = null; + } if (jumpListener != null) { jumpListener.clearInGamePlayers(); jumpListener.unregister(); @@ -156,7 +164,15 @@ public class GameplayDamageSystem { */ public void clearManagers() { if (killerController != null) killerController.shutdown(); - if (playerManager != null) playerManager.clear(); + if (playerManager != null) { + for (BasePlayer bp : playerManager.getAllPlayers()) { + Player p = bp.getPlayer(); + if (p != null && p.isOnline()) { + p.setSwimming(false); + } + } + playerManager.clear(); + } } // ========================================================================= diff --git a/src/main/java/dev/loki/phase/GameplayHookHandler.java b/src/main/java/dev/loki/phase/GameplayHookHandler.java index 2c1ede6..55df515 100644 --- a/src/main/java/dev/loki/phase/GameplayHookHandler.java +++ b/src/main/java/dev/loki/phase/GameplayHookHandler.java @@ -4,8 +4,10 @@ import dev.loki.config.GameConfig; import dev.loki.core.GameSession; import dev.loki.hook.*; import dev.loki.player.BasePlayer; +import dev.loki.player.PlayerState; import dev.loki.player.Survivor; import org.bukkit.Bukkit; +import org.bukkit.Location; import org.bukkit.entity.Player; /** @@ -18,6 +20,10 @@ public class GameplayHookHandler { private final HookSacrifice hookSacrifice; private final HookStruggle hookStruggle; private final HookRecovery hookRecovery; + private GameSession session; + + private static final double CAMP_REGRESS_RANGE = 7.0; + private static final double CAMP_STALL_RANGE = 13.0; public GameplayHookHandler(HookManager hookManager, HookSacrifice hookSacrifice, HookStruggle hookStruggle, HookRecovery hookRecovery) { @@ -27,12 +33,17 @@ public class GameplayHookHandler { this.hookRecovery = hookRecovery; } + public void setSession(GameSession session) { + this.session = session; + } + private int hookTpTick; /** - * Called every tick - checks hook stage progression and teleports hooked players back. + * Called every tick - checks hook stage progression, killer proximity, and teleports hooked players back. */ public void tick() { + applyKillerProximity(); checkHookStages(); hookTpTick++; if (hookTpTick >= 20) { @@ -41,6 +52,45 @@ public class GameplayHookHandler { } } + /** + * If the killer is near a hook, slow or regress the hook progression. + * < 7 blocks → regress (goes negative) + * 7-13 blocks → stall (no progression) + * > 13 blocks → normal + */ + private void applyKillerProximity() { + if (session == null) return; + + Location killerLoc = null; + for (BasePlayer bp : session.getPlayers().values()) { + if (bp.isKiller()) { + Player p = bp.getPlayer(); + if (p != null && p.isOnline()) { + killerLoc = p.getLocation(); + } + break; + } + } + if (killerLoc == null) return; + + for (Hook hook : hookManager.getAllHooks()) { + if (!hook.isOccupied()) continue; + if (!killerLoc.getWorld().equals(hook.getLocation().getWorld())) continue; + + double dist = killerLoc.distance(hook.getLocation()); + + if (dist < CAMP_REGRESS_RANGE) { + // Regress: -2 per tick (faster than normal +1) + hook.addStageProgress(-2); + // If STAGE_1 and progress goes to 0, that's as low as it gets + } else if (dist < CAMP_STALL_RANGE) { + // Stall: cancel the normal +1 by adding -1 + hook.addStageProgress(-1); + } + // else: normal progression (Hook.tick already adds +1) + } + } + /** * Teleport hooked survivors back to their hook if they've moved too far. */ diff --git a/src/main/java/dev/loki/phase/GameplayInteraction.java b/src/main/java/dev/loki/phase/GameplayInteraction.java index f13a92a..bf03269 100644 --- a/src/main/java/dev/loki/phase/GameplayInteraction.java +++ b/src/main/java/dev/loki/phase/GameplayInteraction.java @@ -110,21 +110,7 @@ public class GameplayInteraction { } private void wireMoveListener(GameSession session) { - if (moveListener == null) return; - 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)); - } - } - }); + // Escape check is handled in GameplayPhase.checkEscapes() every tick during POSTGAME } private void wireInteractListener(GameSession session) { @@ -156,10 +142,13 @@ public class GameplayInteraction { Survivor carried = (Survivor) other; if (hookManager.tryHookSurvivor(carried, player.getLocation())) { killer.setCarrying(false); - Hook nearest = hookManager.findNearestOccupiedHook(player.getLocation(), 5.0); - if (nearest != null) { - Player vp = carried.getPlayer(); - if (vp != null) vp.teleport(nearest.getLocation()); + // Only teleport if actually hooked (not dead from exceeding hook limit) + if (carried.getState() == PlayerState.HOOKED) { + Hook nearest = hookManager.findNearestOccupiedHook(player.getLocation(), 5.0); + if (nearest != null) { + Player vp = carried.getPlayer(); + if (vp != null) vp.teleport(nearest.getLocation()); + } } } break; @@ -190,10 +179,12 @@ public class GameplayInteraction { Survivor carried = (Survivor) other; if (hookManager.tryHookSurvivor(carried, player.getLocation())) { killer.setCarrying(false); - Hook nearest = hookManager.findNearestOccupiedHook(player.getLocation(), 5.0); - if (nearest != null) { - Player vp = carried.getPlayer(); - if (vp != null) vp.teleport(nearest.getLocation()); + if (carried.getState() == PlayerState.HOOKED) { + Hook nearest = hookManager.findNearestOccupiedHook(player.getLocation(), 5.0); + if (nearest != null) { + Player vp = carried.getPlayer(); + if (vp != null) vp.teleport(nearest.getLocation()); + } } return; } diff --git a/src/main/java/dev/loki/phase/GameplayPhase.java b/src/main/java/dev/loki/phase/GameplayPhase.java index c185496..44ce48f 100644 --- a/src/main/java/dev/loki/phase/GameplayPhase.java +++ b/src/main/java/dev/loki/phase/GameplayPhase.java @@ -25,6 +25,8 @@ import dev.loki.player.Survivor; import dev.loki.ui.ActionBar; import dev.loki.ui.TitleUI; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.title.Title; import org.bukkit.Bukkit; import org.bukkit.GameMode; import org.bukkit.Location; @@ -33,15 +35,20 @@ import org.bukkit.Sound; import org.bukkit.block.Block; import org.bukkit.entity.Player; +import java.time.Duration; import java.util.ArrayList; import java.util.List; public class GameplayPhase implements Phase { + private enum EndgameState { NONE, PREGAME, POSTGAME } + private final GeneratorManager generatorManager; private int tickCounter; private boolean shouldEnd; private boolean allGensDone; + private EndgameState endgameState = EndgameState.NONE; + private int endgameTimer; // секунд до смены состояния // Sub-handlers private GameplayHookHandler hookHandler; @@ -81,6 +88,8 @@ public class GameplayPhase implements Phase { tickCounter = 0; shouldEnd = false; allGensDone = false; + endgameState = EndgameState.NONE; + endgameTimer = 0; winConditions = new GameplayWinConditions(); initManagers(session, plugin, cfg); @@ -104,7 +113,6 @@ public class GameplayPhase implements Phase { if (generatorRepair != null) generatorRepair.tick(); if (interaction != null) interaction.tickSurvivors(); hookHandler.tick(); - boolean timeExpired = ui.tickMatchTimer(session); // Particle effects around generators (every 10 ticks) if (tickCounter % 10 == 0 && generatorManager != null) { @@ -119,19 +127,29 @@ public class GameplayPhase implements Phase { } if (tickCounter % 20 == 0) { + tickEndgame(session); checkGeneratorProgress(session); - winConditions.checkWinConditions(session); - if (winConditions.isShouldEnd() || timeExpired) { - shouldEnd = true; + if (endgameState == EndgameState.NONE) { + winConditions.checkWinConditions(session); + if (winConditions.isShouldEnd()) { + shouldEnd = true; + } + } else { + // Во время эндгейма игра заканчивается когда все выжившие мертвы/сбежали + if (allSurvivorsIncapacitated(session)) { + shouldEnd = true; + } + // Или когда истёк таймер после открытия ворот + if (endgameState == EndgameState.POSTGAME && endgameTimer <= 0) { + shouldEnd = true; + } } ui.updateSidebar(session, generatorManager); } - if (timeExpired) { - showGameResults(session); - teleportAllToLobby(session); - session.stop(); - return; + // Escape check — каждый тик для мгновенной реакции + if (endgameState == EndgameState.POSTGAME) { + checkEscapes(session); } } @@ -193,6 +211,7 @@ public class GameplayPhase implements Phase { hookRescue = new HookRescue(plugin, ab, cfg); hookHandler = new GameplayHookHandler(hookManager, hookSacrifice, hookStruggle, hookRecovery); + hookHandler.setSession(session); hookRescue.setOnRescueComplete((rescuer, victim) -> hookHandler.onRescued(victim)); chestManager = new ChestManager(plugin, plugin.getConfigManager().getLocations(), cfg); @@ -267,12 +286,30 @@ public class GameplayPhase implements Phase { // Tick helpers // ========================================================================= + /** Все выжившие мертвы или сбежали? */ + private boolean allSurvivorsIncapacitated(GameSession session) { + boolean hasSurvivor = false; + for (BasePlayer bp : session.getPlayers().values()) { + if (bp.isKiller()) continue; + hasSurvivor = true; + PlayerState st = bp.getState(); + if (st != PlayerState.DEAD && st != PlayerState.ESCAPED + && st != PlayerState.DOWNED && st != PlayerState.HOOKED + && st != PlayerState.CARRIED) { + return false; + } + } + return hasSurvivor; + } + private void checkGeneratorProgress(GameSession session) { if (allGensDone) return; if (generatorManager.areAllRequiredCompleted()) { allGensDone = true; interaction.setAllGensDone(true); gateManager.activateEndgame(); + endgameState = EndgameState.PREGAME; + endgameTimer = session.getConfig().getPregameSeconds(); Bukkit.broadcast(session.getPlugin().getConfigManager().getMessages().get("generator.repair.all-done")); for (BasePlayer bp : session.getPlayers().values()) { Player p = bp.getPlayer(); @@ -281,6 +318,63 @@ public class GameplayPhase implements Phase { } } + private void tickEndgame(GameSession session) { + if (endgameState == EndgameState.NONE) return; + + endgameTimer--; + + if (endgameState == EndgameState.PREGAME) { + // Каждую секунду шлём сообщение о начале эндгейма + if (endgameTimer > 0) { + for (BasePlayer bp : session.getPlayers().values()) { + Player p = bp.getPlayer(); + if (p == null) continue; + p.sendActionBar(net.kyori.adventure.text.Component.text( + "§c⚡ Эндгейм! Ворота откроются через §e" + endgameTimer + "с")); + } + } else { + // Таймер истёк — открываем ворота + gateManager.openAllGates(); + endgameState = EndgameState.POSTGAME; + endgameTimer = session.getConfig().getPostgameSeconds(); + for (BasePlayer bp : session.getPlayers().values()) { + Player p = bp.getPlayer(); + if (p == null) continue; + Title title = Title.title( + Component.text("ВОРОТА ОТКРЫТЫ!").color(NamedTextColor.GREEN), + Component.text("Бегите к выходу! (" + endgameTimer + "с)").color(NamedTextColor.YELLOW), + Title.Times.times(Duration.ofMillis(500), Duration.ofSeconds(3), Duration.ofMillis(500)) + ); + p.showTitle(title); + p.playSound(p.getLocation(), Sound.BLOCK_IRON_DOOR_OPEN, 1.0f, 1.0f); + } + } + } else if (endgameState == EndgameState.POSTGAME) { + if (endgameTimer > 0 && endgameTimer % 10 == 0 && endgameTimer <= 30) { + Bukkit.broadcast(Component.text("§c⚠ До конца игры: §e" + endgameTimer + "с")); + } + } + } + + private void checkEscapes(GameSession session) { + double escapeRange = session.getConfig().getEscapeRange(); + for (BasePlayer bp : session.getPlayers().values()) { + if (bp.isKiller()) continue; + if (bp.getState() == PlayerState.ESCAPED) continue; + if (bp.getState() != PlayerState.HEALTHY && bp.getState() != PlayerState.INJURED) continue; + Survivor survivor = (Survivor) bp; + Player p = survivor.getPlayer(); + if (p == null || !p.isOnline()) continue; + ExitGate gate = gateManager.getGateAt(p.getLocation(), escapeRange); + if (gate != null && gate.isOpen()) { + survivor.setState(PlayerState.ESCAPED); + p.playSound(p.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.2f); + Bukkit.broadcast(Component.text( + "§6[DBD] §a" + p.getName() + " §6сбежал через ворота!")); + } + } + } + private void spawnGameBlocks(mdbd plugin, GameConfig cfg) { // Спавн блоков на местах генераторов Material genMat = Material.matchMaterial(cfg.getGeneratorBlock()); @@ -333,35 +427,6 @@ public class GameplayPhase implements Phase { // Cleanup // ========================================================================= - private void showGameResults(GameSession session) { - List results = new ArrayList<>(); - results.add("§6§l=== РЕЗУЛЬТАТЫ ==="); - int escaped = 0, died = 0; - for (BasePlayer player : session.getPlayers().values()) { - if (player.isKiller()) { - Player p = player.getPlayer(); - String name = p != null ? p.getName() : "Unknown"; - results.add("§c☠ " + name + " — Маньяк"); - 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++; - } - } - results.add("§7Выживших: §a" + escaped + " §7/ Погибших: §c" + died); - String msg = String.join("\n", results); - for (BasePlayer player : session.getPlayers().values()) { - Player p = player.getPlayer(); - if (p != null) p.sendMessage(msg); - } - } - private void teleportAllToLobby(GameSession session) { for (BasePlayer bp : session.getPlayers().values()) { Player p = bp.getPlayer(); @@ -395,12 +460,11 @@ public class GameplayPhase implements Phase { if (hookStruggle != null) hookStruggle.clear(); if (chestManager != null) chestManager.stopLootResetTimer(); if (chestHologram != null) chestHologram.stop(); - // Clean up gate blocks + // Clean up gate blocks — don't reset gate state (EndgamePhase needs to know gates were open) if (gateManager != null) { for (ExitGate gate : gateManager.getAllGates()) { gate.removeGateBlock(); } - gateManager.resetAll(); } session.getPlugin().getInteractListener().setGeneratorManager(null); } diff --git a/src/main/java/dev/loki/survivor/SurvivorHealing.java b/src/main/java/dev/loki/survivor/SurvivorHealing.java index efaa516..b34ae8c 100644 --- a/src/main/java/dev/loki/survivor/SurvivorHealing.java +++ b/src/main/java/dev/loki/survivor/SurvivorHealing.java @@ -4,8 +4,8 @@ import dev.loki.config.GameConfig; import dev.loki.mdbd; import dev.loki.player.PlayerState; import dev.loki.player.Survivor; +import dev.loki.util.TextUtil; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.entity.Player; import java.util.UUID; @@ -98,6 +98,10 @@ public class SurvivorHealing { if (targetSurvivor == null) { cancelHealing(); return; } PlayerState targetState = targetSurvivor.getState(); + if (targetState == PlayerState.DEAD || targetState == PlayerState.ESCAPED) { + cancelHealing(); + return; + } if (targetState != PlayerState.INJURED && targetState != PlayerState.DOWNED) { completeHealing(targetSurvivor); return; @@ -166,7 +170,7 @@ public class SurvivorHealing { String bar = renderProgressBar(progress); String template = mdbd.getInstance().getConfigManager().getMessages().getString( isSelfHeal ? "survivor.heal.self-start" : "survivor.heal.other-start", bar); - Component message = MiniMessage.miniMessage().deserialize(template); + Component message = TextUtil.color(template); healingTarget.sendActionBar(message); // Also show on healer Player healer = controller.getPlayer(); diff --git a/src/main/java/dev/loki/survivor/SurvivorMovement.java b/src/main/java/dev/loki/survivor/SurvivorMovement.java index c4c4e2a..8f5830e 100644 --- a/src/main/java/dev/loki/survivor/SurvivorMovement.java +++ b/src/main/java/dev/loki/survivor/SurvivorMovement.java @@ -77,7 +77,7 @@ public class SurvivorMovement { if (player == null) return; if (isCrawling) { - player.addPotionEffect(new PotionEffect(PotionEffectType.SLOWNESS, 5, 2, false, false)); + player.addPotionEffect(new PotionEffect(PotionEffectType.SLOW_FALLING, 10, 0, false, false)); } } diff --git a/src/main/java/dev/loki/ui/ActionBar.java b/src/main/java/dev/loki/ui/ActionBar.java index daf40d6..d942064 100644 --- a/src/main/java/dev/loki/ui/ActionBar.java +++ b/src/main/java/dev/loki/ui/ActionBar.java @@ -4,6 +4,7 @@ import dev.loki.hook.Hook; import dev.loki.hook.HookStage; import dev.loki.player.PlayerState; import dev.loki.player.Survivor; +import dev.loki.util.TextUtil; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.format.NamedTextColor; @@ -12,7 +13,7 @@ import org.bukkit.entity.Player; public class ActionBar { public void sendMessage(Player player, String message) { - player.sendActionBar(Component.text(message)); + player.sendActionBar(TextUtil.color(message)); } public void sendComponent(Player player, Component component) { @@ -26,20 +27,20 @@ public class ActionBar { public void showRepairProgress(Player player, double percent, boolean skillCheckActive) { Component bar = ProgressBar.create(percent); - TextComponent message = Component.text("Repairing ") + TextComponent message = Component.text("Починка: ") .color(NamedTextColor.WHITE) .append(bar) .append(Component.text(" " + (int) percent + "%").color(NamedTextColor.GRAY)); if (skillCheckActive) { - message = message.append(Component.text(" ⚠ SKILL CHECK!").color(NamedTextColor.YELLOW)); + message = message.append(Component.text(" ⚠ СКИЛЛ-ЧЕК!").color(NamedTextColor.YELLOW)); } player.sendActionBar(message); } public void showHealProgress(Player player, double percent, boolean selfHeal) { - String label = selfHeal ? "Healing self" : "Healing ally"; + String label = selfHeal ? "Лечение себя" : "Лечение союзника"; Component bar = ProgressBar.create(percent); Component message = Component.text(label + " ") .color(NamedTextColor.WHITE) @@ -50,7 +51,7 @@ public class ActionBar { public void showRescueProgress(Player player, double percent) { Component bar = ProgressBar.create(percent); - Component message = Component.text("Rescuing ") + Component message = Component.text("Спасение: ") .color(NamedTextColor.WHITE) .append(bar) .append(Component.text(" " + (int) percent + "%").color(NamedTextColor.GRAY)); @@ -71,15 +72,15 @@ public class ActionBar { if (stage == HookStage.STAGE_1) { stageIcon = StatusIcons.HOOK; stageColor = NamedTextColor.YELLOW; - stageName = "Stage 1"; + stageName = "Стадия 1"; } else if (stage == HookStage.STAGE_2) { stageIcon = StatusIcons.HOOK; stageColor = NamedTextColor.RED; - stageName = "Stage 2 - STRUGGLE!"; + stageName = "Стадия 2 — ВЫРВИСЬ!"; } else { stageIcon = StatusIcons.DEATH_SKULL; stageColor = NamedTextColor.DARK_RED; - stageName = "SACRIFICE IMMINENT"; + stageName = "ЖЕРТВОПРИНОШЕНИЕ"; } Component bar = stage == HookStage.STAGE_2 @@ -91,7 +92,7 @@ public class ActionBar { .append(bar); if (stage == HookStage.STAGE_2) { - message = message.append(Component.text(" CROUCH to struggle!").color(NamedTextColor.YELLOW)); + message = message.append(Component.text(" ПРИСЯДЬ чтобы вырваться!").color(NamedTextColor.YELLOW)); } player.sendActionBar(message); @@ -108,22 +109,22 @@ public class ActionBar { private Component formatStatus(PlayerState state) { return switch (state) { case HEALTHY -> StatusIcons.HEALTHY_HEART - .append(Component.text(" Healthy").color(NamedTextColor.GREEN)); + .append(Component.text(" Здоров").color(NamedTextColor.GREEN)); case INJURED -> StatusIcons.INJURED_HEART - .append(Component.text(" Injured").color(NamedTextColor.YELLOW)); - case DOWNED -> Component.text("🆘 Downed - need help!").color(NamedTextColor.RED); + .append(Component.text(" Ранен").color(NamedTextColor.YELLOW)); + case DOWNED -> Component.text("🆘 В нокдауне — нужна помощь!").color(NamedTextColor.RED); case HOOKED -> StatusIcons.HOOK - .append(Component.text(" Hooked!").color(NamedTextColor.DARK_RED)); - case CARRIED -> Component.text("🎒 Being carried").color(NamedTextColor.RED); + .append(Component.text(" На крюке!").color(NamedTextColor.DARK_RED)); + case CARRIED -> Component.text("🎒 Унесен маньяком").color(NamedTextColor.RED); default -> Component.text(""); }; } public void showCarrying(Player killer, String survivorName) { - Component message = Component.text("Carrying: ") + Component message = Component.text("Несет: ") .color(NamedTextColor.WHITE) .append(Component.text(survivorName).color(NamedTextColor.YELLOW)) - .append(Component.text(" - Find a hook!").color(NamedTextColor.GRAY)); + .append(Component.text(" — Найди крюк!").color(NamedTextColor.GRAY)); killer.sendActionBar(message); } diff --git a/src/main/java/dev/loki/ui/Sidebar.java b/src/main/java/dev/loki/ui/Sidebar.java index 96f3579..fd3eb62 100644 --- a/src/main/java/dev/loki/ui/Sidebar.java +++ b/src/main/java/dev/loki/ui/Sidebar.java @@ -94,14 +94,14 @@ public class Sidebar { // Clear ALL old entries in this objective first time only if (lastSeconds == null) { for (String entry : new ArrayList<>(board.getEntries())) { - if (timerObj.getScore(entry).getScore() == 0 || entry.contains("⏱") || entry.contains("До конца")) { + if (timerObj.getScore(entry).getScore() == 0 || entry.contains("⏱") || entry.contains("Time left")) { board.resetScores(entry); } } } // Use a FIXED entry key that never changes - only the score updates - String timerEntry = "§6⏱ До конца: §c" + seconds + " сек"; + String timerEntry = "§6⏱ Осталось: §c" + seconds + "с"; // Simply update the score - the entry persists Score score = timerObj.getScore(timerEntry); @@ -124,7 +124,7 @@ public class Sidebar { // Clear entries and hide for (String entry : board.getEntries()) { - if (entry.contains("⏱") || entry.contains("До конца")) { + if (entry.contains("⏱") || entry.contains("Осталось")) { board.resetScores(entry); } } @@ -155,19 +155,17 @@ public class Sidebar { if (endgameSecondsLeft >= 0) { String timerColor = endgameSecondsLeft <= 30 ? "§c" : "§e"; setScore(objective, "§8─────────────────", line--); - setScore(objective, timerColor + "⏱ До конца: " + endgameSecondsLeft + " сек", line--); + setScore(objective, timerColor + "⏱ Осталось: " + endgameSecondsLeft + "с", line--); setScore(objective, "§8─────────────────", line--); } - // Generator progress + // Generator progress — use actual placed count, not config max int completedGens = generatorManager != null ? generatorManager.getCompletedCount() : 0; - int requiredGens = cfg.getGeneratorRequired(); - int totalGens = cfg.getGeneratorTotal(); + int totalGens = generatorManager != null ? generatorManager.getTotalCount() : cfg.getGeneratorRequired(); setScore(objective, "§eГенераторы:", line--); - String genBar = buildGenBar(completedGens, requiredGens); - setScore(objective, " §f" + completedGens + "/" + requiredGens + " " + genBar, line--); - setScore(objective, " §7(всего: " + totalGens + ")", line--); + String genBar = buildGenBar(completedGens, totalGens); + setScore(objective, " §f" + completedGens + "/" + totalGens + " " + genBar, line--); setScore(objective, " ", line--); // Survivor list with hearts and colors diff --git a/src/main/java/dev/loki/ui/TitleUI.java b/src/main/java/dev/loki/ui/TitleUI.java index 22791bc..13c8c95 100644 --- a/src/main/java/dev/loki/ui/TitleUI.java +++ b/src/main/java/dev/loki/ui/TitleUI.java @@ -33,16 +33,16 @@ public class TitleUI { Component subtitle; if (isKiller) { - title = Component.text("KILLER") + title = Component.text("МАНЬЯК") .color(NamedTextColor.DARK_RED) .decoration(TextDecoration.BOLD, true); - subtitle = Component.text("Sacrifice them all") + subtitle = Component.text("Принеси их в жертву") .color(NamedTextColor.RED); } else { - title = Component.text("SURVIVOR") + title = Component.text("ВЫЖИВШИЙ") .color(NamedTextColor.DARK_GREEN) .decoration(TextDecoration.BOLD, true); - subtitle = Component.text("Repair generators and escape") + subtitle = Component.text("Чини генераторы и выбирайся") .color(NamedTextColor.GREEN); } @@ -57,17 +57,17 @@ public class TitleUI { ); Component title = escaped - ? Component.text("ESCAPED!") + ? Component.text("СБЕЖАЛ!") .color(NamedTextColor.GREEN) .decoration(TextDecoration.BOLD, true) - : Component.text("VICTORY!") + : Component.text("ПОБЕДА!") .color(NamedTextColor.DARK_RED) .decoration(TextDecoration.BOLD, true); Component subtitle = escaped - ? Component.text("You survived the night") + ? Component.text("Ты пережил ночь") .color(NamedTextColor.DARK_GREEN) - : Component.text("All survivors sacrificed") + : Component.text("Все выжившие принесены в жертву") .color(NamedTextColor.RED); player.showTitle(Title.title(title, subtitle, times)); @@ -80,11 +80,11 @@ public class TitleUI { Duration.ofSeconds(1) ); - Component title = Component.text("DEFEAT") + Component title = Component.text("ПОРАЖЕНИЕ") .color(NamedTextColor.DARK_RED) .decoration(TextDecoration.BOLD, true); - Component subtitle = Component.text("The Entity claims all") + Component subtitle = Component.text("Сущность забирает всех") .color(NamedTextColor.RED); player.showTitle(Title.title(title, subtitle, times)); diff --git a/src/main/resources/game.yml b/src/main/resources/game.yml index 05bf4f6..8c843b7 100644 --- a/src/main/resources/game.yml +++ b/src/main/resources/game.yml @@ -195,12 +195,21 @@ endgame: # Время открытия ворот (секунд) gate-open-seconds: 20 + # Секунд до открытия ворот (после починки всех ген) + pregame-seconds: 30 + + # Секунд после открытия ворот до конца игры + postgame-seconds: 60 + # Открытие сундуков (секунд) chest-open-seconds: 3 # Расстояние для взаимодействия с воротами (блоков) gate-interaction-range: 3.0 + # Расстояние до ворот для побега (блоков) + escape-range: 2.0 + # Уведомление маньяка о генераторе killer-notification: # Время действия компаса (секунд) diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml index 2db8982..289728c 100644 --- a/src/main/resources/messages.yml +++ b/src/main/resources/messages.yml @@ -116,6 +116,11 @@ setup: survivor-spawn-placed: "%setup-prefix%&aСпавн для &6%s &aвыжившего установлен! (всего: %d/4) %s" wand-point: "%setup-prefix%&a[Wand] Точка %d: %s" map-cleared: "%setup-prefix%&aКарта очищена!" + cleared: "%setup-prefix%&aВсе объекты удалены из конфига!" + cleared-type: "%setup-prefix%&aОбъекты &6%s &aудалены из конфига!" + delete-usage: "%setup-prefix%&cИспользование: /dbdsetup delete <тип> <номер>" + out-of-range: "%setup-prefix%&cИндекс &6%d &cвне диапазона!" + deleted: "%setup-prefix%&aОбъект &6%s &aудалён!" spawns-cleared: "%setup-prefix%&aВсе спавны (киллер + выжившие) очищены!" lobby-set: "%setup-prefix%&aЛобби установлено: %s" @@ -200,7 +205,7 @@ survivor: actionbar: "&a⚕ %s %d%%" done: "%prefix%&a✔ Вы вылечились!" cancelled: "%prefix%&eЛечение прервано." - depleted: "&cYour medkit has been depleted!" + depleted: "&cМедкит закончился!" # ── Крюки ────────────────────────────────────── @@ -228,18 +233,22 @@ hook: sacrifice: message: "%prefix%&4Вы были принесены в жертву. Наблюдайте за матчем." title: "☠" - subtitle: "&cYou have been sacrificed" - broadcast: "%s has been sacrificed to the Entity" + subtitle: "&cВы были принесены в жертву" + broadcast: "%s принесен в жертву Сущности" # ── Сундуки ──────────────────────────────────── chest: - empty: "&cThis chest is empty!" - opening: "&eOpening chest..." - cancelled: "&cOpening cancelled!" + empty: "&cСундук пуст!" + opening: "&eОткрытие сундука..." + cancelled: "&cОткрытие прервано!" too-far: "%prefix%&cСлишком далеко!" actionbar: "&6Открытие: %s %d%%" found: "%prefix%&aВы нашли: %s" + +# ── Ворота ───────────────────────────────────── + +gate: door-open: "&aОткрываем ворота... (зажмите ПКМ)" door-opened: "&aВорота открыты!" door-nearby: "&cНет ворот поблизости!" @@ -307,6 +316,8 @@ usage: setup-killer-spawn: "&6/dbdsetup set killer-spawn — Спавн киллера (только 1)" setup-survivor-spawn: "&6/dbdsetup set survivor-spawn — Спавн выжившего (до 4)" setup-gate: "&6/dbdsetup set gate A|B — Установить ворота (из точек ванда)" + setup-clear: "&6/dbdsetup clear [тип] — Очистить объекты (без разрушения блоков)" + setup-delete: "&6/dbdsetup delete <тип> <#> — Удалить конкретный объект" setup-clear-spawns: "&6/dbdsetup clear-spawns — Очистить все спавны" setup-lobby: "&6/dbdsetup lobby — Установить точку лобби" admin-list-header: "&6=== DBD Admin ==="