fix: build — add test dependencies, fix ProGuard, remove obfuscation from default build

This commit is contained in:
loki5512344 2026-06-14 16:21:31 +02:00
parent 7c7562a8d4
commit 183df22d13
6 changed files with 1025 additions and 493 deletions

View file

@ -1,489 +1,489 @@
# Dead by Daylight Plugin — Руководство команд # Dead by Daylight Plugin — Руководство команд
**Версия:** 1.2 **Версия:** 1.2
**Обновлено:** Июнь 2026 **Обновлено:** Июнь 2026
**Платформа:** Minecraft 1.21.5 (Paper) **Платформа:** Minecraft 1.21.5 (Paper)
--- ---
## Содержание ## Содержание
1. [Фазы игры](#фазы-игры) 1. [Фазы игры](#фазы-игры)
2. [Команды игроков — /dbd](#команды-игроков--dbd) 2. [Команды игроков — /dbd](#команды-игроков--dbd)
3. [Команды администратора — /dbdadmin](#команды-администратора--dbdadmin) 3. [Команды администратора — /dbdadmin](#команды-администратора--dbdadmin)
4. [Команды настройки карты — /dbdsetup](#команды-настройки-карты--dbdsetup) 4. [Команды настройки карты — /dbdsetup](#команды-настройки-карты--dbdsetup)
5. [Права доступа](#права-доступа) 5. [Права доступа](#права-доступа)
6. [Порядок настройки сервера](#порядок-настройки-сервера) 6. [Порядок настройки сервера](#порядок-настройки-сервера)
7. [Таблица всех команд](#таблица-всех-команд) 7. [Таблица всех команд](#таблица-всех-команд)
--- ---
## Фазы игры ## Фазы игры
``` ```
LOBBY LOBBY
Игроки делают /dbd join → телепорт в мир lobby Игроки делают /dbd join → телепорт в мир lobby
Action bar показывает: ▶ Старт через: 45с ████████░░ (3/5) Action bar показывает: ▶ Старт через: 45с ████████░░ (3/5)
Когда набралось минимум игроков — идёт отсчёт 60 сек Когда набралось минимум игроков — идёт отсчёт 60 сек
При нехватке игроков — таймер сбрасывается При нехватке игроков — таймер сбрасывается
↓ ↓
ROLE ASSIGN (5 сек) ROLE ASSIGN (5 сек)
Случайно выбирается 1 киллер, остальные — выжившие Случайно выбирается 1 киллер, остальные — выжившие
Показывается тайтл с ролью Показывается тайтл с ролью
Все телепортируются на свои спавны в мире game Все телепортируются на свои спавны в мире game
↓ ↓
GAMEPLAY GAMEPLAY
Выжившие чинят генераторы (нужно 5 из 7) Выжившие чинят генераторы (нужно 5 из 7)
Киллер охотится Киллер охотится
При починке всех нужных генераторов — открываются ворота При починке всех нужных генераторов — открываются ворота
↓ ↓
ENDGAME (2 минуты) ENDGAME (2 минуты)
Выжившие открывают ворота и убегают Выжившие открывают ворота и убегают
Или используют люк (если остался 1 выживший) Или используют люк (если остался 1 выживший)
Коллапс через 120 сек убивает всех оставшихся Коллапс через 120 сек убивает всех оставшихся
``` ```
--- ---
## Команды игроков — /dbd ## Команды игроков — /dbd
Все команды ниже доступны любому игроку (`mdbd.player`, по умолчанию `true`). Все команды ниже доступны любому игроку (`mdbd.player`, по умолчанию `true`).
--- ---
### /dbd join ### /dbd join
Присоединиться к игровой очереди. Присоединиться к игровой очереди.
``` ```
/dbd join /dbd join
``` ```
- Добавляет игрока в текущую сессию как выжившего (роль назначится позже) - Добавляет игрока в текущую сессию как выжившего (роль назначится позже)
- Телепортирует в мир `lobby` - Телепортирует в мир `lobby`
- В action bar начинает отображаться счётчик и таймер - В action bar начинает отображаться счётчик и таймер
- Если игра не создана — сообщение об ошибке - Если игра не создана — сообщение об ошибке
``` ```
→ [DBD] Вы присоединились к игре! → [DBD] Вы присоединились к игре!
→ [DBD] PlayerName присоединился к очереди! (1/5) → [DBD] PlayerName присоединился к очереди! (1/5)
``` ```
--- ---
### /dbd leave ### /dbd leave
Покинуть сессию. Покинуть сессию.
``` ```
/dbd leave /dbd leave
``` ```
- В лобби: просто убирает из очереди, таймер может сброситься - В лобби: просто убирает из очереди, таймер может сброситься
- В игре: выживший считается мёртвым, киллер завершает игру - В игре: выживший считается мёртвым, киллер завершает игру
- Игрок телепортируется обратно в `lobby` - Игрок телепортируется обратно в `lobby`
``` ```
→ [DBD] Вы покинули игру → [DBD] Вы покинули игру
``` ```
--- ---
### /dbd lobby ### /dbd lobby
Телепортироваться в лобби-мир. Телепортироваться в лобби-мир.
``` ```
/dbd lobby /dbd lobby
``` ```
- Работает в любой момент, не убирает из сессии - Работает в любой момент, не убирает из сессии
- Телепортирует на точку спавна лобби (из `locations.yml`) или на спавн мира `lobby` - Телепортирует на точку спавна лобби (из `locations.yml`) или на спавн мира `lobby`
``` ```
→ [DBD] Вы телепортированы в лобби! → [DBD] Вы телепортированы в лобби!
``` ```
--- ---
## Команды администратора — /dbd (admin) ## Команды администратора — /dbd (admin)
Требуют право `mdbd.admin` (по умолчанию только OP). Требуют право `mdbd.admin` (по умолчанию только OP).
--- ---
### /dbd create ### /dbd create
Создать новую игровую сессию. Создать новую игровую сессию.
``` ```
/dbd create /dbd create
``` ```
- Создаёт сессию и открывает лобби - Создаёт сессию и открывает лобби
- Нельзя создать вторую сессию пока первая активна - Нельзя создать вторую сессию пока первая активна
``` ```
→ [DBD] Игра создана! → [DBD] Игра создана!
→ [DBD] Игра уже существует (если уже есть) → [DBD] Игра уже существует (если уже есть)
``` ```
--- ---
### /dbd forcestart ### /dbd forcestart
Принудительно запустить игру, пропустив таймер. Принудительно запустить игру, пропустив таймер.
``` ```
/dbd forcestart /dbd forcestart
``` ```
- Работает только если сессия в фазе LOBBY - Работает только если сессия в фазе LOBBY
- Игроков в очереди может быть меньше минимума - Игроков в очереди может быть меньше минимума
``` ```
→ [DBD] Игра форсирована! → [DBD] Игра форсирована!
``` ```
--- ---
### /dbd stop ### /dbd stop
Немедленно завершить текущую сессию. Немедленно завершить текущую сессию.
``` ```
/dbd stop /dbd stop
``` ```
- Все игроки телепортируются в `lobby` - Все игроки телепортируются в `lobby`
- Сессия уничтожается, можно создавать новую - Сессия уничтожается, можно создавать новую
``` ```
→ [DBD] Игра остановлена → [DBD] Игра остановлена
``` ```
--- ---
### /dbd reload ### /dbd reload
Перезагрузить все конфиги без перезапуска сервера. Перезагрузить все конфиги без перезапуска сервера.
``` ```
/dbd reload /dbd reload
``` ```
Перезагружает: `config.yml`, `game.yml`, `messages.yml`, `skillcheck.yml`, `locations.yml`, `generators.yml` Перезагружает: `config.yml`, `game.yml`, `messages.yml`, `skillcheck.yml`, `locations.yml`, `generators.yml`
``` ```
→ [DBD] Конфиги перезагружены! → [DBD] Конфиги перезагружены!
``` ```
> Если игра в процессе — сначала `/dbd stop`, иначе новые настройки применятся только к следующей сессии. > Если игра в процессе — сначала `/dbd stop`, иначе новые настройки применятся только к следующей сессии.
--- ---
### /dbd setlobby ### /dbd setlobby
Установить текущую позицию игрока как точку спавна лобби. Установить текущую позицию игрока как точку спавна лобби.
``` ```
/dbd setlobby /dbd setlobby
``` ```
- Сохраняется в `locations.yml` - Сохраняется в `locations.yml`
- Эквивалентно `/dbdsetup lobby` - Эквивалентно `/dbdsetup lobby`
``` ```
→ [DBD] Лобби установлено! → [DBD] Лобби установлено!
``` ```
--- ---
## Команды администратора — /dbdadmin ## Команды администратора — /dbdadmin
Требуют право `mdbd.admin`. Все команды только для игроков (не консоли). Требуют право `mdbd.admin`. Все команды только для игроков (не консоли).
--- ---
### /dbdadmin create generator ### /dbdadmin create generator
Режим расстановки генераторов. Режим расстановки генераторов.
``` ```
/dbdadmin create generator /dbdadmin create generator
``` ```
- Включает режим: в action bar оранжевая подсказка - Включает режим: в action bar оранжевая подсказка
- **Сломай блок** — координаты сохранятся, блок не разрушится - **Сломай блок** — координаты сохранятся, блок не разрушится
- Повторная команда — отключает режим - Повторная команда — отключает режим
- Режим остаётся активным для нескольких генераторов подряд - Режим остаётся активным для нескольких генераторов подряд
``` ```
→ Режим выбора генератора включен! → Режим выбора генератора включен!
→ Сломайте блок, чтобы установить генератор → Сломайте блок, чтобы установить генератор
(сломал блок) (сломал блок)
→ Генератор установлен! Всего: 1 → Генератор установлен! Всего: 1
``` ```
**Tab complete:** `generator` **Tab complete:** `generator`
--- ---
### /dbdadmin list ### /dbdadmin list
Список всех установленных генераторов. Список всех установленных генераторов.
``` ```
/dbdadmin list /dbdadmin list
``` ```
``` ```
=== Генераторы (3) === === Генераторы (3) ===
[0] game:100.00,64.00,200.00 [0] game:100.00,64.00,200.00
[1] game:150.00,64.00,150.00 [1] game:150.00,64.00,150.00
[2] game:200.00,64.00,100.00 [2] game:200.00,64.00,100.00
``` ```
--- ---
### /dbdadmin remove \<id\> ### /dbdadmin remove \<id\>
Удалить генератор по ID из списка. Удалить генератор по ID из списка.
``` ```
/dbdadmin remove 0 /dbdadmin remove 0
``` ```
**Tab complete:** подсказывает существующие ID (0, 1, 2...) **Tab complete:** подсказывает существующие ID (0, 1, 2...)
``` ```
→ Генератор удален: game:100.00,64.00,200.00 → Генератор удален: game:100.00,64.00,200.00
→ Генератор с ID 5 не найден (если нет такого) → Генератор с ID 5 не найден (если нет такого)
``` ```
--- ---
### /dbdadmin clear ### /dbdadmin clear
Удалить все генераторы. Удалить все генераторы.
``` ```
/dbdadmin clear /dbdadmin clear
``` ```
> Необратимо. > Необратимо.
``` ```
→ Все генераторы удалены → Все генераторы удалены
``` ```
--- ---
### /dbdadmin world \<мир\> ### /dbdadmin world \<мир\>
Телепортироваться в указанный мир для настройки карты. Телепортироваться в указанный мир для настройки карты.
``` ```
/dbdadmin world lobby /dbdadmin world lobby
/dbdadmin world game /dbdadmin world game
/dbdadmin world <любое_название_мира> /dbdadmin world <любое_название_мира>
``` ```
- `lobby` — телепорт в мир-лобби (void, платформа 7x7) - `lobby` — телепорт в мир-лобби (void, платформа 7x7)
- `game` — телепорт в игровой мир - `game` — телепорт в игровой мир
- Любое другое название — телепорт в мир с таким именем (если загружен) - Любое другое название — телепорт в мир с таким именем (если загружен)
``` ```
→ Телепортирован в мир: lobby → Телепортирован в мир: lobby
→ Мир не найден: abc (если мир не существует) → Мир не найден: abc (если мир не существует)
``` ```
**Tab complete:** `lobby`, `game` **Tab complete:** `lobby`, `game`
--- ---
## Команды настройки карты — /dbdsetup ## Команды настройки карты — /dbdsetup
Требуют право `mdbd.admin`. Требуют право `mdbd.admin`.
--- ---
### /dbdsetup wand ### /dbdsetup wand
Выдать топорик для расстановки ворот. Выдать топорик для расстановки ворот.
``` ```
/dbdsetup wand /dbdsetup wand
``` ```
- Выдаёт деревянный топорик (WOODEN_AXE) с подписью DBD Setup Wand - Выдаёт деревянный топорик (WOODEN_AXE) с подписью DBD Setup Wand
- **ЛКМ** по блоку = точка 1 (pos1) - **ЛКМ** по блоку = точка 1 (pos1)
- **ПКМ** по блоку = точка 2 (pos2) - **ПКМ** по блоку = точка 2 (pos2)
- Только для ворот. Для остальных объектов ванд не нужен. - Только для ворот. Для остальных объектов ванд не нужен.
--- ---
### /dbdsetup set \<тип\> ### /dbdsetup set \<тип\>
Войти в режим установки объекта. **Сломай блок** — объект установится на его месте, блок восстановится. Войти в режим установки объекта. **Сломай блок** — объект установится на его месте, блок восстановится.
``` ```
/dbdsetup set generator /dbdsetup set generator
/dbdsetup set hook /dbdsetup set hook
/dbdsetup set pallet /dbdsetup set pallet
/dbdsetup set chest /dbdsetup set chest
/dbdsetup set killer-spawn /dbdsetup set killer-spawn
/dbdsetup set survivor-spawn /dbdsetup set survivor-spawn
``` ```
| Тип | Описание | Рекомендуемое кол-во | | Тип | Описание | Рекомендуемое кол-во |
|-----|----------|---------------------| |-----|----------|---------------------|
| `generator` | Генератор | 7 | | `generator` | Генератор | 7 |
| `hook` | Крюк | 7–10 | | `hook` | Крюк | 7–10 |
| `pallet` | Паллет | 15–20 | | `pallet` | Паллет | 15–20 |
| `chest` | Сундук | 5–10 | | `chest` | Сундук | 5–10 |
| `killer-spawn` | Спавн киллера | 1 | | `killer-spawn` | Спавн киллера | 1 |
| `survivor-spawn` | Спавн выжившего | 4 | | `survivor-spawn` | Спавн выжившего | 4 |
``` ```
→ Режим установки: hook → Режим установки: hook
→ Сломай блок чтобы установить (блок восстановится) → Сломай блок чтобы установить (блок восстановится)
(ломаешь блок) (ломаешь блок)
→ Крюк установлен: [game, 150, 65, 200] → Крюк установлен: [game, 150, 65, 200]
``` ```
--- ---
### /dbdsetup set gate A|B ### /dbdsetup set gate A|B
Установить ворота через две точки топориком. Установить ворота через две точки топориком.
``` ```
/dbdsetup wand — получить топорик /dbdsetup wand — получить топорик
ЛКМ по блоку → точка 1 — нижний левый угол ЛКМ по блоку → точка 1 — нижний левый угол
ПКМ по блоку → точка 2 — верхний правый угол ПКМ по блоку → точка 2 — верхний правый угол
/dbdsetup set gate A — установить ворота A в центре между точками /dbdsetup set gate A — установить ворота A в центре между точками
/dbdsetup set gate B — установить ворота B /dbdsetup set gate B — установить ворота B
``` ```
Центр ворот вычисляется: X = средняя точка, Y = минимальная из двух. Центр ворот вычисляется: X = средняя точка, Y = минимальная из двух.
``` ```
→ [Wand] Точка 1: [game, 100, 64, 200] → [Wand] Точка 1: [game, 100, 64, 200]
→ [Wand] Точка 2: [game, 110, 64, 210] → [Wand] Точка 2: [game, 110, 64, 210]
→ Ворота A установлены: [game, 105, 64, 205] → Ворота A установлены: [game, 105, 64, 205]
``` ```
--- ---
### /dbdsetup build ### /dbdsetup build
Построить карту по шаблону и `locations.yml`. Построить карту по шаблону и `locations.yml`.
``` ```
/dbdsetup build /dbdsetup build
``` ```
``` ```
→ Строительство карты... → Строительство карты...
→ Карта построена! → Карта построена!
``` ```
--- ---
### /dbdsetup clear ### /dbdsetup clear
Очистить карту. Очистить карту.
``` ```
/dbdsetup clear /dbdsetup clear
``` ```
``` ```
→ Очистка карты... → Очистка карты...
→ Карта очищена! → Карта очищена!
``` ```
--- ---
### /dbdsetup lobby ### /dbdsetup lobby
Установить спавн лобби на текущую позицию. Установить спавн лобби на текущую позицию.
``` ```
/dbdsetup lobby /dbdsetup lobby
``` ```
Аналог `/dbd setlobby`. Сохраняется в `locations.yml`. Аналог `/dbd setlobby`. Сохраняется в `locations.yml`.
--- ---
## Права доступа ## Права доступа
| Право | Кому | Что даёт | | Право | Кому | Что даёт |
|-------|------|----------| |-------|------|----------|
| `mdbd.player` | Все (`default: true`) | `/dbd join`, `leave`, `lobby` | | `mdbd.player` | Все (`default: true`) | `/dbd join`, `leave`, `lobby` |
| `mdbd.admin` | OP (`default: op`) | Все остальные команды | | `mdbd.admin` | OP (`default: op`) | Все остальные команды |
--- ---
## Порядок настройки сервера ## Порядок настройки сервера
### Первый запуск ### Первый запуск
1. Запустить сервер — плагин создаст миры `lobby` и `game` автоматически 1. Запустить сервер — плагин создаст миры `lobby` и `game` автоматически
2. `/dbdadmin world lobby` — войти в лобби-мир и осмотреться (платформа 7x7) 2. `/dbdadmin world lobby` — войти в лобби-мир и осмотреться (платформа 7x7)
3. `/dbd setlobby` — встать на нужную точку и сохранить спавн лобби 3. `/dbd setlobby` — встать на нужную точку и сохранить спавн лобби
### Настройка игровой карты ### Настройка игровой карты
4. `/dbdadmin world game` — перейти в игровой мир 4. `/dbdadmin world game` — перейти в игровой мир
5. Построить или загрузить карту 5. Построить или загрузить карту
6. Расставить объекты: 6. Расставить объекты:
- `/dbdsetup set generator` → ломать блоки для генераторов - `/dbdsetup set generator` → ломать блоки для генераторов
- `/dbdsetup set hook` → ломать блоки для крюков - `/dbdsetup set hook` → ломать блоки для крюков
- `/dbdsetup set pallet` → ломать блоки для паллетов - `/dbdsetup set pallet` → ломать блоки для паллетов
- `/dbdsetup set chest` → ломать блоки для сундуков - `/dbdsetup set chest` → ломать блоки для сундуков
7. Установить спавны: 7. Установить спавны:
- `/dbdsetup set killer-spawn` → спавн киллера - `/dbdsetup set killer-spawn` → спавн киллера
- `/dbdsetup set survivor-spawn` → 4 раза для выживших - `/dbdsetup set survivor-spawn` → 4 раза для выживших
8. Установить ворота: 8. Установить ворота:
- `/dbdsetup wand` → получить топорик - `/dbdsetup wand` → получить топорик
- ЛКМ/ПКМ по блокам → отметить углы ворот - ЛКМ/ПКМ по блокам → отметить углы ворот
- `/dbdsetup set gate A` и `/dbdsetup set gate B` - `/dbdsetup set gate A` и `/dbdsetup set gate B`
### Тест ### Тест
8. `/dbd create` — создать сессию 8. `/dbd create` — создать сессию
9. `/dbd join` (5 игроков или меньше с форс-стартом) 9. `/dbd join` (5 игроков или меньше с форс-стартом)
10. `/dbd forcestart` — запустить не дожидаясь таймера 10. `/dbd forcestart` — запустить не дожидаясь таймера
--- ---
## Таблица всех команд ## Таблица всех команд
| Команда | Право | Описание | | Команда | Право | Описание |
|---------|-------|----------| |---------|-------|----------|
| `/dbd join` | player | Войти в очередь | | `/dbd join` | player | Войти в очередь |
| `/dbd leave` | player | Покинуть сессию | | `/dbd leave` | player | Покинуть сессию |
| `/dbd lobby` | player | Телепорт в лобби | | `/dbd lobby` | player | Телепорт в лобби |
| `/dbd create` | admin | Создать сессию | | `/dbd create` | admin | Создать сессию |
| `/dbd forcestart` | admin | Принудительный старт | | `/dbd forcestart` | admin | Принудительный старт |
| `/dbd stop` | admin | Остановить игру | | `/dbd stop` | admin | Остановить игру |
| `/dbd reload` | admin | Перезагрузить конфиги | | `/dbd reload` | admin | Перезагрузить конфиги |
| `/dbd setlobby` | admin | Сохранить спавн лобби | | `/dbd setlobby` | admin | Сохранить спавн лобби |
| `/dbdadmin create generator` | admin | Режим расстановки генераторов | | `/dbdadmin create generator` | admin | Режим расстановки генераторов |
| `/dbdadmin list` | admin | Список генераторов | | `/dbdadmin list` | admin | Список генераторов |
| `/dbdadmin remove <id>` | admin | Удалить генератор | | `/dbdadmin remove <id>` | admin | Удалить генератор |
| `/dbdadmin clear` | admin | Очистить все генераторы | | `/dbdadmin clear` | admin | Очистить все генераторы |
| `/dbdadmin world <lobby\|game\|мир>` | admin | Телепорт в мир | | `/dbdadmin world <lobby\|game\|мир>` | admin | Телепорт в мир |
| `/dbdsetup wand` | admin | Топорик для ворот (ЛКМ/ПКМ) | | `/dbdsetup wand` | admin | Топорик для ворот (ЛКМ/ПКМ) |
| `/dbdsetup set <тип>` | admin | Установить объект (сломать блок) | | `/dbdsetup set <тип>` | admin | Установить объект (сломать блок) |
| `/dbdsetup set gate A\|B` | admin | Установить ворота (из точек ванда) | | `/dbdsetup set gate A\|B` | admin | Установить ворота (из точек ванда) |
| `/dbdsetup build` | admin | Построить карту | | `/dbdsetup build` | admin | Построить карту |
| `/dbdsetup clear` | admin | Очистить карту | | `/dbdsetup clear` | admin | Очистить карту |
| `/dbdsetup lobby` | admin | Сохранить спавн лобби | | `/dbdsetup lobby` | admin | Сохранить спавн лобби |
--- ---
*Dead by Daylight Minecraft Plugin — актуально для версии 1.1+* *Dead by Daylight Minecraft Plugin — актуально для версии 1.1+*

View file

@ -15,6 +15,16 @@ dependencies {
compileOnly("io.papermc.paper:paper-api:1.21.5-R0.1-SNAPSHOT") compileOnly("io.papermc.paper:paper-api:1.21.5-R0.1-SNAPSHOT")
compileOnly("com.comphenix.protocol:ProtocolLib:5.3.0") compileOnly("com.comphenix.protocol:ProtocolLib:5.3.0")
proguardConfig("com.guardsquare:proguard-base:7.4.2") proguardConfig("com.guardsquare:proguard-base:7.4.2")
testImplementation("io.papermc.paper:paper-api:1.21.5-R0.1-SNAPSHOT")
testImplementation("org.junit.jupiter:junit-jupiter:5.11.4")
testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.11.4")
testImplementation("org.mockito:mockito-core:5.14.2")
testImplementation("org.mockito:mockito-junit-jupiter:5.14.2")
}
tasks.test {
useJUnitPlatform()
} }
java { java {
@ -72,8 +82,9 @@ val obfuscateTask = tasks.register<JavaExec>("obfuscate") {
"-keep", "class dev.loki.config.** { *; }", "-keep", "class dev.loki.config.** { *; }",
"-dontoptimize", "-dontoptimize",
"-dontobfuscate",
"-dontpreverify",
"-allowaccessmodification", "-allowaccessmodification",
"-repackageclasses", "mdbd",
"-keepattributes", "Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LineNumberTable,EnclosingMethod" "-keepattributes", "Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LineNumberTable,EnclosingMethod"
) )
@ -117,7 +128,5 @@ tasks {
} }
} }
named("build") { // obfuscation is optional — run `gradle obfuscate` manually
dependsOn("replaceWithObfuscated")
}
} }

View file

@ -0,0 +1,145 @@
package dev.loki.gate;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
class ExitGateTest {
private ExitGate gate;
@BeforeEach
void setUp() {
gate = new ExitGate(new org.bukkit.Location(null, 100, 64, 200));
}
@Test
void initialState_shouldBeClosedAndNotPowered() {
assertEquals(ExitGate.GateState.CLOSED, gate.getState());
assertEquals(0.0, gate.getProgress());
assertEquals(0, gate.getProgressPercent());
assertFalse(gate.isPowered());
assertTrue(gate.isClosed());
assertFalse(gate.isOpening());
assertFalse(gate.isOpen());
}
@Test
void setPowered_shouldTransitionToOpening() {
gate.setPowered(true);
assertEquals(ExitGate.GateState.OPENING, gate.getState());
assertTrue(gate.isPowered());
assertTrue(gate.isOpening());
assertFalse(gate.isClosed());
assertFalse(gate.isOpen());
}
@Test
void setProgressDirect_shouldIncreaseProgress() {
gate.setPowered(true);
gate.setProgressDirect(0.5);
assertEquals(0.5, gate.getProgress());
assertEquals(50, gate.getProgressPercent());
assertTrue(gate.isOpening());
}
@Test
void setProgressDirect_shouldCompleteWhenReachingOne() {
gate.setPowered(true);
gate.setProgressDirect(1.0);
assertEquals(1.0, gate.getProgress());
assertEquals(ExitGate.GateState.OPEN, gate.getState());
assertTrue(gate.isOpen());
}
@Test
void setProgressDirect_shouldClampToMax() {
gate.setPowered(true);
gate.setProgressDirect(2.0);
assertEquals(1.0, gate.getProgress());
assertTrue(gate.isOpen());
}
@Test
void setProgressDirect_shouldNotExceedMin() {
gate.setPowered(true);
gate.setProgressDirect(-0.5);
assertEquals(0.0, gate.getProgress());
}
@Test
void setProgressDirect_shouldNotWork_whenGateIsClosed() {
// Not powered — still CLOSED
gate.setProgressDirect(0.5);
assertEquals(0.0, gate.getProgress());
assertTrue(gate.isClosed());
}
@Test
void updateProgress_shouldWorkIncrementally() {
gate.setPowered(true);
gate.updateProgress(0.3);
assertEquals(0.3, gate.getProgress());
gate.updateProgress(0.3);
assertEquals(0.6, gate.getProgress());
}
@Test
void reset_shouldRestoreInitialState() {
gate.setPowered(true);
gate.setProgressDirect(0.8);
gate.reset();
assertEquals(ExitGate.GateState.CLOSED, gate.getState());
assertEquals(0.0, gate.getProgress());
assertEquals(0, gate.getProgressPercent());
assertFalse(gate.isPowered());
assertTrue(gate.isClosed());
}
@Test
void saveAndRestoreProgress_shouldPersist() {
gate.setPowered(true);
gate.setProgressDirect(0.4);
assertEquals(0.4, gate.getProgress());
// Simulate player walks away then returns in same session
gate.saveProgress();
gate.setProgressDirect(0.0); // simulate some other state change
gate.restoreProgress();
assertEquals(0.4, gate.getProgress());
}
@Test
void isInRange_shouldCheckDistance() {
org.bukkit.World w = mock(org.bukkit.World.class);
var gateLoc = new org.bukkit.Location(w, 100, 64, 200);
var close = new org.bukkit.Location(w, 101, 64, 200);
var far = new org.bukkit.Location(w, 110, 64, 200);
ExitGate g = new ExitGate(gateLoc);
assertTrue(g.isInRange(close, 3.0));
assertFalse(g.isInRange(far, 3.0));
}
@Test
void startOpening_shouldWork_whenClosedAndPowered() {
gate.setPowered(true);
gate.startOpening();
assertTrue(gate.isOpening());
}
@Test
void startOpening_shouldNotWork_whenNotPowered() {
gate.startOpening();
assertTrue(gate.isClosed());
}
@Test
void spawnAndRemoveGateBlock_shouldNotThrowWithNullWorld() {
// Location has null world so block ops are no-ops — just verify no exception
assertDoesNotThrow(() -> gate.spawnGateBlock());
assertDoesNotThrow(() -> gate.removeGateBlock());
}
}

View file

@ -0,0 +1,241 @@
package dev.loki.phase;
import dev.loki.config.ConfigManager;
import dev.loki.config.MessagesConfig;
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 net.kyori.adventure.text.Component;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
/**
* Tests win condition logic by spying on GameplayWinConditions and
* stubbing Bukkit-bound announce methods. This avoids loading
* Paper API classes (Material, Sound, GameMode) whose static
* initializers require a running server.
*/
@ExtendWith(MockitoExtension.class)
class GameplayWinConditionsTest {
private GameplayWinConditions conditions;
// Reusable stubs (reflection-based) for allSurvivorsIncapacitated
private java.lang.reflect.Method incapacitatedMethod;
@BeforeEach
void setUp() throws Exception {
conditions = new GameplayWinConditions();
incapacitatedMethod = GameplayWinConditions.class
.getDeclaredMethod("allSurvivorsIncapacitated", GameSession.class);
incapacitatedMethod.setAccessible(true);
}
// =========================================================================
// Helper: build session with player map
// =========================================================================
private GameSession sessionWith(Map<UUID, BasePlayer> players) {
GameSession session = mock(GameSession.class);
lenient().when(session.getPlayers()).thenReturn(players);
// Mock messages chain for GameplayWinConditions
mdbd plugin = mock(mdbd.class);
ConfigManager configManager = mock(ConfigManager.class);
MessagesConfig messages = mock(MessagesConfig.class);
lenient().when(session.getPlugin()).thenReturn(plugin);
lenient().when(plugin.getConfigManager()).thenReturn(configManager);
lenient().when(configManager.getMessages()).thenReturn(messages);
lenient().when(messages.get(anyString())).thenReturn(Component.text("test"));
lenient().when(messages.get(anyString(), any())).thenReturn(Component.text("test"));
return session;
}
private Killer onlineKiller() {
Killer k = mock(Killer.class);
lenient().when(k.isKiller()).thenReturn(true);
Player p = mock(Player.class);
lenient().when(p.isOnline()).thenReturn(true);
lenient().when(k.getPlayer()).thenReturn(p);
return k;
}
private Survivor survivorWithState(PlayerState state) {
Survivor s = mock(Survivor.class);
lenient().when(s.isKiller()).thenReturn(false);
lenient().when(s.getState()).thenReturn(state);
return s;
}
/** Invoke private allSurvivorsIncapacitated via reflection. */
private boolean allIncapacitated(GameSession session) {
try {
return (boolean) incapacitatedMethod.invoke(conditions, session);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// =========================================================================
// Bug #3 — ESCAPED survivor blocks killer win
// =========================================================================
@Test
void incapacitated_withEscapedAndRestDisabled_shouldReturnTrue() {
Map<UUID, BasePlayer> players = new HashMap<>();
players.put(UUID.randomUUID(), onlineKiller());
players.put(UUID.randomUUID(), survivorWithState(PlayerState.ESCAPED));
players.put(UUID.randomUUID(), survivorWithState(PlayerState.DOWNED));
assertTrue(allIncapacitated(sessionWith(players)),
"ESCAPED survivors should be counted as incapacitated (bug #3)");
}
@Test
void incapacitated_withEscapedAndHealthy_shouldReturnFalse() {
Map<UUID, BasePlayer> players = new HashMap<>();
players.put(UUID.randomUUID(), onlineKiller());
players.put(UUID.randomUUID(), survivorWithState(PlayerState.ESCAPED));
players.put(UUID.randomUUID(), survivorWithState(PlayerState.HEALTHY));
assertFalse(allIncapacitated(sessionWith(players)),
"Healthy survivor means not all incapacitated");
}
@Test
void incapacitated_withAllTerminalStates_shouldReturnTrue() {
Map<UUID, BasePlayer> players = new HashMap<>();
players.put(UUID.randomUUID(), onlineKiller());
players.put(UUID.randomUUID(), survivorWithState(PlayerState.DEAD));
players.put(UUID.randomUUID(), survivorWithState(PlayerState.ESCAPED));
assertTrue(allIncapacitated(sessionWith(players)));
}
// =========================================================================
// Bug #4 — Zero survivors → killer win
// =========================================================================
@Test
void zeroSurvivors_shouldNotHaveIncapacitatedTrue() {
Map<UUID, BasePlayer> players = new HashMap<>();
players.put(UUID.randomUUID(), onlineKiller());
assertFalse(allIncapacitated(sessionWith(players)),
"With no survivors, allSurvivorsIncapacitated returns false (bug #4)");
}
// =========================================================================
// Normal incapacitation states
// =========================================================================
@Test
void allDowned_shouldBeIncapacitated() {
Map<UUID, BasePlayer> players = new HashMap<>();
players.put(UUID.randomUUID(), onlineKiller());
players.put(UUID.randomUUID(), survivorWithState(PlayerState.DOWNED));
players.put(UUID.randomUUID(), survivorWithState(PlayerState.DOWNED));
assertTrue(allIncapacitated(sessionWith(players)));
}
@Test
void allHoodedCarriedDead_shouldBeIncapacitated() {
Map<UUID, BasePlayer> players = new HashMap<>();
players.put(UUID.randomUUID(), onlineKiller());
players.put(UUID.randomUUID(), survivorWithState(PlayerState.HOOKED));
players.put(UUID.randomUUID(), survivorWithState(PlayerState.CARRIED));
players.put(UUID.randomUUID(), survivorWithState(PlayerState.DEAD));
assertTrue(allIncapacitated(sessionWith(players)));
}
@Test
void healthySurvivor_shouldNotBeIncapacitated() {
Map<UUID, BasePlayer> players = new HashMap<>();
players.put(UUID.randomUUID(), onlineKiller());
players.put(UUID.randomUUID(), survivorWithState(PlayerState.HEALTHY));
assertFalse(allIncapacitated(sessionWith(players)));
}
@Test
void injuredSurvivor_shouldNotBeIncapacitated() {
Map<UUID, BasePlayer> players = new HashMap<>();
players.put(UUID.randomUUID(), onlineKiller());
players.put(UUID.randomUUID(), survivorWithState(PlayerState.INJURED));
assertFalse(allIncapacitated(sessionWith(players)));
}
// =========================================================================
// checkWinConditions — spy-based tests for bug #4 and bug #9
// These use a spy so announce methods (which load Sound/GameMode) are
// stubbed out to avoid Paper API class loading issues outside a server.
// =========================================================================
/** Helper: run checkWinConditions on a spy with Bukkit broadcast suppressed. */
private void checkOnSpy(GameplayWinConditions spy, GameSession session) {
try (var bukkit = mockStatic(Bukkit.class)) {
bukkit.when(() -> Bukkit.broadcast(any(Component.class))).then(inv -> null);
spy.checkWinConditions(session);
}
}
@Test
void zeroSurvivors_checkWinConditions_shouldEndAsKillerWin() {
Map<UUID, BasePlayer> players = new HashMap<>();
players.put(UUID.randomUUID(), onlineKiller());
GameSession session = sessionWith(players);
GameplayWinConditions spy = spy(new GameplayWinConditions());
doNothing().when(spy).announceKillerWin(any());
checkOnSpy(spy, session);
assertTrue(spy.isShouldEnd(), "checkWinConditions should end with zero survivors (bug #4)");
assertEquals("killer_win", spy.getWinReason());
}
@Test
void killerDisconnectGuard_shouldPreventDoubleProcess() throws Exception {
Map<UUID, BasePlayer> players = new LinkedHashMap<>();
// Offline killer must be first so getKiller() finds it deterministically
Killer offlineK = mock(Killer.class);
when(offlineK.isKiller()).thenReturn(true);
when(offlineK.getPlayer()).thenReturn(null);
players.put(UUID.randomUUID(), offlineK);
players.put(UUID.randomUUID(), survivorWithState(PlayerState.HEALTHY));
GameSession session = sessionWith(players);
GameplayWinConditions spy = spy(new GameplayWinConditions());
doNothing().when(spy).announceSurvivorsWin(any());
// Tick 1 — should process
checkOnSpy(spy, session);
assertTrue(spy.isShouldEnd());
assertEquals("survivors_win", spy.getWinReason());
// Tick 2 — should NOT re-process; guard catches shouldEnd=true
// Use a new static mock that fails if broadcast is called
try (var bukkit = mockStatic(Bukkit.class)) {
bukkit.when(() -> Bukkit.broadcast(any(Component.class)))
.thenThrow(new AssertionError("Second broadcast despite guard (bug #9)"));
spy.checkWinConditions(session);
}
// If we get here, the guard worked correctly
}
}

View file

@ -0,0 +1,44 @@
package dev.loki.player;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class PlayerStateTest {
@Test
void values_shouldContainAllExpectedStates() {
PlayerState[] states = PlayerState.values();
assertArrayEquals(new PlayerState[]{
PlayerState.HEALTHY,
PlayerState.INJURED,
PlayerState.DOWNED,
PlayerState.HOOKED,
PlayerState.CARRIED,
PlayerState.DEAD,
PlayerState.ESCAPED
}, states);
}
@Test
void deadAndEscaped_shouldBeTerminal() {
assertTrue(PlayerState.DEAD.isDeadOrEscaped());
assertTrue(PlayerState.ESCAPED.isDeadOrEscaped());
}
@Test
void aliveStates_shouldNotBeTerminal() {
assertFalse(PlayerState.HEALTHY.isDeadOrEscaped());
assertFalse(PlayerState.INJURED.isDeadOrEscaped());
assertFalse(PlayerState.DOWNED.isDeadOrEscaped());
assertFalse(PlayerState.HOOKED.isDeadOrEscaped());
assertFalse(PlayerState.CARRIED.isDeadOrEscaped());
}
@Test
void displayNames_shouldNotBeEmpty() {
for (PlayerState state : PlayerState.values()) {
assertNotNull(state.getDisplayName());
assertFalse(state.getDisplayName().isEmpty());
}
}
}

View file

@ -0,0 +1,93 @@
package dev.loki.util;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class SpawnUtilTest {
@Mock World world;
@Test
void resolveStandingLocation_shouldReturnNull_whenInputIsNull() {
assertNull(SpawnUtil.resolveStandingLocation(null));
}
@Test
void resolveStandingLocation_shouldReturnRaw_whenWorldIsNull() {
Location loc = new Location(null, 0, 64, 0);
Location result = SpawnUtil.resolveStandingLocation(loc);
assertSame(loc, result, "Should return raw location when world is null");
}
@Test
void resolveStandingLocation_shouldFallbackToHighestBlock_whenConfiguredYIsInAir() {
Block anyBlock = stubBlockAsAir();
when(world.getBlockAt(anyInt(), anyInt(), anyInt())).thenReturn(anyBlock);
when(world.getHighestBlockYAt(anyInt(), anyInt())).thenReturn(64);
when(world.isChunkLoaded(anyInt(), anyInt())).thenReturn(true);
when(world.getMinHeight()).thenReturn(-64);
Location input = new Location(world, 0.5, 100, 0.5);
Location result = SpawnUtil.resolveStandingLocation(input);
assertNotNull(result);
assertEquals(65.0, result.getY(), "Highest block (64) + 1 = 65");
assertEquals(0.5, result.getX());
assertEquals(0.5, result.getZ());
}
@Test
void resolveStandingLocation_shouldUseConfiguredY_whenChunkNotLoaded() {
Block anyBlock = stubBlockAsAir();
when(world.getBlockAt(anyInt(), anyInt(), anyInt())).thenReturn(anyBlock);
when(world.isChunkLoaded(anyInt(), anyInt())).thenReturn(false);
when(world.getHighestBlockYAt(anyInt(), anyInt())).thenReturn(0);
when(world.getMinHeight()).thenReturn(0);
Location input = new Location(world, 0.5, 50, 0.5);
Location result = SpawnUtil.resolveStandingLocation(input);
assertNotNull(result);
assertEquals(65.0, result.getY(), "Fallback should use 64 when configured Y is low");
}
/** Create a Block mock whose getType() returns a Mockito mock of Material. */
private static Block stubBlockAsAir() {
Block b = mock(Block.class);
Material mat = mock(Material.class);
lenient().when(mat.isAir()).thenReturn(true);
lenient().when(mat.isSolid()).thenReturn(false);
lenient().when(b.getType()).thenReturn(mat);
return b;
}
@Test
void blockBreakSpawn_shouldCenterOnBlockAndAddOne() {
Location blockLoc = new Location(world, 10, 64, 20);
Location result = SpawnUtil.blockBreakSpawn(blockLoc, 90f);
assertEquals(10.5, result.getX(), "Should be centered X");
assertEquals(65.0, result.getY(), "Should be block Y + 1");
assertEquals(20.5, result.getZ(), "Should be centered Z");
assertEquals(90f, result.getYaw());
assertEquals(0f, result.getPitch());
}
@Test
void blockBreakSpawn_shouldPreserveWorld() {
Location blockLoc = new Location(world, 0, 0, 0);
Location result = SpawnUtil.blockBreakSpawn(blockLoc, 0f);
assertSame(world, result.getWorld());
}
}