refactor: generator minigame system (gui + text), fix color codes, holograms, messages, & config

This commit is contained in:
loki5512344 2026-06-14 16:04:25 +02:00
parent 957ab4a5a2
commit 7c7562a8d4
48 changed files with 1628 additions and 733 deletions

View file

@ -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) {

View file

@ -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;
}

View file

@ -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());
}

View file

@ -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<Location> 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 <game|название мира>"));
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 <game|мир> — Телепорт в мир"));
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

View file

@ -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 <игрок> <HEALTHY|INJURED|DOWNED|HOOKED|...>", 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<String> tabComplete(String[] args) {

View file

@ -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 <name> [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 <name> [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 <name> — удалить лобби
// -------------------------------------------------------------------------
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<String> 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<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
if (args.length == 1) {
List<String> subs = new ArrayList<>(List.of("join", "leave", "lobby"));
List<String> 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();
}
}

View file

@ -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")) {

View file

@ -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", "&#00FF00");
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; }
}

View file

@ -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<Location> getSurvivorSpawns() {
return loadLocationList("survivor-spawns");
List<Location> 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<Location> 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());

View file

@ -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", "&#00FF00Игра началась! Бегите от Киллера!");
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", "&#00CED1Вы начали чинить генератор...");
setDefault("generator.repair.stop", "&#87CEEBВы остановились.");
setDefault("generator.repair.complete", "&#00FF00✓ Генератор активирован!");
setDefault("generator.skillcheck.success", "&#00FF00Отлично!");
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", "&#8B0000Сердце колотится...");
setDefault("killer.chase.start", "&#FF0000⚠ Киллер преследует вас!");
setDefault("killer.chase.stop", "&#00FF00Вы ушли от погони.");
setDefault("killer.hit", "&#DC143CВы ранены!");
setDefault("killer.down", "&#8B0000Вы в нокдауне!");
setDefault("killer.pickup", "&#800080Киллер поднял вас...");
setDefault("killer.hook", "&#4B0082Вас повесили на крюк!");
// Survivor messages
setDefault("survivor.heal.start", "&#00FA9AВы лечите...");
setDefault("survivor.heal.complete", "&#00FF00Вылечено!");
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", "&#00FF00Успех!");
setDefault("hook.struggle.fail", "&#FF0000Провал!");
setDefault("hook.rescue.start", "&#00CED1Спасение...");
setDefault("hook.rescue.complete", "&#00FF00Спасены!");
setDefault("hook.sacrifice", "&#8B0000☠ Вы жертвованы...");
// Item messages
setDefault("item.found", "&#FFD700Найдено: %s");
setDefault("item.medkit.use", "&#00FA9AИспользуете аптечку...");
// Endgame
setDefault("endgame.gates.powered", "&#00FF00Ворота запитаны!");
setDefault("endgame.gates.opening", "&#FFD700Открываете ворота...");
setDefault("endgame.hatch.open", "&#4B0082Люк открыт!");
setDefault("endgame.escape", "&#00FF00✓ ПОБЕГ!");
// 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;
}
}

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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<UUID, OpeningProgress> 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

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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"));
}
}
}

View file

@ -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) {

View file

@ -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<UUID, BukkitTask> sabotageCharging = new HashMap<>();
/** hookUUID → recovery task */
private final Map<UUID, BukkitTask> recoveryTasks = new HashMap<>();
/** Called when sabotage completes successfully */
private java.util.function.Consumer<UUID> 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<UUID> 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);

View file

@ -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)
);
}

View file

@ -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) {

View file

@ -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(
"<gray>Лечит одно состояние здоровья.",
"<yellow>ПКМ <gray>— начать самолечение"
),
"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("<gray>Лечит одно состояние здоровья."),
TextUtil.color("<yellow>ПКМ <gray>— начать самолечение"),
Component.empty(),
TextUtil.color("<#FFD700>Заряды: <#00FF00>████████ <white>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()); }

View file

@ -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

View file

@ -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"));
}
}

View file

@ -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<UUID, BukkitTask> 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;

View file

@ -61,6 +61,9 @@ public class PlayerInteractListener implements Listener {
public void setOnHealSneak(BiConsumer<Player, Player> h) { this.onHealSneak = h; }
private BiConsumer<Player, Boolean> onCarriedShift;
public void setOnCarriedShift(BiConsumer<Player, Boolean> h) { this.onCarriedShift = h; }
/** Called when a survivor sneaks while a text skill check is in progress */
private BiConsumer<Player, Boolean> onGeneratorSkillCheckSneak;
public void setOnGeneratorSkillCheckSneak(BiConsumer<Player, Boolean> 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);
}

View file

@ -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();
}
}

View file

@ -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() {

View file

@ -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;
}

View file

@ -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<Player, Boolean> callback);
boolean isActive(UUID playerId);
boolean handleRightClick(Player player);
boolean handleSneak(Player player);
void cleanup();
}

View file

@ -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<UUID, RouletteSession> 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<Player, Boolean> 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<Player, Boolean> 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<Player, Boolean> callback) {
this.player = player;
this.callback = callback;
}
}
}

View file

@ -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<UUID, SkillCheckSession> 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<Player, Boolean> 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("<gray>[");
for (int i = 0; i < width; i++) {
if (i == ptr) {
sb.append("<white>◆");
} else if (i >= zStart && i <= zEnd) {
sb.append("<green>█");
} else {
sb.append("<dark_gray>▒");
}
}
sb.append("<gray>]");
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<Player, Boolean> callback;
final double zoneStart;
final double zoneEnd;
double pointer = 0.0;
int ticks = 0;
BukkitTask task;
SkillCheckSession(Player player, BiConsumer<Player, Boolean> callback,
double zoneStart, double zoneEnd) {
this.player = player;
this.callback = callback;
this.zoneStart = zoneStart;
this.zoneEnd = zoneEnd;
}
}
}

View file

@ -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);

View file

@ -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<UUID, RepairSession> 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(
"<yellow>Skill Check! " + visual
));
}
}
private void showProgressBar(Player player, RepairSession session) {
double progress = session.generator.getProgress();
String bar = renderProgressBar(progress);
player.sendActionBar(MiniMessage.miniMessage().deserialize(
"<gray>Починка: " + 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(
"<green>Генератор починен! <gray>(" + 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("<green>✓ Отлично! +" + (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("<red>✗ Ошибка! -" + (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(
"<dark_red>[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();
}

View file

@ -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;
}
}
}

View file

@ -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<BasePlayer> 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++;
}
}

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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;

View file

@ -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);

View file

@ -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

View file

@ -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<Location, Location> 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<Location> killerGameSpawns = killerSpawns.stream()
.map(fixWorld).peek(s -> s.getWorld().loadChunk(s.getBlockX() >> 4, s.getBlockZ() >> 4))
.collect(java.util.stream.Collectors.toList());
List<Location> 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++;
}
}

View file

@ -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
? "<green>Вы полностью вылечились!"
: "<yellow>Вы встали на ноги!";
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 ? "<gray>Лечение себя: " + bar
: "<gray>Лечение союзника: " + 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();

View file

@ -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));

View file

@ -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;
}

View file

@ -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<Character, String> 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;
}
}

View file

@ -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 <игрок> <HEALTHY|INJURED|DOWNED|HOOKED|...>"
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: "&#00CED1 #%d %s [%.1f, %.1f, %.1f]"
world-usage: "%setup-prefix%&cИспользование: /dbdadmin world <game|название мира>"
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: "&#00FF00Игра началась! Бегите от Киллера!"
end-victory-escape: "&#FFD700Выжившие сбежали! Победа!"
end-victory-killer: "&#DC143CКиллер победил!"
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Роли назначены!"
# Ожидание
waiting: "&#FFFF00Ожидание игроков... (%s/%s)"
countdown: "&#FFA500До начала: %s секунд"
countdown-short: "&#FF0000%s..."
# ── Лобби ──────────────────────────────────────
# Роли
role-killer: "&#DC143CВы Киллер! Поймайте всех выживших."
role-survivor: "&#00CED1Вы Выживший! Почините генераторы и сбегите."
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: "&#00CED1Вы начали чинить генератор..."
repair-stop: "&#87CEEBВы остановились."
repair-complete: "&#00FF00✓ Генератор активирован! (%s/%s)"
repair-progress: "&#FFD700Прогресс: %s%%"
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] ⚡ Выживший допустил ошибку! Компас укажет на генератор!"
skillcheck-success: "&#00FF00Отлично! +%s%%"
skillcheck-super: "&#FFD700Великолепно! +%s%%"
skillcheck-fail: "&#FF0000✗ Провал! -%s%%"
# ── Маньяк ─────────────────────────────────────
explosion: "&#FF4500⚠ Взрыв генератора!"
regression: "&#FF6347Генератор откатывается..."
kicked: "&#FF4500Киллер пнул генератор!"
# Киллер
killer:
terror-near: "&#8B0000Сердце колотится..."
terror-medium: "&#FF4500Опасно рядом..."
terror-far: "&#FFA500Киллер где-то рядом"
carried-escape: "&6Попытка вырваться: %d%%"
chase-start: "&#FF0000⚠ Киллер преследует вас!"
chase-stop: "&#00FF00Вы ушли от погони."
# ── Выжившие ───────────────────────────────────
hit: "&#DC143CВы ранены! Ищите укрытие!"
down: "&#8B0000Вы в нокдауне! Ползите!"
pickup: "&#800080Киллер поднял вас..."
hook: "&#4B0082Вас повесили на крюк!"
# Выжившие
survivor:
heal-self-start: "&#00FA9AЛечите себя..."
heal-self-complete: "&#00FF00Вылечены!"
heal-other-start: "&#00FA9AЛечите союзника..."
heal-other-complete: "&#00FF00Союзник вылечен!"
heal-interrupt: "&#FFD700Лечение прервано!"
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!"
sprint-exhausted: "&#FF8C00Вы устали... (Истощение)"
sprint-ready: "&#00FF00Можно бежать!"
# ── Крюки ──────────────────────────────────────
vault-fast: "&#00CED1Быстрый прыжок!"
vault-slow: "&#87CEEBМедленный прыжок..."
rescue-start: "&#00CED1Спасаете с крюка..."
rescue-complete: "&#00FF00Союзник спасён!"
# Крюки
hook:
stage1: "&#FFFF00Крюк: Фаза 1 — Не двигайтесь"
stage2: "&#FFA500Крюк: Фаза 2 — БОРИТЕСЬ!"
struggle-success: "&#00FF00Успех! Держитесь!"
struggle-fail: "&#FF0000Провал! Этап ускорен!"
sacrifice: "&#8B0000☠ Вы жертвованы..."
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"
# Предметы
item:
found: "&#FFD700Найдено: %s"
# ── Сундуки ────────────────────────────────────
medkit-use: "&#00FA9AИспользуете аптечку..."
medkit-depleted: "&#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Открываем ворота... (зажмите ПКМ)"
flashlight-blind: "&#FFFF00Ослепили киллера! Уходите!"
flashlight-depleted: "&#FF6347Батарейка села"
# ── Люк ────────────────────────────────────────
key-hatch: "&#00CED1Ключ открывает люк!"
map-reveal: "&#87CEEBОбъекты показаны на карте"
hatch:
too-far: "&cПодойдите ближе к люку!"
hidden: "&7Люк скрыт..."
closed: "&cЛюк закрыт!"
open-notify: "&e&lЛюк открыт!"
escape-title: "&a&lПОБЕГ!"
escape-subtitle: "&7Вы сбежали через люк!"
reward: "&a+5000 Очков — Побег через люк!"
chest-opening: "&#FFD700Открываете сундук..."
chest-found: "&#00FF00Нашли: %s"
# ── Игрок вышел ────────────────────────────────
# Эндгейм
endgame:
gates-powered: "&#00FF00Ворота запитаны! Бегите!"
gates-opening: "&#FFD700Открываете ворота..."
quit:
killer-disconnect: "%prefix%&cКиллер покинул игру. Игра завершена."
survivor-left: "%prefix%&eВыживший покинул игру. Осталось живых: %d"
hatch-open: "&#4B0082Люк открыт! Сбегите!"
hatch-escape: "&#00FF00✓ Вы сбежали через люк!"
# ── Мини-игры ──────────────────────────────────
escape: "&#00FF00✓ ПОБЕГ!"
minigames:
generator-roulette:
title: "⚡ Skill Check!"
success-label: "✓ Нажми!"
text-skillcheck:
subtitle: "&eНажми ПКМ или отпусти шифт в зоне!"
# Ошибки
error:
no-permission: "&#FF0000У вас нет прав!"
not-in-game: "&#FF6347Вы не в игре!"
already-in-game: "&#FF6347Вы уже в игре!"
game-full: "&#FF6347Игра заполнена!"
no-game: "&#FF6347Нет активной игры!"
not-survivor: "&#FF6347Только для выживших!"
not-killer: "&#FF6347Только для киллера!"
# ── Использование команд ───────────────────────
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 <game|мир> — Телепорт в мир"