From 1adb8c84fd5abb95ac804b936125f6f41fdfea53 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Wed, 3 Jun 2026 19:34:10 +0200 Subject: [PATCH] fix: colors, /dbd join flow, world creation, role assign, action bar lobby, /dbdadmin world, tab complete --- COMMANDS_GUIDE.md | 524 ++++++++++++++++++ build.gradle.kts | 43 +- .../dev/loki/command/DBDAdminCommand.java | 87 ++- .../java/dev/loki/command/DBDCommand.java | 2 +- .../java/dev/loki/command/SetupCommand.java | 72 +-- src/main/java/dev/loki/core/GameManager.java | 10 + src/main/java/dev/loki/core/GameSession.java | 12 + .../listener/GeneratorSelectListener.java | 13 +- .../loki/listener/PlayerInteractListener.java | 4 +- src/main/java/dev/loki/mdbd.java | 19 +- .../dev/loki/objective/GeneratorRepair.java | 7 +- .../java/dev/loki/phase/GameplayPhase.java | 25 +- src/main/java/dev/loki/phase/LobbyPhase.java | 59 +- src/main/java/dev/loki/phase/Phase.java | 2 + .../java/dev/loki/phase/RoleAssignPhase.java | 4 +- src/main/java/dev/loki/role/RoleAssigner.java | 51 +- src/main/java/dev/loki/util/TextUtil.java | 29 + .../java/dev/loki/world/LobbyManager.java | 11 +- src/main/resources/plugin.yml | 12 +- 19 files changed, 874 insertions(+), 112 deletions(-) create mode 100644 COMMANDS_GUIDE.md create mode 100644 src/main/java/dev/loki/util/TextUtil.java diff --git a/COMMANDS_GUIDE.md b/COMMANDS_GUIDE.md new file mode 100644 index 0000000..c857b8a --- /dev/null +++ b/COMMANDS_GUIDE.md @@ -0,0 +1,524 @@ +# Dead by Daylight Plugin — Руководство команд + +**Версия:** 1.1 +**Обновлено:** Июнь 2026 +**Платформа:** Minecraft 1.21.1 (Paper) + +--- + +## Содержание + +1. [Фазы игры](#фазы-игры) +2. [Команды игроков — /dbd](#команды-игроков--dbd) +3. [Команды администратора — /dbdadmin](#команды-администратора--dbdadmin) +4. [Команды настройки карты — /dbdsetup](#команды-настройки-карты--dbdsetup) +5. [Права доступа](#права-доступа) +6. [Порядок настройки сервера](#порядок-настройки-сервера) +7. [Таблица всех команд](#таблица-всех-команд) + +--- + +## Фазы игры + +``` +LOBBY + Игроки делают /dbd join → телепорт в мир lobby + Action bar показывает: ▶ Старт через: 45с ████████░░ (3/5) + Когда набралось минимум игроков — идёт отсчёт 60 сек + При нехватке игроков — таймер сбрасывается + ↓ +ROLE ASSIGN (5 сек) + Случайно выбирается 1 киллер, остальные — выжившие + Показывается тайтл с ролью + Все телепортируются на свои спавны в мире game + ↓ +GAMEPLAY + Выжившие чинят генераторы (нужно 5 из 7) + Киллер охотится + При починке всех нужных генераторов — открываются ворота + ↓ +ENDGAME (2 минуты) + Выжившие открывают ворота и убегают + Или используют люк (если остался 1 выживший) + Коллапс через 120 сек убивает всех оставшихся +``` + +--- + +## Команды игроков — /dbd + +Все команды ниже доступны любому игроку (`mdbd.player`, по умолчанию `true`). + +--- + +### /dbd join + +Присоединиться к игровой очереди. + +``` +/dbd join +``` + +- Добавляет игрока в текущую сессию как выжившего (роль назначится позже) +- Телепортирует в мир `lobby` +- В action bar начинает отображаться счётчик и таймер +- Если игра не создана — сообщение об ошибке + +``` +→ [DBD] Вы присоединились к игре! +→ [DBD] PlayerName присоединился к очереди! (1/5) +``` + +--- + +### /dbd leave + +Покинуть сессию. + +``` +/dbd leave +``` + +- В лобби: просто убирает из очереди, таймер может сброситься +- В игре: выживший считается мёртвым, киллер завершает игру +- Игрок телепортируется обратно в `lobby` + +``` +→ [DBD] Вы покинули игру +``` + +--- + +### /dbd lobby + +Телепортироваться в лобби-мир. + +``` +/dbd lobby +``` + +- Работает в любой момент, не убирает из сессии +- Телепортирует на точку спавна лобби (из `locations.yml`) или на спавн мира `lobby` + +``` +→ [DBD] Вы телепортированы в лобби! +``` + +--- + +## Команды администратора — /dbd (admin) + +Требуют право `mdbd.admin` (по умолчанию только OP). + +--- + +### /dbd create + +Создать новую игровую сессию. + +``` +/dbd create +``` + +- Создаёт сессию и открывает лобби +- Нельзя создать вторую сессию пока первая активна + +``` +→ [DBD] Игра создана! +→ [DBD] Игра уже существует (если уже есть) +``` + +--- + +### /dbd forcestart + +Принудительно запустить игру, пропустив таймер. + +``` +/dbd forcestart +``` + +- Работает только если сессия в фазе LOBBY +- Игроков в очереди может быть меньше минимума + +``` +→ [DBD] Игра форсирована! +``` + +--- + +### /dbd stop + +Немедленно завершить текущую сессию. + +``` +/dbd stop +``` + +- Все игроки телепортируются в `lobby` +- Сессия уничтожается, можно создавать новую + +``` +→ [DBD] Игра остановлена +``` + +--- + +### /dbd reload + +Перезагрузить все конфиги без перезапуска сервера. + +``` +/dbd reload +``` + +Перезагружает: `config.yml`, `game.yml`, `messages.yml`, `skillcheck.yml`, `locations.yml`, `generators.yml` + +``` +→ [DBD] Конфиги перезагружены! +``` + +> Если игра в процессе — сначала `/dbd stop`, иначе новые настройки применятся только к следующей сессии. + +--- + +### /dbd setlobby + +Установить текущую позицию игрока как точку спавна лобби. + +``` +/dbd setlobby +``` + +- Сохраняется в `locations.yml` +- Эквивалентно `/dbdsetup lobby` + +``` +→ [DBD] Лобби установлено! +``` + +--- + +## Команды администратора — /dbdadmin + +Требуют право `mdbd.admin`. Все команды только для игроков (не консоли). + +--- + +### /dbdadmin create generator + +Режим расстановки генераторов. + +``` +/dbdadmin create generator +``` + +- Включает режим: в action bar оранжевая подсказка +- **Сломай блок** — координаты сохранятся, блок не разрушится +- Повторная команда — отключает режим +- Режим остаётся активным для нескольких генераторов подряд + +``` +→ Режим выбора генератора включен! +→ Сломайте блок, чтобы установить генератор +(сломал блок) +→ Генератор установлен! Всего: 1 +``` + +**Tab complete:** `generator` + +--- + +### /dbdadmin list + +Список всех установленных генераторов. + +``` +/dbdadmin list +``` + +``` +=== Генераторы (3) === +[0] game:100.00,64.00,200.00 +[1] game:150.00,64.00,150.00 +[2] game:200.00,64.00,100.00 +``` + +--- + +### /dbdadmin remove \ + +Удалить генератор по ID из списка. + +``` +/dbdadmin remove 0 +``` + +**Tab complete:** подсказывает существующие ID (0, 1, 2...) + +``` +→ Генератор удален: game:100.00,64.00,200.00 +→ Генератор с ID 5 не найден (если нет такого) +``` + +--- + +### /dbdadmin clear + +Удалить все генераторы. + +``` +/dbdadmin clear +``` + +> Необратимо. + +``` +→ Все генераторы удалены +``` + +--- + +### /dbdadmin world \<мир\> + +Телепортироваться в указанный мир для настройки карты. + +``` +/dbdadmin world lobby +/dbdadmin world game +/dbdadmin world <любое_название_мира> +``` + +- `lobby` — телепорт в мир-лобби (void, платформа 7x7) +- `game` — телепорт в игровой мир +- Любое другое название — телепорт в мир с таким именем (если загружен) + +``` +→ Телепортирован в мир: lobby +→ Мир не найден: abc (если мир не существует) +``` + +**Tab complete:** `lobby`, `game` + +--- + +## Команды настройки карты — /dbdsetup + +Требуют право `mdbd.admin`. + +--- + +### /dbdsetup wand + +Выдать палочку для выделения регионов. + +``` +/dbdsetup wand +``` + +- Выдаёт палку (STICK) с подписью +- ЛКМ = первая точка (pos1) +- ПКМ = вторая точка (pos2) + +--- + +### /dbdsetup pos1 / pos2 + +Установить точку выделения вручную (по позиции игрока). + +``` +/dbdsetup pos1 +/dbdsetup pos2 +``` + +``` +→ Установлена точка 1: [game, 100, 64, 200] +``` + +--- + +### /dbdsetup save \<название\> + +Сохранить блоки между pos1 и pos2 как шаблон карты. + +``` +/dbdsetup save main_map +``` + +**Tab complete для load/save:** показывает существующие шаблоны. + +``` +→ Шаблон 'main_map' сохранен! +``` + +--- + +### /dbdsetup load \<название\> + +Загрузить шаблон на текущую позицию игрока. + +``` +/dbdsetup load main_map +``` + +**Tab complete:** список сохранённых шаблонов. + +``` +→ Шаблон 'main_map' загружен! +→ Шаблон не найден! (если нет такого) +``` + +--- + +### /dbdsetup list + +Показать список сохранённых шаблонов карт. + +``` +/dbdsetup list +``` + +--- + +### /dbdsetup set \<тип\> + +Сохранить текущую позицию игрока как объект указанного типа. + +``` +/dbdsetup set generator +/dbdsetup set hook +/dbdsetup set pallet +/dbdsetup set chest +/dbdsetup set totem +/dbdsetup set gate +/dbdsetup set lobby +``` + +| Тип | Описание | Рекомендуемое кол-во | +|-----|----------|---------------------| +| `generator` | Генератор | 7 | +| `hook` | Крюк | 7–10 | +| `pallet` | Паллет | 15–20 | +| `gate` | Ворота выхода | 2 | +| `lobby` | Спавн в лобби | 1 | + +> `chest`, `totem` — сохраняются в `locations.yml`, но пока не подключены к игровой логике. + +**Tab complete:** все типы выше. + +``` +→ Установлен: hook на [game, 150, 65, 200] +``` + +--- + +### /dbdsetup build + +Построить карту по шаблону и `locations.yml`. + +``` +/dbdsetup build +``` + +``` +→ Строительство карты... +→ Карта построена! +``` + +--- + +### /dbdsetup clear + +Очистить карту. + +``` +/dbdsetup clear +``` + +``` +→ Очистка карты... +→ Карта очищена! +``` + +--- + +### /dbdsetup lobby + +Установить спавн лобби на текущую позицию. + +``` +/dbdsetup lobby +``` + +Аналог `/dbd setlobby`. Сохраняется в `locations.yml`. + +--- + +## Права доступа + +| Право | Кому | Что даёт | +|-------|------|----------| +| `mdbd.player` | Все (`default: true`) | `/dbd join`, `leave`, `lobby` | +| `mdbd.admin` | OP (`default: op`) | Все остальные команды | + +--- + +## Порядок настройки сервера + +### Первый запуск + +1. Запустить сервер — плагин создаст миры `lobby` и `game` автоматически +2. `/dbdadmin world lobby` — войти в лобби-мир и осмотреться (платформа 7x7) +3. `/dbd setlobby` — встать на нужную точку и сохранить спавн лобби + +### Настройка игровой карты + +4. `/dbdadmin world game` — перейти в игровой мир +5. Построить или загрузить карту +6. Расставить объекты: + - `/dbdadmin create generator` → ломать блоки для 7 генераторов + - `/dbdsetup set hook` → встать у каждого крюка (7–10 штук) + - `/dbdsetup set pallet` → паллеты (15–20 штук) + - `/dbdsetup set gate` → 2 ворот выхода +7. Установить спавны ролей в `locations.yml` вручную или через `/dbdsetup set`: + - `killer-spawn` — 1 точка + - `survivor-spawns` — 4 точки + +### Тест + +8. `/dbd create` — создать сессию +9. `/dbd join` (5 игроков или меньше с форс-стартом) +10. `/dbd forcestart` — запустить не дожидаясь таймера + +--- + +## Таблица всех команд + +| Команда | Право | Описание | +|---------|-------|----------| +| `/dbd join` | player | Войти в очередь | +| `/dbd leave` | player | Покинуть сессию | +| `/dbd lobby` | player | Телепорт в лобби | +| `/dbd create` | admin | Создать сессию | +| `/dbd forcestart` | admin | Принудительный старт | +| `/dbd stop` | admin | Остановить игру | +| `/dbd reload` | admin | Перезагрузить конфиги | +| `/dbd setlobby` | admin | Сохранить спавн лобби | +| `/dbdadmin create generator` | admin | Режим расстановки генераторов | +| `/dbdadmin list` | admin | Список генераторов | +| `/dbdadmin remove ` | admin | Удалить генератор | +| `/dbdadmin clear` | admin | Очистить все генераторы | +| `/dbdadmin world ` | admin | Телепорт в мир | +| `/dbdsetup wand` | admin | Палочка выделения | +| `/dbdsetup pos1` / `pos2` | admin | Точки выделения | +| `/dbdsetup save ` | admin | Сохранить шаблон карты | +| `/dbdsetup load ` | admin | Загрузить шаблон | +| `/dbdsetup list` | admin | Список шаблонов | +| `/dbdsetup set <тип>` | admin | Сохранить позицию объекта | +| `/dbdsetup build` | admin | Построить карту | +| `/dbdsetup clear` | admin | Очистить карту | +| `/dbdsetup lobby` | admin | Сохранить спавн лобби | + +--- + +*Dead by Daylight Minecraft Plugin — актуально для версии 1.1+* diff --git a/build.gradle.kts b/build.gradle.kts index bd264ce..463a918 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -21,26 +21,30 @@ java { toolchain.languageVersion = JavaLanguageVersion.of(21) } +tasks.withType { + options.encoding = "UTF-8" +} + val obfuscateTask = tasks.register("obfuscate") { description = "Obfuscate the plugin jar using ProGuard" mainClass.set("proguard.ProGuard") classpath = proguardConfig - val inJar = tasks.named("jar").get().archiveFile.get().asFile.path - val outJar = layout.buildDirectory.file("libs/${project.name}-${version}-obf.jar").get().asFile.path + val jarFile = tasks.named("jar").get().archiveFile.get().asFile + // Write to separate dir — avoids conflicts with jar task output on Windows + val obfJar = layout.buildDirectory.file("obfuscated/${project.name}-${version}.jar").get().asFile args( - "-injars", inJar, - "-outjars", outJar, + "-injars", jarFile.path, + "-outjars", obfJar.path, "-dontwarn", "-ignorewarnings", "-keep", "class dev.loki.mdbd", "-keep", "class dev.loki.mdbdBootstrap", "-keep", "class dev.loki.mdbdLoader", - "-keep", "class dev.loki.command.**", - "-keep", "class dev.loki.listener.**", - "-keepclassmembers", "class * { @org.bukkit.event.EventHandler *; }", + "-keep", "class dev.loki.command.** { *; }", + "-keep", "class dev.loki.listener.** { *; }", "-keepnames", "class dev.loki.**", "-dontoptimize", @@ -49,9 +53,32 @@ val obfuscateTask = tasks.register("obfuscate") { "-keepattributes", "Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LineNumberTable,EnclosingMethod" ) + outputs.file(obfJar) dependsOn(tasks.named("jar")) } +// Save original jar as -orig, then replace with obfuscated +val jarName = "${project.name}-${version}" + +tasks.register("saveOriginalJar") { + val jarSrc = layout.buildDirectory.file("libs/$jarName.jar") + val jarDst = layout.buildDirectory.file("libs/$jarName-orig.jar") + dependsOn(tasks.named("jar")) + inputs.file(jarSrc) + outputs.file(jarDst) + doLast { + jarSrc.get().asFile.copyTo(jarDst.get().asFile, overwrite = true) + } +} + +tasks.register("replaceWithObfuscated") { + dependsOn(obfuscateTask, "saveOriginalJar") + from(layout.buildDirectory.dir("obfuscated")) + include("$jarName.jar") + into(layout.buildDirectory.dir("libs")) + outputs.upToDateWhen { false } +} + tasks { runServer { minecraftVersion("1.21.1") @@ -66,6 +93,6 @@ tasks { } named("build") { - dependsOn(obfuscateTask) + dependsOn("replaceWithObfuscated") } } diff --git a/src/main/java/dev/loki/command/DBDAdminCommand.java b/src/main/java/dev/loki/command/DBDAdminCommand.java index b5ff9d3..f21e6b8 100644 --- a/src/main/java/dev/loki/command/DBDAdminCommand.java +++ b/src/main/java/dev/loki/command/DBDAdminCommand.java @@ -2,6 +2,7 @@ package dev.loki.command; import dev.loki.data.GeneratorDataManager; import dev.loki.mdbd; +import dev.loki.util.TextUtil; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.minimessage.MiniMessage; @@ -43,7 +44,7 @@ public class DBDAdminCommand implements TabExecutor { Player p = Bukkit.getPlayer(uuid); if (p != null && p.isOnline()) { p.sendActionBar(mm.deserialize( - "&#FFA500⚒ Режим установки: �FF00сломайте блок для генератора &#FFA500⚒")); + "<#FFA500>⚒ Режим установки: <#00FF00>сломайте блок для генератора <#FFA500>⚒")); } } }, 0L, 20L); @@ -72,6 +73,7 @@ public class DBDAdminCommand implements TabExecutor { case "list" -> handleList(player); case "remove" -> handleRemove(player, args); case "clear" -> handleClear(player); + case "world" -> handleWorld(player, args); default -> { sendUsage(sender); yield true; @@ -82,7 +84,7 @@ public class DBDAdminCommand implements TabExecutor { @SuppressWarnings("UnreachableBranchError") private boolean handleCreate(Player player, String[] args) { if (args.length < 2) { - player.sendMessage(Component.text("&#FF0000Использование: /dbdadmin create ")); + player.sendMessage(TextUtil.color("<#FF0000>Использование: /dbdadmin create ")); return true; } @@ -91,18 +93,18 @@ public class DBDAdminCommand implements TabExecutor { case "generator" -> { if (selectingGenerator.contains(player.getUniqueId())) { selectingGenerator.remove(player.getUniqueId()); - player.sendMessage(Component.text("&#FFA500Режим выбора генератора отключен")); + player.sendMessage(TextUtil.color("<#FFA500>Режим выбора генератора отключен")); } else { selectingGenerator.add(player.getUniqueId()); - player.sendMessage(Component.text("�FF00Режим выбора генератора включен!")); - player.sendMessage(Component.text("&#FFD700Сломайте блок, чтобы установить генератор")); - player.sendMessage(Component.text("&#FFD700(блок не сломается, а сохранятся его координаты)")); - player.sendMessage(Component.text("&#FFA500Введите /dbdadmin create generator снова, чтобы отключить режим")); + player.sendMessage(TextUtil.color("<#00FF00>Режим выбора генератора включен!")); + player.sendMessage(TextUtil.color("<#FFD700>Сломайте блок, чтобы установить генератор")); + player.sendMessage(TextUtil.color("<#FFD700>(блок не сломается, а сохранятся его координаты)")); + player.sendMessage(TextUtil.color("<#FFA500>Введите /dbdadmin create generator снова, чтобы отключить режим")); } return true; } default -> { - player.sendMessage(Component.text("&#FF0000Неизвестный тип: " + type)); + player.sendMessage(TextUtil.color("<#FF0000>Неизвестный тип: " + type)); return true; } } @@ -110,12 +112,12 @@ public class DBDAdminCommand implements TabExecutor { private boolean handleList(Player player) { var entries = generatorData.getGeneratorEntries(); - player.sendMessage(Component.text("&#FFD700=== Генераторы (" + entries.size() + ") ===")); + player.sendMessage(TextUtil.color("<#FFD700>=== Генераторы (" + entries.size() + ") ===")); if (entries.isEmpty()) { - player.sendMessage(Component.text("󅑀Нет установленных генераторов")); + player.sendMessage(TextUtil.color("<#808000>Нет установленных генераторов")); } else { for (int i = 0; i < entries.size(); i++) { - player.sendMessage(Component.text("�CED1[" + i + "] " + entries.get(i))); + player.sendMessage(TextUtil.color("<#00CED1>[" + i + "] " + entries.get(i))); } } return true; @@ -123,7 +125,7 @@ public class DBDAdminCommand implements TabExecutor { private boolean handleRemove(Player player, String[] args) { if (args.length < 2) { - player.sendMessage(Component.text("&#FF0000Использование: /dbdadmin remove ")); + player.sendMessage(TextUtil.color("<#FF0000>Использование: /dbdadmin remove ")); return true; } @@ -131,30 +133,65 @@ public class DBDAdminCommand implements TabExecutor { int id = Integer.parseInt(args[1]); var entries = generatorData.getGeneratorEntries(); if (id < 0 || id >= entries.size()) { - player.sendMessage(Component.text("&#FF0000Генератор с ID " + id + " не найден")); + player.sendMessage(TextUtil.color("<#FF0000>Генератор с ID " + id + " не найден")); return true; } String removed = entries.get(id); generatorData.removeGenerator(id); - player.sendMessage(Component.text("�FF00Генератор удален: " + removed)); + player.sendMessage(TextUtil.color("<#00FF00>Генератор удален: " + removed)); } catch (NumberFormatException e) { - player.sendMessage(Component.text("&#FF0000ID должен быть числом")); + player.sendMessage(TextUtil.color("<#FF0000>ID должен быть числом")); } return true; } private boolean handleClear(Player player) { generatorData.clearGenerators(); - player.sendMessage(Component.text("&#FFA500Все генераторы удалены")); + player.sendMessage(TextUtil.color("<#FFA500>Все генераторы удалены")); + return true; + } + + private boolean handleWorld(Player player, String[] args) { + if (args.length < 2) { + player.sendMessage(TextUtil.color("<#FF0000>Использование: /dbdadmin world >")); + return true; + } + + String worldArg = args[1].toLowerCase(); + org.bukkit.World target; + + if (worldArg.equals("lobby")) { + target = plugin.getLobbyManager().getLobbyWorld(); + if (target == null) { + player.sendMessage(TextUtil.color("<#FF0000>Мир lobby не загружен!")); + return true; + } + } else if (worldArg.equals("game")) { + target = plugin.getLobbyManager().getGameWorld(); + if (target == null) { + player.sendMessage(TextUtil.color("<#FF0000>Мир game не загружен!")); + return true; + } + } else { + target = org.bukkit.Bukkit.getWorld(args[1]); + if (target == null) { + player.sendMessage(TextUtil.color("<#FF0000>Мир не найден: " + args[1])); + return true; + } + } + + player.teleport(target.getSpawnLocation()); + player.sendMessage(TextUtil.color("<#00FF00>Телепортирован в мир: <#FFD700>" + target.getName())); return true; } private void sendUsage(CommandSender sender) { - sender.sendMessage(Component.text("&#FF6B00=== DBD Admin ===")); - sender.sendMessage(Component.text("&#FFD700/dbdadmin create generator — Режим установки генератора")); - sender.sendMessage(Component.text("&#FFD700/dbdadmin list — Список генераторов")); - sender.sendMessage(Component.text("&#FFD700/dbdadmin remove — Удалить генератор")); - sender.sendMessage(Component.text("&#FFD700/dbdadmin clear — Очистить все генераторы")); + sender.sendMessage(TextUtil.color("<#FF6B00>=== DBD Admin ===")); + sender.sendMessage(TextUtil.color("<#FFD700>/dbdadmin create generator — Режим установки генератора")); + sender.sendMessage(TextUtil.color("<#FFD700>/dbdadmin list — Список генераторов")); + sender.sendMessage(TextUtil.color("<#FFD700>/dbdadmin remove — Удалить генератор")); + sender.sendMessage(TextUtil.color("<#FFD700>/dbdadmin clear — Очистить все генераторы")); + sender.sendMessage(TextUtil.color("<#FFD700>/dbdadmin world — Телепорт в мир")); } public boolean isSelectingGenerator(UUID uuid) { @@ -175,7 +212,7 @@ public class DBDAdminCommand implements TabExecutor { if (!sender.hasPermission("mdbd.admin")) return List.of(); if (args.length == 1) { - return Arrays.asList("create", "list", "remove", "clear").stream() + return Arrays.asList("create", "list", "remove", "clear", "world").stream() .filter(s -> s.startsWith(args[0].toLowerCase())) .toList(); } @@ -186,6 +223,12 @@ public class DBDAdminCommand implements TabExecutor { .toList(); } + if (args.length == 2 && args[0].equalsIgnoreCase("world")) { + return Arrays.asList("lobby", "game").stream() + .filter(s -> s.startsWith(args[1].toLowerCase())) + .toList(); + } + if (args.length == 2 && args[0].equalsIgnoreCase("remove")) { var entries = generatorData.getGeneratorEntries(); List ids = new ArrayList<>(); diff --git a/src/main/java/dev/loki/command/DBDCommand.java b/src/main/java/dev/loki/command/DBDCommand.java index 69640ac..1aaf884 100644 --- a/src/main/java/dev/loki/command/DBDCommand.java +++ b/src/main/java/dev/loki/command/DBDCommand.java @@ -173,7 +173,7 @@ public class DBDCommand implements TabExecutor { public @Nullable List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { if (args.length == 1) { - List completions = new ArrayList<>(Arrays.asList("join", "leave")); + List completions = new ArrayList<>(Arrays.asList("join", "leave", "lobby")); if (sender.hasPermission("mdbd.admin")) { completions.addAll(Arrays.asList("create", "forcestart", "stop", "reload", "setlobby")); } diff --git a/src/main/java/dev/loki/command/SetupCommand.java b/src/main/java/dev/loki/command/SetupCommand.java index b6bf11c..1e1ac69 100644 --- a/src/main/java/dev/loki/command/SetupCommand.java +++ b/src/main/java/dev/loki/command/SetupCommand.java @@ -1,6 +1,7 @@ package dev.loki.command; 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; @@ -8,11 +9,9 @@ import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.command.TabExecutor; import org.bukkit.entity.Player; -import org.bukkit.util.BlockVector; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -65,14 +64,14 @@ public class SetupCommand implements TabExecutor { private boolean giveWand(Player player) { var wand = new org.bukkit.inventory.ItemStack(org.bukkit.Material.STICK); wand.editMeta(meta -> { - meta.displayName(Component.text("&#FF6B00DBD Wand").append(Component.text(" (Выделение)"))); + meta.displayName(TextUtil.color("<#FF6B00>DBD Wand (Выделение)")); meta.lore(List.of( - Component.text("&#FFD700ЛКМ — Первая точка"), - Component.text("&#FFD700ПКМ — Вторая точка") + TextUtil.color("<#FFD700>ЛКМ — Первая точка"), + TextUtil.color("<#FFD700>ПКМ — Вторая точка") )); }); player.getInventory().addItem(wand); - player.sendMessage(Component.text("�FF00Получена палочка выделения!", NamedTextColor.GREEN)); + player.sendMessage(TextUtil.color("<#00FF00>Получена палочка выделения!")); return true; } @@ -80,13 +79,13 @@ public class SetupCommand implements TabExecutor { String key = player.getUniqueId().toString(); Location loc = player.getLocation().getBlock().getLocation(); selectionCorners.put(key + "_pos" + num, loc); - player.sendMessage(Component.text("�FF00Установлена точка " + num + ": " + formatLoc(loc))); + player.sendMessage(TextUtil.color("<#00FF00>Установлена точка " + num + ": " + formatLoc(loc))); return true; } private boolean saveTemplate(Player player, String[] args) { if (args.length < 2) { - player.sendMessage(Component.text("&#FF0000Использование: /dbdsetup save ")); + player.sendMessage(TextUtil.color("<#FF0000>Использование: /dbdsetup save ")); return true; } String key = player.getUniqueId().toString(); @@ -94,38 +93,38 @@ public class SetupCommand implements TabExecutor { Location pos2 = selectionCorners.get(key + "_pos2"); if (pos1 == null || pos2 == null) { - player.sendMessage(Component.text("&#FF0000Сначала установите обе точки!")); + player.sendMessage(TextUtil.color("<#FF0000>Сначала установите обе точки!")); return true; } plugin.getTemplateManager().saveTemplate(args[1], pos1, pos2); - player.sendMessage(Component.text("�FF00Шаблон '" + args[1] + "' сохранен!")); + player.sendMessage(TextUtil.color("<#00FF00>Шаблон '" + args[1] + "' сохранен!")); return true; } private boolean loadTemplate(Player player, String[] args) { if (args.length < 2) { - player.sendMessage(Component.text("&#FF0000Использование: /dbdsetup load ")); + player.sendMessage(TextUtil.color("<#FF0000>Использование: /dbdsetup load ")); return true; } var template = plugin.getTemplateManager().getTemplate(args[1]); if (template == null) { - player.sendMessage(Component.text("&#FF0000Шаблон не найден!")); + player.sendMessage(TextUtil.color("<#FF0000>Шаблон не найден!")); return true; } template.place(player.getLocation(), false); - player.sendMessage(Component.text("�FF00Шаблон '" + args[1] + "' загружен!")); + player.sendMessage(TextUtil.color("<#00FF00>Шаблон '" + args[1] + "' загружен!")); return true; } private boolean listTemplates(CommandSender sender) { var templates = plugin.getTemplateManager().getAllTemplates(); - sender.sendMessage(Component.text("&#FFD700=== Шаблоны ===")); + sender.sendMessage(TextUtil.color("<#FFD700>=== Шаблоны ===")); if (templates.isEmpty()) { - sender.sendMessage(Component.text("󅒐Нет шаблонов")); + sender.sendMessage(TextUtil.color("<#808080>Нет шаблонов")); } else { templates.keySet().forEach(name -> - sender.sendMessage(Component.text("�CED1- " + name)) + sender.sendMessage(TextUtil.color("<#00CED1>- " + name)) ); } return true; @@ -133,7 +132,7 @@ public class SetupCommand implements TabExecutor { private boolean setObject(Player player, String[] args) { if (args.length < 2) { - player.sendMessage(Component.text("&#FF0000Использование: /dbdsetup set ")); + player.sendMessage(TextUtil.color("<#FF0000>Использование: /dbdsetup set ")); return true; } Location loc = player.getLocation().getBlock().getLocation(); @@ -149,26 +148,26 @@ public class SetupCommand implements TabExecutor { case "gate" -> locations.addLocation("exitgates", loc); case "lobby" -> locations.setLobby(loc); default -> { - player.sendMessage(Component.text("&#FF0000Неизвестный тип: " + type)); + player.sendMessage(TextUtil.color("<#FF0000>Неизвестный тип: " + type)); return true; } } plugin.getConfigManager().saveConfig("locations.yml", locations.getConfig()); - player.sendMessage(Component.text("�FF00Установлен: " + type + " на " + formatLoc(loc))); + player.sendMessage(TextUtil.color("<#00FF00>Установлен: " + type + " на " + formatLoc(loc))); return true; } private boolean buildMap(Player player) { - player.sendMessage(Component.text("&#FFD700Строительство карты...")); + player.sendMessage(TextUtil.color("<#FFD700>Строительство карты...")); plugin.getMapBuilder().buildMap(plugin.getConfigManager().getLocations()); - player.sendMessage(Component.text("�FF00Карта построена!")); + player.sendMessage(TextUtil.color("<#00FF00>Карта построена!")); return true; } private boolean clearMap(Player player) { - player.sendMessage(Component.text("&#FFA500Очистка карты...")); + player.sendMessage(TextUtil.color("<#FFA500>Очистка карты...")); plugin.getMapBuilder().clearMap(plugin.getConfigManager().getLocations()); - player.sendMessage(Component.text("�FF00Карта очищена!")); + player.sendMessage(TextUtil.color("<#00FF00>Карта очищена!")); return true; } @@ -177,21 +176,21 @@ public class SetupCommand implements TabExecutor { plugin.getConfigManager().getLocations().setLobby(loc); plugin.getConfigManager().saveConfig("locations.yml", plugin.getConfigManager().getLocations().getConfig()); - player.sendMessage(Component.text("�FF00Лобби установлено на " + formatLoc(loc))); + player.sendMessage(TextUtil.color("<#00FF00>Лобби установлено на " + formatLoc(loc))); return true; } private void sendUsage(CommandSender sender) { - sender.sendMessage(Component.text("&#FF6B00=== DBD Setup ===")); - sender.sendMessage(Component.text("&#FFD700/dbdsetup wand — Получить палочку")); - sender.sendMessage(Component.text("&#FFD700/dbdsetup pos1/pos2 — Установить точки")); - sender.sendMessage(Component.text("&#FFD700/dbdsetup save — Сохранить шаблон")); - sender.sendMessage(Component.text("&#FFD700/dbdsetup load — Загрузить шаблон")); - sender.sendMessage(Component.text("&#FFD700/dbdsetup list — Список шаблонов")); - sender.sendMessage(Component.text("&#FFD700/dbdsetup set — Установить объект")); - sender.sendMessage(Component.text("&#FFD700/dbdsetup build — Построить карту")); - sender.sendMessage(Component.text("&#FFD700/dbdsetup clear — Очистить карту")); - sender.sendMessage(Component.text("&#FFD700/dbdsetup lobby — Установить лобби")); + sender.sendMessage(TextUtil.color("<#FF6B00>=== DBD Setup ===")); + sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup wand — Получить палочку")); + sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup pos1/pos2 — Установить точки")); + sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup save — Сохранить шаблон")); + sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup load — Загрузить шаблон")); + sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup list — Список шаблонов")); + sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup set — Установить объект")); + sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup build — Построить карту")); + sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup clear — Очистить карту")); + sender.sendMessage(TextUtil.color("<#FFD700>/dbdsetup lobby — Установить лобби")); } private String formatLoc(Location loc) { @@ -216,6 +215,11 @@ public class SetupCommand implements TabExecutor { .filter(s -> s.startsWith(args[1].toLowerCase())) .toList(); } + if (args.length == 2 && (args[0].equalsIgnoreCase("save") || args[0].equalsIgnoreCase("load"))) { + return plugin.getTemplateManager().getAllTemplates().keySet().stream() + .filter(s -> s.startsWith(args[1].toLowerCase())) + .toList(); + } return List.of(); } } diff --git a/src/main/java/dev/loki/core/GameManager.java b/src/main/java/dev/loki/core/GameManager.java index ccac765..5a6e549 100644 --- a/src/main/java/dev/loki/core/GameManager.java +++ b/src/main/java/dev/loki/core/GameManager.java @@ -1,6 +1,7 @@ package dev.loki.core; import dev.loki.mdbd; +import dev.loki.player.Survivor; import org.bukkit.entity.Player; import java.util.HashMap; @@ -33,7 +34,14 @@ public class GameManager { if (playerSessions.containsKey(player.getUniqueId())) { return false; } + + // Register the player in the session as a Survivor (role assigned later) + Survivor survivor = new Survivor(player.getUniqueId()); + currentSession.addPlayer(survivor); + playerSessions.put(player.getUniqueId(), currentSession); + // Notify session that player joined (teleport to lobby etc) + currentSession.notifyPlayerJoined(player); return true; } @@ -41,6 +49,8 @@ public class GameManager { GameSession session = playerSessions.remove(player.getUniqueId()); if (session != null) { session.removePlayer(player.getUniqueId()); + // Teleport after removing from session + plugin.getLobbyManager().teleportToLobby(player); return true; } return false; diff --git a/src/main/java/dev/loki/core/GameSession.java b/src/main/java/dev/loki/core/GameSession.java index e5e1580..a3be4fd 100644 --- a/src/main/java/dev/loki/core/GameSession.java +++ b/src/main/java/dev/loki/core/GameSession.java @@ -100,6 +100,11 @@ public class GameSession { eventBus.emit(new PlayerJoinEvent(player)); } + /** Replaces an existing player entry with a new role-assigned instance (Killer/Survivor). */ + public void replacePlayer(UUID uuid, BasePlayer newPlayer) { + players.put(uuid, newPlayer); + } + public void removePlayer(UUID uuid) { BasePlayer removed = players.remove(uuid); if (removed != null) { @@ -154,6 +159,13 @@ public class GameSession { return eventBus; } + public void notifyPlayerJoined(org.bukkit.entity.Player player) { + // Called when player joins from command, updates UI + if (currentPhase != null) { + currentPhase.onPlayerJoined(this, player); + } + } + public record PlayerJoinEvent(BasePlayer player) {} public record PlayerLeaveEvent(BasePlayer player) {} } diff --git a/src/main/java/dev/loki/listener/GeneratorSelectListener.java b/src/main/java/dev/loki/listener/GeneratorSelectListener.java index 85f30e5..2efaa29 100644 --- a/src/main/java/dev/loki/listener/GeneratorSelectListener.java +++ b/src/main/java/dev/loki/listener/GeneratorSelectListener.java @@ -2,8 +2,7 @@ package dev.loki.listener; import dev.loki.command.DBDAdminCommand; import dev.loki.data.GeneratorDataManager; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; +import dev.loki.util.TextUtil; import org.bukkit.Location; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -37,10 +36,10 @@ public class GeneratorSelectListener implements Listener { generatorData.addGenerator(genLoc); - player.sendMessage(Component.text("�FF00Генератор установлен!")); - player.sendMessage(Component.text("�CED1Координаты: " + GeneratorDataManager.formatLocation(genLoc))); - player.sendMessage(Component.text("&#FFD700Всего генераторов: " + generatorData.getCount())); - player.sendMessage(Component.text("&#FFA500Режим выбора остается активным. Ломайте следующий блок или")); - player.sendMessage(Component.text("&#FFA500введите /dbdadmin create generator для отключения")); + player.sendMessage(TextUtil.color("<#00FF00>Генератор установлен!")); + player.sendMessage(TextUtil.color("<#00CED1>Координаты: " + GeneratorDataManager.formatLocation(genLoc))); + player.sendMessage(TextUtil.color("<#FFD700>Всего генераторов: " + generatorData.getCount())); + player.sendMessage(TextUtil.color("<#FFA500>Режим выбора остается активным. Ломайте следующий блок или")); + player.sendMessage(TextUtil.color("<#FFA500>введите /dbdadmin create generator для отключения")); } } diff --git a/src/main/java/dev/loki/listener/PlayerInteractListener.java b/src/main/java/dev/loki/listener/PlayerInteractListener.java index aa5829c..d5a52fd 100644 --- a/src/main/java/dev/loki/listener/PlayerInteractListener.java +++ b/src/main/java/dev/loki/listener/PlayerInteractListener.java @@ -50,7 +50,9 @@ public class PlayerInteractListener implements Listener { if (event.getHand() != EquipmentSlot.HAND) return; Player player = event.getPlayer(); if (!gameManager.isInGame(player.getUniqueId())) return; - BasePlayer bp = gameManager.getPlayerSession(player.getUniqueId()).getPlayer(player.getUniqueId()); + var session = gameManager.getPlayerSession(player.getUniqueId()); + if (session == null) return; + BasePlayer bp = session.getPlayer(player.getUniqueId()); if (bp == null || isIncapacitated(bp)) { event.setCancelled(true); return; } Action action = event.getAction(); diff --git a/src/main/java/dev/loki/mdbd.java b/src/main/java/dev/loki/mdbd.java index 6cbd752..7dfbbc7 100644 --- a/src/main/java/dev/loki/mdbd.java +++ b/src/main/java/dev/loki/mdbd.java @@ -7,6 +7,8 @@ import dev.loki.config.ConfigManager; import dev.loki.core.GameManager; import dev.loki.data.GeneratorDataManager; import dev.loki.listener.GeneratorSelectListener; +import dev.loki.listener.PlayerInteractListener; +import dev.loki.listener.PlayerQuitListener; import dev.loki.map.MapBuilder; import dev.loki.map.TemplateManager; import dev.loki.world.LobbyManager; @@ -19,7 +21,9 @@ public final class mdbd extends JavaPlugin { private TemplateManager templateManager; private MapBuilder mapBuilder; private GeneratorDataManager generatorData; + private DBDCommand dbdCommand; private DBDAdminCommand dbdAdminCommand; + private SetupCommand setupCommand; private LobbyManager lobbyManager; @Override @@ -44,10 +48,13 @@ public final class mdbd extends JavaPlugin { this.gameManager = new GameManager(this); - getCommand("dbd").setExecutor(new DBDCommand(this, gameManager)); - getCommand("dbd").setTabCompleter(new DBDCommand(this, gameManager)); - getCommand("dbdsetup").setExecutor(new SetupCommand(this)); - getCommand("dbdsetup").setTabCompleter(new SetupCommand(this)); + this.dbdCommand = new DBDCommand(this, gameManager); + getCommand("dbd").setExecutor(dbdCommand); + getCommand("dbd").setTabCompleter(dbdCommand); + + this.setupCommand = new SetupCommand(this); + getCommand("dbdsetup").setExecutor(setupCommand); + getCommand("dbdsetup").setTabCompleter(setupCommand); this.dbdAdminCommand = new DBDAdminCommand(this, generatorData); getCommand("dbdadmin").setExecutor(dbdAdminCommand); @@ -55,6 +62,10 @@ public final class mdbd extends JavaPlugin { getServer().getPluginManager().registerEvents( new GeneratorSelectListener(dbdAdminCommand, generatorData), this); + getServer().getPluginManager().registerEvents( + new PlayerQuitListener(this, gameManager), this); + getServer().getPluginManager().registerEvents( + new PlayerInteractListener(this, gameManager, null), this); getLogger().info("Mdbd Plugin enabled successfully!"); getLogger().info("Loaded " + templateManager.getAllTemplates().size() + " templates"); diff --git a/src/main/java/dev/loki/objective/GeneratorRepair.java b/src/main/java/dev/loki/objective/GeneratorRepair.java index 07ebcde..ee11d5f 100644 --- a/src/main/java/dev/loki/objective/GeneratorRepair.java +++ b/src/main/java/dev/loki/objective/GeneratorRepair.java @@ -49,7 +49,7 @@ public class GeneratorRepair { generator.setCurrentRepairer(uuid); RepairSession session = new RepairSession(player, generator); sessions.put(uuid, session); - generator.setBlocked(true); + // Do NOT set blocked here - blocked means nobody can repair it (e.g. after a kick) player.sendMessage(MiniMessage.miniMessage().deserialize("Зажмите ПКМ для починки")); } @@ -60,7 +60,10 @@ public class GeneratorRepair { if (session != null) { session.generator.setCurrentRepairer(null); - session.generator.setBlocked(false); + // Only unblock if it was blocked due to an ongoing repair (not a kick) + if (!session.generator.isUnderRegression()) { + session.generator.setBlocked(false); + } player.sendActionBar(Component.empty()); } } diff --git a/src/main/java/dev/loki/phase/GameplayPhase.java b/src/main/java/dev/loki/phase/GameplayPhase.java index 9a7a00f..5ae81ec 100644 --- a/src/main/java/dev/loki/phase/GameplayPhase.java +++ b/src/main/java/dev/loki/phase/GameplayPhase.java @@ -1,10 +1,11 @@ package dev.loki.phase; import dev.loki.core.GameSession; -import dev.loki.objective.Generator; import dev.loki.objective.GeneratorManager; import dev.loki.player.BasePlayer; import dev.loki.player.PlayerState; +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; @@ -24,14 +25,14 @@ public class GameplayPhase implements Phase { @Override public void enter(GameSession session) { - Bukkit.broadcastMessage("[DBD] Игра началась! Выжившие, чините генераторы!"); + Bukkit.broadcast(Component.text("[DBD] Игра началась! Выжившие, чините генераторы!", NamedTextColor.GREEN)); tickCounter = 0; + // Teleport is handled by RoleAssigner.assignRoles — no double-teleport needed here } @Override public void tick(GameSession session) { tickCounter++; - generatorManager.tick(); if (tickCounter % 20 == 0) { @@ -45,7 +46,8 @@ public class GameplayPhase implements Phase { if (generatorManager.areAllRequiredCompleted()) { allGensDone = true; - Bukkit.broadcastMessage("[DBD] §a§lВсе генераторы починены! Открывайте ворота!"); + Bukkit.broadcast(Component.text( + "[DBD] Все генераторы починены! Открывайте ворота!", NamedTextColor.GREEN)); for (BasePlayer bp : session.getPlayers().values()) { Player p = bp.getPlayer(); if (p != null) { @@ -58,13 +60,11 @@ public class GameplayPhase implements Phase { private void checkWinConditions(GameSession session) { int aliveSurvivors = 0; int escapedSurvivors = 0; - boolean killerDead = true; + int totalSurvivors = 0; for (BasePlayer bp : session.getPlayers().values()) { - if (bp.isKiller()) { - killerDead = false; - continue; - } + if (bp.isKiller()) continue; + totalSurvivors++; PlayerState state = bp.getState(); if (state == PlayerState.ESCAPED) { escapedSurvivors++; @@ -73,16 +73,17 @@ public class GameplayPhase implements Phase { } } - boolean allSurvivorsDead = (aliveSurvivors == 0 && escapedSurvivors == 0); + boolean allSurvivorsDead = (aliveSurvivors == 0 && escapedSurvivors == 0 && totalSurvivors > 0); + boolean allEscaped = (escapedSurvivors == totalSurvivors && totalSurvivors > 0); - if (allSurvivorsDead || escapedSurvivors == session.getPlayerCount() - 1) { + if (allSurvivorsDead || allEscaped) { shouldEnd = true; } } @Override public void exit(GameSession session) { - Bukkit.broadcastMessage("[DBD] §6Игровая фаза завершена!"); + Bukkit.broadcast(Component.text("[DBD] Игровая фаза завершена!", NamedTextColor.GOLD)); } @Override diff --git a/src/main/java/dev/loki/phase/LobbyPhase.java b/src/main/java/dev/loki/phase/LobbyPhase.java index 5e76619..76a56e4 100644 --- a/src/main/java/dev/loki/phase/LobbyPhase.java +++ b/src/main/java/dev/loki/phase/LobbyPhase.java @@ -2,9 +2,11 @@ package dev.loki.phase; import dev.loki.config.GameConfig; import dev.loki.core.GameSession; +import dev.loki.player.BasePlayer; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Bukkit; +import org.bukkit.entity.Player; public class LobbyPhase implements Phase { private final GameConfig config; @@ -22,6 +24,14 @@ public class LobbyPhase implements Phase { @Override public void enter(GameSession session) { Bukkit.broadcast(Component.text("[DBD] Лобби открыто! Используйте /dbd join", NamedTextColor.GREEN)); + // Teleport already-in-session players to lobby (e.g. on restart) + var lobbyManager = session.getPlugin().getLobbyManager(); + for (var player : session.getPlayers().values()) { + var bukkitPlayer = player.getPlayer(); + if (bukkitPlayer != null && bukkitPlayer.isOnline()) { + lobbyManager.teleportToLobby(bukkitPlayer); + } + } } @Override @@ -33,6 +43,9 @@ public class LobbyPhase implements Phase { secondsLeft = config.getLobbyCountdown(); countdown = 0; started = false; + // Show waiting message in action bar to all players in lobby + sendActionBarToAll(session, + Component.text("⏳ Ожидание игроков: " + playerCount + "/" + minPlayers, NamedTextColor.YELLOW)); return; } @@ -40,12 +53,18 @@ public class LobbyPhase implements Phase { if (countdown >= 20) { countdown = 0; secondsLeft--; + } - if (secondsLeft <= 5 || secondsLeft % 10 == 0) { - Bukkit.broadcast(Component.text( - "[DBD] До начала: " + secondsLeft + " сек. (" + playerCount + "/" + minPlayers + ")", - NamedTextColor.YELLOW)); - } + // Action bar: показываем каждый тик (обновляется плавно) + String bar = buildCountdownBar(secondsLeft, config.getLobbyCountdown()); + sendActionBarToAll(session, + Component.text("▶ Старт через: " + secondsLeft + "с " + bar + + " (" + playerCount + "/" + minPlayers + ")", NamedTextColor.GREEN)); + + // Chat-announce на ключевых отметках + if (countdown == 0 && (secondsLeft <= 5 || secondsLeft == 10 || secondsLeft == 30)) { + Bukkit.broadcast(Component.text( + "[DBD] До начала: " + secondsLeft + " сек.", NamedTextColor.YELLOW)); } if (secondsLeft <= 0) { @@ -53,11 +72,41 @@ public class LobbyPhase implements Phase { } } + private void sendActionBarToAll(GameSession session, Component msg) { + for (BasePlayer bp : session.getPlayers().values()) { + Player p = bp.getPlayer(); + if (p != null && p.isOnline()) { + p.sendActionBar(msg); + } + } + } + + private String buildCountdownBar(int current, int total) { + int filled = total > 0 ? (int) (10.0 * current / total) : 0; + int empty = 10 - filled; + return "§a" + "█".repeat(Math.max(0, filled)) + "§7" + "░".repeat(Math.max(0, empty)); + } + @Override public void exit(GameSession session) { + // Clear action bar for all + for (BasePlayer bp : session.getPlayers().values()) { + Player p = bp.getPlayer(); + if (p != null && p.isOnline()) p.sendActionBar(Component.empty()); + } Bukkit.broadcast(Component.text("[DBD] Игра начинается!", NamedTextColor.GREEN)); } + @Override + public void onPlayerJoined(GameSession session, Player player) { + session.getPlugin().getLobbyManager().teleportToLobby(player); + int playerCount = session.getPlayerCount(); + int minPlayers = config.getMinPlayers(); + Bukkit.broadcast(Component.text( + "[DBD] " + player.getName() + " присоединился к очереди! (" + playerCount + "/" + minPlayers + ")", + NamedTextColor.YELLOW)); + } + @Override public boolean isComplete() { return started; diff --git a/src/main/java/dev/loki/phase/Phase.java b/src/main/java/dev/loki/phase/Phase.java index e730505..3fc72e9 100644 --- a/src/main/java/dev/loki/phase/Phase.java +++ b/src/main/java/dev/loki/phase/Phase.java @@ -1,10 +1,12 @@ package dev.loki.phase; import dev.loki.core.GameSession; +import org.bukkit.entity.Player; public interface Phase { void enter(GameSession session); void tick(GameSession session); void exit(GameSession session); boolean isComplete(); + default void onPlayerJoined(GameSession session, Player player) {} } diff --git a/src/main/java/dev/loki/phase/RoleAssignPhase.java b/src/main/java/dev/loki/phase/RoleAssignPhase.java index ad1d444..101ca2d 100644 --- a/src/main/java/dev/loki/phase/RoleAssignPhase.java +++ b/src/main/java/dev/loki/phase/RoleAssignPhase.java @@ -2,6 +2,8 @@ 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 { @@ -30,7 +32,7 @@ public class RoleAssignPhase implements Phase { @Override public void exit(GameSession session) { - Bukkit.broadcastMessage("[DBD] Роли назначены!"); + Bukkit.broadcast(Component.text("[DBD] Роли назначены!", NamedTextColor.GREEN)); } @Override diff --git a/src/main/java/dev/loki/role/RoleAssigner.java b/src/main/java/dev/loki/role/RoleAssigner.java index b57e91d..fa70bc3 100644 --- a/src/main/java/dev/loki/role/RoleAssigner.java +++ b/src/main/java/dev/loki/role/RoleAssigner.java @@ -5,7 +5,11 @@ import dev.loki.core.GameSession; import dev.loki.player.BasePlayer; import dev.loki.player.Killer; 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.Location; +import org.bukkit.entity.Player; import java.util.*; @@ -31,19 +35,54 @@ public class RoleAssigner { if (players.isEmpty()) return; List uuids = new ArrayList<>(players.keySet()); - UUID killerUuid = selectKiller(uuids); + List survivorSpawns = locations.getSurvivorSpawns(); + int spawnIndex = 0; + for (UUID uuid : uuids) { - BasePlayer player; + BasePlayer newPlayer; if (uuid.equals(killerUuid)) { - player = new Killer(uuid); + newPlayer = new Killer(uuid); Location spawn = locations.getKillerSpawn(); - player.setSpawnLocation(spawn); + if (spawn != null) newPlayer.setSpawnLocation(spawn); } else { - player = new Survivor(uuid); + newPlayer = new Survivor(uuid); + if (!survivorSpawns.isEmpty()) { + Location spawn = survivorSpawns.get(spawnIndex % survivorSpawns.size()); + newPlayer.setSpawnLocation(spawn); + spawnIndex++; + } } - session.getEventBus().emit(new RoleAssignedEvent(uuid, player)); + + // Actually replace the player object in session + session.replacePlayer(uuid, newPlayer); + + // Announce role and teleport + Player bukkit = Bukkit.getPlayer(uuid); + if (bukkit != null && bukkit.isOnline()) { + if (newPlayer.isKiller()) { + bukkit.sendTitle("§4§lКИЛЛЕР", "§cОхоться на выживших!", 10, 60, 20); + bukkit.sendMessage(Component.text("[DBD] Ваша роль: Киллер", NamedTextColor.RED)); + } else { + bukkit.sendTitle("§a§lВЫЖИВШИЙ", "§eЧини генераторы и сбегай!", 10, 60, 20); + bukkit.sendMessage(Component.text("[DBD] Ваша роль: Выживший", NamedTextColor.GREEN)); + } + + // Teleport to game world spawn (or role spawn) + Location spawn = newPlayer.getSpawnLocation(); + if (spawn != null) { + bukkit.teleport(spawn); + } else { + // Fallback: game world spawn + var gameWorld = session.getPlugin().getLobbyManager().getGameWorld(); + if (gameWorld != null) { + bukkit.teleport(gameWorld.getSpawnLocation()); + } + } + } + + session.getEventBus().emit(new RoleAssignedEvent(uuid, newPlayer)); } queue.rotate(); diff --git a/src/main/java/dev/loki/util/TextUtil.java b/src/main/java/dev/loki/util/TextUtil.java new file mode 100644 index 0000000..9c346ee --- /dev/null +++ b/src/main/java/dev/loki/util/TextUtil.java @@ -0,0 +1,29 @@ +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. + */ +public final class TextUtil { + private static final MiniMessage MM = MiniMessage.miniMessage(); + + 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>"); + } +} diff --git a/src/main/java/dev/loki/world/LobbyManager.java b/src/main/java/dev/loki/world/LobbyManager.java index f0a8706..77f24ba 100644 --- a/src/main/java/dev/loki/world/LobbyManager.java +++ b/src/main/java/dev/loki/world/LobbyManager.java @@ -6,7 +6,6 @@ import org.bukkit.GameRule; import org.bukkit.Material; import org.bukkit.World; import org.bukkit.WorldCreator; -import org.bukkit.WorldType; import java.io.File; @@ -31,7 +30,6 @@ public class LobbyManager { WorldCreator creator = new WorldCreator("lobby"); creator.generator(new VoidWorldGenerator()); - creator.type(WorldType.FLAT); creator.environment(World.Environment.NORMAL); creator.generateStructures(false); @@ -143,7 +141,14 @@ public class LobbyManager { public void teleportToLobby(org.bukkit.entity.Player player) { if (lobbyWorld != null) { - player.teleport(lobbyWorld.getSpawnLocation()); + // Use the configured lobby location from locations.yml if available, + // otherwise fall back to spawn location on the platform + org.bukkit.Location configLobby = plugin.getConfigManager().getLocations().getLobby(); + if (configLobby != null && configLobby.getWorld() != null) { + player.teleport(configLobby); + } else { + player.teleport(lobbyWorld.getSpawnLocation()); + } } } } diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 3502c01..2e11e04 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -5,22 +5,22 @@ api-version: '1.21' commands: dbd: - description: "Главная команда DBD плагина" + description: "Main DBD command" usage: "/dbd " permission: mdbd.player dbdsetup: - description: "Команды настройки карты и объектов" + description: "Map setup commands" usage: "/dbdsetup " permission: mdbd.admin dbdadmin: - description: "Админ-команды для управления генераторами и объектами" - usage: "/dbdadmin " + description: "Admin commands for generators and objects" + usage: "/dbdadmin " permission: mdbd.admin permissions: mdbd.admin: - description: "Полный доступ к админ-командам плагина" + description: "Full access to admin commands" default: op mdbd.player: - description: "Базовые команды для игроков" + description: "Basic player commands" default: true