From 7c7562a8d453078a13ff72f6745060425f4981ce Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Sun, 14 Jun 2026 16:04:25 +0200 Subject: [PATCH] refactor: generator minigame system (gui + text), fix color codes, holograms, messages, & config --- .vscode/settings.json | 14 +- .../java/dev/loki/chest/ChestHologram.java | 14 +- .../java/dev/loki/chest/ChestManager.java | 11 + .../java/dev/loki/chest/ChestOpening.java | 33 +- .../dev/loki/command/DBDAdminCommand.java | 41 +- .../loki/command/DBDAdminPlayerCommand.java | 59 +-- .../java/dev/loki/command/DBDCommand.java | 249 ++++++++--- .../java/dev/loki/command/SetupCommand.java | 146 +++---- src/main/java/dev/loki/config/GameConfig.java | 11 +- .../java/dev/loki/config/LocationsConfig.java | 40 +- .../java/dev/loki/config/MessagesConfig.java | 136 ++---- src/main/java/dev/loki/core/GameSession.java | 9 +- src/main/java/dev/loki/gate/ExitGate.java | 14 + src/main/java/dev/loki/gate/GateOpening.java | 16 +- src/main/java/dev/loki/hatch/HatchEscape.java | 19 +- src/main/java/dev/loki/hook/HookHologram.java | 9 +- src/main/java/dev/loki/hook/HookRecovery.java | 21 +- src/main/java/dev/loki/hook/HookRescue.java | 11 +- src/main/java/dev/loki/hook/HookSabotage.java | 31 +- .../java/dev/loki/hook/HookSacrifice.java | 11 +- src/main/java/dev/loki/hook/HookStruggle.java | 9 +- src/main/java/dev/loki/item/DBDItems.java | 25 +- src/main/java/dev/loki/item/ItemMedkit.java | 3 +- .../java/dev/loki/item/ItemUseListener.java | 20 +- .../java/dev/loki/killer/GeneratorKick.java | 21 +- .../loki/listener/PlayerInteractListener.java | 78 ++-- .../dev/loki/listener/PlayerQuitListener.java | 25 +- .../dev/loki/listener/SkillCheckListener.java | 21 +- src/main/java/dev/loki/mdbd.java | 16 +- .../dev/loki/minigames/GeneratorMinigame.java | 14 + .../generator/GuiRouletteMinigame.java | 214 ++++++++++ .../generator/TextSkillCheckMinigame.java | 150 +++++++ .../dev/loki/objective/GeneratorHologram.java | 3 +- .../dev/loki/objective/GeneratorRepair.java | 93 +++-- .../loki/objective/SkillCheckInventory.java | 17 +- .../java/dev/loki/phase/EndgamePhase.java | 55 ++- .../dev/loki/phase/GameplayInteraction.java | 58 ++- .../java/dev/loki/phase/GameplayPhase.java | 58 ++- src/main/java/dev/loki/phase/GameplayUI.java | 2 +- .../dev/loki/phase/GameplayWinConditions.java | 33 +- src/main/java/dev/loki/phase/LobbyPhase.java | 36 +- .../java/dev/loki/phase/RoleAssignPhase.java | 4 +- src/main/java/dev/loki/role/RoleAssigner.java | 37 +- .../dev/loki/survivor/SurvivorHealing.java | 24 +- src/main/java/dev/loki/ui/TitleUI.java | 5 +- src/main/java/dev/loki/util/SpawnUtil.java | 2 +- src/main/java/dev/loki/util/TextUtil.java | 50 ++- src/main/resources/messages.yml | 393 +++++++++++++----- 48 files changed, 1628 insertions(+), 733 deletions(-) create mode 100644 src/main/java/dev/loki/minigames/GeneratorMinigame.java create mode 100644 src/main/java/dev/loki/minigames/generator/GuiRouletteMinigame.java create mode 100644 src/main/java/dev/loki/minigames/generator/TextSkillCheckMinigame.java diff --git a/.vscode/settings.json b/.vscode/settings.json index affe967..f705ef5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,8 @@ -{ - "chat.tools.terminal.autoApprove": { - "/^\\.\\\\gradlew\\.bat build --no-daemon 2>&1 \\| Select-Object -Last 40$/": { - "approve": true, - "matchCommandLine": true - } - } +{ + "chat.tools.terminal.autoApprove": { + "/^\\.\\\\gradlew\\.bat build --no-daemon 2>&1 \\| Select-Object -Last 40$/": { + "approve": true, + "matchCommandLine": true + } + } } \ No newline at end of file diff --git a/src/main/java/dev/loki/chest/ChestHologram.java b/src/main/java/dev/loki/chest/ChestHologram.java index 7a09ac3..d61a787 100644 --- a/src/main/java/dev/loki/chest/ChestHologram.java +++ b/src/main/java/dev/loki/chest/ChestHologram.java @@ -1,6 +1,7 @@ package dev.loki.chest; import dev.loki.mdbd; +import dev.loki.util.TextUtil; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.entity.ArmorStand; @@ -77,7 +78,7 @@ public class ChestHologram { if (useTextDisplay) { TextDisplay display = loc.getWorld().spawn(holoLoc, TextDisplay.class, d -> { d.setBillboard(Display.Billboard.VERTICAL); - d.setSeeThrough(true); + d.setSeeThrough(false); d.setShadowed(true); d.setDefaultBackground(false); d.setPersistent(false); @@ -117,7 +118,14 @@ public class ChestHologram { String status; if (chest.isEmpty()) { - status = "§7Пуст"; + long secs = manager.getSecondsUntilNextReset(); + if (secs > 60) { + status = "§7Пуст §8(" + (secs / 60) + "м)"; + } else if (secs > 0) { + status = "§7Пуст §8(" + secs + "с)"; + } else { + status = "§7Пуст"; + } } else if (chest.isOpened()) { status = "§6Открывается..."; } else if (chest.hasLoot()) { @@ -130,7 +138,7 @@ public class ChestHologram { String text = "§e📦 Сундук §f#" + visibleIndex + "/" + total + "\n§7" + status; if (display instanceof TextDisplay td) { - td.text(net.kyori.adventure.text.Component.text(text)); + td.text(TextUtil.color(text)); Location base = chest.getLocation().clone().add(0.5, 2.0 + heightOffset, 0.5); td.teleport(base); } else if (display instanceof ArmorStand as) { diff --git a/src/main/java/dev/loki/chest/ChestManager.java b/src/main/java/dev/loki/chest/ChestManager.java index b8fc1f8..f38b4e7 100644 --- a/src/main/java/dev/loki/chest/ChestManager.java +++ b/src/main/java/dev/loki/chest/ChestManager.java @@ -21,6 +21,7 @@ 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; @@ -73,6 +74,7 @@ public class ChestManager { chest.setEmpty(false); refilled++; } + this.lastResetTime = System.currentTimeMillis(); plugin.getLogger().info("[Chest] Loot reset — " + refilled + " chests refilled"); } @@ -115,6 +117,7 @@ public class ChestManager { chest.setEmpty(false); chest.setLoot(chestLoot.rollLoot()); } + this.lastResetTime = System.currentTimeMillis(); plugin.getLogger().info("Reset all chests"); } @@ -137,6 +140,14 @@ 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 16eed29..32a4122 100644 --- a/src/main/java/dev/loki/chest/ChestOpening.java +++ b/src/main/java/dev/loki/chest/ChestOpening.java @@ -1,6 +1,9 @@ package dev.loki.chest; +import dev.loki.config.MessagesConfig; import dev.loki.item.ChestLoot; +import dev.loki.mdbd; +import net.kyori.adventure.text.Component; import org.bukkit.Location; import org.bukkit.Sound; import org.bukkit.World; @@ -28,6 +31,10 @@ public class ChestOpening { this.activeOpenings = new HashMap<>(); } + private MessagesConfig messages() { + return ((mdbd) plugin).getConfigManager().getMessages(); + } + /** Передать конфиг для весов лута */ public void setChestLoot(ChestLoot loot) { this.chestLoot = loot; @@ -44,7 +51,7 @@ public class ChestOpening { if (!chest.hasLoot()) { ItemStack rolled = chestLoot.rollLoot(); if (rolled == null) { - player.sendMessage("§cThis chest is empty!"); + player.sendMessage(messages().get("chest.empty")); return false; } chest.setLoot(rolled); @@ -52,13 +59,13 @@ public class ChestOpening { } if (!chest.isAvailable()) { - player.sendMessage("§cThis chest is empty!"); + player.sendMessage(messages().get("chest.empty")); return false; } chest.setOpened(true); chest.openVisual(); - player.sendMessage("§eOpening chest..."); + player.sendMessage(messages().get("chest.opening")); playOpeningSound(player, chest.getLocation()); BukkitRunnable task = new BukkitRunnable() { @@ -72,12 +79,11 @@ public class ChestOpening { return; } if (!chest.isInRange(player.getLocation(), OPEN_RANGE)) { - player.sendMessage(net.kyori.adventure.text.Component.text( - "[DBD] Слишком далеко!", net.kyori.adventure.text.format.NamedTextColor.RED)); + player.sendMessage(messages().get("chest.too-far")); chest.setOpened(false); chest.closeVisual(); activeOpenings.remove(playerId); - player.sendActionBar(net.kyori.adventure.text.Component.empty()); + player.sendActionBar(Component.empty()); cancel(); return; } @@ -85,10 +91,10 @@ public class ChestOpening { int filled = (int)(8.0 * ticks / OPEN_TIME_TICKS); int empty = 8 - filled; int pct = (int)(100.0 * ticks / OPEN_TIME_TICKS); - player.sendActionBar(net.kyori.adventure.text.Component.text( - "§6Открытие: §e" + "█".repeat(filled) + "§7" + "░".repeat(empty) + " " + pct + "%")); + String bar = "§e" + "█".repeat(filled) + "§7" + "░".repeat(empty); + player.sendActionBar(messages().get("chest.actionbar", bar, pct)); if (ticks >= OPEN_TIME_TICKS) { - player.sendActionBar(net.kyori.adventure.text.Component.empty()); + player.sendActionBar(Component.empty()); completeOpening(player, chest); cancel(); } @@ -107,7 +113,7 @@ public class ChestOpening { if (task != null) { task.cancel(); - player.sendMessage("§cOpening cancelled!"); + player.sendMessage(messages().get("chest.cancelled")); } } @@ -133,8 +139,7 @@ public class ChestOpening { // Use pre-set loot from chest, or roll new if none ItemStack loot = chest.hasLoot() ? chest.getLoot() : chestLoot.rollLoot(); if (loot == null) { - player.sendMessage(net.kyori.adventure.text.Component.text( - "[DBD] Сундук пуст!", net.kyori.adventure.text.format.NamedTextColor.RED)); + player.sendMessage(messages().get("chest.empty")); chest.setOpened(false); chest.closeVisual(); return; @@ -146,6 +151,7 @@ public class ChestOpening { player.getWorld().dropItemNaturally(player.getLocation(), item); } } + chest.setOpened(false); chest.setEmpty(true); chest.setLoot(null); // Clear loot after giving chest.closeVisual(); @@ -156,8 +162,7 @@ public class ChestOpening { .serialize(loot.getItemMeta().displayName()) : loot.getType().name(); - player.sendMessage(net.kyori.adventure.text.Component.text( - "[DBD] Вы нашли: " + itemName, net.kyori.adventure.text.format.NamedTextColor.GREEN)); + player.sendMessage(messages().get("chest.found", itemName)); playSuccessSound(player, chest.getLocation()); } diff --git a/src/main/java/dev/loki/command/DBDAdminCommand.java b/src/main/java/dev/loki/command/DBDAdminCommand.java index bb9682d..516f0c4 100644 --- a/src/main/java/dev/loki/command/DBDAdminCommand.java +++ b/src/main/java/dev/loki/command/DBDAdminCommand.java @@ -1,10 +1,6 @@ package dev.loki.command; -import dev.loki.config.LocationsConfig; import dev.loki.mdbd; -import dev.loki.util.TextUtil; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Location; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; @@ -27,13 +23,15 @@ public class DBDAdminCommand implements TabExecutor { @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + var messages = plugin.getConfigManager().getMessages(); + if (!sender.hasPermission("mdbd.admin")) { - sender.sendMessage(Component.text("Нет прав!", NamedTextColor.RED)); + sender.sendMessage(messages.get("cmd.no-permission")); return true; } if (!(sender instanceof Player player)) { - sender.sendMessage(Component.text("Только игроки!", NamedTextColor.RED)); + sender.sendMessage(messages.get("cmd.players-only")); return true; } @@ -64,14 +62,14 @@ public class DBDAdminCommand implements TabExecutor { case "killer-spawn", "killerspawn" -> listType(player, "Спавн киллера", locations.getKillerSpawns()); case "survivor-spawn", "survivorspawn" -> listType(player, "Спавн выживших", locations.getSurvivorSpawns()); default -> { - player.sendMessage(TextUtil.color("<#FF0000>Неизвестный тип: " + args[1])); + player.sendMessage(plugin.getConfigManager().getMessages().get("dbdadmin.unknown-type", args[1])); yield true; } }; } // Показать всё - player.sendMessage(TextUtil.color("<#FF6B00>=== Все установленные объекты ===")); + player.sendMessage(plugin.getConfigManager().getMessages().get("dbdadmin-list.header")); listType(player, "Генераторы", locations.getGenerators()); listType(player, "Крюки", locations.getHooks()); @@ -83,24 +81,24 @@ public class DBDAdminCommand implements TabExecutor { } private boolean listType(Player player, String label, List locations) { + var messages = plugin.getConfigManager().getMessages(); if (locations.isEmpty()) { - player.sendMessage(TextUtil.color("<#808000>" + label + ": нет")); + player.sendMessage(messages.get("dbdadmin-list.none", label)); return true; } - player.sendMessage(TextUtil.color("<#FFD700>" + label + " (" + locations.size() + "):")); + player.sendMessage(messages.get("dbdadmin-list.count", label, locations.size())); for (int i = 0; i < locations.size(); i++) { Location loc = locations.get(i); - player.sendMessage(TextUtil.color(String.format( - "<#00CED1> #%d %s [%.1f, %.1f, %.1f]", + player.sendMessage(messages.get("dbdadmin-list.entry", i + 1, loc.getWorld().getName(), loc.getX(), loc.getY(), loc.getZ() - ))); + )); } return true; } private boolean handleWorld(Player player, String[] args) { if (args.length < 2) { - player.sendMessage(TextUtil.color("<#FF0000>Использование: /dbdadmin world ")); + player.sendMessage(plugin.getConfigManager().getMessages().get("dbdadmin-list.world-usage")); return true; } @@ -110,27 +108,28 @@ public class DBDAdminCommand implements TabExecutor { if (worldArg.equals("game")) { target = plugin.getLobbyManager().getGameWorld(); if (target == null) { - player.sendMessage(TextUtil.color("<#FF0000>Мир game не загружен!")); + player.sendMessage(plugin.getConfigManager().getMessages().get("dbdadmin-list.world-not-found")); return true; } } else { target = org.bukkit.Bukkit.getWorld(args[1]); if (target == null) { - player.sendMessage(TextUtil.color("<#FF0000>Мир не найден: " + args[1])); + player.sendMessage(plugin.getConfigManager().getMessages().get("dbdadmin-list.world-not-exist", args[1])); return true; } } player.teleport(target.getSpawnLocation()); - player.sendMessage(TextUtil.color("<#00FF00>Телепортирован в мир: <#FFD700>" + target.getName())); + player.sendMessage(plugin.getConfigManager().getMessages().get("dbdadmin-list.world-teleport", target.getName())); return true; } private void sendUsage(CommandSender sender) { - sender.sendMessage(TextUtil.color("<#FF6B00>=== DBD Admin ===")); - sender.sendMessage(TextUtil.color("<#FFD700>/dbdadmin list — Список всех объектов")); - sender.sendMessage(TextUtil.color("<#FFD700>/dbdadmin list <тип> — Список по типу")); - sender.sendMessage(TextUtil.color("<#FFD700>/dbdadmin world — Телепорт в мир")); + var messages = plugin.getConfigManager().getMessages(); + sender.sendMessage(messages.get("usage.admin-list-header")); + sender.sendMessage(messages.get("usage.admin-list-list")); + sender.sendMessage(messages.get("usage.admin-list-type")); + sender.sendMessage(messages.get("usage.admin-world")); } @Override diff --git a/src/main/java/dev/loki/command/DBDAdminPlayerCommand.java b/src/main/java/dev/loki/command/DBDAdminPlayerCommand.java index acadefc..4393d10 100644 --- a/src/main/java/dev/loki/command/DBDAdminPlayerCommand.java +++ b/src/main/java/dev/loki/command/DBDAdminPlayerCommand.java @@ -4,12 +4,8 @@ import dev.loki.core.GameManager; import dev.loki.core.GameSession; import dev.loki.mdbd; import dev.loki.player.BasePlayer; -import dev.loki.player.Killer; 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.format.NamedTextColor; import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; @@ -31,8 +27,10 @@ public class DBDAdminPlayerCommand { } public boolean handle(CommandSender sender, String[] args) { + var messages = plugin.getConfigManager().getMessages(); + if (!sender.hasPermission("mdbd.admin")) { - sender.sendMessage(Component.text("Нет прав!", NamedTextColor.RED)); + sender.sendMessage(messages.get("cmd.no-permission")); return true; } if (args.length < 2) { @@ -43,7 +41,7 @@ public class DBDAdminPlayerCommand { String action = args[1].toLowerCase(Locale.ROOT); GameSession session = resolveSession(sender, args); if (session == null) { - sender.sendMessage(Component.text("[DBD] Нет активной игровой сессии.", NamedTextColor.RED)); + sender.sendMessage(messages.get("dbdadmin.no-session")); return true; } @@ -63,6 +61,7 @@ public class DBDAdminPlayerCommand { } private boolean heal(CommandSender sender, GameSession session, String[] args) { + var messages = plugin.getConfigManager().getMessages(); Survivor target = resolveSurvivor(sender, session, args, 2); if (target == null) return true; target.setState(PlayerState.HEALTHY); @@ -70,13 +69,14 @@ public class DBDAdminPlayerCommand { if (p != null) { p.setSwimming(false); p.setHealth(20.0); - p.sendMessage(Component.text("[DBD] Админ вылечил вас.", NamedTextColor.GREEN)); + p.sendMessage(messages.get("dbdadmin.heal.target")); } - sender.sendMessage(Component.text("[DBD] " + playerName(target) + " → HEALTHY", NamedTextColor.GREEN)); + sender.sendMessage(messages.get("dbdadmin.heal.sender", playerName(target))); return true; } private boolean setState(CommandSender sender, GameSession session, String[] args, PlayerState state) { + var messages = plugin.getConfigManager().getMessages(); Survivor target = resolveSurvivor(sender, session, args, 2); if (target == null) return true; target.setState(state); @@ -86,28 +86,30 @@ public class DBDAdminPlayerCommand { else p.setSwimming(false); p.setHealth(20.0); } - sender.sendMessage(Component.text("[DBD] " + playerName(target) + " → " + state.name(), NamedTextColor.YELLOW)); + sender.sendMessage(messages.get("dbdadmin.state.sender", playerName(target), state.name())); return true; } private boolean setStateArg(CommandSender sender, GameSession session, String[] args) { + var messages = plugin.getConfigManager().getMessages(); if (args.length < 4) { - sender.sendMessage(Component.text("[DBD] /dbd admin state <игрок> ", NamedTextColor.RED)); + sender.sendMessage(messages.get("dbdadmin.state.usage")); return true; } PlayerState state; try { state = PlayerState.valueOf(args[3].toUpperCase(Locale.ROOT)); } catch (IllegalArgumentException e) { - sender.sendMessage(Component.text("[DBD] Неизвестный статус.", NamedTextColor.RED)); + sender.sendMessage(messages.get("dbdadmin.state.unknown")); return true; } return setState(sender, session, args, state); } private boolean setHooks(CommandSender sender, GameSession session, String[] args) { + var messages = plugin.getConfigManager().getMessages(); if (args.length < 4) { - sender.sendMessage(Component.text("[DBD] /dbd admin hooks <игрок> <0-3>", NamedTextColor.RED)); + sender.sendMessage(messages.get("dbdadmin.hooks.usage")); return true; } Survivor target = resolveSurvivor(sender, session, args, 2); @@ -116,33 +118,33 @@ public class DBDAdminPlayerCommand { try { hooks = Integer.parseInt(args[3]); } catch (NumberFormatException e) { - sender.sendMessage(Component.text("[DBD] Число 0–3.", NamedTextColor.RED)); + sender.sendMessage(messages.get("dbdadmin.hooks.invalid")); return true; } hooks = Math.max(0, Math.min(3, hooks)); target.setHooksTaken(hooks); - sender.sendMessage(Component.text( - "[DBD] " + playerName(target) + " hooks=" + target.getHooksTaken(), NamedTextColor.GREEN)); + sender.sendMessage(messages.get("dbdadmin.hooks.sender", playerName(target), target.getHooksTaken())); return true; } private Survivor resolveSurvivor(CommandSender sender, GameSession session, String[] args, int nameIndex) { + var messages = plugin.getConfigManager().getMessages(); if (args.length <= nameIndex) { if (sender instanceof Player pl) { BasePlayer bp = session.getPlayer(pl.getUniqueId()); if (bp instanceof Survivor s) return s; } - sender.sendMessage(Component.text("[DBD] Укажите игрока.", NamedTextColor.RED)); + sender.sendMessage(messages.get("dbdadmin.resolve.no-player")); return null; } Player target = Bukkit.getPlayer(args[nameIndex]); if (target == null) { - sender.sendMessage(Component.text("[DBD] Игрок не в сети.", NamedTextColor.RED)); + sender.sendMessage(messages.get("dbdadmin.resolve.offline")); return null; } BasePlayer bp = session.getPlayer(target.getUniqueId()); if (!(bp instanceof Survivor s)) { - sender.sendMessage(Component.text("[DBD] Игрок не в этой игре или не выживший.", NamedTextColor.RED)); + sender.sendMessage(messages.get("dbdadmin.resolve.not-in-game")); return null; } return s; @@ -158,8 +160,8 @@ public class DBDAdminPlayerCommand { newState = !dev.loki.listener.PlayerJumpListener.isJumpingAllowed(); } dev.loki.listener.PlayerJumpListener.setJumpingAllowed(newState); - sender.sendMessage(Component.text( - "[DBD] Прыжки " + (newState ? "§aВКЛЮЧЕНЫ" : "§cВЫКЛЮЧЕНЫ"), NamedTextColor.GOLD)); + sender.sendMessage(plugin.getConfigManager().getMessages().get("dbdadmin.jump.sender", + newState ? "ВКЛЮЧЕНЫ" : "ВЫКЛЮЧЕНЫ")); return true; } @@ -184,14 +186,15 @@ public class DBDAdminPlayerCommand { } private void sendUsage(CommandSender sender) { - sender.sendMessage(TextUtil.color("<#FF6B00>=== /dbd admin ===")); - sender.sendMessage(TextUtil.color("<#FFD700>/dbd admin heal [игрок]")); - sender.sendMessage(TextUtil.color("<#FFD700>/dbd admin down [игрок]")); - sender.sendMessage(TextUtil.color("<#FFD700>/dbd admin unhook [игрок]")); - sender.sendMessage(TextUtil.color("<#FFD700>/dbd admin state <игрок> <статус>")); - sender.sendMessage(TextUtil.color("<#FFD700>/dbd admin hooks <игрок> <0-3>")); - sender.sendMessage(TextUtil.color("<#FFD700>/dbd admin kill [игрок]")); - sender.sendMessage(TextUtil.color("<#FFD700>/dbd admin jump [on/off] — вкл/выкл прыжки")); + var messages = plugin.getConfigManager().getMessages(); + sender.sendMessage(messages.get("usage.admin-header")); + sender.sendMessage(messages.get("usage.admin-heal")); + sender.sendMessage(messages.get("usage.admin-down")); + sender.sendMessage(messages.get("usage.admin-unhook")); + sender.sendMessage(messages.get("usage.admin-state")); + sender.sendMessage(messages.get("usage.admin-hooks")); + sender.sendMessage(messages.get("usage.admin-kill")); + sender.sendMessage(messages.get("usage.admin-jump")); } public static java.util.List tabComplete(String[] args) { diff --git a/src/main/java/dev/loki/command/DBDCommand.java b/src/main/java/dev/loki/command/DBDCommand.java index 25c5c2d..59ce45d 100644 --- a/src/main/java/dev/loki/command/DBDCommand.java +++ b/src/main/java/dev/loki/command/DBDCommand.java @@ -1,10 +1,12 @@ package dev.loki.command; import dev.loki.core.GameManager; +import dev.loki.core.GameSession; import dev.loki.core.GameState; import dev.loki.mdbd; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; +import dev.loki.player.BasePlayer; +import dev.loki.world.LobbyRegistry; +import org.bukkit.attribute.Attribute; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.command.TabExecutor; @@ -13,16 +15,19 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; public class DBDCommand implements TabExecutor { private final mdbd plugin; private final GameManager gameManager; + private final LobbyRegistry lobbyRegistry; private final DBDAdminPlayerCommand adminPlayerCommand; - public DBDCommand(mdbd plugin, GameManager gameManager) { - this.plugin = plugin; - this.gameManager = gameManager; + public DBDCommand(mdbd plugin, GameManager gameManager, LobbyRegistry lobbyRegistry) { + this.plugin = plugin; + this.gameManager = gameManager; + this.lobbyRegistry = lobbyRegistry; this.adminPlayerCommand = new DBDAdminPlayerCommand(plugin, gameManager); } @@ -34,12 +39,14 @@ public class DBDCommand implements TabExecutor { return switch (args[0].toLowerCase()) { case "join" -> handleJoin(sender, args); case "leave" -> handleLeave(sender); + case "create" -> handleCreate(sender, args); + case "list" -> handleList(sender); + case "delete" -> handleDelete(sender, args); case "forcestart" -> handleForceStart(sender); case "stop" -> handleStop(sender); case "reload" -> handleReload(sender); case "save" -> handleMapSave(sender, args); case "load" -> handleMapLoad(sender, args); - case "maps" -> handleMapsList(sender); case "lobby" -> handleLobby(sender); case "setlobby" -> handleSetLobby(sender); case "admin" -> adminPlayerCommand.handle(sender, args); @@ -52,14 +59,25 @@ public class DBDCommand implements TabExecutor { // ------------------------------------------------------------------------- private boolean handleJoin(CommandSender sender, String[] args) { if (!(sender instanceof Player player)) { - sender.sendMessage(Component.text("Только игроки!", NamedTextColor.RED)); + sender.sendMessage(plugin.getConfigManager().getMessages().get("cmd.players-only")); return true; } + // /dbd join [password] — присоединиться к именованному лобби + if (args.length >= 2) { + String[] lobbyArgs = java.util.Arrays.copyOfRange(args, 1, args.length); + lobbyRegistry.joinLobby(player, lobbyArgs); + return true; // всегда true чтобы не показывать usage из plugin.yml + } + + // /dbd join — присоединиться к дефолтной сессии + if (gameManager.getCurrentSession() == null) { + gameManager.createGame(); + } if (gameManager.joinGame(player)) { - player.sendMessage(Component.text("[DBD] Вы присоединились к игре!", NamedTextColor.GREEN)); + player.sendMessage(plugin.getConfigManager().getMessages().get("dbd.join.success")); } else { - player.sendMessage(Component.text("[DBD] Не удалось присоединиться. Возможно, игра уже идёт.", NamedTextColor.RED)); + player.sendMessage(plugin.getConfigManager().getMessages().get("dbd.join.fail")); } return true; } @@ -69,56 +87,164 @@ public class DBDCommand implements TabExecutor { // ------------------------------------------------------------------------- private boolean handleLeave(CommandSender sender) { if (!(sender instanceof Player player)) { - sender.sendMessage(Component.text("Только игроки!", NamedTextColor.RED)); + sender.sendMessage(plugin.getConfigManager().getMessages().get("cmd.players-only")); return true; } + GameSession session = gameManager.getPlayerSession(player.getUniqueId()); if (gameManager.leaveGame(player)) { - player.sendMessage(Component.text("[DBD] Вы покинули игру.", NamedTextColor.YELLOW)); + player.sendMessage(plugin.getConfigManager().getMessages().get("dbd.leave.success")); + // Если лобби опустело — удаляем + if (session != null && lobbyRegistry != null) { + lobbyRegistry.onPlayerLeftSession(session, player); + } } else { - player.sendMessage(Component.text("[DBD] Вы не в игре.", NamedTextColor.RED)); + player.sendMessage(plugin.getConfigManager().getMessages().get("dbd.join.not-in-game")); } return true; } + // ------------------------------------------------------------------------- + // /dbd create [password] [private] — создать лобби + // ------------------------------------------------------------------------- + private boolean handleCreate(CommandSender sender, String[] args) { + if (!(sender instanceof Player player)) { + sender.sendMessage(plugin.getConfigManager().getMessages().get("cmd.players-only")); + return true; + } + if (args.length < 2) { + sender.sendMessage(plugin.getConfigManager().getMessages().get("dbd.create.usage")); + return true; + } + String[] createArgs = java.util.Arrays.copyOfRange(args, 1, args.length); + lobbyRegistry.createLobby(player, createArgs); + return true; // всегда true чтобы не показывать usage из plugin.yml + } + + // ------------------------------------------------------------------------- + // /dbd list — список лобби + // ------------------------------------------------------------------------- + private boolean handleList(CommandSender sender) { + if (!(sender instanceof Player player)) { + sender.sendMessage(plugin.getConfigManager().getMessages().get("cmd.players-only")); + return true; + } + lobbyRegistry.listLobbies(player); + return true; + } + + // ------------------------------------------------------------------------- + // /dbd delete — удалить лобби + // ------------------------------------------------------------------------- + private boolean handleDelete(CommandSender sender, String[] args) { + if (!(sender instanceof Player player)) { + sender.sendMessage(plugin.getConfigManager().getMessages().get("cmd.players-only")); + return true; + } + if (args.length < 2) { + sender.sendMessage(plugin.getConfigManager().getMessages().get("dbd.delete.usage")); + return true; + } + lobbyRegistry.deleteLobby(player, args[1]); + return true; // всегда true чтобы не показывать usage из plugin.yml + } + // ------------------------------------------------------------------------- // Admin commands // ------------------------------------------------------------------------- private boolean handleForceStart(CommandSender sender) { if (!sender.hasPermission("mdbd.admin")) { - sender.sendMessage(Component.text("Нет прав!", NamedTextColor.RED)); + sender.sendMessage(plugin.getConfigManager().getMessages().get("cmd.no-permission")); return true; } - var session = gameManager.getCurrentSession(); + + // Определяем сессию — сначала по игроку, потом currentSession + GameSession session = null; + if (sender instanceof Player player) { + session = gameManager.getPlayerSession(player.getUniqueId()); + } if (session == null) { - sender.sendMessage(Component.text("Нет активной игры!", NamedTextColor.RED)); + session = gameManager.getCurrentSession(); + } + if (session == null) { + sender.sendMessage(plugin.getConfigManager().getMessages().get("dbd.forcestart.no-game")); return true; } if (session.getState() == GameState.LOBBY) { session.forceStart(); - sender.sendMessage(Component.text("[DBD] Игра форсирована!", NamedTextColor.GREEN)); + sender.sendMessage(plugin.getConfigManager().getMessages().get("dbd.forcestart.success")); } else { - sender.sendMessage(Component.text("Нельзя форсировать сейчас.", NamedTextColor.RED)); + sender.sendMessage(plugin.getConfigManager().getMessages().get("dbd.forcestart.fail")); } return true; } private boolean handleStop(CommandSender sender) { if (!sender.hasPermission("mdbd.admin")) { - sender.sendMessage(Component.text("Нет прав!", NamedTextColor.RED)); + sender.sendMessage(plugin.getConfigManager().getMessages().get("cmd.no-permission")); return true; } - gameManager.endGame(); - sender.sendMessage(Component.text("[DBD] Игра остановлена.", NamedTextColor.YELLOW)); + + // Останавливаем текущую сессию игрока, если есть + GameSession session = null; + if (sender instanceof Player player) { + session = gameManager.getPlayerSession(player.getUniqueId()); + } + if (session == null) { + // endGame — телепортируем всех участников в лобби + teleportAllToLobby(gameManager.getCurrentSession()); + gameManager.endGame(); + } else { + teleportAllToLobby(session); + session.stop(); + } + sender.sendMessage(plugin.getConfigManager().getMessages().get("dbd.stop.success")); return true; } + /** Телепорт всех игроков сессии в лобби со сбросом атрибутов. */ + private void teleportAllToLobby(GameSession session) { + if (session == null) return; + for (BasePlayer bp : session.getPlayers().values()) { + Player p = bp.getPlayer(); + if (p != null && p.isOnline()) { + resetPlayerAttributes(p); + plugin.getLobbyManager().teleportToLobby(p); + } + } + } + + private static void resetPlayerAttributes(Player p) { + p.setGameMode(org.bukkit.GameMode.ADVENTURE); + p.getInventory().clear(); + p.setWalkSpeed(0.2f); + p.setFlySpeed(0.1f); + p.setSwimming(false); + p.setSprinting(false); + p.setSneaking(false); + p.setHealth(20.0); + p.setFoodLevel(20); + p.setSaturation(5); + try { + var scale = p.getAttribute(Attribute.SCALE); + if (scale != null) scale.setBaseValue(1.0); + } catch (Exception ignored) {} + try { + var maxHp = p.getAttribute(Attribute.MAX_HEALTH); + if (maxHp != null) maxHp.setBaseValue(20.0); + } catch (Exception ignored) {} + try { + var speed = p.getAttribute(Attribute.MOVEMENT_SPEED); + if (speed != null) speed.setBaseValue(0.1); + } catch (Exception ignored) {} + } + private boolean handleReload(CommandSender sender) { if (!sender.hasPermission("mdbd.admin")) { - sender.sendMessage(Component.text("Нет прав!", NamedTextColor.RED)); + sender.sendMessage(plugin.getConfigManager().getMessages().get("cmd.no-permission")); return true; } plugin.reloadConfigs(); - sender.sendMessage(Component.text("[DBD] Конфиги перезагружены!", NamedTextColor.GREEN)); + sender.sendMessage(plugin.getConfigManager().getMessages().get("dbd.reload.success")); return true; } @@ -127,79 +253,64 @@ public class DBDCommand implements TabExecutor { // ------------------------------------------------------------------------- private boolean handleMapSave(CommandSender sender, String[] args) { if (!sender.hasPermission("mdbd.admin")) { - sender.sendMessage(Component.text("Нет прав!", NamedTextColor.RED)); + sender.sendMessage(plugin.getConfigManager().getMessages().get("cmd.no-permission")); return true; } if (args.length < 2) { - sender.sendMessage(Component.text("[DBD] Использование: /dbd save <название>", NamedTextColor.RED)); + sender.sendMessage(plugin.getConfigManager().getMessages().get("dbd.map.save-usage")); return true; } String name = args[1].toLowerCase(); if (plugin.getMapManager().saveMap(name)) { - sender.sendMessage(Component.text("[DBD] Карта «" + name + "» сохранена в plugins/Mdbd/maps/", NamedTextColor.GREEN)); + sender.sendMessage(plugin.getConfigManager().getMessages().get("dbd.map.save-success", name)); } else { - sender.sendMessage(Component.text("[DBD] Не удалось сохранить карту.", NamedTextColor.RED)); + sender.sendMessage(plugin.getConfigManager().getMessages().get("dbd.map.save-fail")); } return true; } private boolean handleMapLoad(CommandSender sender, String[] args) { if (!sender.hasPermission("mdbd.admin")) { - sender.sendMessage(Component.text("Нет прав!", NamedTextColor.RED)); + sender.sendMessage(plugin.getConfigManager().getMessages().get("cmd.no-permission")); return true; } if (args.length < 2) { - sender.sendMessage(Component.text("[DBD] Использование: /dbd load <название>", NamedTextColor.RED)); + sender.sendMessage(plugin.getConfigManager().getMessages().get("dbd.map.load-usage")); return true; } String name = args[1].toLowerCase(); if (plugin.getMapManager().loadMap(name)) { - sender.sendMessage(Component.text("[DBD] Карта «" + name + "» загружена!", NamedTextColor.GREEN)); - sender.sendMessage(Component.text("[DBD] Перезагрузите карту или перезапустите сервер для применения.", NamedTextColor.YELLOW)); + sender.sendMessage(plugin.getConfigManager().getMessages().get("dbd.map.load-success", name)); + sender.sendMessage(plugin.getConfigManager().getMessages().get("dbd.map.load-warn")); } else { - sender.sendMessage(Component.text("[DBD] Карта «" + name + "» не найдена.", NamedTextColor.RED)); - } - return true; - } - - private boolean handleMapsList(CommandSender sender) { - if (!sender.hasPermission("mdbd.admin")) { - sender.sendMessage(Component.text("Нет прав!", NamedTextColor.RED)); - return true; - } - List maps = plugin.getMapManager().listMaps(); - if (maps.isEmpty()) { - sender.sendMessage(Component.text("[DBD] Нет сохранённых карт.", NamedTextColor.YELLOW)); - } else { - sender.sendMessage(Component.text("=== Сохранённые карты ===", NamedTextColor.GOLD)); - for (String m : maps) sender.sendMessage(Component.text(" §f" + m, NamedTextColor.GRAY)); + sender.sendMessage(plugin.getConfigManager().getMessages().get("dbd.map.load-not-found", name)); } return true; } private boolean handleLobby(CommandSender sender) { if (!(sender instanceof Player player)) { - sender.sendMessage(Component.text("Только игроки!", NamedTextColor.RED)); + sender.sendMessage(plugin.getConfigManager().getMessages().get("cmd.players-only")); return true; } plugin.getLobbyManager().teleportToLobby(player); - player.sendMessage(Component.text("[DBD] Телепорт в лобби!", NamedTextColor.GREEN)); + player.sendMessage(plugin.getConfigManager().getMessages().get("dbd.lobby.teleport")); return true; } private boolean handleSetLobby(CommandSender sender) { if (!sender.hasPermission("mdbd.admin")) { - sender.sendMessage(Component.text("Нет прав!", NamedTextColor.RED)); + sender.sendMessage(plugin.getConfigManager().getMessages().get("cmd.no-permission")); return true; } if (!(sender instanceof Player player)) { - sender.sendMessage(Component.text("Только игроки!", NamedTextColor.RED)); + sender.sendMessage(plugin.getConfigManager().getMessages().get("cmd.players-only")); return true; } plugin.getConfigManager().getLocations().setLobby(player.getLocation()); plugin.getConfigManager().saveConfig("locations.yml", plugin.getConfigManager().getLocations().getConfig()); - player.sendMessage(Component.text("[DBD] Точка лобби установлена!", NamedTextColor.GREEN)); + player.sendMessage(plugin.getConfigManager().getMessages().get("dbd.setlobby.success")); return true; } @@ -207,18 +318,21 @@ public class DBDCommand implements TabExecutor { // Usage // ------------------------------------------------------------------------- private void sendUsage(CommandSender sender) { - sender.sendMessage(Component.text("=== DBD Команды ===", NamedTextColor.GOLD)); - sender.sendMessage(Component.text("/dbd join — присоединиться к игре", NamedTextColor.GRAY)); - sender.sendMessage(Component.text("/dbd leave — покинуть игру", NamedTextColor.GRAY)); - sender.sendMessage(Component.text("/dbd lobby — телепорт в лобби", NamedTextColor.GRAY)); + sender.sendMessage(plugin.getConfigManager().getMessages().get("usage.dbd-header")); + sender.sendMessage(plugin.getConfigManager().getMessages().get("usage.dbd-join")); + sender.sendMessage(plugin.getConfigManager().getMessages().get("usage.dbd-leave")); + sender.sendMessage(plugin.getConfigManager().getMessages().get("usage.dbd-lobby")); + sender.sendMessage(plugin.getConfigManager().getMessages().get("usage.dbd-create")); + sender.sendMessage(plugin.getConfigManager().getMessages().get("usage.dbd-list")); + sender.sendMessage(plugin.getConfigManager().getMessages().get("usage.dbd-delete")); if (sender.hasPermission("mdbd.admin")) { - sender.sendMessage(Component.text("/dbd admin — управление игроками в матче", NamedTextColor.YELLOW)); - sender.sendMessage(Component.text("/dbd forcestart — форс старт", NamedTextColor.YELLOW)); - sender.sendMessage(Component.text("/dbd stop — остановить игру", NamedTextColor.YELLOW)); - sender.sendMessage(Component.text("/dbd reload — перезагрузить конфиги", NamedTextColor.YELLOW)); - sender.sendMessage(Component.text("/dbd setlobby — установить точку лобби", NamedTextColor.YELLOW)); - sender.sendMessage(Component.text("/dbd save — сохранить карту", NamedTextColor.YELLOW)); - sender.sendMessage(Component.text("/dbd load — загрузить карту", NamedTextColor.YELLOW)); + sender.sendMessage(plugin.getConfigManager().getMessages().get("usage.dbd-admin")); + sender.sendMessage(plugin.getConfigManager().getMessages().get("usage.dbd-forcestart")); + sender.sendMessage(plugin.getConfigManager().getMessages().get("usage.dbd-stop")); + sender.sendMessage(plugin.getConfigManager().getMessages().get("usage.dbd-reload")); + sender.sendMessage(plugin.getConfigManager().getMessages().get("usage.dbd-setlobby")); + sender.sendMessage(plugin.getConfigManager().getMessages().get("usage.dbd-save")); + sender.sendMessage(plugin.getConfigManager().getMessages().get("usage.dbd-load")); } } @@ -226,15 +340,22 @@ public class DBDCommand implements TabExecutor { public @Nullable List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { if (args.length == 1) { - List subs = new ArrayList<>(List.of("join", "leave", "lobby")); + List subs = new ArrayList<>(List.of("join", "leave", "lobby", "create", "list", "delete")); if (sender.hasPermission("mdbd.admin")) { - subs.addAll(List.of("admin", "forcestart", "stop", "reload", "setlobby", "save", "load", "maps")); + subs.addAll(List.of("admin", "forcestart", "stop", "reload", "setlobby", "save", "load")); } return subs.stream().filter(s -> s.startsWith(args[0].toLowerCase())).toList(); } if (args.length >= 2 && args[0].equalsIgnoreCase("admin") && sender.hasPermission("mdbd.admin")) { return DBDAdminPlayerCommand.tabComplete(args); } + // Предлагаем имена лобби для join / delete + if (args.length == 2 && (args[0].equalsIgnoreCase("join") || args[0].equalsIgnoreCase("delete"))) { + return lobbyRegistry.getAllLobbies().stream() + .map(dev.loki.world.LobbySession::getName) + .filter(s -> s.startsWith(args[1].toLowerCase())) + .toList(); + } return List.of(); } } diff --git a/src/main/java/dev/loki/command/SetupCommand.java b/src/main/java/dev/loki/command/SetupCommand.java index 72b272e..1390d2e 100644 --- a/src/main/java/dev/loki/command/SetupCommand.java +++ b/src/main/java/dev/loki/command/SetupCommand.java @@ -1,11 +1,9 @@ package dev.loki.command; -import dev.loki.config.LocationsConfig; import dev.loki.mdbd; import dev.loki.util.SpawnUtil; import dev.loki.util.TextUtil; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; +import dev.loki.config.MessagesConfig; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.command.Command; @@ -52,15 +50,19 @@ public class SetupCommand implements TabExecutor { this.plugin = plugin; } + private MessagesConfig messages() { + return plugin.getConfigManager().getMessages(); + } + @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { if (!sender.hasPermission("mdbd.admin")) { - sender.sendMessage(Component.text("Нет прав!", NamedTextColor.RED)); + sender.sendMessage(messages().get("cmd.no-permission")); return true; } if (!(sender instanceof Player player)) { - sender.sendMessage(Component.text("Только игроки!", NamedTextColor.RED)); + sender.sendMessage(messages().get("cmd.players-only")); return true; } if (args.length == 0) { sendUsage(sender); return true; } @@ -68,7 +70,6 @@ public class SetupCommand implements TabExecutor { return switch (args[0].toLowerCase()) { case "wand" -> giveWand(player); case "set" -> handleSet(player, args); - case "build" -> buildMap(player); case "clear" -> clearMap(player); case "clear-spawns" -> clearSpawns(player); case "lobby" -> setLobby(player); @@ -92,7 +93,7 @@ public class SetupCommand implements TabExecutor { org.bukkit.persistence.PersistentDataType.BYTE, (byte) 1); }); player.getInventory().addItem(wand); - player.sendMessage(TextUtil.color("<#00FF00>Получен DBD Setup Wand")); + player.sendMessage(messages().get("setup.wand-given")); return true; } @@ -100,11 +101,9 @@ public class SetupCommand implements TabExecutor { private boolean handleSet(Player player, String[] args) { if (args.length < 2) { - player.sendMessage(TextUtil.color("<#FF0000>Использование: /dbdsetup set <тип> [номер]")); - player.sendMessage(TextUtil.color( - "<#FFD700>Типы: hook, chest, generator, killer-spawn, survivor-spawn, gate")); - player.sendMessage(TextUtil.color( - "<#FFD700>Для спавнов можно указать номер: /dbdsetup set killer-spawn 0")); + player.sendMessage(messages().get("setup.set-usage")); + player.sendMessage(messages().get("setup.set-types")); + player.sendMessage(messages().get("setup.set-spawn-info")); return true; } @@ -113,12 +112,12 @@ public class SetupCommand implements TabExecutor { if (type.equals("gate")) { if (args.length < 3) { - player.sendMessage(TextUtil.color("<#FF0000>Укажи: /dbdsetup set gate A (или B)")); + player.sendMessage(messages().get("setup.gate-usage")); return true; } String gateId = args[2].toUpperCase(); if (!gateId.equals("A") && !gateId.equals("B")) { - player.sendMessage(TextUtil.color("<#FF0000>Ворота могут быть только A или B")); + player.sendMessage(messages().get("setup.gate-invalid")); return true; } return placeGate(player, gateId); @@ -131,7 +130,7 @@ public class SetupCommand implements TabExecutor { }; if (!List.of("hook", "chest", "generator", "killer-spawn", "survivor-spawn").contains(mode)) { - player.sendMessage(TextUtil.color("<#FF0000>Неизвестный тип: " + type)); + player.sendMessage(messages().get("setup.unknown-type", type)); return true; } @@ -139,13 +138,22 @@ public class SetupCommand implements TabExecutor { int spawnIndex = -1; if (args.length >= 3 && (mode.equals("killer-spawn") || mode.equals("survivor-spawn"))) { try { - spawnIndex = Integer.parseInt(args[2]); - if (spawnIndex < 0) { - player.sendMessage(TextUtil.color("<#FF0000>Номер спавна должен быть >= 0")); - return true; + int raw = Integer.parseInt(args[2]); + if (mode.equals("survivor-spawn")) { + if (raw < 1 || raw > 4) { + player.sendMessage(messages().get("setup.survivor-spawn-range")); + return true; + } + spawnIndex = raw; // 1-4, храним как есть + } else { + if (raw < 0) { + player.sendMessage(messages().get("setup.killer-spawn-range")); + return true; + } + spawnIndex = raw; } } catch (NumberFormatException e) { - player.sendMessage(TextUtil.color("<#FF0000>Номер спавна должен быть числом: " + args[2])); + player.sendMessage(messages().get("setup.invalid-number", args[2])); return true; } } @@ -154,22 +162,16 @@ public class SetupCommand implements TabExecutor { SetupData currentData = setupMode.get(uuid); if (currentData != null && mode.equals(currentData.mode)) { setupMode.remove(uuid); - player.sendMessage(TextUtil.color("<#FFA500>Режим установки <#FFD700>" + mode + " <#FFA500>выключен")); + player.sendMessage(messages().get("setup.mode-off", mode)); return true; } setupMode.put(uuid, new SetupData(mode, spawnIndex)); String indexMsg = spawnIndex >= 0 ? " #" + spawnIndex : ""; if (mode.contains("spawn")) { - player.sendMessage(TextUtil.color( - "<#00FF00>Режим установки: <#FFD700>" + mode + indexMsg + - "\n<#00FF00>Сломай блок чтобы установить спавн (блок восстановится)" + - "\n<#FFA500>Повторно введи команду для выключения")); + player.sendMessage(messages().get("setup.mode-on-spawn", mode, indexMsg)); } else { - player.sendMessage(TextUtil.color( - "<#00FF00>Режим установки: <#FFD700>" + mode + indexMsg + - "\n<#00FF00>Сломай блок чтобы установить (блок восстановится)" + - "\n<#FFA500>Введи команду снова чтобы выключить режим")); + player.sendMessage(messages().get("setup.mode-on", mode, indexMsg)); } return true; } @@ -179,13 +181,12 @@ public class SetupCommand implements TabExecutor { Location p2 = pos2.get(player.getUniqueId()); if (p1 == null || p2 == null) { - player.sendMessage(TextUtil.color( - "<#FF0000>Сначала выбери оба угла топориком! (ЛКМ=точка 1, ПКМ=точка 2)")); + player.sendMessage(messages().get("setup.gate-need-points")); return true; } if (!p1.getWorld().equals(p2.getWorld())) { - player.sendMessage(TextUtil.color("<#FF0000>Точки должны быть в одном мире!")); + player.sendMessage(messages().get("setup.gate-same-world")); return true; } @@ -202,8 +203,7 @@ public class SetupCommand implements TabExecutor { locations.setGate(gateId, center); plugin.getConfigManager().saveConfig("locations.yml", locations.getConfig()); - player.sendMessage(TextUtil.color( - "<#00FF00>Ворота <#FFFFFF>" + gateId + " <#00FF00>установлены: " + formatLoc(center))); + player.sendMessage(messages().get("setup.gate-placed", gateId, formatLoc(center))); return true; } @@ -229,39 +229,42 @@ public class SetupCommand implements TabExecutor { switch (mode) { case "hook" -> { locations.addLocation("hooks", blockLoc.clone().add(0.5, 0, 0.5)); - player.sendMessage(TextUtil.color("<#00FF00>Крюк установлен: " + formatLoc(loc))); + player.sendMessage(messages().get("setup.hook-placed", formatLoc(loc))); } case "chest" -> { locations.addLocation("chests", blockLoc.clone().add(0.5, 0, 0.5)); - player.sendMessage(TextUtil.color("<#00FF00>Сундук добавлен: " + formatLoc(loc))); + player.sendMessage(messages().get("setup.chest-placed", formatLoc(loc))); } case "generator" -> { locations.addLocation("generators", blockLoc.clone().add(0.5, 0, 0.5)); plugin.getGeneratorData().addGenerator(blockLoc.clone().add(0.5, 0, 0.5)); - player.sendMessage(TextUtil.color("<#00FF00>Генератор добавлен: " + formatLoc(loc))); + player.sendMessage(messages().get("setup.generator-placed", formatLoc(loc))); } case "killer-spawn" -> { + if (locations.getKillerSpawn() != null) { + player.sendMessage(messages().get("setup.killer-spawn-exists")); + break; + } locations.setSingleLocation("killer-spawn", loc); - player.sendMessage(TextUtil.color("<#FF4444>Спавн киллера установлен: " + formatLoc(loc))); + player.sendMessage(messages().get("setup.killer-spawn-placed", formatLoc(loc))); data.spawnIndex = -1; } case "survivor-spawn" -> { - int currentCount = locations.getSurvivorSpawns().size(); - if (currentCount >= 4) { - player.sendMessage(TextUtil.color("<#FF0000>Достигнут лимит спавнов выживших (4)! Используй /dbdsetup clear-spawns для сброса.")); + int slot = data.spawnIndex >= 1 ? data.spawnIndex : locations.firstFreeSurvivorSlot(); + if (slot == -1) { + player.sendMessage(messages().get("setup.survivor-spawn-limit")); break; } - saveSpawn(locations, "survivor-spawns", loc, data.spawnIndex); + locations.setSurvivorSpawn(slot, loc); int newCount = locations.getSurvivorSpawns().size(); - String label = switch (newCount) { + String label = switch (slot) { case 1 -> "первого"; case 2 -> "второго"; case 3 -> "третьего"; case 4 -> "четвёртого"; - default -> "#" + newCount; + default -> "#" + slot; }; - player.sendMessage(TextUtil.color("<#00FF00>Спавн для <#FFD700>" + label + " <#00FF00>выжившего установлен! (всего: " + newCount + "/4) " + formatLoc(loc))); - // Сбрасываем индекс после использования, чтобы следующие блоки добавлялись + player.sendMessage(messages().get("setup.survivor-spawn-placed", label, newCount, formatLoc(loc))); data.spawnIndex = -1; } } @@ -279,23 +282,12 @@ public class SetupCommand implements TabExecutor { // НЕ выходим из режима — чтобы можно было установить несколько подряд } - /** - * Saves a spawn location at a specific index, or appends if index is -1. - */ - private void saveSpawn(LocationsConfig locations, String path, Location loc, int index) { - if (index >= 0) { - locations.setLocationAtIndex(path, index, loc); - } else { - locations.addLocation(path, loc); - } - } - // ── Wand position callbacks ──────────────────────────────────────────────── public void setWandPos(Player player, int num, Location loc) { if (num == 1) pos1.put(player.getUniqueId(), loc); else pos2.put(player.getUniqueId(), loc); - player.sendMessage(TextUtil.color("<#00FF00>[Wand] Точка " + num + ": " + formatLoc(loc))); + player.sendMessage(messages().get("setup.wand-point", num, formatLoc(loc))); } public boolean isInSetupMode(UUID uuid) { @@ -315,26 +307,20 @@ public class SetupCommand implements TabExecutor { return data != null ? data.spawnIndex : -1; } - // ── Build / clear / lobby ───────────────────────────────────────────────── - - private boolean buildMap(Player player) { - plugin.getMapBuilder().buildMap(plugin.getConfigManager().getLocations()); - player.sendMessage(TextUtil.color("<#00FF00>Карта построена!")); - return true; - } + // ── Clear / lobby ───────────────────────────────────────────────── private boolean clearMap(Player player) { plugin.getMapBuilder().clearMap(plugin.getConfigManager().getLocations()); - player.sendMessage(TextUtil.color("<#00FF00>Карта очищена!")); + player.sendMessage(messages().get("setup.map-cleared")); return true; } private boolean clearSpawns(Player player) { var locations = plugin.getConfigManager().getLocations(); locations.clearLocationList("killer-spawn"); - locations.clearLocationList("survivor-spawns"); + locations.clearSurvivorSpawns(); plugin.getConfigManager().saveConfig("locations.yml", locations.getConfig()); - player.sendMessage(TextUtil.color("<#00FF00>Все спавны (киллер + выжившие) очищены!")); + player.sendMessage(messages().get("setup.spawns-cleared")); return true; } @@ -342,23 +328,23 @@ public class SetupCommand implements TabExecutor { plugin.getConfigManager().getLocations().setLobby(player.getLocation()); plugin.getConfigManager().saveConfig("locations.yml", plugin.getConfigManager().getLocations().getConfig()); - player.sendMessage(TextUtil.color("<#00FF00>Лобби установлено: " + formatLoc(player.getLocation()))); + player.sendMessage(messages().get("setup.lobby-set", formatLoc(player.getLocation()))); return true; } // ── Usage / helpers ─────────────────────────────────────────────────────── private void sendUsage(CommandSender sender) { - sender.sendMessage(TextUtil.color("<#FF6B00>=== DBD Setup ===")); - sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup wand — Получить ванд (ЛКМ/ПКМ для ворот)")); - sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup set hook — Установить крюк (ломай блок)")); - sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup set chest — Установить сундук")); - sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup set generator — Установить генератор")); - sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup set killer-spawn — Спавн киллера (только 1)")); - sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup set survivor-spawn — Спавн выжившего (до 4)")); - sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup set gate A|B — Установить ворота (из точек ванда)")); - sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup clear-spawns — Очистить все спавны")); - sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup lobby — Установить точку лобби")); + sender.sendMessage(messages().get("usage.setup-header")); + sender.sendMessage(messages().get("usage.setup-wand")); + sender.sendMessage(messages().get("usage.setup-hook")); + sender.sendMessage(messages().get("usage.setup-chest")); + sender.sendMessage(messages().get("usage.setup-generator")); + 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-spawns")); + sender.sendMessage(messages().get("usage.setup-lobby")); } private String formatLoc(Location loc) { @@ -371,7 +357,7 @@ 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", "build", "clear", "clear-spawns", "lobby").stream() + return List.of("wand", "set", "clear", "clear-spawns", "lobby").stream() .filter(s -> s.startsWith(args[0].toLowerCase())).toList(); } if (args.length == 2 && args[0].equalsIgnoreCase("set")) { diff --git a/src/main/java/dev/loki/config/GameConfig.java b/src/main/java/dev/loki/config/GameConfig.java index fb0fd3b..9187ba4 100644 --- a/src/main/java/dev/loki/config/GameConfig.java +++ b/src/main/java/dev/loki/config/GameConfig.java @@ -35,6 +35,7 @@ public class GameConfig { setDefault("generators.kick-cooldown-seconds", 30); setDefault("generators.kick-charge-seconds", 3); setDefault("generators.interaction-range", 3.0); + setDefault("generators.minigame", "gui"); // Hooks setDefault("hooks.stage1-seconds", 60); @@ -97,6 +98,9 @@ public class GameConfig { setDefault("scoreboard.color-healing", "GREEN"); setDefault("scoreboard.color-hooked", "DARK_RED"); + // Debug + setDefault("debug-mode", false); + // Colors for UI setDefault("colors.progress-bar-complete", "�FF00"); setDefault("colors.progress-bar-incomplete", "&#FF0000"); @@ -116,9 +120,9 @@ public class GameConfig { setDefault("holograms.generator.enabled", true); setDefault("holograms.generator.height-offset", -1.0); setDefault("holograms.hook.enabled", true); - setDefault("holograms.hook.height-offset", 0.0); + setDefault("holograms.hook.height-offset", -1.0); setDefault("holograms.chest.enabled", true); - setDefault("holograms.chest.height-offset", 0.0); + setDefault("holograms.chest.height-offset", -1.0); // Skill check setDefault("skillcheck.chance", 0.005); @@ -171,6 +175,7 @@ public class GameConfig { public int getKickCooldownSeconds() { return config.getInt("generators.kick-cooldown-seconds", 30); } public int getKickChargeSeconds() { return config.getInt("generators.kick-charge-seconds", 3); } public double getGeneratorInteractionRange() { return config.getDouble("generators.interaction-range", 3.0); } + public String getGeneratorMinigame() { return config.getString("generators.minigame", "gui"); } // ------------------------------------------------------------------------- // Hooks @@ -286,5 +291,7 @@ public class GameConfig { public String getSkillCheckFailBlock() { return config.getString("skillcheck.fail-block", "RED_STAINED_GLASS_PANE"); } public String getSkillCheckNeutralBlock() { return config.getString("skillcheck.neutral-block", "GRAY_STAINED_GLASS_PANE"); } + public boolean isDebugMode() { return config.getBoolean("debug-mode", false); } + public FileConfiguration getConfig() { return config; } } diff --git a/src/main/java/dev/loki/config/LocationsConfig.java b/src/main/java/dev/loki/config/LocationsConfig.java index f075f16..4fe6847 100644 --- a/src/main/java/dev/loki/config/LocationsConfig.java +++ b/src/main/java/dev/loki/config/LocationsConfig.java @@ -7,6 +7,7 @@ import org.bukkit.configuration.file.FileConfiguration; import java.util.ArrayList; import java.util.List; +import java.util.Locale; public class LocationsConfig { private final FileConfiguration config; @@ -45,9 +46,7 @@ public class LocationsConfig { if (!config.contains("killer-spawn") || config.getList("killer-spawn") == null) { config.set("killer-spawn", new ArrayList<>()); } - if (!config.contains("survivor-spawns") || config.getList("survivor-spawns") == null) { - config.set("survivor-spawns", new ArrayList<>()); - } + // Survivor spawns are stored as individual keys survivor-spawn-1..4 (no list) } public Location getLobby() { @@ -103,8 +102,39 @@ public class LocationsConfig { return spawns.isEmpty() ? null : spawns.get(0); } + /** Read survivor spawns from individual keys survivor-spawn-1..4 (no list, avoids YAML bugs). */ public List getSurvivorSpawns() { - return loadLocationList("survivor-spawns"); + List result = new ArrayList<>(); + for (int i = 1; i <= 4; i++) { + String str = config.getString("survivor-spawn-" + i); + if (str != null && !str.isEmpty()) { + Location loc = parseLocation(str); + if (loc != null) result.add(loc); + } + } + return result; + } + + /** Set a specific survivor spawn slot (index 1-4). */ + public void setSurvivorSpawn(int index, Location loc) { + if (index < 1 || index > 4) throw new IllegalArgumentException("Index must be 1-4"); + config.set("survivor-spawn-" + index, locationToString(loc)); + } + + /** Find first empty survivor spawn slot (1-4), or -1 if all full. */ + public int firstFreeSurvivorSlot() { + for (int i = 1; i <= 4; i++) { + String str = config.getString("survivor-spawn-" + i); + if (str == null || str.isEmpty()) return i; + } + return -1; + } + + /** Clear all survivor spawn slots. */ + public void clearSurvivorSpawns() { + for (int i = 1; i <= 4; i++) { + config.set("survivor-spawn-" + i, null); + } } private List loadLocationList(String path) { @@ -217,7 +247,7 @@ public class LocationsConfig { private String locationToString(Location loc) { // Use | as separator instead of , to avoid issues with negative coordinates in YAML - return String.format("%s|%.2f|%.2f|%.2f|%.1f|%.1f", + return String.format(Locale.US, "%s|%.2f|%.2f|%.2f|%.1f|%.1f", loc.getWorld().getName(), loc.getX(), loc.getY(), loc.getZ(), loc.getYaw(), loc.getPitch()); diff --git a/src/main/java/dev/loki/config/MessagesConfig.java b/src/main/java/dev/loki/config/MessagesConfig.java index a92c237..f8858ea 100644 --- a/src/main/java/dev/loki/config/MessagesConfig.java +++ b/src/main/java/dev/loki/config/MessagesConfig.java @@ -1,117 +1,69 @@ package dev.loki.config; +import dev.loki.util.TextUtil; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; -import org.bukkit.configuration.file.FileConfiguration; + +import java.util.Locale; public class MessagesConfig { - private final FileConfiguration config; + private final org.bukkit.configuration.file.FileConfiguration config; private final MiniMessage miniMessage; - public MessagesConfig(FileConfiguration config) { + public MessagesConfig(org.bukkit.configuration.file.FileConfiguration config) { this.config = config; this.miniMessage = MiniMessage.miniMessage(); - setDefaults(); - } - - private void setDefaults() { - // Prefix (orange) - setDefault("prefix", "&#FF6B00&l[DBD]&r "); - - // Game messages - setDefault("game.start", "�FF00Игра началась! Бегите от Киллера!"); - setDefault("game.end.victory-escape", "&#FFD700Выжившие сбежали!"); - setDefault("game.end.victory-killer", "&#DC143CКиллер победил!"); - setDefault("game.waiting", "&#FFFF00Ожидание игроков... (%s/%s)"); - setDefault("game.countdown", "&#FFA500До начала: %s сек"); - - // Generator messages - setDefault("generator.repair.start", "�CED1Вы начали чинить генератор..."); - setDefault("generator.repair.stop", "WCEEBВы остановились."); - setDefault("generator.repair.complete", "�FF00✓ Генератор активирован!"); - setDefault("generator.skillcheck.success", "�FF00Отлично!"); - setDefault("generator.skillcheck.fail", "&#FF0000✗ Провал!"); - setDefault("generator.explosion", "&#FF4500⚠ Взрыв генератора!"); - setDefault("generator.regression", "&#FF6347Генератор откатывается..."); - setDefault("generator.progress", "&#FFD700Генераторы: %s/%s"); - - // Killer messages - setDefault("killer.terror-near", "B0000Сердце колотится..."); - setDefault("killer.chase.start", "&#FF0000⚠ Киллер преследует вас!"); - setDefault("killer.chase.stop", "�FF00Вы ушли от погони."); - setDefault("killer.hit", "&#DC143CВы ранены!"); - setDefault("killer.down", "B0000Вы в нокдауне!"); - setDefault("killer.pickup", "󃕐Киллер поднял вас..."); - setDefault("killer.hook", "B0082Вас повесили на крюк!"); - - // Survivor messages - setDefault("survivor.heal.start", "�FA9AВы лечите..."); - setDefault("survivor.heal.complete", "�FF00Вылечено!"); - setDefault("survivor.heal.interrupt", "&#FFD700Лечение прервано!"); - setDefault("survivor.sprint.exhausted", "&#FF8C00Вы устали..."); - - // Hook messages - setDefault("hook.stage1", "&#FFFF00Крюк: Фаза 1"); - setDefault("hook.stage2", "&#FFA500Крюк: Фаза 2 — БОРИТЕСЬ!"); - setDefault("hook.struggle.success", "�FF00Успех!"); - setDefault("hook.struggle.fail", "&#FF0000Провал!"); - setDefault("hook.rescue.start", "�CED1Спасение..."); - setDefault("hook.rescue.complete", "�FF00Спасены!"); - setDefault("hook.sacrifice", "B0000☠ Вы жертвованы..."); - - // Item messages - setDefault("item.found", "&#FFD700Найдено: %s"); - setDefault("item.medkit.use", "�FA9AИспользуете аптечку..."); - - // Endgame - setDefault("endgame.gates.powered", "�FF00Ворота запитаны!"); - setDefault("endgame.gates.opening", "&#FFD700Открываете ворота..."); - setDefault("endgame.hatch.open", "B0082Люк открыт!"); - setDefault("endgame.escape", "�FF00✓ ПОБЕГ!"); - - // Error messages - setDefault("error.no-permission", "&#FF0000Нет прав!"); - setDefault("error.not-in-game", "&#FF6347Вы не в игре!"); - setDefault("error.already-in-game", "&#FF6347Вы уже в игре!"); - setDefault("error.game-full", "&#FF6347Игра заполнена!"); - setDefault("error.no-game", "&#FF6347Нет активной игры!"); - } - - private void setDefault(String path, String value) { - if (!config.contains(path)) { - config.set(path, value); - } } + /** + * Get a message by path. Replaces %prefix%, %admin-prefix%, %setup-prefix% + * with configured values. + */ public Component get(String path, Object... args) { String raw = config.getString(path, "&cMissing: " + path); - String withPrefix = config.getString("prefix", "") + raw; - - // Replace &#RRGGBB with MiniMessage <#RRGGBB> - String mini = withPrefix.replaceAll("&#([0-9A-Fa-f]{6})", "<#$1>"); - - // Replace %s with arguments - if (args.length > 0) { - for (Object arg : args) { - mini = mini.replaceFirst("%s", String.valueOf(arg)); - } - } - - return miniMessage.deserialize(mini); + return deserialize(raw, args); } - public Component getRaw(String path, Object... args) { + /** + * Get a message as a plain string by path (no MiniMessage processing). + */ + public String getString(String path, Object... args) { String raw = config.getString(path, "&cMissing: " + path); - String mini = raw.replaceAll("&#([0-9A-Fa-f]{6})", "<#$1>"); + raw = replacePrefixes(raw); if (args.length > 0) { - for (Object arg : args) { - mini = mini.replaceFirst("%s", String.valueOf(arg)); - } + raw = String.format(Locale.US, raw, args); } + return raw; + } + + private Component deserialize(String raw, Object... args) { + raw = replacePrefixes(raw); + String mini = raw; + if (args.length > 0) { + mini = String.format(Locale.US, raw, args); + } + mini = TextUtil.toMiniMessage(mini); return miniMessage.deserialize(mini); } - public FileConfiguration getConfig() { + private String replacePrefixes(String raw) { + String prefix = config.getString("prefix", ""); + String adminPrefix = config.getString("admin-prefix", prefix); + String setupPrefix = config.getString("setup-prefix", prefix); + // Replace admin-prefix first (it may contain %prefix%) + if (raw.contains("%admin-prefix%")) { + String resolvedAdmin = adminPrefix.replace("%prefix%", prefix); + raw = raw.replace("%admin-prefix%", resolvedAdmin); + } + if (raw.contains("%setup-prefix%")) { + String resolvedSetup = setupPrefix.replace("%prefix%", prefix); + raw = raw.replace("%setup-prefix%", resolvedSetup); + } + raw = raw.replace("%prefix%", prefix); + return raw; + } + + public org.bukkit.configuration.file.FileConfiguration getConfig() { return config; } } diff --git a/src/main/java/dev/loki/core/GameSession.java b/src/main/java/dev/loki/core/GameSession.java index aa1e59e..4a2b524 100644 --- a/src/main/java/dev/loki/core/GameSession.java +++ b/src/main/java/dev/loki/core/GameSession.java @@ -56,16 +56,17 @@ public class GameSession { tickTask.cancel(); tickTask = null; } - if (currentPhase != null) { - currentPhase.exit(this); - currentPhase = null; + Phase phase = currentPhase; + currentPhase = null; // сбрасываем ДО exit() чтобы избежать рекурсии + if (phase != null) { + phase.exit(this); } players.clear(); eventBus.clear(); plugin.getLogger().info("Game session stopped"); if (onStopped != null) { Runnable cb = onStopped; - onStopped = null; // предотвращаем повторный вызов + onStopped = null; Bukkit.getScheduler().runTask(plugin, cb); } } diff --git a/src/main/java/dev/loki/gate/ExitGate.java b/src/main/java/dev/loki/gate/ExitGate.java index 8074653..14f143c 100644 --- a/src/main/java/dev/loki/gate/ExitGate.java +++ b/src/main/java/dev/loki/gate/ExitGate.java @@ -29,6 +29,18 @@ public class ExitGate { return location; } + /** Place a HOPPER block at this gate's location for player interaction. */ + public void spawnGateBlock() { + if (location.getWorld() == null) return; + location.getBlock().setType(org.bukkit.Material.HOPPER, false); + } + + /** Remove the interaction block (e.g. when gate opens or on cleanup). */ + public void removeGateBlock() { + if (location.getWorld() == null) return; + location.getBlock().setType(org.bukkit.Material.AIR, false); + } + public GateState getState() { return state; } @@ -90,6 +102,7 @@ public class ExitGate { if (progress >= 1.0) { progress = 1.0; state = GateState.OPEN; + removeGateBlock(); playOpenedSound(); } } @@ -103,6 +116,7 @@ public class ExitGate { public void restoreProgress() { progress = savedProgress; } public void reset() { + removeGateBlock(); this.state = GateState.CLOSED; this.progress = 0.0; this.savedProgress = 0.0; diff --git a/src/main/java/dev/loki/gate/GateOpening.java b/src/main/java/dev/loki/gate/GateOpening.java index dbaf845..5e2da93 100644 --- a/src/main/java/dev/loki/gate/GateOpening.java +++ b/src/main/java/dev/loki/gate/GateOpening.java @@ -1,6 +1,8 @@ package dev.loki.gate; import dev.loki.config.GameConfig; +import dev.loki.config.MessagesConfig; +import dev.loki.mdbd; import dev.loki.player.Survivor; import org.bukkit.Location; import org.bukkit.Particle; @@ -19,11 +21,13 @@ public class GateOpening { private final GateManager gateManager; private final int openTicks; private final Map openingPlayers; + private final MessagesConfig messages; public GateOpening(GateManager gateManager, GameConfig config) { this.gateManager = gateManager; this.openTicks = config.getConfig().getInt("endgame.gate-open-seconds", 20) * 20; this.openingPlayers = new HashMap<>(); + this.messages = mdbd.getInstance().getConfigManager().getMessages(); } public boolean tryStartOpening(Survivor survivor, org.bukkit.plugin.Plugin plugin) { @@ -34,17 +38,17 @@ public class GateOpening { ExitGate gate = gateManager.getGateAt(player.getLocation(), INTERACTION_RADIUS); if (gate == null) { - player.sendMessage("§cНет ворот поблизости!"); + player.sendMessage(messages.get("chest.door-nearby")); return false; } if (!gate.isPowered()) { - player.sendMessage("§cВорота обесточены!"); + player.sendMessage(messages.get("chest.door-no-power")); return false; } if (!gate.isClosed() && !gate.isOpening()) { - player.sendMessage("§aВорота уже открыты!"); + player.sendMessage(messages.get("chest.door-already")); return false; } @@ -57,7 +61,7 @@ public class GateOpening { if (progress != null) { Player player = survivor.getPlayer(); if (player != null) { - player.sendMessage("§7Открытие прервано!"); + player.sendMessage(messages.get("chest.door-interrupted")); } } } @@ -74,7 +78,7 @@ public class GateOpening { gate.restoreProgress(); openingPlayers.put(survivor.getUuid(), new OpeningProgress(gate, 0)); - player.sendMessage("§aОткрываем ворота... (зажмите ПКМ)"); + player.sendMessage(messages.get("chest.door-open")); new OpeningTask(survivor, gate, plugin).runTaskTimer(plugin, 0L, 1L); } @@ -85,7 +89,7 @@ public class GateOpening { World world = loc.getWorld(); if (player != null) { - player.sendMessage("§aВорота открыты!"); + player.sendMessage(messages.get("chest.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/hatch/HatchEscape.java b/src/main/java/dev/loki/hatch/HatchEscape.java index 1af96c6..0dca00f 100644 --- a/src/main/java/dev/loki/hatch/HatchEscape.java +++ b/src/main/java/dev/loki/hatch/HatchEscape.java @@ -1,5 +1,7 @@ package dev.loki.hatch; +import dev.loki.config.MessagesConfig; +import dev.loki.mdbd; import dev.loki.player.PlayerState; import dev.loki.player.Survivor; import org.bukkit.Location; @@ -11,9 +13,11 @@ public class HatchEscape { private static final double ESCAPE_RADIUS = 1.5; private final Hatch hatch; + private final MessagesConfig messages; public HatchEscape(Hatch hatch) { this.hatch = hatch; + this.messages = mdbd.getInstance().getConfigManager().getMessages(); } public boolean tryEscape(Survivor survivor) { @@ -25,7 +29,7 @@ public class HatchEscape { if (player == null) return false; if (!isAtHatch(player.getLocation())) { - player.sendMessage("§cПодойдите ближе к люку!"); + player.sendMessage(messages.get("hatch.too-far")); return false; } @@ -47,12 +51,12 @@ public class HatchEscape { if (player == null) return false; if (hatch.isHidden()) { - player.sendMessage("§7Люк скрыт..."); + player.sendMessage(messages.get("hatch.hidden")); return false; } if (hatch.isClosed()) { - player.sendMessage("§cЛюк закрыт!"); + player.sendMessage(messages.get("hatch.closed")); return false; } @@ -72,7 +76,10 @@ public class HatchEscape { player.getWorld().playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.0f); - player.sendTitle("§a§lПОБЕГ!", "§7Вы сбежали через люк!", 10, 60, 10); + player.sendTitle( + messages.getString("hatch.escape-title"), + messages.getString("hatch.escape-subtitle"), + 10, 60, 10); survivor.setState(PlayerState.ESCAPED); @@ -83,7 +90,7 @@ public class HatchEscape { Player player = survivor.getPlayer(); if (player == null) return; - player.sendMessage("§a+5000 Очков — Побег через люк!"); + player.sendMessage(messages.get("hatch.reward")); } public void forceOpenForEscape() { @@ -102,7 +109,7 @@ public class HatchEscape { for (Player player : world.getPlayers()) { if (player.getLocation().distance(hatchLoc) < 30) { - player.sendMessage("§e§lЛюк открыт!"); + player.sendMessage(messages.get("hatch.open-notify")); player.playSound(player.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 1.0f, 2.0f); } } diff --git a/src/main/java/dev/loki/hook/HookHologram.java b/src/main/java/dev/loki/hook/HookHologram.java index e3b9995..93f2653 100644 --- a/src/main/java/dev/loki/hook/HookHologram.java +++ b/src/main/java/dev/loki/hook/HookHologram.java @@ -1,6 +1,7 @@ package dev.loki.hook; import dev.loki.mdbd; +import dev.loki.util.TextUtil; import org.bukkit.Location; import org.bukkit.entity.ArmorStand; import org.bukkit.entity.Display; @@ -71,12 +72,12 @@ public class HookHologram { continue; } - Location holoLoc = loc.clone().add(0.5, 2.5 + heightOffset, 0.5); + Location holoLoc = loc.clone().add(0.5, 2.0 + heightOffset, 0.5); try { if (useTextDisplay) { TextDisplay display = loc.getWorld().spawn(holoLoc, TextDisplay.class, d -> { d.setBillboard(Display.Billboard.VERTICAL); - d.setSeeThrough(true); + d.setSeeThrough(false); d.setShadowed(true); d.setDefaultBackground(false); d.setPersistent(false); @@ -127,10 +128,10 @@ public class HookHologram { String text = "§c⚓ Крюк §f#" + visibleIndex + "/" + total + "\n§7" + status; double heightOffset = plugin.getConfigManager().getGame().getHookHologramHeightOffset(); - Location base = hook.getLocation().clone().add(0.5, 2.5 + heightOffset, 0.5); + Location base = hook.getLocation().clone().add(0.5, 2.0 + heightOffset, 0.5); if (display instanceof TextDisplay td) { - td.text(net.kyori.adventure.text.Component.text(text)); + td.text(TextUtil.color(text)); td.teleport(base); } else if (display instanceof ArmorStand as) { as.setCustomName(text); diff --git a/src/main/java/dev/loki/hook/HookRecovery.java b/src/main/java/dev/loki/hook/HookRecovery.java index 2970836..f4d4c8f 100644 --- a/src/main/java/dev/loki/hook/HookRecovery.java +++ b/src/main/java/dev/loki/hook/HookRecovery.java @@ -1,10 +1,10 @@ package dev.loki.hook; import dev.loki.config.GameConfig; +import dev.loki.mdbd; import dev.loki.player.PlayerState; import dev.loki.player.Survivor; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; import org.bukkit.scheduler.BukkitRunnable; @@ -71,7 +71,8 @@ public class HookRecovery { elapsed++; double pct = (double) elapsed / durationTicks * 100; - p.sendActionBar(Component.text("§6Попытка вырваться: " + (int) pct + "%")); + p.sendActionBar(((mdbd) plugin).getConfigManager().getMessages() + .get("hook.struggle.actionbar", (int) pct)); if (elapsed >= durationTicks) { resolveAttempt(survivor, hook); @@ -83,8 +84,8 @@ public class HookRecovery { activeTasks.put(uuid, task); Player p = survivor.getPlayer(); if (p != null) { - p.sendMessage(Component.text( - "[DBD] Держите Shift чтобы вырваться с крюка...", NamedTextColor.YELLOW)); + p.sendMessage(((mdbd) plugin).getConfigManager().getMessages() + .get("hook.struggle.hold-shift")); } return true; } @@ -134,7 +135,8 @@ public class HookRecovery { failedAttempts.remove(uuid); if (p != null && p.isOnline()) { p.sendActionBar(Component.empty()); - p.sendMessage(Component.text("[DBD] Вы вырвались с крюка!", NamedTextColor.GREEN)); + p.sendMessage(((mdbd) plugin).getConfigManager().getMessages() + .get("hook.struggle.success")); } } else { // Failure @@ -142,9 +144,8 @@ public class HookRecovery { failedAttempts.put(uuid, newFailures); if (p != null && p.isOnline()) { p.sendActionBar(Component.empty()); - p.sendMessage(Component.text( - "[DBD] Попытка не удалась! (" + newFailures + "/" + MAX_ATTEMPTS + ")", - NamedTextColor.RED)); + p.sendMessage(((mdbd) plugin).getConfigManager().getMessages() + .get("hook.struggle.fail", newFailures, MAX_ATTEMPTS)); } if (newFailures >= MAX_ATTEMPTS) { @@ -152,8 +153,8 @@ public class HookRecovery { hook.advanceToSacrifice(); failedAttempts.remove(uuid); if (p != null && p.isOnline()) { - p.sendMessage(Component.text( - "[DBD] Сущность поглотила вас!", NamedTextColor.DARK_RED)); + p.sendMessage(((mdbd) plugin).getConfigManager().getMessages() + .get("hook.struggle.sacrifice")); } } } diff --git a/src/main/java/dev/loki/hook/HookRescue.java b/src/main/java/dev/loki/hook/HookRescue.java index 3f5eb0c..44429a0 100644 --- a/src/main/java/dev/loki/hook/HookRescue.java +++ b/src/main/java/dev/loki/hook/HookRescue.java @@ -6,7 +6,6 @@ import dev.loki.player.PlayerState; import dev.loki.player.Survivor; import dev.loki.ui.ActionBar; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Location; import org.bukkit.entity.Player; import org.bukkit.potion.PotionEffect; @@ -57,7 +56,7 @@ public class HookRescue { session.setTask(task); if (player.isOnline()) { - player.sendMessage(Component.text("[DBD] Спасаем... держите Shift!", NamedTextColor.YELLOW)); + player.sendMessage(plugin.getConfigManager().getMessages().get("hook.rescue.start")); } return true; } @@ -78,7 +77,7 @@ public class HookRescue { double rescueRange = config.getHookInteractionRange(); if (player.getLocation().distance(hook.getLocation()) > rescueRange) { cancelRescue(uuid); - player.sendMessage(Component.text("[DBD] Слишком далеко! Спасение прервано.", NamedTextColor.RED)); + player.sendMessage(plugin.getConfigManager().getMessages().get("hook.rescue.too-far")); return; } @@ -111,14 +110,14 @@ public class HookRescue { victimPlayer.addPotionEffect( new PotionEffect(PotionEffectType.SPEED, 5 * 20, 0, false, true)); - victimPlayer.sendMessage(Component.text("[DBD] Вас спасли! Бегите!", NamedTextColor.GREEN)); + victimPlayer.sendMessage(plugin.getConfigManager().getMessages().get("hook.rescue.victim-saved")); } } Player rescuerPlayer = rescuer.getPlayer(); if (rescuerPlayer != null && rescuerPlayer.isOnline()) { - rescuerPlayer.sendActionBar(Component.text("[DBD] Союзник спасён!", NamedTextColor.GREEN)); - rescuerPlayer.sendMessage(Component.text("[DBD] Спасение завершено!", NamedTextColor.GREEN)); + rescuerPlayer.sendActionBar(plugin.getConfigManager().getMessages().get("hook.rescue.rescuer-actionbar")); + rescuerPlayer.sendMessage(plugin.getConfigManager().getMessages().get("hook.rescue.rescuer-done")); } if (onRescueComplete != null && victim != null) { diff --git a/src/main/java/dev/loki/hook/HookSabotage.java b/src/main/java/dev/loki/hook/HookSabotage.java index 092c67d..cd08470 100644 --- a/src/main/java/dev/loki/hook/HookSabotage.java +++ b/src/main/java/dev/loki/hook/HookSabotage.java @@ -1,9 +1,9 @@ package dev.loki.hook; import dev.loki.config.GameConfig; +import dev.loki.mdbd; import dev.loki.player.Survivor; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Bukkit; import org.bukkit.Sound; import org.bukkit.entity.Player; @@ -30,6 +30,8 @@ public class HookSabotage { private final Map sabotageCharging = new HashMap<>(); /** hookUUID → recovery task */ private final Map recoveryTasks = new HashMap<>(); + /** Called when sabotage completes successfully */ + private java.util.function.Consumer onSabotageComplete; public HookSabotage(Plugin plugin, GameConfig config, HookManager hookManager) { this.plugin = plugin; @@ -75,8 +77,8 @@ public class HookSabotage { } elapsed++; double pct = (double) elapsed / chargeTicks * 100; - player.sendActionBar(Component.text( - "§e⚙ Саботаж крюка: " + (int) pct + "%")); + player.sendActionBar(((mdbd) plugin).getConfigManager().getMessages() + .get("hook.sabotage.actionbar", (int) pct)); if (elapsed >= chargeTicks) { executeSabotage(survivor, hook); @@ -87,8 +89,8 @@ public class HookSabotage { sabotageCharging.put(uuid, task); if (p.isOnline()) { - p.sendMessage(Component.text("[DBD] Держите Shift для саботажа крюка...", - NamedTextColor.YELLOW)); + p.sendMessage(((mdbd) plugin).getConfigManager().getMessages() + .get("hook.sabotage.start")); } return true; } @@ -107,6 +109,10 @@ public class HookSabotage { return sabotageCharging.containsKey(survivorUuid); } + public void setOnSabotageComplete(java.util.function.Consumer callback) { + this.onSabotageComplete = callback; + } + public void shutdown() { sabotageCharging.values().forEach(BukkitTask::cancel); sabotageCharging.clear(); @@ -126,12 +132,17 @@ public class HookSabotage { Player p = survivor.getPlayer(); if (p != null && p.isOnline()) { p.sendActionBar(Component.empty()); - p.sendMessage(Component.text("[DBD] Крюк сломан!", NamedTextColor.GREEN)); + p.sendMessage(((mdbd) plugin).getConfigManager().getMessages() + .get("hook.sabotage.done")); p.getWorld().playSound(p.getLocation(), Sound.ENTITY_ITEM_BREAK, 1.0f, 0.8f); } - Bukkit.broadcast(Component.text("[DBD] ⚙ Крюк был сломан выжившим!", - NamedTextColor.YELLOW)); + Bukkit.broadcast(((mdbd) plugin).getConfigManager().getMessages() + .get("hook.sabotage.broadcast")); + + if (onSabotageComplete != null) { + onSabotageComplete.accept(survivor.getUuid()); + } // Запускаем восстановление int recoveryTicks = config.getSabotageRecoverySeconds() * 20; @@ -140,8 +151,8 @@ public class HookSabotage { public void run() { hook.setDisabled(false); recoveryTasks.remove(hook.getId()); - Bukkit.broadcast(Component.text("[DBD] 🔧 Крюк восстановлен.", - NamedTextColor.GRAY)); + Bukkit.broadcast(((mdbd) plugin).getConfigManager().getMessages() + .get("hook.sabotage.recovery")); } }.runTaskLater(plugin, recoveryTicks); diff --git a/src/main/java/dev/loki/hook/HookSacrifice.java b/src/main/java/dev/loki/hook/HookSacrifice.java index da7dd3c..04e0f43 100644 --- a/src/main/java/dev/loki/hook/HookSacrifice.java +++ b/src/main/java/dev/loki/hook/HookSacrifice.java @@ -5,7 +5,6 @@ import dev.loki.player.PlayerState; import dev.loki.player.Survivor; 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.Sound; import org.bukkit.entity.Player; @@ -33,7 +32,7 @@ public class HookSacrifice { // Convert to SPECTATOR mode if (player != null && player.isOnline()) { player.setGameMode(org.bukkit.GameMode.SPECTATOR); - player.sendMessage(Component.text("[DBD] Вы были принесены в жертву. Наблюдайте за матчем.", NamedTextColor.DARK_RED)); + player.sendMessage(plugin.getConfigManager().getMessages().get("hook.sacrifice.message")); } announceSacrifice(survivor); @@ -47,8 +46,8 @@ public class HookSacrifice { ); Title title = Title.title( - Component.text("☠").color(NamedTextColor.DARK_RED), - Component.text("You have been sacrificed").color(NamedTextColor.RED), + plugin.getConfigManager().getMessages().get("hook.sacrifice.title"), + plugin.getConfigManager().getMessages().get("hook.sacrifice.subtitle"), times ); @@ -64,10 +63,8 @@ public class HookSacrifice { name = player.getName(); } - final String finalName = name; plugin.getServer().broadcast( - Component.text(finalName + " has been sacrificed to the Entity") - .color(NamedTextColor.DARK_RED) + plugin.getConfigManager().getMessages().get("hook.sacrifice.broadcast", name) ); } diff --git a/src/main/java/dev/loki/hook/HookStruggle.java b/src/main/java/dev/loki/hook/HookStruggle.java index da15960..89df8d5 100644 --- a/src/main/java/dev/loki/hook/HookStruggle.java +++ b/src/main/java/dev/loki/hook/HookStruggle.java @@ -19,19 +19,14 @@ public class HookStruggle { private final int struggleIntervalTicks; private static final int STRUGGLE_WINDOW_TICKS = 20; - public HookStruggle(mdbd plugin, TitleUI titleUI) { - this(plugin, titleUI, null); - } - public HookStruggle(mdbd plugin, TitleUI titleUI, GameConfig config) { this.plugin = plugin; this.titleUI = titleUI; this.struggleTasks = new HashMap<>(); this.struggleProgress = new HashMap<>(); - // Читаем из конфига: struggle-interval в секундах → тики this.struggleIntervalTicks = (config != null) ? config.getStruggleInterval() * 20 - : 40; // дефолт 2 сек + : 40; } public void startStruggle(Survivor survivor, Hook hook) { @@ -86,7 +81,7 @@ public class HookStruggle { private void failStruggle(Survivor survivor) { UUID uuid = survivor.getUuid(); - struggleProgress.put(uuid, struggleProgress.getOrDefault(uuid, 0) - 2); + struggleProgress.put(uuid, Math.max(0, struggleProgress.getOrDefault(uuid, 0) - 2)); } public void sacrifice(Survivor survivor) { diff --git a/src/main/java/dev/loki/item/DBDItems.java b/src/main/java/dev/loki/item/DBDItems.java index 55b4d8d..ffe9b15 100644 --- a/src/main/java/dev/loki/item/DBDItems.java +++ b/src/main/java/dev/loki/item/DBDItems.java @@ -36,16 +36,21 @@ public final class DBDItems { /** Медкит — лечит одно состояние здоровья. */ public static ItemStack medkit(int charges) { - return build( - Material.PAPER, CMD_MEDKIT, - "<#00FF7F>Медкит", - charges, ItemType.MEDKIT.getDefaultCharges(), - List.of( - "Лечит одно состояние здоровья.", - "ПКМ — начать самолечение" - ), - "MEDKIT" - ); + var item = new ItemStack(Material.PAPER); + item.editMeta(meta -> { + meta.setCustomModelData(CMD_MEDKIT); + meta.displayName(TextUtil.color("<#00FF7F>Медкит")); + meta.lore(java.util.List.of( + TextUtil.color("Лечит одно состояние здоровья."), + TextUtil.color("ПКМ — начать самолечение"), + Component.empty(), + TextUtil.color("<#FFD700>Заряды: <#00FF00>████████ 1/1") + )); + var pdc = meta.getPersistentDataContainer(); + pdc.set(KEY_TYPE, PersistentDataType.STRING, "MEDKIT"); + pdc.set(KEY_CHARGES, PersistentDataType.INTEGER, charges); + }); + return item; } public static ItemStack medkit() { return medkit(ItemType.MEDKIT.getDefaultCharges()); } diff --git a/src/main/java/dev/loki/item/ItemMedkit.java b/src/main/java/dev/loki/item/ItemMedkit.java index a26076f..46ad11d 100644 --- a/src/main/java/dev/loki/item/ItemMedkit.java +++ b/src/main/java/dev/loki/item/ItemMedkit.java @@ -1,5 +1,6 @@ package dev.loki.item; +import dev.loki.mdbd; import org.bukkit.Material; import org.bukkit.NamespacedKey; import org.bukkit.entity.Player; @@ -66,7 +67,7 @@ public class ItemMedkit implements BaseItem { @Override public void onDeplete(Player player) { player.getInventory().setItemInMainHand(null); - player.sendMessage("§cYour medkit has been depleted!"); + player.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.medkit.depleted")); } @Override diff --git a/src/main/java/dev/loki/item/ItemUseListener.java b/src/main/java/dev/loki/item/ItemUseListener.java index e00d93d..18dcfb4 100644 --- a/src/main/java/dev/loki/item/ItemUseListener.java +++ b/src/main/java/dev/loki/item/ItemUseListener.java @@ -1,11 +1,11 @@ package dev.loki.item; import dev.loki.core.GameSession; +import dev.loki.mdbd; import dev.loki.player.BasePlayer; import dev.loki.player.PlayerState; import dev.loki.player.Survivor; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Location; import org.bukkit.Sound; import org.bukkit.entity.Player; @@ -72,8 +72,7 @@ public class ItemUseListener implements Listener { if ("MEDKIT".equals(type)) { // Медкит: нужен Shift (sneaking) + стоять на месте if (!player.isSneaking()) { - player.sendMessage(Component.text( - "[DBD] Зажмите Shift и стойте на месте для лечения.", NamedTextColor.YELLOW)); + player.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.medkit.need-shift")); return; } useMedkit(player, (Survivor) bp, hand); @@ -92,18 +91,18 @@ public class ItemUseListener implements Listener { } if (survivor.getState() != PlayerState.INJURED) { - player.sendMessage(Component.text("[DBD] Вы не ранены!", NamedTextColor.RED)); + player.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.medkit.not-injured")); return; } int charges = DBDItems.getCharges(item); if (charges <= 0) { - player.sendMessage(Component.text("[DBD] Медкит пуст!", NamedTextColor.RED)); + player.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.medkit.empty")); player.getInventory().setItemInMainHand(null); return; } - player.sendMessage(Component.text("[DBD] Самолечение... держите Shift и не двигайтесь.", NamedTextColor.GREEN)); + player.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.medkit.start")); // Запоминаем позицию для проверки движения final Location startLoc = player.getLocation().clone(); @@ -136,9 +135,8 @@ public class ItemUseListener implements Listener { int filled = (int) (8.0 * ticks / healTicks); int empty = 8 - filled; int pct = (int) (100.0 * ticks / healTicks); - player.sendActionBar(Component.text( - "⚕ " + "█".repeat(filled) + "░".repeat(empty) + " " + pct + "%", - NamedTextColor.GREEN)); + String barStr = "█".repeat(filled) + "░".repeat(empty); + player.sendActionBar(mdbd.getInstance().getConfigManager().getMessages().get("survivor.medkit.actionbar", barStr, pct)); if (ticks % 20 == 0) { player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_HURT, 0.15f, 1.8f); @@ -146,7 +144,7 @@ public class ItemUseListener implements Listener { if (ticks >= healTicks) { survivor.setState(PlayerState.HEALTHY); - player.sendMessage(Component.text("[DBD] ✔ Вы вылечились!", NamedTextColor.GREEN)); + player.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.medkit.done")); player.sendActionBar(Component.empty()); player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 0.6f, 1.5f); // Полностью убираем медкит (один раз использован) @@ -165,7 +163,7 @@ public class ItemUseListener implements Listener { if (task != null) { task.cancel(); player.sendActionBar(Component.empty()); - player.sendMessage(Component.text("Лечение прервано.", NamedTextColor.YELLOW)); + player.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.medkit.cancelled")); } } diff --git a/src/main/java/dev/loki/killer/GeneratorKick.java b/src/main/java/dev/loki/killer/GeneratorKick.java index 2b7c685..110bf7c 100644 --- a/src/main/java/dev/loki/killer/GeneratorKick.java +++ b/src/main/java/dev/loki/killer/GeneratorKick.java @@ -1,11 +1,12 @@ package dev.loki.killer; import dev.loki.config.GameConfig; +import dev.loki.config.MessagesConfig; +import dev.loki.mdbd; import dev.loki.objective.Generator; import dev.loki.objective.GeneratorManager; import dev.loki.player.Killer; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Bukkit; import org.bukkit.Sound; import org.bukkit.entity.Player; @@ -29,6 +30,7 @@ public class GeneratorKick { private final Plugin plugin; private final GameConfig config; private final GeneratorManager generatorManager; + private final MessagesConfig messages; /** UUID → ongoing charging task */ private final Map chargingTasks = new HashMap<>(); @@ -41,6 +43,7 @@ public class GeneratorKick { this.plugin = plugin; this.config = config; this.generatorManager = generatorManager; + this.messages = mdbd.getInstance().getConfigManager().getMessages(); } // ------------------------------------------------------------------------- @@ -55,7 +58,7 @@ public class GeneratorKick { Player p = killer.getPlayer(); if (p != null) { int secsLeft = (int) Math.ceil(globalCooldownTicks / 20.0); - p.sendMessage(Component.text("[DBD] Кулдаун удара: " + secsLeft + " сек", NamedTextColor.RED)); + p.sendMessage(messages.get("generator.kicked.cooldown", secsLeft)); } return false; } @@ -66,6 +69,12 @@ public class GeneratorKick { Generator gen = generatorManager.getNearestGenerator(p.getLocation(), config.getGeneratorInteractionRange()); if (gen == null || !gen.canBeRepaired()) return false; + if (gen.getProgress() <= 0) { + if (p.isOnline()) { + p.sendMessage(messages.get("generator.kicked.already-broken")); + } + return false; + } int chargeTicks = config.getKickChargeSeconds() * 20; @@ -92,7 +101,7 @@ public class GeneratorKick { elapsed++; double pct = (double) elapsed / chargeTicks * 100; - player.sendActionBar(Component.text("§c⚡ Разрушение: " + (int) pct + "%")); + player.sendActionBar(messages.get("generator.kicked.actionbar", (int) pct)); if (elapsed >= chargeTicks) { executeKick(killer, gen); @@ -102,7 +111,7 @@ public class GeneratorKick { }.runTaskTimer(plugin, 1L, 1L); chargingTasks.put(uuid, task); - if (p.isOnline()) p.sendMessage(Component.text("[DBD] Держите Shift для разрушения генератора...", NamedTextColor.RED)); + if (p.isOnline()) p.sendMessage(messages.get("generator.kicked.begin")); return true; } @@ -146,12 +155,12 @@ public class GeneratorKick { Player p = killer.getPlayer(); if (p != null && p.isOnline()) { p.sendActionBar(Component.empty()); - p.sendMessage(Component.text("[DBD] Генератор поврежден! -" + (int) regPct + "% прогресса", NamedTextColor.RED)); + p.sendMessage(messages.get("generator.kicked.success", (int) regPct)); p.getWorld().playSound(p.getLocation(), Sound.ENTITY_GENERIC_EXPLODE, 0.6f, 1.3f); } // Notify all players - Bukkit.broadcast(Component.text("[DBD] ⚡ Маньяк разрушил генератор! -" + (int)regPct + "%", NamedTextColor.DARK_RED)); + Bukkit.broadcast(messages.get("generator.kicked.broadcast", (int) regPct)); // Unblock generator after kick-duration (separate from global cooldown) int unblockTicks = config.getKickDuration() * 20; diff --git a/src/main/java/dev/loki/listener/PlayerInteractListener.java b/src/main/java/dev/loki/listener/PlayerInteractListener.java index 1e6c47b..5b3b1ec 100644 --- a/src/main/java/dev/loki/listener/PlayerInteractListener.java +++ b/src/main/java/dev/loki/listener/PlayerInteractListener.java @@ -61,6 +61,9 @@ public class PlayerInteractListener implements Listener { public void setOnHealSneak(BiConsumer h) { this.onHealSneak = h; } private BiConsumer onCarriedShift; public void setOnCarriedShift(BiConsumer h) { this.onCarriedShift = h; } + /** Called when a survivor sneaks while a text skill check is in progress */ + private BiConsumer onGeneratorSkillCheckSneak; + public void setOnGeneratorSkillCheckSneak(BiConsumer h) { this.onGeneratorSkillCheckSneak = h; } public void setGeneratorManager(GeneratorManager manager) { this.generatorManager = manager; @@ -71,43 +74,36 @@ public class PlayerInteractListener implements Listener { Player player = event.getPlayer(); if (!gameManager.isInGame(player.getUniqueId())) return; - // For survivors: trigger generator repair when sneaking - if (event.isSneaking()) { - var session = gameManager.getPlayerSession(player.getUniqueId()); - if (session != null) { - BasePlayer bp = session.getPlayer(player.getUniqueId()); - if (bp != null && !bp.isKiller()) { - // #11: Carried survivor shift-spam to escape - if (bp.getState() == PlayerState.CARRIED && onCarriedShift != null) { - onCarriedShift.accept(player, true); - return; - } + var session = gameManager.getPlayerSession(player.getUniqueId()); + if (session == null) return; + BasePlayer bp = session.getPlayer(player.getUniqueId()); + if (bp == null) return; - // Only HEALTHY or INJURED survivors can repair/heal - PlayerState st = bp.getState(); - if (st != PlayerState.HEALTHY && st != PlayerState.INJURED) return; - - // Generator repair takes priority over healing - if (tryGeneratorInteract(player)) return; - - // Healing: check if sneaking near an injured/downed survivor - if (onHealSneak != null) { - Player nearestTarget = findNearestHealTarget(player, session); - if (nearestTarget != null) { - onHealSneak.accept(player, nearestTarget); - } - } - } + if (!bp.isKiller()) { + // Carried survivor shift-spam + if (bp.getState() == PlayerState.CARRIED) { + if (onCarriedShift != null) onCarriedShift.accept(player, event.isSneaking()); + return; } - } else { - // When un-sneaking - var session = gameManager.getPlayerSession(player.getUniqueId()); - if (session != null) { - BasePlayer bp = session.getPlayer(player.getUniqueId()); - if (bp != null && !bp.isKiller()) { - if (bp.getState() == PlayerState.CARRIED && onCarriedShift != null) { - onCarriedShift.accept(player, false); - return; + + // Text skill check: complete on sneak release + if (!event.isSneaking() && onGeneratorSkillCheckSneak != null) { + onGeneratorSkillCheckSneak.accept(player, false); + } + + // Only HEALTHY or INJURED survivors can repair/heal + PlayerState st = bp.getState(); + if (st != PlayerState.HEALTHY && st != PlayerState.INJURED) return; + + if (event.isSneaking()) { + // Generator repair takes priority over healing + if (tryGeneratorInteract(player)) return; + + // Healing: check if sneaking near an injured/downed survivor + if (onHealSneak != null) { + Player nearestTarget = findNearestHealTarget(player, session); + if (nearestTarget != null) { + onHealSneak.accept(player, nearestTarget); } } } @@ -246,14 +242,21 @@ 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") && onChestInteract != null && !bp.isKiller()) { + if (type.contains("CHEST")) { event.setCancelled(true); - onChestInteract.accept(player, true); + if (onChestInteract != null && !bp.isKiller()) { + onChestInteract.accept(player, true); + } return; } // Hook rescue: if survivor clicks on a hook block, attempt rescue @@ -261,7 +264,6 @@ public class PlayerInteractListener implements Listener { onRescueAttempt.accept(player, true); if (type.contains("IRON_BARS") || type.contains("IRON")) event.setCancelled(true); } - if ((type.contains("HOPPER") || type.contains("PORTAL")) && onHatchInteract != null && !bp.isKiller()) onHatchInteract.accept(player, 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/listener/PlayerQuitListener.java b/src/main/java/dev/loki/listener/PlayerQuitListener.java index 67b1166..62f4f4c 100644 --- a/src/main/java/dev/loki/listener/PlayerQuitListener.java +++ b/src/main/java/dev/loki/listener/PlayerQuitListener.java @@ -2,12 +2,13 @@ package dev.loki.listener; import dev.loki.core.GameManager; import dev.loki.core.GameSession; +import dev.loki.mdbd; import dev.loki.player.BasePlayer; import dev.loki.player.Killer; import dev.loki.player.PlayerState; import dev.loki.player.Survivor; +import dev.loki.world.LobbyRegistry; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -20,10 +21,12 @@ import java.util.UUID; public class PlayerQuitListener implements Listener { private final Plugin plugin; private final GameManager gameManager; + private final LobbyRegistry lobbyRegistry; - public PlayerQuitListener(Plugin plugin, GameManager gameManager) { + public PlayerQuitListener(Plugin plugin, GameManager gameManager, LobbyRegistry lobbyRegistry) { this.plugin = plugin; this.gameManager = gameManager; + this.lobbyRegistry = lobbyRegistry; } @EventHandler @@ -49,6 +52,11 @@ public class PlayerQuitListener implements Listener { session.removePlayer(uuid); gameManager.leaveGame(player); + // Если лобби опустело — удаляем его + if (lobbyRegistry != null) { + lobbyRegistry.onPlayerLeftSession(session, player); + } + // Гарантируем чистый инвентарь и scale при следующем входе player.getInventory().clear(); player.setSwimming(false); @@ -59,9 +67,7 @@ public class PlayerQuitListener implements Listener { } private void handleKillerDisconnect(GameSession session, Killer killer) { - Bukkit.broadcast(Component.text( - "[DBD] Киллер покинул игру. Игра завершена.", - NamedTextColor.RED)); + Bukkit.broadcast(mdbd.getInstance().getConfigManager().getMessages().get("quit.killer-disconnect")); session.stop(); } @@ -93,8 +99,7 @@ public class PlayerQuitListener implements Listener { } // Только участникам сессии - Component msg = Component.text( - "[DBD] Выживший покинул игру. Осталось живых: " + alive, NamedTextColor.YELLOW); + Component msg = mdbd.getInstance().getConfigManager().getMessages().get("quit.survivor-left", alive); for (BasePlayer bp : session.getPlayers().values()) { Player p = bp.getPlayer(); if (p != null && p.isOnline()) p.sendMessage(msg); @@ -113,9 +118,9 @@ public class PlayerQuitListener implements Listener { } if (livingSurvivors == 0) { - Bukkit.broadcast(Component.text( - "[DBD] Все выжившие мертвы. Киллер победил!", - NamedTextColor.RED)); + Bukkit.broadcast(mdbd.getInstance().getConfigManager().getMessages().get("game.all-dead")); + // Останавливаем сессию сразу при выходе последнего выжившего + session.stop(); } } diff --git a/src/main/java/dev/loki/listener/SkillCheckListener.java b/src/main/java/dev/loki/listener/SkillCheckListener.java index 384cefa..b949356 100644 --- a/src/main/java/dev/loki/listener/SkillCheckListener.java +++ b/src/main/java/dev/loki/listener/SkillCheckListener.java @@ -1,6 +1,6 @@ package dev.loki.listener; -import dev.loki.objective.SkillCheckInventory; +import dev.loki.minigames.generator.GuiRouletteMinigame; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; @@ -11,37 +11,29 @@ import org.bukkit.plugin.Plugin; import java.util.UUID; -/** - * Listens for clicks on skill check inventory GUI. - * Blocks all item manipulation — only clicking is allowed. - */ public class SkillCheckListener implements Listener { private final Plugin plugin; - private final SkillCheckInventory skillCheckGUI; + private final GuiRouletteMinigame minigame; - public SkillCheckListener(Plugin plugin, SkillCheckInventory skillCheckGUI) { + public SkillCheckListener(Plugin plugin, GuiRouletteMinigame minigame) { this.plugin = plugin; - this.skillCheckGUI = skillCheckGUI; + this.minigame = minigame; } @EventHandler public void onInventoryClick(InventoryClickEvent event) { - // Check if this is a skill check inventory (title contains "Skill Check") String title = event.getView().getTitle(); if (!title.contains("Skill Check")) return; - // Block ALL item manipulation: shift-click, number keys, double-click, etc. event.setCancelled(true); if (!(event.getWhoClicked() instanceof Player player)) return; - UUID playerId = player.getUniqueId(); - skillCheckGUI.handleClick(event, playerId); + minigame.handleClick(player.getUniqueId(), event.getRawSlot()); } @EventHandler public void onInventoryDrag(InventoryDragEvent event) { - // Block dragging items out of the skill check inventory String title = event.getView().getTitle(); if (!title.contains("Skill Check")) return; event.setCancelled(true); @@ -49,11 +41,10 @@ public class SkillCheckListener implements Listener { @EventHandler public void onInventoryClose(InventoryCloseEvent event) { - // Check if this is a skill check inventory (title contains "Skill Check") String title = event.getView().getTitle(); if (!title.contains("Skill Check")) return; - skillCheckGUI.onInventoryClose(event); + minigame.handleClose(event.getPlayer().getUniqueId()); } public void register() { diff --git a/src/main/java/dev/loki/mdbd.java b/src/main/java/dev/loki/mdbd.java index 9b76022..d1b2d93 100644 --- a/src/main/java/dev/loki/mdbd.java +++ b/src/main/java/dev/loki/mdbd.java @@ -14,6 +14,7 @@ import dev.loki.map.MapBuilder; import dev.loki.map.MapManager; import dev.loki.map.TemplateManager; import dev.loki.world.LobbyManager; +import dev.loki.world.LobbyRegistry; import org.bukkit.plugin.java.JavaPlugin; public final class mdbd extends JavaPlugin { @@ -24,6 +25,7 @@ public final class mdbd extends JavaPlugin { private MapManager mapManager; private MapBuilder mapBuilder; private GeneratorDataManager generatorData; + private LobbyRegistry lobbyRegistry; private DBDCommand dbdCommand; private DBDAdminCommand dbdAdminCommand; private SetupCommand setupCommand; @@ -57,7 +59,10 @@ public final class mdbd extends JavaPlugin { this.gameManager = new GameManager(this); - this.dbdCommand = new DBDCommand(this, gameManager); + this.lobbyRegistry = new LobbyRegistry(this, gameManager); + this.lobbyRegistry.load(); + + this.dbdCommand = new DBDCommand(this, gameManager, lobbyRegistry); getCommand("dbd").setExecutor(dbdCommand); getCommand("dbd").setTabCompleter(dbdCommand); @@ -71,7 +76,7 @@ public final class mdbd extends JavaPlugin { getServer().getPluginManager().registerEvents( new dev.loki.listener.SetupBlockBreakListener(setupCommand, this), this); getServer().getPluginManager().registerEvents( - new PlayerQuitListener(this, gameManager), this); + new PlayerQuitListener(this, gameManager, lobbyRegistry), this); getServer().getPluginManager().registerEvents( new PlayerJoinListener(lobbyManager), this); this.interactListener = new PlayerInteractListener(this, gameManager, null); @@ -98,6 +103,9 @@ public final class mdbd extends JavaPlugin { if (generatorData != null) { generatorData.save(); } + if (lobbyRegistry != null) { + lobbyRegistry.save(); + } instance = null; } @@ -133,6 +141,10 @@ public final class mdbd extends JavaPlugin { return dbdAdminCommand; } + public LobbyRegistry getLobbyRegistry() { + return lobbyRegistry; + } + public LobbyManager getLobbyManager() { return lobbyManager; } diff --git a/src/main/java/dev/loki/minigames/GeneratorMinigame.java b/src/main/java/dev/loki/minigames/GeneratorMinigame.java new file mode 100644 index 0000000..653d78b --- /dev/null +++ b/src/main/java/dev/loki/minigames/GeneratorMinigame.java @@ -0,0 +1,14 @@ +package dev.loki.minigames; + +import org.bukkit.entity.Player; + +import java.util.UUID; +import java.util.function.BiConsumer; + +public interface GeneratorMinigame { + void start(Player player, BiConsumer callback); + boolean isActive(UUID playerId); + boolean handleRightClick(Player player); + boolean handleSneak(Player player); + void cleanup(); +} diff --git a/src/main/java/dev/loki/minigames/generator/GuiRouletteMinigame.java b/src/main/java/dev/loki/minigames/generator/GuiRouletteMinigame.java new file mode 100644 index 0000000..da29b5c --- /dev/null +++ b/src/main/java/dev/loki/minigames/generator/GuiRouletteMinigame.java @@ -0,0 +1,214 @@ +package dev.loki.minigames.generator; + +import dev.loki.config.GameConfig; +import dev.loki.mdbd; +import dev.loki.minigames.GeneratorMinigame; +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.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.HashMap; +import java.util.Map; +import java.util.Random; +import java.util.UUID; +import java.util.function.BiConsumer; + +public class GuiRouletteMinigame implements GeneratorMinigame { + private final mdbd plugin; + private final Map activeSessions = new HashMap<>(); + private static final int TIMEOUT_TICKS = 60; + 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 GuiRouletteMinigame(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; + } + + @Override + public boolean isActive(UUID playerId) { + return activeSessions.containsKey(playerId); + } + + @Override + public boolean handleRightClick(Player player) { + return false; + } + + @Override + public boolean handleSneak(Player player) { + return false; + } + + @Override + public void start(Player player, BiConsumer callback) { + UUID playerId = player.getUniqueId(); + RouletteSession session = new RouletteSession(player, callback); + activeSessions.put(playerId, session); + + String titleStr = plugin.getConfigManager().getMessages().getString("minigames.generator-roulette.title", "⚡ Skill Check!"); + Inventory inv = Bukkit.createInventory(null, 9, Component.text(titleStr).color(NamedTextColor.YELLOW)); + fillInventory(inv, session.successSlot); + player.openInventory(inv); + + plugin.getServer().getScheduler().runTaskLater(plugin, () -> { + if (activeSessions.containsKey(playerId)) { + timeout(playerId); + } + }, TIMEOUT_TICKS); + + runRoulette(playerId); + } + + private void runRoulette(UUID playerId) { + RouletteSession session = activeSessions.get(playerId); + if (session == null || !session.player.isOnline()) { + activeSessions.remove(playerId); + return; + } + + if (session.totalTicks >= TIMEOUT_TICKS) { + timeout(playerId); + return; + } + + session.lastSuccessSlot = session.successSlot; + session.successSlot = (session.successSlot + 1) % 9; + session.switchesDone++; + + Inventory inv = session.player.getOpenInventory().getTopInventory(); + if (inv != null && inv.getSize() == 9) { + inv.setItem(session.lastSuccessSlot, createNeutralGlass()); + inv.setItem(session.successSlot, createSuccessGlass()); + session.player.playSound(session.player.getLocation(), Sound.BLOCK_NOTE_BLOCK_HAT, 0.5f, 1.5f); + } + + int nextDelay; + int done = session.switchesDone; + int total = session.totalSwitches; + + if (done < total * 0.4) { + nextDelay = 2 + (int)(Math.random() * 2); + } else if (done < total * 0.8) { + nextDelay = 4 + (int)(Math.random() * 3); + } else { + nextDelay = 8 + (int)(Math.random() * 5); + } + + session.totalTicks += nextDelay; + + if (done >= total) return; + + plugin.getServer().getScheduler().runTaskLater(plugin, () -> runRoulette(playerId), nextDelay); + } + + public void handleClick(UUID playerId, int clickedSlot) { + RouletteSession session = activeSessions.remove(playerId); + if (session == null) return; + + Player player = session.player; + if (player == null || !player.isOnline()) return; + + player.closeInventory(); + + 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.callback.accept(player, success); + } + + public void handleClose(UUID playerId) { + if (activeSessions.containsKey(playerId)) { + timeout(playerId); + } + } + + private void timeout(UUID playerId) { + RouletteSession 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); + } + + session.callback.accept(player, false); + } + + 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) { + String name = plugin.getConfigManager().getMessages().getString("minigames.generator-roulette.success-label", "✓ Нажми!"); + meta.displayName(Component.text(name).color(NamedTextColor.GREEN)); + 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; + } + + @Override + public void cleanup() { + for (UUID uuid : new java.util.HashSet<>(activeSessions.keySet())) { + RouletteSession session = activeSessions.remove(uuid); + if (session.player != null && session.player.isOnline()) { + session.player.closeInventory(); + } + } + activeSessions.clear(); + } + + private static class RouletteSession { + final Player player; + final BiConsumer callback; + 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; + + RouletteSession(Player player, BiConsumer callback) { + this.player = player; + this.callback = callback; + } + } +} diff --git a/src/main/java/dev/loki/minigames/generator/TextSkillCheckMinigame.java b/src/main/java/dev/loki/minigames/generator/TextSkillCheckMinigame.java new file mode 100644 index 0000000..07f448e --- /dev/null +++ b/src/main/java/dev/loki/minigames/generator/TextSkillCheckMinigame.java @@ -0,0 +1,150 @@ +package dev.loki.minigames.generator; + +import dev.loki.mdbd; +import dev.loki.minigames.GeneratorMinigame; +import dev.loki.util.TextUtil; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.title.Title; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitTask; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.function.BiConsumer; + +public class TextSkillCheckMinigame implements GeneratorMinigame { + private final mdbd plugin; + private final Map active = new HashMap<>(); + + private static final double ZONE_SIZE = 0.15; + private static final double ROTATION_SPEED = 3.0; + private static final int MAX_TICKS = 100; + + public TextSkillCheckMinigame(mdbd plugin) { + this.plugin = plugin; + } + + @Override + public void start(Player player, BiConsumer callback) { + UUID uuid = player.getUniqueId(); + + double zoneStart = Math.random() * 0.7; + double zoneEnd = zoneStart + ZONE_SIZE; + + SkillCheckSession session = new SkillCheckSession(player, callback, zoneStart, zoneEnd); + active.put(uuid, session); + + BukkitTask task = plugin.getServer().getScheduler().runTaskTimer(plugin, () -> { + SkillCheckSession s = active.get(uuid); + if (s == null) { return; } + + s.pointer += ROTATION_SPEED * 0.01; + if (s.pointer >= 1.0) s.pointer = 0.0; + s.ticks++; + + if (s.ticks > MAX_TICKS) { + expire(uuid); + return; + } + + render(player, s); + }, 1L, 1L); + + session.task = task; + } + + private void render(Player player, SkillCheckSession s) { + int width = 15; + int zStart = (int) (s.zoneStart * width); + int zEnd = (int) (s.zoneEnd * width); + int ptr = (int) (s.pointer * width); + + StringBuilder sb = new StringBuilder("["); + for (int i = 0; i < width; i++) { + if (i == ptr) { + sb.append("◆"); + } else if (i >= zStart && i <= zEnd) { + sb.append("█"); + } else { + sb.append("▒"); + } + } + sb.append("]"); + + Component title = TextUtil.color(sb.toString()); + String subStr = plugin.getConfigManager().getMessages() + .getString("minigames.text-skillcheck.subtitle", "&eНажми ПКМ когда будешь в зоне!"); + Component subtitle = TextUtil.color(subStr); + + Title.Times times = Title.Times.times(Duration.ZERO, Duration.ofMillis(150), Duration.ZERO); + player.showTitle(Title.title(title, subtitle, times)); + } + + @Override + public boolean isActive(UUID playerId) { + return active.containsKey(playerId); + } + + @Override + public boolean handleRightClick(Player player) { + return complete(player); + } + + @Override + public boolean handleSneak(Player player) { + return complete(player); + } + + private boolean complete(Player player) { + UUID uuid = player.getUniqueId(); + SkillCheckSession s = active.remove(uuid); + if (s == null) return false; + + if (s.task != null) s.task.cancel(); + player.clearTitle(); + + boolean inZone = s.pointer >= s.zoneStart && s.pointer <= s.zoneEnd; + s.callback.accept(player, inZone); + return true; + } + + private void expire(UUID uuid) { + SkillCheckSession s = active.remove(uuid); + if (s == null) return; + + if (s.task != null) s.task.cancel(); + Player player = s.player; + if (player != null && player.isOnline()) { + player.clearTitle(); + } + s.callback.accept(player, false); + } + + @Override + public void cleanup() { + for (UUID uuid : new java.util.HashSet<>(active.keySet())) { + expire(uuid); + } + active.clear(); + } + + private static class SkillCheckSession { + final Player player; + final BiConsumer callback; + final double zoneStart; + final double zoneEnd; + double pointer = 0.0; + int ticks = 0; + BukkitTask task; + + SkillCheckSession(Player player, BiConsumer callback, + double zoneStart, double zoneEnd) { + this.player = player; + this.callback = callback; + this.zoneStart = zoneStart; + this.zoneEnd = zoneEnd; + } + } +} diff --git a/src/main/java/dev/loki/objective/GeneratorHologram.java b/src/main/java/dev/loki/objective/GeneratorHologram.java index 9c56242..1ea0a91 100644 --- a/src/main/java/dev/loki/objective/GeneratorHologram.java +++ b/src/main/java/dev/loki/objective/GeneratorHologram.java @@ -1,6 +1,7 @@ package dev.loki.objective; import dev.loki.mdbd; +import dev.loki.util.TextUtil; import org.bukkit.Location; import org.bukkit.entity.ArmorStand; import org.bukkit.entity.Display; @@ -120,7 +121,7 @@ public class GeneratorHologram { Location base = gen.getLocation().clone().add(0.5, 2.0 + heightOffset, 0.5); if (display instanceof TextDisplay td) { - td.text(net.kyori.adventure.text.Component.text(text)); + td.text(TextUtil.color(text)); td.teleport(base); } else if (display instanceof ArmorStand as) { as.setCustomName(text); diff --git a/src/main/java/dev/loki/objective/GeneratorRepair.java b/src/main/java/dev/loki/objective/GeneratorRepair.java index 747e192..18c75d9 100644 --- a/src/main/java/dev/loki/objective/GeneratorRepair.java +++ b/src/main/java/dev/loki/objective/GeneratorRepair.java @@ -1,9 +1,8 @@ package dev.loki.objective; import dev.loki.mdbd; -import dev.loki.player.Survivor; +import dev.loki.minigames.GeneratorMinigame; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Material; @@ -19,18 +18,16 @@ public class GeneratorRepair { private final mdbd plugin; private final GeneratorManager manager; private final Map sessions; - private final SkillCheck skillCheck; - private final SkillCheckInventory skillCheckGUI; // GUI-based skill check + private final GeneratorMinigame minigame; private static final double REPAIR_PER_TICK = 100.0 / 1600.0; private static final double REPAIR_RANGE = 3.0; - public GeneratorRepair(mdbd plugin, GeneratorManager manager) { + public GeneratorRepair(mdbd plugin, GeneratorManager manager, GeneratorMinigame minigame) { this.plugin = plugin; this.manager = manager; + this.minigame = minigame; this.sessions = new HashMap<>(); - this.skillCheck = new SkillCheck(plugin, this); - this.skillCheckGUI = new SkillCheckInventory(plugin); } public void startRepair(Player player, Generator generator) { @@ -49,12 +46,12 @@ public class GeneratorRepair { } if (!generator.canBeRepaired()) { - player.sendMessage(Component.text("Генератор не может быть починен!")); + player.sendMessage(plugin.getConfigManager().getMessages().get("generator.repair.cannot")); return; } if (generator.isBeingRepaired() && !generator.getCurrentRepairer().equals(uuid)) { - player.sendMessage(Component.text("Кто-то уже чинит этот генератор!")); + player.sendMessage(plugin.getConfigManager().getMessages().get("generator.repair.busy")); return; } @@ -83,8 +80,6 @@ public class GeneratorRepair { } public void tick() { - skillCheck.tick(); - for (RepairSession session : new HashMap<>(sessions).values()) { if (!updateRepair(session)) { stopRepair(session.player); @@ -101,17 +96,26 @@ public class GeneratorRepair { if (generator.getDistance(player.getLocation()) > REPAIR_RANGE) return false; if (generator.isCompleted()) return false; - if (skillCheck.isActive(player.getUniqueId())) { - showSkillCheckUI(player, session); - return true; - } + if (minigame.isActive(player.getUniqueId())) return true; if (session.skillCheckCooldown > 0) { session.skillCheckCooldown--; } else if (shouldTriggerSkillCheck()) { - // Start GUI-based skill check - Player p = session.player; - skillCheckGUI.startSkillCheck(p, player.getUniqueId(), skillCheck); + minigame.start(player, (p, success) -> { + Generator g = session.generator; + if (success) { + double winPercent = plugin.getConfigManager().getGame().getSkillCheckWinPercent(); + g.addProgress(winPercent); + p.sendActionBar(plugin.getConfigManager().getMessages().get("generator.repair.skillcheck-success", (int)winPercent)); + p.playSound(p.getLocation(), org.bukkit.Sound.ENTITY_PLAYER_LEVELUP, 0.8f, 1.2f); + } else { + double lossPercent = plugin.getConfigManager().getGame().getSkillCheckLossPercent(); + g.removeProgress(lossPercent); + p.sendActionBar(plugin.getConfigManager().getMessages().get("generator.repair.skillcheck-fail", (int)lossPercent)); + p.playSound(p.getLocation(), org.bukkit.Sound.ENTITY_BLAZE_HURT, 1.0f, 0.5f); + notifyKillerWithCompass(p, g); + } + }); session.skillCheckCooldown = 100 + (int)(Math.random() * 200); return true; } @@ -127,22 +131,10 @@ public class GeneratorRepair { return true; } - private void showSkillCheckUI(Player player, RepairSession session) { - SkillCheck.CheckState state = skillCheck.getState(player.getUniqueId()); - if (state != null) { - String visual = state.getVisualRepresentation(); - player.sendActionBar(MiniMessage.miniMessage().deserialize( - "Skill Check! " + visual - )); - } - } - private void showProgressBar(Player player, RepairSession session) { double progress = session.generator.getProgress(); String bar = renderProgressBar(progress); - player.sendActionBar(MiniMessage.miniMessage().deserialize( - "Починка: " + bar - )); + player.sendActionBar(plugin.getConfigManager().getMessages().get("generator.repair.actionbar", bar)); } private String renderProgressBar(double progress) { @@ -158,9 +150,8 @@ public class GeneratorRepair { private void onGeneratorComplete(Generator generator, Player player) { manager.onGeneratorCompleted(generator); - Bukkit.broadcast(MiniMessage.miniMessage().deserialize( - "Генератор починен! (" + manager.getCompletedCount() + "/" + manager.getRequiredCount() + ")" - )); + Bukkit.broadcast(plugin.getConfigManager().getMessages().get( + "generator.repair.broadcast", manager.getCompletedCount(), manager.getRequiredCount())); } private boolean shouldTriggerSkillCheck() { @@ -171,16 +162,15 @@ public class GeneratorRepair { public void onSkillCheckResult(UUID playerId, boolean success, Generator generator) { Player player = Bukkit.getPlayer(playerId); if (player == null) return; - if (success) { - double winPercent = plugin.getConfigManager().getGame().getSkillCheckWinPercent(); - generator.addProgress(winPercent); - player.sendActionBar(MiniMessage.miniMessage().deserialize("✓ Отлично! +" + (int)winPercent + "%")); + double w = plugin.getConfigManager().getGame().getSkillCheckWinPercent(); + generator.addProgress(w); + player.sendActionBar(plugin.getConfigManager().getMessages().get("generator.repair.skillcheck-success", (int)w)); player.playSound(player.getLocation(), org.bukkit.Sound.ENTITY_PLAYER_LEVELUP, 0.8f, 1.2f); } else { - double lossPercent = plugin.getConfigManager().getGame().getSkillCheckLossPercent(); - generator.removeProgress(lossPercent); - player.sendActionBar(MiniMessage.miniMessage().deserialize("✗ Ошибка! -" + (int)lossPercent + "%. Продолжай чинить.")); + double l = plugin.getConfigManager().getGame().getSkillCheckLossPercent(); + generator.removeProgress(l); + player.sendActionBar(plugin.getConfigManager().getMessages().get("generator.repair.skillcheck-fail", (int)l)); player.playSound(player.getLocation(), org.bukkit.Sound.ENTITY_BLAZE_HURT, 1.0f, 0.5f); notifyKillerWithCompass(player, generator); } @@ -194,8 +184,7 @@ public class GeneratorRepair { for (Player online : Bukkit.getOnlinePlayers()) { if (online.getUniqueId().equals(failedPlayer.getUniqueId())) continue; - online.sendMessage(MiniMessage.miniMessage().deserialize( - "[DBD] ⚡ Выживший допустил ошибку! Компас укажет на генератор!")); + online.sendMessage(plugin.getConfigManager().getMessages().get("generator.kicked.notification")); ItemStack compass = new ItemStack(Material.COMPASS); CompassMeta meta = (CompassMeta) compass.getItemMeta(); @@ -225,12 +214,24 @@ public class GeneratorRepair { return sessions.get(uuid); } - public SkillCheckInventory getSkillCheckGUI() { - return skillCheckGUI; + public GeneratorMinigame getMinigame() { + return minigame; + } + + public boolean isMinigameActive(UUID playerId) { + return minigame.isActive(playerId); + } + + public void handleMinigameClick(Player player) { + minigame.handleRightClick(player); + } + + public void handleMinigameSneak(Player player) { + minigame.handleSneak(player); } public void shutdown() { - skillCheckGUI.clear(); + minigame.cleanup(); sessions.clear(); } diff --git a/src/main/java/dev/loki/objective/SkillCheckInventory.java b/src/main/java/dev/loki/objective/SkillCheckInventory.java index 3094502..3ffe8c6 100644 --- a/src/main/java/dev/loki/objective/SkillCheckInventory.java +++ b/src/main/java/dev/loki/objective/SkillCheckInventory.java @@ -2,6 +2,8 @@ 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; @@ -48,9 +50,8 @@ public class SkillCheckInventory { return m != null ? m : fallback; } - public void startSkillCheck(Player player, UUID playerId, SkillCheck skillCheck) { - // Start from slot 0, roulette animation - SkillCheckSession session = new SkillCheckSession(player, playerId, skillCheck); + 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)); @@ -158,7 +159,7 @@ public class SkillCheckInventory { player.playSound(player.getLocation(), Sound.ENTITY_BLAZE_HURT, 1.0f, 0.5f); } - session.skillCheck.tryComplete(playerId, success); + session.generatorRepair.onSkillCheckResult(playerId, success, session.generator); } private void onTimeout(UUID playerId) { @@ -172,7 +173,7 @@ public class SkillCheckInventory { } // Timeout = failure - session.skillCheck.tryComplete(playerId, false); + session.generatorRepair.onSkillCheckResult(playerId, false, session.generator); } private void fillInventory(Inventory inv, int greenSlot) { @@ -225,16 +226,20 @@ public class SkillCheckInventory { 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) { + 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/phase/EndgamePhase.java b/src/main/java/dev/loki/phase/EndgamePhase.java index d520254..3b1d45a 100644 --- a/src/main/java/dev/loki/phase/EndgamePhase.java +++ b/src/main/java/dev/loki/phase/EndgamePhase.java @@ -59,7 +59,8 @@ public class EndgamePhase implements Phase { // показываем результаты сразу, без геймплея эндгейма int aliveCount = 0; for (BasePlayer bp : session.getPlayers().values()) { - if (!bp.isKiller() && bp.getState() != PlayerState.DEAD && bp.getState() != PlayerState.ESCAPED) { + if (!bp.isKiller() && bp.getState() != PlayerState.DEAD && bp.getState() != PlayerState.ESCAPED + && bp.getState() != PlayerState.DOWNED && bp.getState() != PlayerState.HOOKED) { aliveCount++; } } @@ -122,20 +123,12 @@ public class EndgamePhase implements Phase { hatch.reset(); } - // Для killer_win: результаты уже показаны и телепорт выполнен в enter(), - // здесь просто останавливаем сессию - if (killerWin) { - session.stop(); - return; - } - - // Нормальный эндгейм: показываем результаты и телепортируем всех в лобби - announceEndgameEnd(session); - teleportAllToLobby(session); + // exit() НЕ вызывает session.stop() — это делает transitionToNextPhase() + // Телепорт и результаты уже обработаны в enter() или будут в teleportAllToLobby } /** - * Телепортирует всех игроков в лобби и останавливает сессию. + * Телепортирует всех игроков в лобби, сбрасывает атрибуты и останавливает сессию. */ private void teleportAllToLobby(GameSession session) { List playersToTeleport = new ArrayList<>(session.getPlayers().values()); @@ -146,12 +139,7 @@ public class EndgamePhase implements Phase { for (BasePlayer bp : playersToTeleport) { Player p = bp.getPlayer(); if (p != null && p.isOnline()) { - p.setGameMode(org.bukkit.GameMode.ADVENTURE); - p.getInventory().clear(); - try { - var attr = p.getAttribute(org.bukkit.attribute.Attribute.SCALE); - if (attr != null) attr.setBaseValue(1.0); - } catch (Exception ignored) {} + resetPlayerAttributes(p); session.getPlugin().getLobbyManager().teleportToLobby(p); } } @@ -160,6 +148,34 @@ public class EndgamePhase implements Phase { }.runTaskLater(session.getPlugin(), 60L); } + /** + * Сбрасывает все атрибуты игрока на значения по умолчанию. + */ + private void resetPlayerAttributes(Player p) { + p.setGameMode(org.bukkit.GameMode.ADVENTURE); + p.getInventory().clear(); + p.setWalkSpeed(0.2f); + p.setFlySpeed(0.1f); + p.setSwimming(false); + p.setSprinting(false); + p.setSneaking(false); + p.setHealth(20.0); + p.setFoodLevel(20); + p.setSaturation(5); + try { + var scale = p.getAttribute(org.bukkit.attribute.Attribute.SCALE); + if (scale != null) scale.setBaseValue(1.0); + } catch (Exception ignored) {} + try { + var maxHp = p.getAttribute(org.bukkit.attribute.Attribute.MAX_HEALTH); + if (maxHp != null) maxHp.setBaseValue(20.0); + } catch (Exception ignored) {} + try { + var moveSpeed = p.getAttribute(org.bukkit.attribute.Attribute.MOVEMENT_SPEED); + if (moveSpeed != null) moveSpeed.setBaseValue(0.1); + } catch (Exception ignored) {} + } + @Override public boolean isComplete() { return complete; @@ -233,7 +249,8 @@ public class EndgamePhase implements Phase { for (BasePlayer player : session.getPlayers().values()) { if (player.isKiller()) continue; PlayerState st = player.getState(); - if (st != PlayerState.DEAD && st != PlayerState.ESCAPED) { + if (st != PlayerState.DEAD && st != PlayerState.ESCAPED + && st != PlayerState.DOWNED && st != PlayerState.HOOKED) { survivorsAlive++; } } diff --git a/src/main/java/dev/loki/phase/GameplayInteraction.java b/src/main/java/dev/loki/phase/GameplayInteraction.java index 0e71a64..f13a92a 100644 --- a/src/main/java/dev/loki/phase/GameplayInteraction.java +++ b/src/main/java/dev/loki/phase/GameplayInteraction.java @@ -16,11 +16,13 @@ import dev.loki.player.Killer; import dev.loki.player.PlayerState; import dev.loki.player.Survivor; import dev.loki.survivor.SurvivorController; +import dev.loki.item.DBDItems; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Bukkit; import org.bukkit.Sound; import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; import java.util.HashMap; import java.util.Map; @@ -168,8 +170,8 @@ public class GameplayInteraction { player, PlayerState.DOWNED, 3.0); if (target != null) { boolean picked = damageSystem.getKillerController().tryPickup(killer, target); - if (!picked) player.sendMessage(Component.text( - "[DBD] Не удалось подобрать выжившего.", NamedTextColor.RED)); + if (!picked) player.sendMessage(net.kyori.adventure.text.Component.text( + "[DBD] Не удалось подобрать выжившего.", net.kyori.adventure.text.format.NamedTextColor.RED)); } } }); @@ -217,21 +219,62 @@ public class GameplayInteraction { } } else { Survivor surv = (Survivor) bp; - if (surv.getState() == PlayerState.HOOKED) { + PlayerState st = surv.getState(); + if (st == PlayerState.HOOKED) { // Hook recovery is handled elsewhere - } else { - if (isSneaking) hookSabotage.startSabotage(surv); - else hookSabotage.stopSabotage(surv.getUuid()); + } else if (st == PlayerState.HEALTHY || st == PlayerState.INJURED) { + if (isSneaking) { + double range = session.getConfig().getHookInteractionRange(); + // Try rescue first (if someone is on a hook nearby) + Hook occupied = hookManager.findNearestOccupiedHook(player.getLocation(), range); + if (occupied != null) { + hookRescue.startRescue(surv, occupied); + return; + } + // Then try sabotage (only if holding hook cutter AND near a hook) + Hook free = hookManager.findNearestUnoccupiedHook(player.getLocation(), range); + if (free != null) { + ItemStack hand = player.getInventory().getItemInMainHand(); + if (!DBDItems.isHookCutter(hand)) { + player.sendMessage(session.getPlugin().getConfigManager().getMessages().get("hook.sabotage.need-cutter")); + return; + } + hookSabotage.startSabotage(surv); + } + } else { + hookSabotage.stopSabotage(surv.getUuid()); + } } } }); - // Generator repair + // Consume Hook Cutter after successful sabotage + hookSabotage.setOnSabotageComplete(uuid -> { + Player p = Bukkit.getPlayer(uuid); + if (p != null && p.isOnline()) { + p.getInventory().setItemInMainHand(null); + } + }); + + // Text skill check: complete on sneak release + interactListener.setOnGeneratorSkillCheckSneak((player, ignored) -> { + if (generatorRepair == null) return; + if (generatorRepair.isMinigameActive(player.getUniqueId())) { + generatorRepair.handleMinigameSneak(player); + } + }); + + // Generator repair + right‑click for text skill check interactListener.setOnGeneratorInteract((player, action) -> { BasePlayer bp = session.getPlayer(player.getUniqueId()); if (bp == null || bp.isKiller()) return; if (generatorRepair == null) return; + if (generatorRepair.isMinigameActive(player.getUniqueId())) { + generatorRepair.handleMinigameClick(player); + return; + } + var genManager = session.getGeneratorManager(); if (genManager == null) return; double range = session.getConfig().getGeneratorInteractionRange(); @@ -301,6 +344,7 @@ public class GameplayInteraction { interactListener.setOnKillerSneak(null); interactListener.setOnHealSneak(null); interactListener.setOnCarriedShift(null); + interactListener.setOnGeneratorSkillCheckSneak(null); interactListener.setOnGeneratorInteract(null); interactListener.setOnHatchInteract(null); } diff --git a/src/main/java/dev/loki/phase/GameplayPhase.java b/src/main/java/dev/loki/phase/GameplayPhase.java index f61ff83..a4bcced 100644 --- a/src/main/java/dev/loki/phase/GameplayPhase.java +++ b/src/main/java/dev/loki/phase/GameplayPhase.java @@ -4,6 +4,7 @@ import dev.loki.chest.ChestManager; import dev.loki.chest.ChestOpening; import dev.loki.config.GameConfig; import dev.loki.core.GameSession; +import dev.loki.gate.ExitGate; import dev.loki.gate.GateManager; import dev.loki.gate.GateOpening; import dev.loki.hook.HookManager; @@ -15,6 +16,8 @@ import dev.loki.hook.HookStruggle; import dev.loki.item.ChestLoot; import dev.loki.killer.GeneratorKick; import dev.loki.mdbd; +import dev.loki.minigames.GeneratorMinigame; +import dev.loki.minigames.generator.GuiRouletteMinigame; import dev.loki.objective.GeneratorManager; import dev.loki.player.BasePlayer; import dev.loki.player.PlayerState; @@ -22,7 +25,6 @@ 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 org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Material; @@ -91,8 +93,7 @@ public class GameplayPhase implements Phase { world.setStorm(false); } - Bukkit.broadcast(Component.text( - "[DBD] Игра началась! Выжившие, чините генераторы!", NamedTextColor.GREEN)); + Bukkit.broadcast(session.getPlugin().getConfigManager().getMessages().get("game.start")); } @Override @@ -133,7 +134,7 @@ public class GameplayPhase implements Phase { damageSystem.clear(); damageSystem.clearManagers(); - Bukkit.broadcast(Component.text("[DBD] Игровая фаза завершена!", NamedTextColor.GOLD)); + Bukkit.broadcast(session.getPlugin().getConfigManager().getMessages().get("game.end")); } @Override @@ -164,6 +165,18 @@ public class GameplayPhase implements Phase { ActionBar ab = new ActionBar(); HookRecovery hookRecovery = new HookRecovery(plugin, cfg); titleUI = new TitleUI(plugin); + + String genMinigameType = cfg.getGeneratorMinigame(); + GeneratorMinigame genMinigame; + if ("gui".equalsIgnoreCase(genMinigameType)) { + genMinigame = new GuiRouletteMinigame(plugin); + } else if ("text".equalsIgnoreCase(genMinigameType)) { + genMinigame = new dev.loki.minigames.generator.TextSkillCheckMinigame(plugin); + } else { + plugin.getLogger().warning("Unknown generator minigame: " + genMinigameType + ", using gui"); + genMinigame = new GuiRouletteMinigame(plugin); + } + hookStruggle = new HookStruggle(plugin, titleUI, cfg); HookSacrifice hookSacrifice = new HookSacrifice(plugin, null); hookSabotage = new HookSabotage(plugin, cfg, hookManager); @@ -176,11 +189,19 @@ public class GameplayPhase implements Phase { chestManager.startLootResetTimer(); chestOpening = new ChestOpening(plugin); chestOpening.setChestLoot(new ChestLoot(cfg)); + + var gameWorld = plugin.getLobbyManager().getGameWorld(); + if (gameWorld != null) { + for (var h : hookManager.getAllHooks()) h.getLocation().setWorld(gameWorld); + for (var c : chestManager.getAllChests()) c.getLocation().setWorld(gameWorld); + for (var g : generatorManager.getAllGenerators()) g.getLocation().setWorld(gameWorld); + } + spawnGameBlocks(plugin, cfg); generatorKick = new GeneratorKick(plugin, cfg, generatorManager); - generatorRepair = new dev.loki.objective.GeneratorRepair(plugin, generatorManager); + generatorRepair = new dev.loki.objective.GeneratorRepair(plugin, generatorManager, genMinigame); // Голограммы ВСЕГДА включены generatorHologram = new dev.loki.objective.GeneratorHologram(plugin, generatorManager); generatorHologram.start(); @@ -222,11 +243,12 @@ public class GameplayPhase implements Phase { if (p != null && p.isOnline()) p.getInventory().clear(); } - // Register skill check GUI listener - dev.loki.listener.SkillCheckListener skillCheckListener = - new dev.loki.listener.SkillCheckListener(plugin, - generatorRepair.getSkillCheckGUI()); - skillCheckListener.register(); + GeneratorMinigame gm = generatorRepair.getMinigame(); + if (gm instanceof GuiRouletteMinigame gui) { + dev.loki.listener.SkillCheckListener listener = + new dev.loki.listener.SkillCheckListener(plugin, gui); + listener.register(); + } interaction.wire(session); } @@ -241,8 +263,7 @@ public class GameplayPhase implements Phase { allGensDone = true; interaction.setAllGensDone(true); gateManager.activateEndgame(); - Bukkit.broadcast(Component.text( - "[DBD] Все генераторы починены! Открывайте ворота!", NamedTextColor.GREEN)); + Bukkit.broadcast(session.getPlugin().getConfigManager().getMessages().get("generator.repair.all-done")); for (BasePlayer bp : session.getPlayers().values()) { Player p = bp.getPlayer(); if (p != null) p.playSound(p.getLocation(), Sound.ENTITY_FIREWORK_ROCKET_BLAST, 1.0f, 1.0f); @@ -289,6 +310,12 @@ public class GameplayPhase implements Phase { if (b.getType() != hookMat) b.setType(hookMat, false); } plugin.getLogger().info("[GameplayPhase] Spawned blocks for " + hookManager.getAllHooks().size() + " hooks"); + + // Спавн блоков на местах ворот (Hopper — для открытия через ПКМ) + for (ExitGate gate : gateManager.getAllGates()) { + gate.spawnGateBlock(); + } + plugin.getLogger().info("[GameplayPhase] Spawned blocks for " + gateManager.getAllGates().size() + " gates"); } // ========================================================================= @@ -342,6 +369,13 @@ public class GameplayPhase implements Phase { if (hookStruggle != null) hookStruggle.clear(); if (chestManager != null) chestManager.stopLootResetTimer(); if (chestHologram != null) chestHologram.stop(); + // Clean up gate blocks + if (gateManager != null) { + for (ExitGate gate : gateManager.getAllGates()) { + gate.removeGateBlock(); + } + gateManager.resetAll(); + } session.getPlugin().getInteractListener().setGeneratorManager(null); } } \ No newline at end of file diff --git a/src/main/java/dev/loki/phase/GameplayUI.java b/src/main/java/dev/loki/phase/GameplayUI.java index d70f156..ba19952 100644 --- a/src/main/java/dev/loki/phase/GameplayUI.java +++ b/src/main/java/dev/loki/phase/GameplayUI.java @@ -104,7 +104,7 @@ public class GameplayUI { matchTimerBar.color(color); if (matchTimerTicks <= 0) { - Bukkit.broadcast(Component.text("[DBD] Время вышло! Маньяк победил!", NamedTextColor.DARK_RED)); + Bukkit.broadcast(session.getPlugin().getConfigManager().getMessages().get("game.time-up")); if (winConditions != null) winConditions.announceKillerWin(session); return true; } diff --git a/src/main/java/dev/loki/phase/GameplayWinConditions.java b/src/main/java/dev/loki/phase/GameplayWinConditions.java index 90d2b14..2f91504 100644 --- a/src/main/java/dev/loki/phase/GameplayWinConditions.java +++ b/src/main/java/dev/loki/phase/GameplayWinConditions.java @@ -8,8 +8,6 @@ import dev.loki.player.Killer; import dev.loki.player.PlayerState; import dev.loki.player.Survivor; import dev.loki.ui.TitleUI; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Bukkit; import org.bukkit.GameMode; import org.bukkit.Sound; @@ -35,14 +33,26 @@ public class GameplayWinConditions { * Check all win conditions each second (20 ticks). */ public void checkWinConditions(GameSession session) { - int aliveSurvivors = countAliveSurvivors(session); Killer killer = getKiller(session); // Killer disconnect: if killer player is null/offline, survivors win if (killer == null || killer.getPlayer() == null || !killer.getPlayer().isOnline()) { - winReason = "survivors_win"; - Bukkit.broadcast(Component.text("[DBD] Маньяк покинул игру! Выжившие победили!", NamedTextColor.GREEN)); - announceSurvivorsWin(session); + if (!shouldEnd) { + winReason = "survivors_win"; + Bukkit.broadcast(session.getPlugin().getConfigManager().getMessages().get("game.killer-disconnect")); + announceSurvivorsWin(session); + shouldEnd = true; + } + return; + } + + // No survivors at all — automatic killer win + long survivorCount = session.getPlayers().values().stream() + .filter(p -> !p.isKiller()).count(); + if (survivorCount == 0) { + winReason = "killer_win"; + Bukkit.broadcast(session.getPlugin().getConfigManager().getMessages().get("game.no-survivors")); + announceKillerWin(session); shouldEnd = true; return; } @@ -50,7 +60,7 @@ public class GameplayWinConditions { // Killer wins: all survivors are downed/hooked/carried/dead if (allSurvivorsIncapacitated(session)) { winReason = "killer_win"; - Bukkit.broadcast(Component.text("[DBD] Все выжившие обезврежены!", NamedTextColor.DARK_RED)); + Bukkit.broadcast(session.getPlugin().getConfigManager().getMessages().get("game.all-incapacitated")); announceKillerWin(session); shouldEnd = true; return; @@ -64,7 +74,7 @@ public class GameplayWinConditions { } } - /** True if NO survivor can act (all are DOWNED, HOOKED, CARRIED, or DEAD). */ + /** True if NO survivor can act (all are DOWNED, HOOKED, CARRIED, DEAD, or ESCAPED). */ private boolean allSurvivorsIncapacitated(GameSession session) { boolean hasSurvivors = false; for (BasePlayer bp : session.getPlayers().values()) { @@ -72,7 +82,8 @@ public class GameplayWinConditions { hasSurvivors = true; PlayerState st = bp.getState(); if (st != PlayerState.DOWNED && st != PlayerState.HOOKED - && st != PlayerState.CARRIED && st != PlayerState.DEAD) { + && st != PlayerState.CARRIED && st != PlayerState.DEAD + && st != PlayerState.ESCAPED) { return false; // at least one survivor can still act } } @@ -106,7 +117,7 @@ public class GameplayWinConditions { public void announceKillerWin(GameSession session) { winReason = "killer_win"; - Bukkit.broadcast(Component.text("[DBD] Маньяк победил! Все выжившие мертвы!", NamedTextColor.DARK_RED)); + Bukkit.broadcast(session.getPlugin().getConfigManager().getMessages().get("game.killer-win")); for (BasePlayer bp : session.getPlayers().values()) { Player p = bp.getPlayer(); if (p == null || !p.isOnline()) continue; @@ -122,7 +133,7 @@ public class GameplayWinConditions { public void announceSurvivorsWin(GameSession session) { winReason = "survivors_win"; - Bukkit.broadcast(Component.text("[DBD] Выжившие победили!", NamedTextColor.GREEN)); + Bukkit.broadcast(session.getPlugin().getConfigManager().getMessages().get("game.survivors-win")); for (BasePlayer bp : session.getPlayers().values()) { Player p = bp.getPlayer(); if (p == null || !p.isOnline()) continue; diff --git a/src/main/java/dev/loki/phase/LobbyPhase.java b/src/main/java/dev/loki/phase/LobbyPhase.java index 721ca4b..55e0af1 100644 --- a/src/main/java/dev/loki/phase/LobbyPhase.java +++ b/src/main/java/dev/loki/phase/LobbyPhase.java @@ -3,8 +3,8 @@ package dev.loki.phase; import dev.loki.config.GameConfig; import dev.loki.core.GameSession; import dev.loki.player.BasePlayer; +import dev.loki.config.MessagesConfig; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Bukkit; import org.bukkit.entity.Player; @@ -38,7 +38,8 @@ public class LobbyPhase implements Phase { if (p != null && p.isOnline()) { p.setGameMode(org.bukkit.GameMode.ADVENTURE); lobbyManager.teleportToLobby(p); - p.sendMessage(Component.text("[DBD] Лобби открыто! Ожидание игроков...", NamedTextColor.GREEN)); + MessagesConfig messages = session.getPlugin().getConfigManager().getMessages(); + p.sendMessage(messages.get("lobby.opened")); } } } @@ -59,9 +60,8 @@ public class LobbyPhase implements Phase { secondsLeft = config.getCountdown4Players(); tickSub = 0; lastPlayerCount = playerCount; - sendActionBarToAll(session, - Component.text("⏳ Ожидание игроков: " + playerCount + "/" + minPlayers, - NamedTextColor.YELLOW)); + MessagesConfig messages = session.getPlugin().getConfigManager().getMessages(); + sendActionBarToAll(session, messages.get("lobby.waiting", playerCount, maxPlayers)); return; } @@ -74,9 +74,8 @@ public class LobbyPhase implements Phase { if (secondsLeft > fast) { secondsLeft = fast; tickSub = 0; - Bukkit.broadcast(Component.text( - "[DBD] Все игроки вошли! Старт через " + fast + " сек!", - NamedTextColor.GREEN)); + MessagesConfig messages = session.getPlugin().getConfigManager().getMessages(); + Bukkit.broadcast(messages.get("lobby.all-ready", fast)); } } } @@ -91,17 +90,14 @@ public class LobbyPhase implements Phase { if (secondsLeft == 30 || secondsLeft == 15 || secondsLeft == 10 || secondsLeft == 5 || secondsLeft == 3 || secondsLeft == 2 || secondsLeft == 1) { - Bukkit.broadcast(Component.text( - "[DBD] До начала: " + secondsLeft + " сек.", - NamedTextColor.YELLOW)); + MessagesConfig messages = session.getPlugin().getConfigManager().getMessages(); + Bukkit.broadcast(messages.get("lobby.countdown", secondsLeft)); } } // ActionBar всем (на русском, без прогресс-бара) - NamedTextColor barColor = (playerCount >= maxPlayers) - ? NamedTextColor.GREEN : NamedTextColor.YELLOW; - sendActionBarToAll(session, - Component.text(">>> Начало через: " + secondsLeft + "с (" + playerCount + "/" + minPlayers + ")", barColor)); + MessagesConfig messages = session.getPlugin().getConfigManager().getMessages(); + sendActionBarToAll(session, messages.get("lobby.actionbar-waiting", secondsLeft, playerCount, maxPlayers)); if (secondsLeft <= 0) { started = true; @@ -129,7 +125,8 @@ public class LobbyPhase implements Phase { Player p = bp.getPlayer(); if (p != null && p.isOnline()) p.sendActionBar(Component.empty()); } - Bukkit.broadcast(Component.text("[DBD] Игра начинается!", NamedTextColor.GREEN)); + MessagesConfig messages = session.getPlugin().getConfigManager().getMessages(); + Bukkit.broadcast(messages.get("lobby.starting")); } @Override @@ -137,11 +134,10 @@ public class LobbyPhase implements Phase { player.setGameMode(org.bukkit.GameMode.ADVENTURE); session.getPlugin().getLobbyManager().teleportToLobby(player); int playerCount = session.getPlayerCount(); - int minPlayers = config.getMinPlayers(); + int maxPlayers = config.getMaxPlayers(); // Сообщаем только участникам этой сессии, не всему серверу - Component msg = Component.text( - "[DBD] " + player.getName() + " присоединился! (" + playerCount + "/" + minPlayers + ")", - NamedTextColor.YELLOW); + MessagesConfig messages = session.getPlugin().getConfigManager().getMessages(); + Component msg = messages.get("lobby.player-joined", player.getName(), playerCount, maxPlayers); for (BasePlayer bp : session.getPlayers().values()) { Player p = bp.getPlayer(); if (p != null && p.isOnline()) p.sendMessage(msg); diff --git a/src/main/java/dev/loki/phase/RoleAssignPhase.java b/src/main/java/dev/loki/phase/RoleAssignPhase.java index 1f3249a..34d722e 100644 --- a/src/main/java/dev/loki/phase/RoleAssignPhase.java +++ b/src/main/java/dev/loki/phase/RoleAssignPhase.java @@ -2,8 +2,6 @@ package dev.loki.phase; import dev.loki.core.GameSession; import dev.loki.role.RoleAssigner; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Bukkit; public class RoleAssignPhase implements Phase { @@ -46,7 +44,7 @@ public class RoleAssignPhase implements Phase { @Override public void exit(GameSession session) { - Bukkit.broadcast(Component.text("[DBD] Роли назначены!", NamedTextColor.GREEN)); + Bukkit.broadcast(session.getPlugin().getConfigManager().getMessages().get("game.roles-assigned")); } @Override diff --git a/src/main/java/dev/loki/role/RoleAssigner.java b/src/main/java/dev/loki/role/RoleAssigner.java index 5ba8486..aea26dc 100644 --- a/src/main/java/dev/loki/role/RoleAssigner.java +++ b/src/main/java/dev/loki/role/RoleAssigner.java @@ -65,6 +65,12 @@ public class RoleAssigner { session.getPlugin().getLogger().warning("[RoleAssigner] survivor-spawns не установлены! Выжившие будут спавниться в лобби."); } else { session.getPlugin().getLogger().info("[RoleAssigner] Загружено " + survivorSpawns.size() + " спавнов выживших"); + int expectedSurvivors = uuids.size() - 1; + if (survivorSpawns.size() < expectedSurvivors) { + session.getPlugin().getLogger().warning( + "[RoleAssigner] Мало спавнов выживших! Установлено " + survivorSpawns.size() + + ", нужно минимум " + expectedSurvivors + ". Выжившие наложатся друг на друга."); + } } // Fix world references to use game world @@ -76,32 +82,33 @@ public class RoleAssigner { java.util.function.Function fixWorld = loc -> new Location(gameWorld, loc.getX(), loc.getY(), loc.getZ(), loc.getYaw(), loc.getPitch()); - // Ensure chunks are loaded at spawn locations before teleporting + // Remap all spawn locations to game world and load chunks Random random = new Random(); - for (Location spawn : killerSpawns) { - if (spawn != null && spawn.getWorld() != null) { - spawn.getWorld().loadChunk(spawn.getBlockX() >> 4, spawn.getBlockZ() >> 4); - } - } - for (Location spawn : survivorSpawns) { - if (spawn != null && spawn.getWorld() != null) { - spawn.getWorld().loadChunk(spawn.getBlockX() >> 4, spawn.getBlockZ() >> 4); - } - } + List killerGameSpawns = killerSpawns.stream() + .map(fixWorld).peek(s -> s.getWorld().loadChunk(s.getBlockX() >> 4, s.getBlockZ() >> 4)) + .collect(java.util.stream.Collectors.toList()); + List survivorGameSpawns = survivorSpawns.stream() + .map(fixWorld).peek(s -> s.getWorld().loadChunk(s.getBlockX() >> 4, s.getBlockZ() >> 4)) + .collect(java.util.stream.Collectors.toList()); + + // Последовательное назначение спавнов выживших (по циклу, чтобы равномерно распределить) + int survivorIndex = 0; for (UUID uuid : uuids) { BasePlayer newPlayer; if (uuid.equals(killerUuid)) { newPlayer = new Killer(uuid); - if (!killerSpawns.isEmpty()) { - Location spawn = fixWorld.apply(killerSpawns.get(random.nextInt(killerSpawns.size()))); + if (!killerGameSpawns.isEmpty()) { + Location spawn = killerGameSpawns.get(random.nextInt(killerGameSpawns.size())); newPlayer.setSpawnLocation(spawn); } } else { newPlayer = new Survivor(uuid); - if (!survivorSpawns.isEmpty()) { - Location spawn = fixWorld.apply(survivorSpawns.get(random.nextInt(survivorSpawns.size()))); + if (!survivorGameSpawns.isEmpty()) { + int idx = survivorIndex % survivorGameSpawns.size(); + Location spawn = survivorGameSpawns.get(idx); newPlayer.setSpawnLocation(spawn); + survivorIndex++; } } diff --git a/src/main/java/dev/loki/survivor/SurvivorHealing.java b/src/main/java/dev/loki/survivor/SurvivorHealing.java index 58ff0e3..efaa516 100644 --- a/src/main/java/dev/loki/survivor/SurvivorHealing.java +++ b/src/main/java/dev/loki/survivor/SurvivorHealing.java @@ -1,6 +1,7 @@ package dev.loki.survivor; import dev.loki.config.GameConfig; +import dev.loki.mdbd; import dev.loki.player.PlayerState; import dev.loki.player.Survivor; import net.kyori.adventure.text.Component; @@ -56,7 +57,7 @@ public class SurvivorHealing { // Check heal limit if (healsUsed >= config.getMaxHealsPerMatch()) { - healer.sendMessage(Component.text("[DBD] Лимит лечений исчерпан!", net.kyori.adventure.text.format.NamedTextColor.RED)); + healer.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.heal.limit")); return; } @@ -72,7 +73,7 @@ public class SurvivorHealing { // Range check for other-heal if (!selfHeal && healer.getLocation().distance(target.getLocation()) > config.getHealRange()) { - healer.sendMessage(Component.text("[DBD] Слишком далеко!", net.kyori.adventure.text.format.NamedTextColor.RED)); + healer.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.heal.too-far")); return; } @@ -120,7 +121,7 @@ public class SurvivorHealing { public void cancelHealing() { if (healingTarget != null && healingTarget.isOnline() && isHealing()) { healingTarget.sendActionBar(Component.empty()); - healingTarget.sendMessage(Component.text("[DBD] Лечение прервано!", net.kyori.adventure.text.format.NamedTextColor.YELLOW)); + healingTarget.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.heal.interrupted")); } healingTarget = null; healingTicks = 0; @@ -145,15 +146,15 @@ public class SurvivorHealing { Player targetPlayer = healingTarget; if (targetPlayer != null && targetPlayer.isOnline()) { - String msg = newState == PlayerState.HEALTHY - ? "Вы полностью вылечились!" - : "Вы встали на ноги!"; - targetPlayer.sendMessage(MiniMessage.miniMessage().deserialize(msg)); + String msgKey = newState == PlayerState.HEALTHY + ? "survivor.heal.self-done" + : "survivor.heal.other-done"; + targetPlayer.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get(msgKey)); } Player healer = controller.getPlayer(); if (healer != null && !healer.getUniqueId().equals(targetPlayer != null ? targetPlayer.getUniqueId() : null)) { - healer.sendMessage(Component.text("[DBD] Союзник вылечен!", net.kyori.adventure.text.format.NamedTextColor.GREEN)); + healer.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.heal.complete")); } cancelHealing(); @@ -163,10 +164,9 @@ public class SurvivorHealing { if (healingTarget == null) return; double progress = 1.0 - ((double) healingTicks / totalHealingTicks); String bar = renderProgressBar(progress); - Component message = MiniMessage.miniMessage().deserialize( - isSelfHeal ? "Лечение себя: " + bar - : "Лечение союзника: " + bar - ); + String template = mdbd.getInstance().getConfigManager().getMessages().getString( + isSelfHeal ? "survivor.heal.self-start" : "survivor.heal.other-start", bar); + Component message = MiniMessage.miniMessage().deserialize(template); healingTarget.sendActionBar(message); // Also show on healer Player healer = controller.getPlayer(); diff --git a/src/main/java/dev/loki/ui/TitleUI.java b/src/main/java/dev/loki/ui/TitleUI.java index 35c7578..22791bc 100644 --- a/src/main/java/dev/loki/ui/TitleUI.java +++ b/src/main/java/dev/loki/ui/TitleUI.java @@ -150,8 +150,9 @@ public class TitleUI { Component title = Component.text(visual.toString()) .color(NamedTextColor.WHITE); - Component subtitle = Component.text("CROUCH when in the zone!") - .color(NamedTextColor.YELLOW); + String subStr = plugin.getConfigManager().getMessages() + .getString("hook.struggle.subtitle", "&eПрисядь когда будешь в зоне!"); + Component subtitle = dev.loki.util.TextUtil.color(subStr); Title.Times times = Title.Times.times(Duration.ZERO, Duration.ofMillis(100), Duration.ZERO); player.showTitle(Title.title(title, subtitle, times)); diff --git a/src/main/java/dev/loki/util/SpawnUtil.java b/src/main/java/dev/loki/util/SpawnUtil.java index 5c6403e..4a77c71 100644 --- a/src/main/java/dev/loki/util/SpawnUtil.java +++ b/src/main/java/dev/loki/util/SpawnUtil.java @@ -31,7 +31,7 @@ public final class SpawnUtil { // Ground is solid at configured Y — use it as is loc.setX(x + 0.5); loc.setZ(z + 0.5); - loc.setY(y + 1.0); + loc.setY(y); return loc; } diff --git a/src/main/java/dev/loki/util/TextUtil.java b/src/main/java/dev/loki/util/TextUtil.java index 9c346ee..5ed8a21 100644 --- a/src/main/java/dev/loki/util/TextUtil.java +++ b/src/main/java/dev/loki/util/TextUtil.java @@ -3,27 +3,53 @@ package dev.loki.util; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; -/** - * Utility for converting legacy &#RRGGBB color codes to MiniMessage format. - * Usage: TextUtil.color("&#FFD700Hello") → renders as gold colored text. - */ +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Pattern; + public final class TextUtil { private static final MiniMessage MM = MiniMessage.miniMessage(); + private static final Map COLORS = new HashMap<>(); + private static final Pattern LEGACY = Pattern.compile("&([0-9a-fklmnor])"); + + static { + COLORS.put('0', "black"); + COLORS.put('1', "dark_blue"); + COLORS.put('2', "dark_green"); + COLORS.put('3', "dark_aqua"); + COLORS.put('4', "dark_red"); + COLORS.put('5', "dark_purple"); + COLORS.put('6', "gold"); + COLORS.put('7', "gray"); + COLORS.put('8', "dark_gray"); + COLORS.put('9', "blue"); + COLORS.put('a', "green"); + COLORS.put('b', "aqua"); + COLORS.put('c', "red"); + COLORS.put('d', "light_purple"); + COLORS.put('e', "yellow"); + COLORS.put('f', "white"); + COLORS.put('k', "obfuscated"); + COLORS.put('l', "bold"); + COLORS.put('m', "strikethrough"); + COLORS.put('n', "underline"); + COLORS.put('o', "italic"); + COLORS.put('r', "reset"); + } private TextUtil() {} - /** - * Converts &#RRGGBB color codes to MiniMessage <#RRGGBB> and deserializes. - */ public static Component color(String text) { return MM.deserialize(toMiniMessage(text)); } - /** - * Converts &#RRGGBB to MiniMessage <#RRGGBB> syntax without deserializing. - */ public static String toMiniMessage(String text) { - // &#RRGGBB → <#RRGGBB> - return text.replaceAll("&#([0-9A-Fa-f]{6})", "<#$1>"); + String result = text.replaceAll("&#([0-9A-Fa-f]{6})", "<#$1>"); + result = result.replace('§', '&'); + result = LEGACY.matcher(result).replaceAll(m -> { + String tag = COLORS.get(m.group(1).charAt(0)); + return tag != null ? "<" + tag + ">" : m.group(); + }); + return result; } } diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml index 8a52e55..1ed8152 100644 --- a/src/main/resources/messages.yml +++ b/src/main/resources/messages.yml @@ -1,113 +1,314 @@ # ============================================ # Mdbd Plugin - Сообщения # Поддерживаются цвета в формате &#rrggbb +# %prefix% - глобальный префикс (DBD в красном) +# %admin-prefix% - префикс для админ-команд +# %setup-prefix% - префикс для команд установки # ============================================ -# Префикс всех сообщений плагина -prefix: "&#FF6B00&l[DBD]&r " +prefix: "&#FF2121D&#E81F1FB&#D11E1ED &7| " +admin-prefix: "&#FF2121D&#E81F1FB&#D11E1ED &7| &7» " +setup-prefix: "&#FF2121D&#E81F1FB&#D11E1ED &7| &7▶ " + +# ── Команды ───────────────────────────────────── + +cmd: + no-permission: "%admin-prefix%&cНет прав!" + players-only: "%admin-prefix%&cТолько игроки!" + +# ── /dbd ──────────────────────────────────────── + +dbd: + join: + success: "%prefix%&aВы присоединились к игре!" + fail: "%prefix%&cНе удалось присоединиться. Возможно, игра уже идёт." + not-in-game: "%prefix%&cВы не в игре." + leave: + success: "%prefix%&eВы покинули игру." + create: + usage: "%prefix%&cИспользование: /dbd create <имя> [пароль] [private]" + delete: + usage: "%prefix%&cИспользование: /dbd delete <имя>" + forcestart: + success: "%prefix%&aИгра форсирована!" + fail: "%admin-prefix%&cНельзя форсировать сейчас." + no-game: "%admin-prefix%&cНет активной игры!" + stop: + success: "%prefix%&eИгра остановлена." + reload: + success: "%admin-prefix%&aКонфиги перезагружены!" + map: + save-success: "%prefix%&aКарта «%s» сохранена в plugins/Mdbd/maps/" + save-fail: "%prefix%&cНе удалось сохранить карту." + load-success: "%prefix%&aКарта «%s» загружена!" + load-warn: "%prefix%&eПерезагрузите карту или перезапустите сервер для применения." + load-not-found: "%prefix%&cКарта «%s» не найдена." + save-usage: "%admin-prefix%&cИспользование: /dbd save <название>" + load-usage: "%admin-prefix%&cИспользование: /dbd load <название>" + lobby: + teleport: "%prefix%&aТелепорт в лобби!" + setlobby: + success: "%admin-prefix%&aТочка лобби установлена!" + +# ── /dbd admin ────────────────────────────────── + +dbdadmin: + no-session: "%admin-prefix%&cНет активной игровой сессии." + heal: + target: "%prefix%&aАдмин вылечил вас." + sender: "%admin-prefix%&a%s → HEALTHY" + state: + sender: "%admin-prefix%&e%s → %s" + usage: "%admin-prefix%&c/dbd admin state <игрок> " + unknown: "%admin-prefix%&cНеизвестный статус." + hooks: + usage: "%admin-prefix%&c/dbd admin hooks <игрок> <0-3>" + sender: "%admin-prefix%&a%s hooks=%d" + invalid: "%admin-prefix%&cЧисло 0–3." + kill: + sender: "%admin-prefix%&c%s → DEAD" + resolve: + no-player: "%admin-prefix%&cУкажите игрока." + offline: "%admin-prefix%&cИгрок не в сети." + not-in-game: "%admin-prefix%&cИгрок не в этой игре или не выживший." + jump: + sender: "%admin-prefix%&6Прыжки %s" + unknown-type: "%admin-prefix%&cНеизвестный тип: %s" + +# ── /dbdadmin (список объектов) ───────────────── + +dbdadmin-list: + header: "%setup-prefix%=== Все установленные объекты ===" + none: "&7%s: нет" + count: "&6%s (&e%s&6):" + entry: "�CED1 #%d %s [%.1f, %.1f, %.1f]" + world-usage: "%setup-prefix%&cИспользование: /dbdadmin world " + world-not-found: "%setup-prefix%&cМир game не загружен!" + world-not-exist: "%setup-prefix%&cМир не найден: %s" + world-teleport: "%setup-prefix%&aТелепортирован в мир: &6%s" + +# ── /dbdsetup ─────────────────────────────────── + +setup: + wand-given: "%setup-prefix%&aПолучен DBD Setup Wand" + set-usage: "%setup-prefix%&cИспользование: /dbdsetup set <тип> [номер]" + set-types: "&6Типы: hook, chest, generator, killer-spawn, survivor-spawn, gate" + set-spawn-info: "&6Для спавнов можно указать номер: /dbdsetup set killer-spawn 0" + gate-usage: "%setup-prefix%&cУкажи: /dbdsetup set gate A (или B)" + gate-invalid: "%setup-prefix%&cВорота могут быть только A или B" + unknown-type: "%setup-prefix%&cНеизвестный тип: %s" + survivor-spawn-range: "%setup-prefix%&cНомер спавна выжившего должен быть от 1 до 4" + killer-spawn-range: "%setup-prefix%&cНомер спавна киллера должен быть >= 0" + invalid-number: "%setup-prefix%&cНомер спавна должен быть числом: %s" + mode-off: "&eРежим установки &6%s &eвыключен" + mode-on-spawn: "%setup-prefix%&aРежим установки: &6%s%s\n&aСломай блок чтобы установить спавн (блок восстановится)\n&eПовторно введи команду для выключения" + mode-on: "%setup-prefix%&aРежим установки: &6%s%s\n&aСломай блок чтобы установить (блок восстановится)\n&eВведи команду снова чтобы выключить режим" + gate-need-points: "%setup-prefix%&cСначала выбери оба угла топориком! (ЛКМ=точка 1, ПКМ=точка 2)" + gate-same-world: "%setup-prefix%&cТочки должны быть в одном мире!" + gate-placed: "%setup-prefix%&aВорота &f%s &aустановлены: %s" + hook-placed: "%setup-prefix%&aКрюк установлен: %s" + chest-placed: "%setup-prefix%&aСундук добавлен: %s" + generator-placed: "%setup-prefix%&aГенератор добавлен: %s" + killer-spawn-exists: "%setup-prefix%&cСпавн киллера уже установлен! Используй /dbdsetup clear-spawns для сброса." + killer-spawn-placed: "&#FF4444Спавн киллера установлен: %s" + survivor-spawn-limit: "%setup-prefix%&cДостигнут лимит спавнов выживших (4)! Используй /dbdsetup clear-spawns для сброса." + 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Карта очищена!" + spawns-cleared: "%setup-prefix%&aВсе спавны (киллер + выжившие) очищены!" + lobby-set: "%setup-prefix%&aЛобби установлено: %s" + +# ── Игровой процесс ───────────────────────────── -# Сообщения игры game: - # Старт/конец - start: "�FF00Игра началась! Бегите от Киллера!" - end-victory-escape: "&#FFD700Выжившие сбежали! Победа!" - end-victory-killer: "&#DC143CКиллер победил!" - - # Ожидание - waiting: "&#FFFF00Ожидание игроков... (%s/%s)" - countdown: "&#FFA500До начала: %s секунд" - countdown-short: "&#FF0000%s..." - - # Роли - role-killer: "&#DC143CВы Киллер! Поймайте всех выживших." - role-survivor: "�CED1Вы Выживший! Почините генераторы и сбегите." + start: "%prefix%&aИгра началась! Выжившие, чините генераторы!" + end: "&6&l=== РЕЗУЛЬТАТЫ ===" + killer-win: "%prefix%&4Маньяк победил! Все выжившие мертвы!" + survivors-win: "%prefix%&aВыжившие победили!" + killer-disconnect: "%prefix%&aМаньяк покинул игру! Выжившие победили!" + no-survivors: "%prefix%&4В игре нет выживших! Маньяк победил!" + all-incapacitated: "%prefix%&4Все выжившие обезврежены!" + all-dead: "%prefix%&4Все выжившие мертвы. Киллер победил!" + time-up: "%prefix%&4Время вышло! Маньяк победил!" + roles-assigned: "%prefix%&aРоли назначены!" + +# ── Лобби ────────────────────────────────────── + +lobby: + opened: "%prefix%&aЛобби открыто! Ожидание игроков..." + waiting: "&e⏳ Ожидание игроков: %d/%d" + all-ready: "&a[DBD] Все игроки вошли! Старт через %d сек!" + countdown: "&e[DBD] До начала: %d сек." + starting: "&a[DBD] Игра начинается!" + actionbar-waiting: "&e>>> Начало через: %dс (%d/%d)" + player-joined: "&e[DBD] %s присоединился! (%d/%d)" + +# ── Роли ─────────────────────────────────────── + +role: + killer: "%prefix%&4Вы — Маньяк! Поймайте всех выживших!" + survivor: "%prefix%&bВы — Выживший! Почините генераторы и сбегите!" + +# ── Генераторы ───────────────────────────────── -# Генераторы generator: - repair-start: "�CED1Вы начали чинить генератор..." - repair-stop: "WCEEBВы остановились." - repair-complete: "�FF00✓ Генератор активирован! (%s/%s)" - repair-progress: "&#FFD700Прогресс: %s%%" - - skillcheck-success: "�FF00Отлично! +%s%%" - skillcheck-super: "&#FFD700Великолепно! +%s%%" - skillcheck-fail: "&#FF0000✗ Провал! -%s%%" - - explosion: "&#FF4500⚠ Взрыв генератора!" - regression: "&#FF6347Генератор откатывается..." - kicked: "&#FF4500Киллер пнул генератор!" + repair: + start: "&bВы начали чинить генератор..." + stop: "&eВы остановились." + complete: "&a✔ Генератор активирован!" + broadcast: "&aГенератор починен! &7(%d/%d)" + cannot: "&cГенератор не может быть починен!" + busy: "&cКто-то уже чинит этот генератор!" + actionbar: "&7Починка: %s" + skillcheck-title: "&eSkill Check! %s" + skillcheck-success: "&a✓ Отлично! +%d%%" + skillcheck-fail: "&c✗ Ошибка! -%d%%. Продолжай чинить." + kicked: + begin: "%prefix%&cДержите Shift для разрушения генератора..." + cooldown: "%prefix%&cКулдаун удара: %d сек" + already-broken: "%prefix%&eГенератор уже сломан! Нечего разрушать." + actionbar: "&c⚡ Разрушение: %d%%" + success: "%prefix%&cГенератор поврежден! -%d%% прогресса" + broadcast: "%prefix%&4⚡ Маньяк разрушил генератор! -%d%%" + complete: "&aГенератор починен!\n%e" + all-done: "%prefix%&aВсе генераторы починены! Открывайте ворота!" + notification: "&4[DBD] ⚡ Выживший допустил ошибку! Компас укажет на генератор!" + +# ── Маньяк ───────────────────────────────────── -# Киллер killer: - terror-near: "B0000Сердце колотится..." - terror-medium: "&#FF4500Опасно рядом..." - terror-far: "&#FFA500Киллер где-то рядом" - - chase-start: "&#FF0000⚠ Киллер преследует вас!" - chase-stop: "�FF00Вы ушли от погони." - - hit: "&#DC143CВы ранены! Ищите укрытие!" - down: "B0000Вы в нокдауне! Ползите!" - pickup: "󃕐Киллер поднял вас..." - hook: "B0082Вас повесили на крюк!" + carried-escape: "&6Попытка вырваться: %d%%" + +# ── Выжившие ─────────────────────────────────── -# Выжившие survivor: - heal-self-start: "�FA9AЛечите себя..." - heal-self-complete: "�FF00Вылечены!" - heal-other-start: "�FA9AЛечите союзника..." - heal-other-complete: "�FF00Союзник вылечен!" - heal-interrupt: "&#FFD700Лечение прервано!" - - sprint-exhausted: "&#FF8C00Вы устали... (Истощение)" - sprint-ready: "�FF00Можно бежать!" - - vault-fast: "�CED1Быстрый прыжок!" - vault-slow: "WCEEBМедленный прыжок..." - - rescue-start: "�CED1Спасаете с крюка..." - rescue-complete: "�FF00Союзник спасён!" + heal: + limit: "%prefix%&cЛимит лечений исчерпан!" + too-far: "%prefix%&cСлишком далеко!" + self-start: "&aЛечение себя: %s" + other-start: "&7Лечение союзника: %s" + self-done: "&aВы полностью вылечились!" + other-done: "&eВы встали на ноги!" + complete: "%prefix%&aСоюзник вылечен!" + interrupted: "%prefix%&eЛечение прервано!" + medkit: + need-shift: "%prefix%&eЗажмите Shift и стойте на месте для лечения." + not-injured: "%prefix%&cВы не ранены!" + empty: "%prefix%&cМедкит пуст!" + start: "%prefix%&aСамолечение... держите Shift и не двигайтесь." + actionbar: "&a⚕ %s %d%%" + done: "%prefix%&a✔ Вы вылечились!" + cancelled: "%prefix%&eЛечение прервано." + depleted: "&cYour medkit has been depleted!" + +# ── Крюки ────────────────────────────────────── -# Крюки hook: - stage1: "&#FFFF00Крюк: Фаза 1 — Не двигайтесь" - stage2: "&#FFA500Крюк: Фаза 2 — БОРИТЕСЬ!" - struggle-success: "�FF00Успех! Держитесь!" - struggle-fail: "&#FF0000Провал! Этап ускорен!" - sacrifice: "B0000☠ Вы жертвованы..." - -# Предметы -item: - found: "&#FFD700Найдено: %s" - - medkit-use: "�FA9AИспользуете аптечку..." - medkit-depleted: "&#FF6347Аптечка закончилась" - - flashlight-blind: "&#FFFF00Ослепили киллера! Уходите!" - flashlight-depleted: "&#FF6347Батарейка села" - - key-hatch: "�CED1Ключ открывает люк!" - map-reveal: "WCEEBОбъекты показаны на карте" - - chest-opening: "&#FFD700Открываете сундук..." - chest-found: "�FF00Нашли: %s" + sabotage: + start: "%prefix%&eДержите Shift для саботажа крюка..." + need-cutter: "%prefix%&cДля саботажа крюка нужны ножницы!" + actionbar: "&e⚙ Саботаж крюка: %d%%" + done: "%prefix%&aКрюк сломан!" + broadcast: "%prefix%&e⚙ Крюк был сломан выжившим!" + recovery: "%prefix%&7🔧 Крюк восстановлен." + rescue: + start: "%prefix%&eСпасаем... держите Shift!" + too-far: "%prefix%&cСлишком далеко! Спасение прервано." + victim-saved: "%prefix%&aВас спасли! Бегите!" + rescuer-actionbar: "%prefix%&aСоюзник спасён!" + rescuer-done: "%prefix%&aСпасение завершено!" + struggle: + hold-shift: "%prefix%&eДержите Shift чтобы вырваться с крюка..." + subtitle: "&eПрисядь когда будешь в зоне!" + actionbar: "&6Попытка вырваться: %d%%" + success: "%prefix%&aВы вырвались с крюка!" + fail: "%prefix%&cПопытка не удалась! (%d/%d)" + sacrifice: "%prefix%&4Сущность поглотила вас!" + sacrifice: + message: "%prefix%&4Вы были принесены в жертву. Наблюдайте за матчем." + title: "☠" + subtitle: "&cYou have been sacrificed" + broadcast: "%s has been sacrificed to the Entity" -# Эндгейм -endgame: - gates-powered: "�FF00Ворота запитаны! Бегите!" - gates-opening: "&#FFD700Открываете ворота..." - - hatch-open: "B0082Люк открыт! Сбегите!" - hatch-escape: "�FF00✓ Вы сбежали через люк!" - - escape: "�FF00✓ ПОБЕГ!" +# ── Сундуки ──────────────────────────────────── -# Ошибки -error: - no-permission: "&#FF0000У вас нет прав!" - not-in-game: "&#FF6347Вы не в игре!" - already-in-game: "&#FF6347Вы уже в игре!" - game-full: "&#FF6347Игра заполнена!" - no-game: "&#FF6347Нет активной игры!" - not-survivor: "&#FF6347Только для выживших!" - not-killer: "&#FF6347Только для киллера!" +chest: + empty: "&cThis chest is empty!" + opening: "&eOpening chest..." + cancelled: "&cOpening cancelled!" + too-far: "%prefix%&cСлишком далеко!" + actionbar: "&6Открытие: %s %d%%" + found: "%prefix%&aВы нашли: %s" + door-open: "&aОткрываем ворота... (зажмите ПКМ)" + door-opened: "&aВорота открыты!" + door-nearby: "&cНет ворот поблизости!" + door-no-power: "&cВорота обесточены!" + door-already: "&aВорота уже открыты!" + door-interrupted: "&7Открытие прервано!" + door-actionbar: "&aОткрываем ворота... (зажмите ПКМ)" + +# ── Люк ──────────────────────────────────────── + +hatch: + too-far: "&cПодойдите ближе к люку!" + hidden: "&7Люк скрыт..." + closed: "&cЛюк закрыт!" + open-notify: "&e&lЛюк открыт!" + escape-title: "&a&lПОБЕГ!" + escape-subtitle: "&7Вы сбежали через люк!" + reward: "&a+5000 Очков — Побег через люк!" + +# ── Игрок вышел ──────────────────────────────── + +quit: + killer-disconnect: "%prefix%&cКиллер покинул игру. Игра завершена." + survivor-left: "%prefix%&eВыживший покинул игру. Осталось живых: %d" + +# ── Мини-игры ────────────────────────────────── + +minigames: + generator-roulette: + title: "⚡ Skill Check!" + success-label: "✓ Нажми!" + text-skillcheck: + subtitle: "&eНажми ПКМ или отпусти шифт в зоне!" + +# ── Использование команд ─────────────────────── + +usage: + dbd-header: "&6=== DBD Команды ===" + dbd-join: "&7/dbd join [name] — присоединиться к игре/лобби" + dbd-leave: "&7/dbd leave — покинуть игру" + dbd-lobby: "&7/dbd lobby — телепорт в лобби" + dbd-create: "&7/dbd create — создать лобби" + dbd-list: "&7/dbd list — список лобби" + dbd-delete: "&7/dbd delete — удалить лобби" + dbd-admin: "&e/dbd admin — управление игроками в матче" + dbd-forcestart: "&e/dbd forcestart — форс старт" + dbd-stop: "&e/dbd stop — остановить игру" + dbd-reload: "&e/dbd reload — перезагрузить конфиги" + dbd-setlobby: "&e/dbd setlobby — установить точку лобби" + dbd-save: "&e/dbd save — сохранить карту" + dbd-load: "&e/dbd load — загрузить карту" + admin-header: "&6=== /dbd admin ===" + admin-heal: "&6/dbd admin heal [игрок]" + admin-down: "&6/dbd admin down [игрок]" + admin-unhook: "&6/dbd admin unhook [игрок]" + admin-state: "&6/dbd admin state <игрок> <статус>" + admin-hooks: "&6/dbd admin hooks <игрок> <0-3>" + admin-kill: "&6/dbd admin kill [игрок]" + admin-jump: "&6/dbd admin jump [on/off] — вкл/выкл прыжки" + setup-header: "&6=== DBD Setup ===" + setup-wand: "&6/dbdsetup wand — Получить ванд (ЛКМ/ПКМ для ворот)" + setup-hook: "&6/dbdsetup set hook — Установить крюк (ломай блок)" + setup-chest: "&6/dbdsetup set chest — Установить сундук" + setup-generator: "&6/dbdsetup set generator — Установить генератор" + 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-spawns: "&6/dbdsetup clear-spawns — Очистить все спавны" + setup-lobby: "&6/dbdsetup lobby — Установить точку лобби" + admin-list-header: "&6=== DBD Admin ===" + admin-list-list: "&6/dbdadmin list — Список всех объектов" + admin-list-type: "&6/dbdadmin list <тип> — Список по типу" + admin-world: "&6/dbdadmin world — Телепорт в мир"