fix: build — add test dependencies, fix ProGuard, remove obfuscation from default build
This commit is contained in:
parent
7c7562a8d4
commit
183df22d13
6 changed files with 1025 additions and 493 deletions
|
|
@ -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+*
|
||||||
|
|
|
||||||
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
145
src/test/java/dev/loki/gate/ExitGateTest.java
Normal file
145
src/test/java/dev/loki/gate/ExitGateTest.java
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
241
src/test/java/dev/loki/phase/GameplayWinConditionsTest.java
Normal file
241
src/test/java/dev/loki/phase/GameplayWinConditionsTest.java
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/test/java/dev/loki/player/PlayerStateTest.java
Normal file
44
src/test/java/dev/loki/player/PlayerStateTest.java
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
93
src/test/java/dev/loki/util/SpawnUtilTest.java
Normal file
93
src/test/java/dev/loki/util/SpawnUtilTest.java
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue