diff --git a/COMMANDS_GUIDE.md b/COMMANDS_GUIDE.md deleted file mode 100644 index 6b94582..0000000 --- a/COMMANDS_GUIDE.md +++ /dev/null @@ -1,489 +0,0 @@ -# Dead by Daylight Plugin — Руководство команд - -**Версия:** 1.2 -**Обновлено:** Июнь 2026 -**Платформа:** Minecraft 1.21.5 (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 -``` - -- Выдаёт деревянный топорик (WOODEN_AXE) с подписью DBD Setup Wand -- **ЛКМ** по блоку = точка 1 (pos1) -- **ПКМ** по блоку = точка 2 (pos2) -- Только для ворот. Для остальных объектов ванд не нужен. - ---- - -### /dbdsetup set \<тип\> - -Войти в режим установки объекта. **Сломай блок** — объект установится на его месте, блок восстановится. - -``` -/dbdsetup set generator -/dbdsetup set hook -/dbdsetup set pallet -/dbdsetup set chest -/dbdsetup set killer-spawn -/dbdsetup set survivor-spawn -``` - -| Тип | Описание | Рекомендуемое кол-во | -|-----|----------|---------------------| -| `generator` | Генератор | 7 | -| `hook` | Крюк | 7–10 | -| `pallet` | Паллет | 15–20 | -| `chest` | Сундук | 5–10 | -| `killer-spawn` | Спавн киллера | 1 | -| `survivor-spawn` | Спавн выжившего | 4 | - -``` -→ Режим установки: hook -→ Сломай блок чтобы установить (блок восстановится) -(ломаешь блок) -→ Крюк установлен: [game, 150, 65, 200] -``` - ---- - -### /dbdsetup set gate A|B - -Установить ворота через две точки топориком. - -``` -/dbdsetup wand — получить топорик -ЛКМ по блоку → точка 1 — нижний левый угол -ПКМ по блоку → точка 2 — верхний правый угол -/dbdsetup set gate A — установить ворота A в центре между точками -/dbdsetup set gate B — установить ворота B -``` - -Центр ворот вычисляется: X = средняя точка, Y = минимальная из двух. - -``` -→ [Wand] Точка 1: [game, 100, 64, 200] -→ [Wand] Точка 2: [game, 110, 64, 210] -→ Ворота A установлены: [game, 105, 64, 205] -``` - ---- - -### /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. Расставить объекты: - - `/dbdsetup set generator` → ломать блоки для генераторов - - `/dbdsetup set hook` → ломать блоки для крюков - - `/dbdsetup set pallet` → ломать блоки для паллетов - - `/dbdsetup set chest` → ломать блоки для сундуков -7. Установить спавны: - - `/dbdsetup set killer-spawn` → спавн киллера - - `/dbdsetup set survivor-spawn` → 4 раза для выживших -8. Установить ворота: - - `/dbdsetup wand` → получить топорик - - ЛКМ/ПКМ по блокам → отметить углы ворот - - `/dbdsetup set gate A` и `/dbdsetup set gate B` - -### Тест - -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 set <тип>` | admin | Установить объект (сломать блок) | -| `/dbdsetup set gate A\|B` | admin | Установить ворота (из точек ванда) | -| `/dbdsetup build` | admin | Построить карту | -| `/dbdsetup clear` | admin | Очистить карту | -| `/dbdsetup lobby` | admin | Сохранить спавн лобби | - ---- - -*Dead by Daylight Minecraft Plugin — актуально для версии 1.1+* diff --git a/DBD_Plugin_TZ.md b/DBD_Plugin_TZ.md deleted file mode 100644 index 4424aa9..0000000 --- a/DBD_Plugin_TZ.md +++ /dev/null @@ -1,523 +0,0 @@ -# DEAD BY DAYLIGHT — Minecraft Plugin -## Техническое Задание (ТЗ) -> Полная реализация механик оригинальной игры 4v1 - ---- - -## Содержание - -1. [Общее описание проекта](#1-общее-описание-проекта) -2. [Распределение ролей и старт игры](#2-распределение-ролей-и-старт-игры) -3. [Механики выжившего](#3-механики-выжившего-survivor) -4. [Генераторы](#4-генераторы-generators) -5. [Механики киллера](#5-механики-киллера-killer) -6. [Паллеты и окна](#6-паллеты-pallets-и-окна-windows) -7. [Выходные ворота и люк](#7-выходные-ворота-exit-gates-и-люк-hatch) -8. [Предметы для выживших](#8-предметы-items-для-выживших) -9. [Bloodpoints и прогрессия](#9-bloodpoints-и-прогрессия) -10. [Система перков](#10-система-перков-perks) -11. [Эндгейм и итоги матча](#11-эндгейм-и-итоги-матча) -12. [Пользовательский интерфейс](#12-пользовательский-интерфейс-hud) -13. [Конфигурация](#13-конфигурация-configyml) -14. [Команды и права](#14-команды-и-права) -15. [Архитектура плагина](#15-архитектура-плагина) -16. [Приоритет разработки](#16-приоритет-разработки) - ---- - -## 1. Общее описание проекта - -### 1.1 Цель плагина - -Плагин реализует полноценную мини-игру Dead by Daylight (4v1) на Minecraft-сервере. Используется готовая карта, предоставленная заказчиком. Плагин управляет всем игровым процессом: распределением ролей, механиками выживших и киллера, победными условиями, интерфейсом и звуками. - -### 1.2 Технические требования - -| Параметр | Значение | -|---|---| -| Платформа | Paper / Spigot 1.20.x+ | -| Язык | Java 17+ | -| Зависимости | ProtocolLib, Citizens2 (опционально), PacketEvents | -| Кол-во игроков | 5 (1 киллер + 4 выживших) | -| Карта | Предоставляется отдельно (worldname задаётся в `config.yml`) | -| Gamemode | Adventure mode на протяжении всей игры | - ---- - -## 2. Распределение ролей и старт игры - -### 2.1 Лобби и ожидание - -- Лобби-мир отдельный от игровой карты. -- Минимум для старта: 5 игроков (настраивается: `min_players`, `max_players`). -- Таймер ожидания: 60 секунд после набора минимума (`countdown_seconds`). -- Если игроков < минимума — таймер сбрасывается. -- В лобби показывается статус: сколько игроков зашло / нужно. - -### 2.2 Назначение ролей - -- Один игрок случайно становится **Киллером (Killer)**. -- Остальные 4 становятся **Выжившими (Survivors)**. -- Реализовать систему очереди: если игрок давно не был киллером — шанс повышается. -- Роли объявляются через Title + Subtitle (красный текст для киллера, белый для выжившего). -- Роль киллера не раскрывается другим выжившим до спавна (5-секундная задержка перед телепортом киллера). - -### 2.3 Спавн - -- Спавн-точки прописаны в `config.yml` (4 точки для выживших + 1 для киллера). -- Выжившие телепортируются одновременно. -- Киллер телепортируется через **5 секунд** после выживших (grace period). -- Во время grace period киллер видит таймер в ActionBar. -- Генераторы, хуки, выходы и другие объекты расставляются при старте игры по координатам из `config.yml`. - -> ⚠ Все координаты (спавны, хуки, генераторы, выходы, подвал) задаются в `config.yml` — не хардкодятся в коде. - ---- - -## 3. Механики выжившего (Survivor) - -### 3.1 Состояния здоровья - -Выживший имеет три состояния — переходы строго последовательные: - -| Состояние | Описание | Визуал в Minecraft | -|---|---|---| -| **Healthy** (здоров) | Может бежать, прыгать, взаимодействовать | Полное здоровье, без эффектов | -| **Injured** (ранен) | Оставляет следы крови, издаёт стоны, скорость чуть снижена | Частицы крови, звук боли каждые 3 сек | -| **Downed** (повален) | Ползёт по полу, не может бежать или взаимодействовать | Crawl-анимация (Sneak + замедление), скорость 30% | -| **Hooked** (на крюке) | Привязан к крюку, ждёт спасения | Заморожен, анимация качания на крюке | -| **Dead** (мёртв) | Выбывает из игры | Spectator mode | - -### 3.2 Переходы состояний - -- **Healthy → Injured:** одно попадание от киллера. -- **Injured → Downed:** второе попадание от киллера. -- **Downed → Hooked:** киллер подбирает (ПКМ) и несёт к крюку. -- **Hooked → Dead:** истекло 2 стадии крюка, никто не спас. -- **Injured → Healthy:** использование аптечки или спасение с крюка. - -### 3.3 Передвижение - -- **Healthy:** обычная скорость + возможность спринта. -- **Injured:** скорость снижена на 5% (speed effect). -- **Downed:** только ползти — отрицательный Speed, принудительный Sneak, без прыжков. -- **Спринт (Exhaustion):** можно спринтовать конечное время (`exhaustion_seconds`, по умолчанию 3 сек спринта, затем 5 сек кулдауна). - -> ⚠ Спринт — ключевая механика для чейса. Реализовать через `isSprinting()` + кастомный кулдаун. - -### 3.4 Исцеление (Healing) - -- **Аптечка (Med-Kit):** зажать ПКМ 10 секунд, прогресс-бар в ActionBar. -- **Исцеление другим выжившим:** подойти вплотную, зажать ПКМ 16 секунд (медленнее, чем аптечкой). -- Если движение прервалось (игрок отошёл, получил удар) — прогресс сбрасывается. -- Иконка прогресса в ActionBar: `[████░░░░░░] Исцеление 40%` - -### 3.5 Механика крюка (Hook) - -После того как киллер повесил выжившего на крюк: - -| Стадия | Длительность | Событие | -|---|---|---| -| Stage 1 | 60 секунд | Выживший висит. Можно спасти. В ActionBar — таймер. | -| Stage 1 → 2 | Автоматически | Если не спасли за 60 сек — переход на Stage 2. | -| Stage 2 | 60 секунд | Выживший борется с Сущностью. Каждые 2 сек — Skill Check (QTE). | -| Stage 2 провален | Мгновенно | Выживший умирает — переход в Spectator. | -| Спасение с крюка | 2 секунды | Другой выживший снимает с крюка (прогресс-бар). Стадия запоминается. | - -- Один и тот же выживший может быть повешен максимум **3 раза** (`hook_count`). На 3-й раз — мгновенная смерть. -- Крюки в подвале (Basement) не уничтожаются и дают бонусные Bloodpoints. - -> ⚠ Skill Check на Stage 2 — случайное появление индикатора в Title. Игрок должен нажать Shift или Drop в нужный момент. Промах — мгновенная смерть. - -### 3.6 Спасение с крюка - -- Выживший подходит к крюку с союзником на расстояние 2 блока. -- Зажимает ПКМ (или Sneak) в течение 2 секунд — прогресс в ActionBar. -- Спасённый получает статус Injured (или остаётся Injured). -- Спасатель не получает защиты — может быть немедленно атакован. -- **Borrowed Time:** если киллер стоит рядом с крюком в момент спасения — спасённый получает 10 сек защиты Endurance (не переходит в Downed с первого удара). - -### 3.7 Ползание (Dying State) - -- Повалённый выживший ползёт со скоростью 30% от нормальной. -- Может ползти к другому выжившему — тот может его поднять (Recovery, 4 секунды). -- После Recovery выживший встаёт в статусе Injured. -- Самовосстановление невозможно без специального перка. -- Повалённый выживший издаёт стоны (слышимые киллеру). - ---- - -## 4. Генераторы (Generators) - -### 4.1 Общие правила - -- На карте спавнится **7 генераторов**, нужно починить **5**. -- Количество настраивается: `total_generators`, `required_generators` в `config.yml`. -- Починённые генераторы светятся (частицы + звук). -- Киллер видит ауру всех непочиненных генераторов (через частицы или Sidebar). - -### 4.2 Процесс починки - -- Выживший подходит к генератору и зажимает ПКМ — начинается починка. -- Прогресс отображается в ActionBar. -- Базовое время починки: **80 секунд** (один выживший). -- Два выжившего на генераторе: скорость ×1.5 (`gen_coop_multiplier`). -- Три и более: скорость ×2.0 (ограничено). -- Если выживший отошёл или получил удар — прогресс частично откатывается (regression). - -### 4.3 Skill Checks (Проверки навыка) - -- В Title показывается круговой индикатор (симулируется через Title-анимации). -- Игрок должен нажать **Drop (Q)** в нужный момент (попасть в зелёную зону). -- **Успешный Skill Check:** +5% к прогрессу генератора + бонусные Bloodpoints. -- **Провальный Skill Check:** взрыв — генератор теряет 10% прогресса, громкий звук (слышен киллеру в радиусе 40 блоков). -- Частота skill check: раз в 20–40 секунд (рандомно). - -> ⚠ Звук провального skill check — важнейший инструмент для киллера. Реализовать через `playSound()` с большим радиусом. - -### 4.4 Регрессия генератора - -- Если все выжившие отошли от работающего генератора — он начинает откатываться (0.25% в секунду). -- Киллер может ударить по генератору (ПКМ) — **Kick:** мощная регрессия 2.5% в секунду на 30 секунд. -- Регрессию можно остановить — выживший подходит и нажимает ПКМ. -- Взорванный генератор издаёт звук и выбрасывает частицы взрыва. - ---- - -## 5. Механики киллера (Killer) - -### 5.1 Базовые характеристики - -| Параметр | Значение | Настройка | -|---|---|---| -| Скорость | 115% от скорости выжившего | `killer_speed` | -| Terror Radius | 32 блока | `terror_radius` | -| Урон атаки | Переводит в Injured / Downed | Не числовой — по состоянию | -| Кулдаун атаки | 2.5 секунды | `attack_cooldown` | -| Перенос выжившего | 92% скорости | `carry_speed` | - -### 5.2 Terror Radius и Red Stain - -- **Terror Radius** — зона 32 блока вокруг киллера. Выжившие в этой зоне слышат сердцебиение. -- Интенсивность звука увеличивается при сближении (3 уровня: далеко / близко / рядом). -- **Red Stain** — красное свечение на земле за киллером (частицы). Показывает направление взгляда. -- Реализация Red Stain: каждые 0.5 сек спавнить Dust-частицы красного цвета позади головы киллера. - -### 5.3 Атака и удар - -- **Базовая атака:** ЛКМ по выжившему. Урон по состояниям (Healthy→Injured или Injured→Downed). -- После удара — кулдаун 2.5 сек (замедление на 0.5 сек). -- Нельзя атаковать сквозь окна / паллеты (проверка LineOfSight). -- **Bloodlust:** если киллер преследует выжившего >15 секунд без попадания — бонус скорости: - - Тир 1: +3% - - Тир 2: +5% - - Тир 3: +8% - - Тиры сбрасываются при ударе или смене цели. - -### 5.4 Подъём и перенос выжившего - -- Когда выживший Downed — киллер нажимает ПКМ для подъёма (2.5 сек анимации). -- Во время переноса: киллер замедлен до 92% скорости, не может атаковать. -- Выживший сидит на киллере (через ArmorStand или Mount). -- Чтобы повесить: киллер подходит к хуку и нажимает ПКМ. -- **Wiggle (вырывание):** выживший нажимает Sneak/Jump быстро — прогресс побега в ActionBar. При 100% — выживший падает рядом в Downed статусе. -- Время вырывания: **16 секунд** (`wiggle_duration`). - -### 5.5 Взаимодействие с генераторами - -- Киллер нажимает ПКМ на генератор — **Kick** (регрессия, см. раздел 4.4). -- Киллер видит ауру всех незаконченных генераторов в Sidebar (иконки + расстояние). - -### 5.6 Подвал (Basement) - -- Специальная зона на карте с 4 крюками. -- Крюки в подвале **нельзя саботировать**. -- Перевод выжившего в подвал засчитывается быстрее + бонус Bloodpoints. - ---- - -## 6. Паллеты (Pallets) и Окна (Windows) - -### 6.1 Паллеты - -- Паллеты — деревянные барьеры на карте (блоки, отмеченные в конфиге). -- Выживший подходит к паллете и нажимает ПКМ — паллета падает (standing → dropped). -- Если киллер коснулся упавшей паллеты — **Stun 2.5 сек** (звук, частицы). -- Киллер может **сломать паллету**: зажать ПКМ на 3 секунды. Паллета исчезает навсегда. -- Количество паллет на карте конечно. Статус каждой хранится в памяти. -- Выживший может пробежать через упавшую паллету (Vault, 0.6 сек). - -### 6.2 Окна (Windows) - -- Окна — прыжковые зоны в стенах карты (координаты в конфиге). -- Выживший запрыгивает в окно: **1 секунда** анимации Vault (замедление → ускорение). -- Киллер тоже может прыгнуть в окно, но медленнее (1.5 сек). -- **Fake Vault (Mindgame):** игрок может зайти в начало вота и выйти обратно. - -> ⚠ Паллеты и окна — основа loop-механики чейса. Без правильного расположения на карте игра будет несбалансированной. - ---- - -## 7. Выходные ворота (Exit Gates) и Люк (Hatch) - -### 7.1 Выходные ворота - -- После починки 5 генераторов активируются **2 выходных ворот** (`gate_locations`). -- Рубильник ворот загорается (смена блока, звук). -- Выживший взаимодействует с рубильником **20 секунд** (прогресс в ActionBar). -- Если прервать — прогресс **не откатывается** (в отличие от генераторов). -- Открытые ворота остаются открытыми навсегда. -- Выживший пересекает границу выхода → **ESCAPED!** (Title). -- **Эндгейм-коллапс:** через 2 минуты после открытия первых ворот — все оставшиеся выжившие умирают. - -### 7.2 Люк (Hatch) - -- Люк появляется, когда выживших меньше, чем непочиненных генераторов. -- Правило: если X выживших живы и < X генераторов починено — люк открыт. -- Последний выживший **всегда** видит открытый люк. -- Координата люка — `hatch_location` в конфиге. -- Прыжок в люк = Escape (побег через люк). -- Киллер может **закрыть люк** (ПКМ). После этого активируется End Game Collapse. -- Закрытый люк можно открыть выжившим с **Ключом (Key)** за 2.5 секунды. - ---- - -## 8. Предметы (Items) для выживших - -Предметы выдаются игрокам перед игрой или находятся в Сундуках (Chests) на карте. Максимум **1 предмет** в руках. - -| Предмет | Действие | MC-предмет | Ограничение | -|---|---|---|---| -| **Med-Kit** | Лечит Injured → Healthy за 10 сек | Red Dye / Custom | 1 заряд | -| **Flashlight** | Слепит киллера (Blindness 2 сек) | Glowstone Dust / Custom | 8 использований | -| **Toolbox** | Ускоряет починку гена на 30% | Iron Pickaxe / Custom | 1 генератор | -| **Key** | Открывает закрытый люк | Gold Nugget / Custom | 1 использование | -| **Map** | Показывает ауру генераторов выжившим | Paper / Custom | Пассивно | - -### 8.1 Сундуки - -- На карте расставлено N сундуков (`chest_locations` в конфиге). -- Каждый сундук при открытии выдаёт случайный предмет (весовая таблица в `loot_table.yml`). -- Открытие: **3 секунды** (прогресс-бар). Один сундук — один предмет навсегда. -- Киллер может видеть ауры сундуков (опционально, через перк). - ---- - -## 9. Bloodpoints и прогрессия - -Bloodpoints — внутриигровая валюта, начисляется за действия в раунде. Хранятся в базе данных (SQLite или YAML). - -| Действие | Выживший (BP) | Киллер (BP) | -|---|---|---| -| Починить генератор (полностью) | +1000 | — | -| Успешный Skill Check | +150 | — | -| Спасти с крюка | +800 | — | -| Исцелить союзника | +600 | — | -| Сбежать | +5000 | — | -| Повесить выжившего на крюк | — | +800 | -| Повалить выжившего | — | +500 | -| Пожертвовать выжившего (3 стадии) | — | +2000 | -| Сломать паллету | — | +200 | -| Ударить по генератору (Kick) | — | +100 | - -- Bloodpoints отображаются в Title после окончания матча. -- Статистика сохраняется в базе данных. - ---- - -## 10. Система перков (Perks) - -Перки — пассивные и активные способности. Каждый игрок берёт **4 перка** перед матчем (в GUI-меню выбора). - -### 10.1 Перки Выживших (минимум 10) - -| Перк | Эффект | -|---|---| -| **Self-Care** | Позволяет лечить себя без аптечки (вдвое медленнее) | -| **Dead Hard** | При нажатии Sneak во время спринта — кратковременный рывок (0.5 сек неуязвимости) | -| **Borrowed Time** | После спасения с крюка — 10 сек защита Endurance | -| **Sprint Burst** | При начале спринта — ускорение ×1.5 на 3 сек (кулдаун 40 сек) | -| **Iron Will** | Убирает звуки стонов в Injured состоянии | -| **Spine Chill** | Предупреждение (иконка + звук), если киллер смотрит на выжившего с 36 блоков | -| **Resilience** | В Injured/Downed — скорость починки +9%, скорость вота +9% | -| **Kindred** | После повешения на крюк — все союзники видят ауру киллера | -| **Prove Thyself** | За каждого выжившего на генераторе — скорость +15% (стакается) | -| **Adrenaline** | При открытии 5-го генератора — мгновенно Healthy + Sprint Burst 5 сек | - -### 10.2 Перки Киллера (минимум 10) - -| Перк | Эффект | -|---|---| -| **Hex: Ruin** | Все генераторы регрессируют вдвое быстрее (пока горит тотем) | -| **Corrupt Intervention** | 3 самых дальних генератора заблокированы 2 минуты в начале игры | -| **Pop Goes the Weasel** | После повешения — следующий Kick откатывает 25% прогресса гена | -| **Deadlock** | После починки любого гена — самый прогрессивный ген блокируется на 30 сек | -| **No Way Out** | После повешения — добавляется задержка при открытии ворот (+12/24/36 сек) | -| **Scourge Hook: Pain Resonance** | Повешение на Scourge Hook взрывает ближайший ген (−25%) | -| **Nowhere to Hide** | После Kick гена — ауры выживших видны 4 сек в радиусе 24 блоков | -| **Lethal Pursuer** | В начале игры — видеть ауры всех выживших 7 секунд | -| **BBQ & Chili** | После повешения — видеть ауры выживших >40 блоков на 4 сек | -| **Tinkerer** | Когда ген достигает 70% — уведомление + Undetectable на 12 сек | - -> ⚠ Перки реализовать через EventListener. Каждый перк — отдельный класс, реализующий интерфейс `IPerk` с методами `onEquip()`, `onTrigger()`, `onRemove()`. - -### 10.3 Тотемы (Totems) - -- На карте **5 тотемов** (`totem_locations` в конфиге). -- **Dull Totem:** выживший ломает за 14 секунд — начисляются Bloodpoints. -- **Hex Totem:** активируется при использовании Hex-перка. Пока горит — перк активен. -- При уничтожении Hex Totema — связанный перк деактивируется навсегда. - ---- - -## 11. Эндгейм и итоги матча - -### 11.1 Условия победы - -| Сторона | Победа | Поражение | -|---|---|---| -| **Выжившие** | Минимум 2 выживших сбежали (ворота или люк) | Все выжившие мертвы до побега | -| **Киллер** | Все 4 выжившие принесены в жертву / умерли | 2+ выжившие сбежали | - -### 11.2 Экран результатов - -- Все игроки телепортируются в лобби. -- В Chat выводится таблица: игрок, роль, Bloodpoints, статус (Escaped / Sacrificed). -- Title на 5 секунд: **ПОБЕДА / ПОРАЖЕНИЕ**. -- Личная статистика матча: генераторы, спасения, хуки, урон. - -### 11.3 Эндгейм Коллапс (End Game Collapse) - -- Активируется при: открытии ворот ИЛИ закрытии люка. -- Таймер **120 секунд** показывается всем в ActionBar красным текстом. -- По истечении — все оставшиеся выжившие мгновенно умирают. - ---- - -## 12. Пользовательский интерфейс (HUD) - -### 12.1 ActionBar - -- Постоянно: статус игрока (`❤ Healthy` / `🩸 Injured` / `💀 Downed`). -- При взаимодействии: прогресс-бар починки / лечения / спасения. -- Эндгейм Коллапс: красный таймер обратного отсчёта. - -### 12.2 Sidebar (Scoreboard) - -- **Выжившие:** количество починенных генераторов (`⚡ 2/5`), сколько выживших живо (`👥 3/4`). -- **Киллер:** сколько выживших на крюке / мертво, прогресс генераторов. - -### 12.3 Title / Subtitle - -- Оповещения: генератор починен, выживший на крюке, ворота открыты. -- Начало игры: роль + краткая инструкция. -- Skill Check: вспышка в Title с таймером. - -### 12.4 Звуки - -- **Terror Radius:** нарастающее сердцебиение по мере приближения киллера (3 уровня). -- **Генератор:** звук работы при починке, взрыв при провале skill check или Kick. -- **Спасение с крюка:** звук успеха. -- **Люк:** звук открытия / закрытия. -- **Ворота:** звук активации, гудение. - -> ⚠ Рекомендуется базовый Resource Pack для кастомных звуков. Без него — стандартные MC-звуки как замена. - ---- - -## 13. Конфигурация (config.yml) - -| Секция | Ключ | Тип | По умолчанию | -|---|---|---|---| -| `game` | `min_players` | int | 5 | -| `game` | `max_players` | int | 5 | -| `game` | `countdown_seconds` | int | 60 | -| `game` | `world_name` | string | `dbd_map` | -| `game` | `lobby_world` | string | `lobby` | -| `killer` | `killer_speed` | double | 1.15 | -| `killer` | `terror_radius` | int | 32 | -| `killer` | `attack_cooldown` | double | 2.5 | -| `killer` | `carry_speed` | double | 0.92 | -| `killer` | `wiggle_duration` | double | 16.0 | -| `survivor` | `sprint_duration` | double | 3.0 | -| `survivor` | `sprint_cooldown` | double | 5.0 | -| `survivor` | `heal_self_time` | double | 10.0 | -| `survivor` | `heal_other_time` | double | 16.0 | -| `generator` | `total_generators` | int | 7 | -| `generator` | `required_generators` | int | 5 | -| `generator` | `repair_time` | double | 80.0 | -| `generator` | `gen_coop_multiplier` | double | 1.5 | -| `generator` | `regression_rate` | double | 0.25 | -| `hook` | `stage1_duration` | int | 60 | -| `hook` | `stage2_duration` | int | 60 | -| `hook` | `max_hook_count` | int | 3 | -| `endgame` | `collapse_duration` | int | 120 | - -**Дополнительные файлы конфигурации:** -- `locations.yml` — все спавн-точки, координаты генераторов, хуков, тотемов, сундуков, ворот, люка. -- `loot_table.yml` — весовая таблица предметов в сундуках. -- `perks.yml` — настройки перков (кулдауны, длительности). - ---- - -## 14. Команды и права - -| Команда | Описание | Право | -|---|---|---| -| `/dbd join` | Войти в очередь на игру | `dbd.play` | -| `/dbd leave` | Выйти из очереди / матча | `dbd.play` | -| `/dbd forcestart` | Принудительно запустить игру | `dbd.admin` | -| `/dbd stop` | Принудительно остановить игру | `dbd.admin` | -| `/dbd setkiller <игрок>` | Назначить игрока киллером | `dbd.admin` | -| `/dbd setspawn ` | Установить спавн-точку (по курсору) | `dbd.admin` | -| `/dbd setgen ` | Отметить блок как генератор | `dbd.admin` | -| `/dbd sethook ` | Отметить блок как крюк | `dbd.admin` | -| `/dbd setgate <1\|2>` | Отметить зону ворот | `dbd.admin` | -| `/dbd sethatch` | Отметить блок как люк | `dbd.admin` | -| `/dbd reload` | Перезагрузить конфиг | `dbd.admin` | -| `/dbd stats [игрок]` | Посмотреть статистику | `dbd.play` | - ---- - -## 15. Архитектура плагина - -### 15.1 Структура пакетов - -| Пакет | Назначение | -|---|---| -| `com.dbd.plugin.core` | Главный класс, GameManager, EventBus | -| `com.dbd.plugin.game` | GameSession, GameState, RoleAssigner, RoundTimer | -| `com.dbd.plugin.roles` | KillerController, SurvivorController, PlayerState | -| `com.dbd.plugin.mechanics` | GeneratorManager, HookManager, PalletManager, HatchManager, GateManager | -| `com.dbd.plugin.perks` | IPerk, PerkRegistry, PerkLoader, [все перки] | -| `com.dbd.plugin.items` | IItem, ItemRegistry, ChestManager | -| `com.dbd.plugin.ui` | HUDManager, ActionBarManager, TitleManager, SoundManager | -| `com.dbd.plugin.data` | DatabaseManager, PlayerStats, LocationsConfig | -| `com.dbd.plugin.commands` | DBDCommand, TabCompleter | - -### 15.2 Ключевые принципы - -- Каждый раунд — отдельный объект `GameSession` со своим состоянием. -- Все механики управляются через `BukkitScheduler` (tick-based системы). -- Перки реализованы через Event-Driven подход (не хардкод в механиках). -- Конфиги не перезаписывают пользовательские настройки при обновлении (`saveDefaultConfig`). -- Логирование всех событий в отдельный лог-файл (`logs/dbd.log`). - -> ⚠ Плагин должен корректно обрабатывать disconnect игрока во время матча — замена ботом или перераспределение ролей. - ---- - -## 16. Приоритет разработки - -| Этап | Функционал | Статус | -|---|---|---| -| **MVP (Этап 1)** | Старт/стоп игры, роли, спавн, базовые состояния выжившего, атака киллера | 🔴 В разработке | -| **Этап 2** | Генераторы (починка, прогресс, регрессия, skill check), хуки (все стадии) | 🔴 В разработке | -| **Этап 3** | Паллеты, окна, перенос, вырывание, люк, ворота, эндгейм | 🔴 В разработке | -| **Этап 4** | Предметы, сундуки, тотемы, базовые перки (10 survivor + 10 killer) | 🔴 В разработке | -| **Этап 5** | Полный HUD, звуки, Terror Radius, Red Stain, Bloodpoints, база данных | 🔴 В разработке | -| **Этап 6** | Балансировка, все остальные перки, тест-раунды, оптимизация | 🔴 В разработке | - ---- - -*ТЗ подготовлено с учётом всех оригинальных механик Dead by Daylight (4v1) | Версия 1.0* diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index 155b3ea..0000000 --- a/PLAN.md +++ /dev/null @@ -1,392 +0,0 @@ -# DBD Plugin — Баги и задачи - -## ✅ Закрыто в текущей сессии - -### SetupCommand (упрощено) -- [x] `/dbdsetup set <тип>` — ломаешь блок, объект ставится (без `select`) -- [x] Ванд только для ворот: ЛКМ=pos1, ПКМ=pos2 -- [x] `/dbdsetup set gate A|B` — установить ворота из точек ванда - -### Медкит -- [x] Требует Shift + ПКМ + стоять на месте (прерывается при движении или отпускании Shift) -- [x] После использования удаляется полностью (1 медкит = 1 лечение) -- [x] Медкит не выдаётся стартово — только из сундуков - -### Маньяк — атака -- [x] Post-hit cooldown 4 сек после ПОПАДАНИЯ (отдельно от swing cooldown 2.5 сек) -- [x] Параметр `killer.post-hit-cooldown` в `game.yml` - -### Маньяк — нокдаун / переноска -- [x] Игрок в нокдауне ложится (`setSwimming(true)`) и поза удерживается каждые 5 тиков -- [x] При подъёме, смерти, спасении — поза сбрасывается -- [x] Переноска: маньяк ПКМ → подбирает DOWNED в радиусе 3 блоков -- [x] При подъёме `setSwimming(false)` отменяет позу нокдауна - -### Таймер игры -- [x] 15 минут + BossBar с таймером для всех игроков (зелёный → жёлтый → красный) -- [x] По истечении — маньяк победил - -### После победы -- [x] Телепортирует всех в лобби -- [x] Scale сбрасывается до 1.0 -- [x] Инвентарь очищается - -### Лобби -- [x] Лимит 1 лобби на игрока -- [x] Broadcast при создании лобби убран — сообщение только создателю -- [x] Сообщение о входе игрока — только участникам сессии (не всему серверу) -- [x] После завершения игры лобби пересоздаёт сессию автоматически (onSessionFinished callback) -- [x] Лобби не засоряются — реестр чистит сессию через `GameSession.setOnStopped()` - -### PlayerQuitListener -- [x] Счётчик живых выживших корректный (считает после пометки DEAD) -- [x] Инвентарь, scale, поза сбрасываются при выходе из игры - -### GameplayPhase -- [x] Разбит на части: - - `GameplayPhase.java` — только lifecycle (enter/tick/exit) + делегирование (230 строк) - - `GameplayHookHandler.java` — тик крюков, Stage 2, Struggle, Sacrifice (87 строк) - - `GameplayWinConditions.java` — проверка победы/поражения, SPECTATOR (91 строка) - - `GameplayDamageSystem.java` — система урона, атаки, предметы (155 строк) - - `GameplayUI.java` — BossBar, sidebar, таймер (142 строки) - - `GameplayInteraction.java` — обработка взаимодействий (198 строк) - -### Лобби — повторный вход -- [x] **FIX**: Исправлен `GameManager.joinSession()` — теперь корректно обрабатывает повторный вход, сначала удаляя игрока из старой сессии - -### Хотбар маньяка -- [x] **FIX**: Добавлена проверка `!bp.isKiller()` в `ChestOpening.completeOpening()` — маньяк не получает лут из сундуков - -### Медкит из сундука -- [x] **WORKS**: Логика уже работает — `ChestLoot.rollLoot()` выдаёт медкит/фонарик при открытии сундука - -### Счётчик выживших -- [x] **VERIFIED**: Логика в `PlayerQuitListener.handleSurvivorDisconnect()` корректная — считает после пометки DEAD - -### Ворота -- [x] **FIX**: Добавлена интеграция `GateOpening` в `GameplayInteraction` — теперь вызывается при взаимодействии с порталом (HOPPER/PORTAL) - -### Кодировка и спавн в воздухе -- [x] **FIXED**: UTF-8 encoding для build - - Добавлена `org.gradle.jvmargs=-Dfile.encoding=UTF-8` в `gradle.properties` - - Добавлен `tasks.withType { filteringCharset = "UTF-8" }` в build.gradle.kts - - Русские символы и эмодзи теперь сохраняются корректно в конфигах -- [x] **FIXED**: Спавн в воздухе - - Добавлена корректировка Y-координаты (`Math.floor(y) + 1.0`) в `RoleAssigner.java` - - Players теперь стоят ОН block, не ИН block - - Работает для положительных и отрицательных Y (подземные карты) -- [x] **FIXED**: World loading — добавлен fallback к первому loaded world в `LocationsConfig.java` -- [x] **FIXED**: Удален неиспользуемый импорт Location из `PalletBreak.java` - ---- - -## ✅ Завершено - -### Заполнение сундуков при старте игры (случайный лут из ChestLoot) -- [x] Лут записывается в поле `Chest.loot` при создании -- [x] `ChestOpening.completeOpening()` использует pre-set loot - -### Визуализация ворот (частицы при открытии) -- [x] Progress particles: CRIT every 5 ticks -- [x] Completion burst: EXPLOSION_EMITTER + FLAME - -### Удаление перков и тотемов -- [x] Все totem файлы удалены -- [x] Все ссылки на тотемы удалены - -## ❌ Новые баги (текущая сессия) - -### ✅ ИСПРАВЛЕНО (7 багов) - -1. **SetupCommand MiniMessage формат** - - [x] **FIXED**: Заменены legacy `§f` и `§a` на MiniMessage `<#FFFFFF>` и `<#00FF00>` - -2. **Телепортация в конце игры не работает** - - [x] **FIXED**: Добавлена телепортация в `GameplayPhase.exit()` - - Также сбрасывается scale до 1.0 - -3. **Downed игрок берёт хук** - - [x] **FIXED**: Добавлена проверка состояния в `GameplayInteraction` - - Теперь DOWNED, CARRIED и HOOKED не могут спасать - -4. **Генераторы не чинятся за игрока** - - [x] **FIXED**: Подключена `GeneratorRepair` в `GameplayPhase` - - Добавлен обработчик в `GameplayInteraction` - - Механика: ПКМ по генератору → зажать ПКМ для починки - ---- - -### ✅ ИСПРАВЛЕНО / ПРОВЕРЕНО В ТЕКУЩЕЙ СЕССИИ (13 ЗАДАЧ) - -1. **Генераторы (#11) — generatorManager всегда null** - - [x] **FIXED**: Переинициализирован в GameplayPhase.initSubHandlers() - -2. **Телепорт (#10) — ранний выход отменял финал** - - [x] **FIXED**: Удалён телепорт из GameplayPhase.exit() - - Телепортация только в EndgamePhase.exit() (100L тиков задержка) - -3. **Wand для ворот — STICK vs WOODEN_AXE** - - [x] **FIXED**: Обновлена документация, код ищет WOODEN_AXE с NBT тегом - -4. **Бесконечный вход в лобби** (/dbd join X, /dbd join X, /dbd join X...) - - [x] **FIXED**: LobbyRegistry.joinLobby() проверяет текущую сессию - - Если игрок уже в другой сессии → выходит оттуда перед входом - -5. **Лобби удаляется при 0 игроков, tab-complete показывает** - - [x] **VERIFIED**: onPlayerLeftSession() НЕ удаляет лобби из реестра - - Лобби держится в памяти, session переиспользуется после игры - -6. **Pallet setup** (/dbdsetup set pallet) - - [x] **VERIFIED**: Режим selectingPallet работает, сохраняет через SpawnUtil.blockBreakSpawn() - -7. **Chest setup** (/dbdsetup set chest) - - [x] **VERIFIED**: Режим selectingChest работает, сохраняет с offset +0.5, +0, +0.5 - -8. **Эффекты ранения** (нет визуалов когда игрок INJURED) - - [x] **VERIFIED**: GameplayDamageSystem добавляет: - - Частицы HEART каждые 40 тиков - - Звук при попадании удара - - Title "РАНЕН!" - -9. **Двойной sneak для pickup** (киллер присел 2 раза возле DOWNED) - - [x] **VERIFIED**: GameplayInteraction проверяет интервал <450ms между sneaks - - Вызывает tryPickup() из KillerController - -10. **Админ-команды** (heal, down, unhook, state, hooks, kill) - - [x] **VERIFIED**: DBDAdminPlayerCommand полностью реализована - - /dbd admin heal [игрок] - - /dbd admin down [игрок] - - /dbd admin unhook [игрок] - - /dbd admin state <игрок> - - /dbd admin hooks <игрок> <0-3> - - /dbd admin kill [игрок] - -11. **Телепорт после завершения игры в лобби** - - [x] **VERIFIED**: EndgamePhase.exit() телепортирует через 100L тиков - - Сбрасывает scale до 1.0, очищает инвентарь - -12. **Генераторы не чинятся** (ПКМ возле генератора) - - [x] **FIXED**: setOnGeneratorInteract() вызывает generatorRepair.startRepair() - - generatorManager переинициализирован, не null - -13. **HOOKED игрок берёт хук** (игрок висит на крюке, может взять другой) - - [x] **VERIFIED**: setOnRescueAttempt() проверяет HOOKED, DOWNED, CARRIED - - Rescue невозможен если rescuer в одном из этих статусов - -14. **Спавн игроков в воздухе** (проблема с высотой) - - [x] **FIXED**: SpawnUtil.resolveStandingLocation() улучшена логика расчета Y - - Добавлена явная загрузка chunks в RoleAssigner перед спавном - - Используется getHighestBlockYAt() + 1 для стояния на блоке - -15. **GameMode.ADVENTURE при начале игры** - - [x] **FIXED**: - - RoleAssigner.assignRoles() устанавливает ADVENTURE при спавне - - LobbyPhase.enter() и onPlayerJoined() устанавливают ADVENTURE - - EndgamePhase.exit() сбрасывает на ADVENTURE перед лобби - -16. **/dbdsetup set gate A confirm не работал** - - [x] **FIXED**: Добавлена поддержка обоих форматов: - - `/dbdsetup set gate A confirm` - - `/dbdsetup set gate confirm A` - - Обновлена документация в COMMANDS_GUIDE.md - -17. **Переноска киллером (carry) — проблемы** - - [x] **FIXED**: Переработана логика KillerCarry с использованием ArmorStand: - - Игрок **видим** (не setInvisible) - - Камера **отделена** (beide мають пассажиры armor stand) - - Игрок **не двигает** киллера (mounted, не телепортируется) - - Пробел **работает** (jump event отслеживается) - ---- - -### ✅ ИТОГО: 17/17 ЗАДАЧ РЕШЕНО!1. **Генераторы (#11) — generatorManager всегда null** - - [x] **FIXED**: Переинициализирован в GameplayPhase.initSubHandlers() - -2. **Телепорт (#10) — ранний выход отменял финал** - - [x] **FIXED**: Удалён телепорт из GameplayPhase.exit() - - Телепортация только в EndgamePhase.exit() (100L тиков задержка) - -3. **Wand для ворот — STICK vs WOODEN_AXE** - - [x] **FIXED**: Обновлена документация, код ищет WOODEN_AXE с NBT тегом - -4. **Бесконечный вход в лобби** (/dbd join X, /dbd join X, /dbd join X...) - - [x] **FIXED**: LobbyRegistry.joinLobby() проверяет текущую сессию - - Если игрок уже в другой сессии → выходит оттуда перед входом в новую - -5. **Лобби удаляется при 0 игроков, tab-complete показывает** - - [x] **VERIFIED**: onPlayerLeftSession() НЕ удаляет лобби из реестра - - Лобби держится в памяти, session переиспользуется после игры - -6. **Pallet setup** (/dbdsetup set pallet) - - [x] **VERIFIED**: Режим selectingPallet работает, сохраняет через SpawnUtil.blockBreakSpawn() - -7. **Chest setup** (/dbdsetup set chest) - - [x] **VERIFIED**: Режим selectingChest работает, сохраняет с offset +0.5, +0, +0.5 - -8. **Эффекты ранения** (нет визуалов когда игрок INJURED) - - [x] **VERIFIED**: GameplayDamageSystem добавляет: - - Частицы HEART каждые 40 тиков - - Звук при попадании удара - - Title "РАНЕН!" - -9. **Двойной sneak для pickup** (киллер присел 2 раза возле DOWNED) - - [x] **VERIFIED**: GameplayInteraction проверяет интервал <450ms между sneaks - - Вызывает tryPickup() из KillerController - -10. **Админ-команды** (heal, down, unhook, state, hooks, kill) - - [x] **VERIFIED**: DBDAdminPlayerCommand полностью реализована - - /dbd admin heal [игрок] - - /dbd admin down [игрок] - - /dbd admin unhook [игрок] - - /dbd admin state <игрок> - - /dbd admin hooks <игрок> <0-3> - - /dbd admin kill [игрок] - -11. **Телепорт после завершения игры в лобби** - - [x] **VERIFIED**: EndgamePhase.exit() телепортирует через 100L тиков - - Сбрасывает scale до 1.0, очищает инвентарь - -12. **Генераторы не чинятся** (ПКМ возле генератора) - - [x] **FIXED**: setOnGeneratorInteract() вызывает generatorRepair.startRepair() - - generatorManager переинициализирован, не null - -13. **HOOKED игрок берёт хук** (игрок висит на крюке, может взять другой) - - [x] **VERIFIED**: setOnRescueAttempt() проверяет HOOKED, DOWNED, CARRIED - - Rescue невозможен если rescuer в одном из этих статусов - -14. **Спавн игроков в воздухе** (проблема с высотой) - - [x] **FIXED**: - - SpawnUtil.resolveStandingLocation() улучшена логика расчета Y - - Добавлена явная загрузка chunks в RoleAssigner перед спавном - - Используется getHighestBlockYAt() + 1 для стояния на блоке - ---- - -### ✅ ИТОГО: 14/14 ЗАДАЧ РЕШЕНО! - - ---- - -## ✅ СЕССИЯ 2: ГЕНЕРАТОР - ПРОГРЕСС И КРАУЧ (18-19 ЗАДАЧИ) - -### 18. Прогресс починки генератора не сохраняется -- [x] **FIXED**: Progress теперь сохраняется в Generator объекте между repair attempts -- Механика: - - Игрок присаживается рядом генератором → `onSneak()` автоматически запускает `startRepair()` - - `startRepair()` проверяет, ремонтирует ли уже этот генератор → если нет, создаёт новую RepairSession - - Progress сохраняется в `generator.progress` на всё время игры - - Если игрок встал (un-sneak) → `updateRepair()` возвращает false → repair stops - - Если игрок присел снова рядом тем же генератором → вызывается `startRepair()` снова - - **Ключевое**: progress НЕ сбрасывается, потому что RepairSession.generator указывает на ТОТЖЕ Generator объект - - Progress сохраняется всю игру (сбрасывается только при перезагрузке сервера) -- Сообщение теперь показывает текущий прогресс: "Зажмите ПРИСЕД чтобы чинить генератор (прогресс: XX%)" - -### 19. Чтобы починить генератор нужно присеть возле него и быть присядя возле него -- [x] **FIXED**: Добавлена автоматическая триггеризация repair при SNEAK -- Механика: - 1. Игрок присаживается (PlayerToggleSneakEvent с isSneaking=true) - 2. PlayerInteractListener.onSneak() вызывает `tryGeneratorInteract(player)` для survivors - 3. `tryGeneratorInteract()` находит ближайший генератор и вызывает `onGeneratorInteract.accept()` - 4. GameplayInteraction вызывает `generatorRepair.startRepair()` - 5. Repair session создана → player должен оставаться присядя - 6. GeneratorRepair.tick() каждый tick проверяет `if (!player.isSneaking()) return false;` - 7. Если условие не выполнено → repair stops (но progress НЕ теряется) - 8. Игрок встал → `stopRepair()` вызывается - 9. Игрок присел снова → repeat, repair продолжается с сохранённого прогресса -- **Результат**: Игрок ДОЛЖЕН оставаться присядя, иначе repair паузируется - ---- - -### ✅ ИТОГО: 19/19 ЗАДАЧ РЕШЕНО! - ---- - -## ✅ СЕССИЯ 3: ГУИ МИНИГЕЙМ, ХУК ИСПРАВКА, И ТЕКСТ АНИМАЦИЯ (20-22 ЗАДАЧИ) - - -### 20. Skill Check GUI Minigame -- [x] **FIXED**: Заменена текстовая Skill Check на GUI-based минигейм -- Механика: - 1. Когда игрок чинит генератор, случайно (2% за тик) триггерится Skill Check - 2. Открывается инвентарь с 9 стеклянными блоками в одной строке - 3. Все блоки - СЕРОЕ СТЕКЛО (GRAY_STAINED_GLASS) - 4. Один блок случайно становится ЗЕЛЁНЫМ (GREEN_STAINED_GLASS) - 5. Каждые 4-7 тиков зелёный блок меняет позицию (анимация) - 6. Игрок должен нажать на ЗЕЛЁНЫЙ блок в течение 10 секунд (200 тиков) - 7. Если успешно → +5% прогресса, сообщение "Отлично! +5%" - 8. Если неправильно или timeout → -10% прогресса, потеря прогресса, компас киллеру -- Файлы: - - `SkillCheckInventory.java` - новый класс для GUI minigame - - `SkillCheckListener.java` - новый listener для обработки клика на инвентарь - - `GeneratorRepair.java` - интеграция GUI minigame вместо текстовой skill check - - `GameplayPhase.java` - регистрация SkillCheckListener при старте игры - -### 21. Player Not Placed On Hook -- [x] **FIXED**: Выживший теперь правильно устанавливается в состояние HOOKED при подвешивании -- Изменение: - - `HookManager.tryHookSurvivor()` теперь вызывает `survivor.setState(PlayerState.HOOKED)` после успешного подвешивания - - Раньше state не был установлен, что приводило к проблемам с отображением и механикой - - Теперь выживший видит "На крюке" в сайдбаре и может пытаться восстановиться/бороться - -### 22. Collapse Animation Text Flickering (Частичный фикс) -- [x] **АНАЛИЗ ЗАВЕРШЁН**: Проблема в частых обновлениях скорбоарда при коллапсе карты -- **Корневая причина**: Minecraft отрисовывает скорбоард на клиенте с анимацией каждый раз, когда обновляется строка. - - Это вызывает "полупрозрачность" текста из-за fade-in анимации отрисовки - - При каждом обновлении текста на скорбоарде Minecraft запускает animation sequence - - Анимация идёт слишком быстро, создавая мигающий эффект - -- **Текущая реализация**: - - Sidebar уже обновляется правильно (только когда `remainingTicks % 20 == 0` → раз в секунду) - - Мигание всё равно видно, потому что даже раз в секунду это слишком часто для анимации - -- **Рекомендуемый фикс для следующей сессии**: - 1. **Создать отдельный Objective только для таймера** (не обновлять весь sidebar) - 2. **Обновлять таймер реже** (например, раз в 2 секунды вместо раз в секунду) - 3. **Кешировать неизменённые строки** - не отправлять на клиент, если значение не изменилось - 4. **Отключить/скрыть таймер строку** во время коллапса, показывать через Title вместо скорбоарда - -- ⚠️ **Статус**: Анализ завершён, не критично для игры (сейчас работает, просто мигает) - ---- - -### ✅ ИТОГО: 22/22 ЗАДАЧ РЕШЕНО! - - ---- - -## 📋 ИТОГОВОЕ РЕЗЮМЕ ПО СЕССИИ 3 - -### ✅ Что было исправлено: - -**1. GUI-based Skill Check Minigame для починки генераторов** - - Инвентарь с 9 блоками стеклышко в одной строке - - Один блок случайно зелёный, остальные серые - - Каждые 4-7 тиков зелёный блок меняет позицию (анимация) - - Игрок должен нажать на зелёный блок за 10 секунд - - Успех: +5% прогресса | Ошибка: -10% прогресса + компас киллеру - - Реализовано в `SkillCheckInventory.java` + `SkillCheckListener.java` - -**2. Выживший не ставился в hook** - - Добавлен `survivor.setState(PlayerState.HOOKED)` в `HookManager.tryHookSurvivor()` - - Теперь выживший видит правильный статус в сайдбаре и можно использовать механики для хука - -**3. Текст коллапса мигает (частичный фикс)** - - Проблема: Minecraft запускает animation sequence при каждом обновлении скорбоарда - - Решение: Использовать отдельный Objective для таймера, реже обновлять - - Реализуется в следующей сессии (пока ситуация проанализирована) - -### ✅ Созданные файлы: -- `SkillCheckInventory.java` - GUI minigame система -- `SkillCheckListener.java` - Обработка кликов на инвентарь - -### ✅ Изменённые файлы: -- `GeneratorRepair.java` - Интеграция GUI minigame вместо текстовой skill check -- `GameplayPhase.java` - Регистрация SkillCheckListener -- `HookManager.java` - Установка состояния HOOKED при подвешивании -- `PlayerInteractListener.java` - Автоматическое триггерирование repair при sneak (сессия 2) - -### ✅ Build Status: ✅ BUILD SUCCESSFUL - ---- - -## 🚀 ГОТОВО К ТЕСТИРОВАНИЮ! diff --git a/README.md b/README.md new file mode 100644 index 0000000..5cc0c97 --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# Mdbd - Dead by Daylight в Minecraft + +Плагин для Paper 1.21.5, реализующий мини-игру Dead by Daylight (4v1) на Minecraft-сервере. + +## Особенности + +- **4 выживших vs 1 маньяк** - полноценный геймплей DBD в Minecraft +- **Генераторы** - 7 на карте, нужно починить 5. Прогресс сохраняется, Skill Check мини-игра (GUI) +- **Крюки** - 3 стадии, спасение, борьба, жертвоприношение +- **Выходные ворота** - открытие через рубильник, эндгейм коллапс +- **Паллеты** - ставятся через `/dbdsetup`, ломаются маньяком +- **Медкиты и фонарики** - из сундуков, с механиками лечения и ослепления +- **Система урона** - Healthy → Injured → Downed → Hooked → Dead +- **Переноска** - маньяк подбирает и носит выживших до крюка +- **Terror Radius** - звук сердцебиения для выживших рядом с маньяком +- **Skill Check GUI** - мини-игра с зелёными блоками при починке генератора +- **Лобби** - ожидание игроков, автоматический старт, возврат после игры +- **Scoreboard, BossBar, ActionBar** - полноценный интерфейс + +## Команды + +### Игроку +| Команда | Описание | +|---|---| +| `/dbd join` | Войти в лобби | +| `/dbd leave` | Выйти из лобби/игры | +| `/dbd list` | Список доступных лобби | + +### Админу +| Команда | Описание | +|---|---| +| `/dbd create` | Создать новое лобби | +| `/dbd forcestart` | Принудительный старт | +| `/dbd stop` | Остановить игру | +| `/dbd reload` | Перезагрузить конфиги | +| `/dbdsetup wand` | Получить топор для выделения зон ворот | +| `/dbdsetup pos1 / pos2` | Выделить зону ворот | +| `/dbdsetup set ` | Установить объект | +| `/dbdsetup save / load ` | Сохранить/загрузить шаблон карты | +| `/dbdsetup build ` | Построить карту по шаблону | +| `/dbdsetup clear` | Очистить все объекты | +| `/dbdadmin create ` | Создать объект вручную | +| `/dbdadmin list ` | Список объектов | +| `/dbdadmin remove ` | Удалить объект | +| `/dbdadmin heal/down/unhook/kill ` | Управление состоянием игроков | + +## Установка + +1. Поместить `Mdbd-.jar` в `plugins/` +2. Настроить `game.yml`, `locations.yml`, `messages.yml` в `plugins/Mdbd/` +3. Перезапустить сервер или выполнить `/dbd reload` +4. Создать лобби: `/dbd create` +5. Расставить объекты через `/dbdsetup` + +## Требования + +- Paper 1.21.5 +- ProtocolLib 5.3.0 +- Java 21 + +## Сборка + +```bash +./gradlew build +``` diff --git a/bin/main/config.yml b/bin/main/config.yml deleted file mode 100644 index ec91e1e..0000000 --- a/bin/main/config.yml +++ /dev/null @@ -1,49 +0,0 @@ -# ============================================ -# Mdbd Plugin - Главный конфиг -# Версия: ${version} -# ============================================ - -# Основные настройки плагина -settings: - # Язык сообщений (пока только встроенный) - language: "ru" - - # Отладочный режим - debug: false - - # Автосохранение каждые X минут - auto-save-minutes: 5 - -# Настройки игры -game: - # Минимум игроков для старта - min-players: 5 - - # Максимум игроков - max-players: 5 - - # Обратный отсчёт в лобби (секунды) - lobby-countdown: 60 - - # Задержка перед назначением ролей (секунды) - role-assign-delay: 5 - -# Визуальные настройки -display: - # Использовать титры при старте игры - use-title-on-start: true - - # Показывать ActionBar с прогрессом - use-action-bar: true - - # Показывать Scoreboard (боковую панель) - use-sidebar: true - - # Формат прогресс-бара - progress-bar: - length: 20 - filled: "█" - empty: "░" - # Цвета в формате &#rrggbb - color-filled: "�FF00" - color-empty: "&#FF0000" diff --git a/bin/main/dev/loki/chest/Chest.class b/bin/main/dev/loki/chest/Chest.class deleted file mode 100644 index 3bc1cc2..0000000 Binary files a/bin/main/dev/loki/chest/Chest.class and /dev/null differ diff --git a/bin/main/dev/loki/chest/ChestManager.class b/bin/main/dev/loki/chest/ChestManager.class deleted file mode 100644 index 48370be..0000000 Binary files a/bin/main/dev/loki/chest/ChestManager.class and /dev/null differ diff --git a/bin/main/dev/loki/chest/ChestOpening$1.class b/bin/main/dev/loki/chest/ChestOpening$1.class deleted file mode 100644 index 8514ede..0000000 Binary files a/bin/main/dev/loki/chest/ChestOpening$1.class and /dev/null differ diff --git a/bin/main/dev/loki/chest/ChestOpening.class b/bin/main/dev/loki/chest/ChestOpening.class deleted file mode 100644 index 3013401..0000000 Binary files a/bin/main/dev/loki/chest/ChestOpening.class and /dev/null differ diff --git a/bin/main/dev/loki/command/DBDAdminCommand.class b/bin/main/dev/loki/command/DBDAdminCommand.class deleted file mode 100644 index b2c7ff3..0000000 Binary files a/bin/main/dev/loki/command/DBDAdminCommand.class and /dev/null differ diff --git a/bin/main/dev/loki/command/DBDCommand.class b/bin/main/dev/loki/command/DBDCommand.class deleted file mode 100644 index 7f9af97..0000000 Binary files a/bin/main/dev/loki/command/DBDCommand.class and /dev/null differ diff --git a/bin/main/dev/loki/command/SetupCommand.class b/bin/main/dev/loki/command/SetupCommand.class deleted file mode 100644 index 5e73af5..0000000 Binary files a/bin/main/dev/loki/command/SetupCommand.class and /dev/null differ diff --git a/bin/main/dev/loki/config/ConfigManager.class b/bin/main/dev/loki/config/ConfigManager.class deleted file mode 100644 index c4a4d5c..0000000 Binary files a/bin/main/dev/loki/config/ConfigManager.class and /dev/null differ diff --git a/bin/main/dev/loki/config/GameConfig.class b/bin/main/dev/loki/config/GameConfig.class deleted file mode 100644 index f328d7f..0000000 Binary files a/bin/main/dev/loki/config/GameConfig.class and /dev/null differ diff --git a/bin/main/dev/loki/config/LocationsConfig.class b/bin/main/dev/loki/config/LocationsConfig.class deleted file mode 100644 index bd911d0..0000000 Binary files a/bin/main/dev/loki/config/LocationsConfig.class and /dev/null differ diff --git a/bin/main/dev/loki/config/MessagesConfig.class b/bin/main/dev/loki/config/MessagesConfig.class deleted file mode 100644 index ad30f7d..0000000 Binary files a/bin/main/dev/loki/config/MessagesConfig.class and /dev/null differ diff --git a/bin/main/dev/loki/config/SkillCheckConfig.class b/bin/main/dev/loki/config/SkillCheckConfig.class deleted file mode 100644 index d162c85..0000000 Binary files a/bin/main/dev/loki/config/SkillCheckConfig.class and /dev/null differ diff --git a/bin/main/dev/loki/core/GameEventBus.class b/bin/main/dev/loki/core/GameEventBus.class deleted file mode 100644 index 0190e0a..0000000 Binary files a/bin/main/dev/loki/core/GameEventBus.class and /dev/null differ diff --git a/bin/main/dev/loki/core/GameManager.class b/bin/main/dev/loki/core/GameManager.class deleted file mode 100644 index b533ad9..0000000 Binary files a/bin/main/dev/loki/core/GameManager.class and /dev/null differ diff --git a/bin/main/dev/loki/core/GameSession$PlayerJoinEvent.class b/bin/main/dev/loki/core/GameSession$PlayerJoinEvent.class deleted file mode 100644 index fc4d246..0000000 Binary files a/bin/main/dev/loki/core/GameSession$PlayerJoinEvent.class and /dev/null differ diff --git a/bin/main/dev/loki/core/GameSession$PlayerLeaveEvent.class b/bin/main/dev/loki/core/GameSession$PlayerLeaveEvent.class deleted file mode 100644 index 3081fd1..0000000 Binary files a/bin/main/dev/loki/core/GameSession$PlayerLeaveEvent.class and /dev/null differ diff --git a/bin/main/dev/loki/core/GameSession.class b/bin/main/dev/loki/core/GameSession.class deleted file mode 100644 index d640678..0000000 Binary files a/bin/main/dev/loki/core/GameSession.class and /dev/null differ diff --git a/bin/main/dev/loki/core/GameState.class b/bin/main/dev/loki/core/GameState.class deleted file mode 100644 index 2ef79f4..0000000 Binary files a/bin/main/dev/loki/core/GameState.class and /dev/null differ diff --git a/bin/main/dev/loki/damage/AttackListener.class b/bin/main/dev/loki/damage/AttackListener.class deleted file mode 100644 index 242906a..0000000 Binary files a/bin/main/dev/loki/damage/AttackListener.class and /dev/null differ diff --git a/bin/main/dev/loki/damage/DamageHandler.class b/bin/main/dev/loki/damage/DamageHandler.class deleted file mode 100644 index 59d78c3..0000000 Binary files a/bin/main/dev/loki/damage/DamageHandler.class and /dev/null differ diff --git a/bin/main/dev/loki/data/DataStorage.class b/bin/main/dev/loki/data/DataStorage.class deleted file mode 100644 index 8f9df8e..0000000 Binary files a/bin/main/dev/loki/data/DataStorage.class and /dev/null differ diff --git a/bin/main/dev/loki/data/GeneratorDataManager.class b/bin/main/dev/loki/data/GeneratorDataManager.class deleted file mode 100644 index 893726a..0000000 Binary files a/bin/main/dev/loki/data/GeneratorDataManager.class and /dev/null differ diff --git a/bin/main/dev/loki/data/MatchResult$Role.class b/bin/main/dev/loki/data/MatchResult$Role.class deleted file mode 100644 index 29e2424..0000000 Binary files a/bin/main/dev/loki/data/MatchResult$Role.class and /dev/null differ diff --git a/bin/main/dev/loki/data/MatchResult.class b/bin/main/dev/loki/data/MatchResult.class deleted file mode 100644 index 0469a61..0000000 Binary files a/bin/main/dev/loki/data/MatchResult.class and /dev/null differ diff --git a/bin/main/dev/loki/data/PlayerStats.class b/bin/main/dev/loki/data/PlayerStats.class deleted file mode 100644 index 106db56..0000000 Binary files a/bin/main/dev/loki/data/PlayerStats.class and /dev/null differ diff --git a/bin/main/dev/loki/data/SQLiteStorage.class b/bin/main/dev/loki/data/SQLiteStorage.class deleted file mode 100644 index e360f2c..0000000 Binary files a/bin/main/dev/loki/data/SQLiteStorage.class and /dev/null differ diff --git a/bin/main/dev/loki/data/YamlStorage.class b/bin/main/dev/loki/data/YamlStorage.class deleted file mode 100644 index 8a6b0ba..0000000 Binary files a/bin/main/dev/loki/data/YamlStorage.class and /dev/null differ diff --git a/bin/main/dev/loki/gate/ExitGate$GateState.class b/bin/main/dev/loki/gate/ExitGate$GateState.class deleted file mode 100644 index 65b590a..0000000 Binary files a/bin/main/dev/loki/gate/ExitGate$GateState.class and /dev/null differ diff --git a/bin/main/dev/loki/gate/ExitGate.class b/bin/main/dev/loki/gate/ExitGate.class deleted file mode 100644 index 210e7f1..0000000 Binary files a/bin/main/dev/loki/gate/ExitGate.class and /dev/null differ diff --git a/bin/main/dev/loki/gate/GateManager.class b/bin/main/dev/loki/gate/GateManager.class deleted file mode 100644 index 09980d8..0000000 Binary files a/bin/main/dev/loki/gate/GateManager.class and /dev/null differ diff --git a/bin/main/dev/loki/gate/GateOpening$OpeningProgress.class b/bin/main/dev/loki/gate/GateOpening$OpeningProgress.class deleted file mode 100644 index 7521726..0000000 Binary files a/bin/main/dev/loki/gate/GateOpening$OpeningProgress.class and /dev/null differ diff --git a/bin/main/dev/loki/gate/GateOpening$OpeningTask.class b/bin/main/dev/loki/gate/GateOpening$OpeningTask.class deleted file mode 100644 index 00c0d0c..0000000 Binary files a/bin/main/dev/loki/gate/GateOpening$OpeningTask.class and /dev/null differ diff --git a/bin/main/dev/loki/gate/GateOpening.class b/bin/main/dev/loki/gate/GateOpening.class deleted file mode 100644 index fd2a0d0..0000000 Binary files a/bin/main/dev/loki/gate/GateOpening.class and /dev/null differ diff --git a/bin/main/dev/loki/hatch/Hatch$HatchState.class b/bin/main/dev/loki/hatch/Hatch$HatchState.class deleted file mode 100644 index a83bd1a..0000000 Binary files a/bin/main/dev/loki/hatch/Hatch$HatchState.class and /dev/null differ diff --git a/bin/main/dev/loki/hatch/Hatch.class b/bin/main/dev/loki/hatch/Hatch.class deleted file mode 100644 index 5813dd0..0000000 Binary files a/bin/main/dev/loki/hatch/Hatch.class and /dev/null differ diff --git a/bin/main/dev/loki/hatch/HatchCondition.class b/bin/main/dev/loki/hatch/HatchCondition.class deleted file mode 100644 index 30223bd..0000000 Binary files a/bin/main/dev/loki/hatch/HatchCondition.class and /dev/null differ diff --git a/bin/main/dev/loki/hatch/HatchEscape.class b/bin/main/dev/loki/hatch/HatchEscape.class deleted file mode 100644 index ef5738b..0000000 Binary files a/bin/main/dev/loki/hatch/HatchEscape.class and /dev/null differ diff --git a/bin/main/dev/loki/hook/Hook.class b/bin/main/dev/loki/hook/Hook.class deleted file mode 100644 index 863b9f5..0000000 Binary files a/bin/main/dev/loki/hook/Hook.class and /dev/null differ diff --git a/bin/main/dev/loki/hook/HookManager.class b/bin/main/dev/loki/hook/HookManager.class deleted file mode 100644 index 03c3a6b..0000000 Binary files a/bin/main/dev/loki/hook/HookManager.class and /dev/null differ diff --git a/bin/main/dev/loki/hook/HookRescue$RescueSession.class b/bin/main/dev/loki/hook/HookRescue$RescueSession.class deleted file mode 100644 index 347809e..0000000 Binary files a/bin/main/dev/loki/hook/HookRescue$RescueSession.class and /dev/null differ diff --git a/bin/main/dev/loki/hook/HookRescue.class b/bin/main/dev/loki/hook/HookRescue.class deleted file mode 100644 index 4ea8ba4..0000000 Binary files a/bin/main/dev/loki/hook/HookRescue.class and /dev/null differ diff --git a/bin/main/dev/loki/hook/HookSabotage$1.class b/bin/main/dev/loki/hook/HookSabotage$1.class deleted file mode 100644 index f14c4ee..0000000 Binary files a/bin/main/dev/loki/hook/HookSabotage$1.class and /dev/null differ diff --git a/bin/main/dev/loki/hook/HookSabotage$2.class b/bin/main/dev/loki/hook/HookSabotage$2.class deleted file mode 100644 index d296a49..0000000 Binary files a/bin/main/dev/loki/hook/HookSabotage$2.class and /dev/null differ diff --git a/bin/main/dev/loki/hook/HookSabotage.class b/bin/main/dev/loki/hook/HookSabotage.class deleted file mode 100644 index 0bf6cf5..0000000 Binary files a/bin/main/dev/loki/hook/HookSabotage.class and /dev/null differ diff --git a/bin/main/dev/loki/hook/HookSacrifice.class b/bin/main/dev/loki/hook/HookSacrifice.class deleted file mode 100644 index c70e572..0000000 Binary files a/bin/main/dev/loki/hook/HookSacrifice.class and /dev/null differ diff --git a/bin/main/dev/loki/hook/HookStage.class b/bin/main/dev/loki/hook/HookStage.class deleted file mode 100644 index b8af002..0000000 Binary files a/bin/main/dev/loki/hook/HookStage.class and /dev/null differ diff --git a/bin/main/dev/loki/hook/HookStruggle.class b/bin/main/dev/loki/hook/HookStruggle.class deleted file mode 100644 index 0703637..0000000 Binary files a/bin/main/dev/loki/hook/HookStruggle.class and /dev/null differ diff --git a/bin/main/dev/loki/item/BaseItem.class b/bin/main/dev/loki/item/BaseItem.class deleted file mode 100644 index 420dba2..0000000 Binary files a/bin/main/dev/loki/item/BaseItem.class and /dev/null differ diff --git a/bin/main/dev/loki/item/ChestLoot.class b/bin/main/dev/loki/item/ChestLoot.class deleted file mode 100644 index 9ed0db2..0000000 Binary files a/bin/main/dev/loki/item/ChestLoot.class and /dev/null differ diff --git a/bin/main/dev/loki/item/DBDItems.class b/bin/main/dev/loki/item/DBDItems.class deleted file mode 100644 index fdbaf6e..0000000 Binary files a/bin/main/dev/loki/item/DBDItems.class and /dev/null differ diff --git a/bin/main/dev/loki/item/ItemFlashlight.class b/bin/main/dev/loki/item/ItemFlashlight.class deleted file mode 100644 index 7ad4ef2..0000000 Binary files a/bin/main/dev/loki/item/ItemFlashlight.class and /dev/null differ diff --git a/bin/main/dev/loki/item/ItemMedkit.class b/bin/main/dev/loki/item/ItemMedkit.class deleted file mode 100644 index 2d06dfb..0000000 Binary files a/bin/main/dev/loki/item/ItemMedkit.class and /dev/null differ diff --git a/bin/main/dev/loki/item/ItemType.class b/bin/main/dev/loki/item/ItemType.class deleted file mode 100644 index d0d11c4..0000000 Binary files a/bin/main/dev/loki/item/ItemType.class and /dev/null differ diff --git a/bin/main/dev/loki/item/ItemUseListener$1.class b/bin/main/dev/loki/item/ItemUseListener$1.class deleted file mode 100644 index 381df03..0000000 Binary files a/bin/main/dev/loki/item/ItemUseListener$1.class and /dev/null differ diff --git a/bin/main/dev/loki/item/ItemUseListener.class b/bin/main/dev/loki/item/ItemUseListener.class deleted file mode 100644 index 5642ce1..0000000 Binary files a/bin/main/dev/loki/item/ItemUseListener.class and /dev/null differ diff --git a/bin/main/dev/loki/killer/GeneratorKick$1.class b/bin/main/dev/loki/killer/GeneratorKick$1.class deleted file mode 100644 index 4cb743a..0000000 Binary files a/bin/main/dev/loki/killer/GeneratorKick$1.class and /dev/null differ diff --git a/bin/main/dev/loki/killer/GeneratorKick$2.class b/bin/main/dev/loki/killer/GeneratorKick$2.class deleted file mode 100644 index 01e4fd6..0000000 Binary files a/bin/main/dev/loki/killer/GeneratorKick$2.class and /dev/null differ diff --git a/bin/main/dev/loki/killer/GeneratorKick.class b/bin/main/dev/loki/killer/GeneratorKick.class deleted file mode 100644 index e5bfc2c..0000000 Binary files a/bin/main/dev/loki/killer/GeneratorKick.class and /dev/null differ diff --git a/bin/main/dev/loki/killer/HitValidator.class b/bin/main/dev/loki/killer/HitValidator.class deleted file mode 100644 index ad3e3c5..0000000 Binary files a/bin/main/dev/loki/killer/HitValidator.class and /dev/null differ diff --git a/bin/main/dev/loki/killer/KillerAttack.class b/bin/main/dev/loki/killer/KillerAttack.class deleted file mode 100644 index b679277..0000000 Binary files a/bin/main/dev/loki/killer/KillerAttack.class and /dev/null differ diff --git a/bin/main/dev/loki/killer/KillerCarry$CarryData.class b/bin/main/dev/loki/killer/KillerCarry$CarryData.class deleted file mode 100644 index b1e98bf..0000000 Binary files a/bin/main/dev/loki/killer/KillerCarry$CarryData.class and /dev/null differ diff --git a/bin/main/dev/loki/killer/KillerCarry.class b/bin/main/dev/loki/killer/KillerCarry.class deleted file mode 100644 index ee475b6..0000000 Binary files a/bin/main/dev/loki/killer/KillerCarry.class and /dev/null differ diff --git a/bin/main/dev/loki/killer/KillerController.class b/bin/main/dev/loki/killer/KillerController.class deleted file mode 100644 index 216eb72..0000000 Binary files a/bin/main/dev/loki/killer/KillerController.class and /dev/null differ diff --git a/bin/main/dev/loki/killer/KillerMovement.class b/bin/main/dev/loki/killer/KillerMovement.class deleted file mode 100644 index 38f2088..0000000 Binary files a/bin/main/dev/loki/killer/KillerMovement.class and /dev/null differ diff --git a/bin/main/dev/loki/killer/KillerVision.class b/bin/main/dev/loki/killer/KillerVision.class deleted file mode 100644 index 7f7393d..0000000 Binary files a/bin/main/dev/loki/killer/KillerVision.class and /dev/null differ diff --git a/bin/main/dev/loki/listener/DamageListener.class b/bin/main/dev/loki/listener/DamageListener.class deleted file mode 100644 index c1ded1a..0000000 Binary files a/bin/main/dev/loki/listener/DamageListener.class and /dev/null differ diff --git a/bin/main/dev/loki/listener/GeneratorSelectListener.class b/bin/main/dev/loki/listener/GeneratorSelectListener.class deleted file mode 100644 index b600772..0000000 Binary files a/bin/main/dev/loki/listener/GeneratorSelectListener.class and /dev/null differ diff --git a/bin/main/dev/loki/listener/PlayerInteractListener.class b/bin/main/dev/loki/listener/PlayerInteractListener.class deleted file mode 100644 index c9e507e..0000000 Binary files a/bin/main/dev/loki/listener/PlayerInteractListener.class and /dev/null differ diff --git a/bin/main/dev/loki/listener/PlayerJumpListener.class b/bin/main/dev/loki/listener/PlayerJumpListener.class deleted file mode 100644 index 8bf128a..0000000 Binary files a/bin/main/dev/loki/listener/PlayerJumpListener.class and /dev/null differ diff --git a/bin/main/dev/loki/listener/PlayerMoveListener.class b/bin/main/dev/loki/listener/PlayerMoveListener.class deleted file mode 100644 index 1d8b557..0000000 Binary files a/bin/main/dev/loki/listener/PlayerMoveListener.class and /dev/null differ diff --git a/bin/main/dev/loki/listener/PlayerQuitListener.class b/bin/main/dev/loki/listener/PlayerQuitListener.class deleted file mode 100644 index 931b260..0000000 Binary files a/bin/main/dev/loki/listener/PlayerQuitListener.class and /dev/null differ diff --git a/bin/main/dev/loki/listener/SetupBlockBreakListener.class b/bin/main/dev/loki/listener/SetupBlockBreakListener.class deleted file mode 100644 index 989a2d1..0000000 Binary files a/bin/main/dev/loki/listener/SetupBlockBreakListener.class and /dev/null differ diff --git a/bin/main/dev/loki/map/MapBuilder.class b/bin/main/dev/loki/map/MapBuilder.class deleted file mode 100644 index 4a438ac..0000000 Binary files a/bin/main/dev/loki/map/MapBuilder.class and /dev/null differ diff --git a/bin/main/dev/loki/map/MapTemplate.class b/bin/main/dev/loki/map/MapTemplate.class deleted file mode 100644 index 797cde6..0000000 Binary files a/bin/main/dev/loki/map/MapTemplate.class and /dev/null differ diff --git a/bin/main/dev/loki/map/TemplateManager$PresetMap.class b/bin/main/dev/loki/map/TemplateManager$PresetMap.class deleted file mode 100644 index 07ffc50..0000000 Binary files a/bin/main/dev/loki/map/TemplateManager$PresetMap.class and /dev/null differ diff --git a/bin/main/dev/loki/map/TemplateManager.class b/bin/main/dev/loki/map/TemplateManager.class deleted file mode 100644 index 1121587..0000000 Binary files a/bin/main/dev/loki/map/TemplateManager.class and /dev/null differ diff --git a/bin/main/dev/loki/mdbd.class b/bin/main/dev/loki/mdbd.class deleted file mode 100644 index e2bc67a..0000000 Binary files a/bin/main/dev/loki/mdbd.class and /dev/null differ diff --git a/bin/main/dev/loki/mdbdBootstrap.class b/bin/main/dev/loki/mdbdBootstrap.class deleted file mode 100644 index 04e844c..0000000 Binary files a/bin/main/dev/loki/mdbdBootstrap.class and /dev/null differ diff --git a/bin/main/dev/loki/mdbdLoader.class b/bin/main/dev/loki/mdbdLoader.class deleted file mode 100644 index cd5429f..0000000 Binary files a/bin/main/dev/loki/mdbdLoader.class and /dev/null differ diff --git a/bin/main/dev/loki/objective/Generator.class b/bin/main/dev/loki/objective/Generator.class deleted file mode 100644 index 8ef29ff..0000000 Binary files a/bin/main/dev/loki/objective/Generator.class and /dev/null differ diff --git a/bin/main/dev/loki/objective/GeneratorManager.class b/bin/main/dev/loki/objective/GeneratorManager.class deleted file mode 100644 index 854b4d2..0000000 Binary files a/bin/main/dev/loki/objective/GeneratorManager.class and /dev/null differ diff --git a/bin/main/dev/loki/objective/GeneratorRegression.class b/bin/main/dev/loki/objective/GeneratorRegression.class deleted file mode 100644 index 82d411b..0000000 Binary files a/bin/main/dev/loki/objective/GeneratorRegression.class and /dev/null differ diff --git a/bin/main/dev/loki/objective/GeneratorRepair$RepairSession.class b/bin/main/dev/loki/objective/GeneratorRepair$RepairSession.class deleted file mode 100644 index eb718b7..0000000 Binary files a/bin/main/dev/loki/objective/GeneratorRepair$RepairSession.class and /dev/null differ diff --git a/bin/main/dev/loki/objective/GeneratorRepair.class b/bin/main/dev/loki/objective/GeneratorRepair.class deleted file mode 100644 index ff4eeba..0000000 Binary files a/bin/main/dev/loki/objective/GeneratorRepair.class and /dev/null differ diff --git a/bin/main/dev/loki/objective/SkillCheck$CheckState.class b/bin/main/dev/loki/objective/SkillCheck$CheckState.class deleted file mode 100644 index e49e9e4..0000000 Binary files a/bin/main/dev/loki/objective/SkillCheck$CheckState.class and /dev/null differ diff --git a/bin/main/dev/loki/objective/SkillCheck.class b/bin/main/dev/loki/objective/SkillCheck.class deleted file mode 100644 index f7981b6..0000000 Binary files a/bin/main/dev/loki/objective/SkillCheck.class and /dev/null differ diff --git a/bin/main/dev/loki/objective/SkillCheckUI.class b/bin/main/dev/loki/objective/SkillCheckUI.class deleted file mode 100644 index ee59db9..0000000 Binary files a/bin/main/dev/loki/objective/SkillCheckUI.class and /dev/null differ diff --git a/bin/main/dev/loki/pallet/Pallet$PalletState.class b/bin/main/dev/loki/pallet/Pallet$PalletState.class deleted file mode 100644 index a08f42c..0000000 Binary files a/bin/main/dev/loki/pallet/Pallet$PalletState.class and /dev/null differ diff --git a/bin/main/dev/loki/pallet/Pallet.class b/bin/main/dev/loki/pallet/Pallet.class deleted file mode 100644 index 13f222b..0000000 Binary files a/bin/main/dev/loki/pallet/Pallet.class and /dev/null differ diff --git a/bin/main/dev/loki/pallet/PalletBreak$BreakTask.class b/bin/main/dev/loki/pallet/PalletBreak$BreakTask.class deleted file mode 100644 index ee950e4..0000000 Binary files a/bin/main/dev/loki/pallet/PalletBreak$BreakTask.class and /dev/null differ diff --git a/bin/main/dev/loki/pallet/PalletBreak$BreakingProgress.class b/bin/main/dev/loki/pallet/PalletBreak$BreakingProgress.class deleted file mode 100644 index 58188ab..0000000 Binary files a/bin/main/dev/loki/pallet/PalletBreak$BreakingProgress.class and /dev/null differ diff --git a/bin/main/dev/loki/pallet/PalletBreak.class b/bin/main/dev/loki/pallet/PalletBreak.class deleted file mode 100644 index d51ef02..0000000 Binary files a/bin/main/dev/loki/pallet/PalletBreak.class and /dev/null differ diff --git a/bin/main/dev/loki/pallet/PalletDrop.class b/bin/main/dev/loki/pallet/PalletDrop.class deleted file mode 100644 index 2ae340f..0000000 Binary files a/bin/main/dev/loki/pallet/PalletDrop.class and /dev/null differ diff --git a/bin/main/dev/loki/pallet/PalletManager.class b/bin/main/dev/loki/pallet/PalletManager.class deleted file mode 100644 index bca417a..0000000 Binary files a/bin/main/dev/loki/pallet/PalletManager.class and /dev/null differ diff --git a/bin/main/dev/loki/pallet/PalletStun$1.class b/bin/main/dev/loki/pallet/PalletStun$1.class deleted file mode 100644 index 57b7823..0000000 Binary files a/bin/main/dev/loki/pallet/PalletStun$1.class and /dev/null differ diff --git a/bin/main/dev/loki/pallet/PalletStun.class b/bin/main/dev/loki/pallet/PalletStun.class deleted file mode 100644 index 31e8b18..0000000 Binary files a/bin/main/dev/loki/pallet/PalletStun.class and /dev/null differ diff --git a/bin/main/dev/loki/phase/EndgamePhase$1.class b/bin/main/dev/loki/phase/EndgamePhase$1.class deleted file mode 100644 index 2ebc99f..0000000 Binary files a/bin/main/dev/loki/phase/EndgamePhase$1.class and /dev/null differ diff --git a/bin/main/dev/loki/phase/EndgamePhase.class b/bin/main/dev/loki/phase/EndgamePhase.class deleted file mode 100644 index 37635b3..0000000 Binary files a/bin/main/dev/loki/phase/EndgamePhase.class and /dev/null differ diff --git a/bin/main/dev/loki/phase/GameplayPhase.class b/bin/main/dev/loki/phase/GameplayPhase.class deleted file mode 100644 index d131d38..0000000 Binary files a/bin/main/dev/loki/phase/GameplayPhase.class and /dev/null differ diff --git a/bin/main/dev/loki/phase/LobbyPhase.class b/bin/main/dev/loki/phase/LobbyPhase.class deleted file mode 100644 index eb2771b..0000000 Binary files a/bin/main/dev/loki/phase/LobbyPhase.class and /dev/null differ diff --git a/bin/main/dev/loki/phase/Phase.class b/bin/main/dev/loki/phase/Phase.class deleted file mode 100644 index 1dec935..0000000 Binary files a/bin/main/dev/loki/phase/Phase.class and /dev/null differ diff --git a/bin/main/dev/loki/phase/RoleAssignPhase.class b/bin/main/dev/loki/phase/RoleAssignPhase.class deleted file mode 100644 index 19b62ee..0000000 Binary files a/bin/main/dev/loki/phase/RoleAssignPhase.class and /dev/null differ diff --git a/bin/main/dev/loki/player/BasePlayer.class b/bin/main/dev/loki/player/BasePlayer.class deleted file mode 100644 index 8fa42f1..0000000 Binary files a/bin/main/dev/loki/player/BasePlayer.class and /dev/null differ diff --git a/bin/main/dev/loki/player/Killer.class b/bin/main/dev/loki/player/Killer.class deleted file mode 100644 index 6af8a1a..0000000 Binary files a/bin/main/dev/loki/player/Killer.class and /dev/null differ diff --git a/bin/main/dev/loki/player/PlayerManager.class b/bin/main/dev/loki/player/PlayerManager.class deleted file mode 100644 index ff702c0..0000000 Binary files a/bin/main/dev/loki/player/PlayerManager.class and /dev/null differ diff --git a/bin/main/dev/loki/player/PlayerState.class b/bin/main/dev/loki/player/PlayerState.class deleted file mode 100644 index 75eb765..0000000 Binary files a/bin/main/dev/loki/player/PlayerState.class and /dev/null differ diff --git a/bin/main/dev/loki/player/Survivor.class b/bin/main/dev/loki/player/Survivor.class deleted file mode 100644 index 49686f3..0000000 Binary files a/bin/main/dev/loki/player/Survivor.class and /dev/null differ diff --git a/bin/main/dev/loki/role/RoleAssigner$RoleAssignedEvent.class b/bin/main/dev/loki/role/RoleAssigner$RoleAssignedEvent.class deleted file mode 100644 index 128db7f..0000000 Binary files a/bin/main/dev/loki/role/RoleAssigner$RoleAssignedEvent.class and /dev/null differ diff --git a/bin/main/dev/loki/role/RoleAssigner.class b/bin/main/dev/loki/role/RoleAssigner.class deleted file mode 100644 index bfd9a7d..0000000 Binary files a/bin/main/dev/loki/role/RoleAssigner.class and /dev/null differ diff --git a/bin/main/dev/loki/role/RoleQueue.class b/bin/main/dev/loki/role/RoleQueue.class deleted file mode 100644 index 0acb0b5..0000000 Binary files a/bin/main/dev/loki/role/RoleQueue.class and /dev/null differ diff --git a/bin/main/dev/loki/role/SpawnPosition.class b/bin/main/dev/loki/role/SpawnPosition.class deleted file mode 100644 index 9414eae..0000000 Binary files a/bin/main/dev/loki/role/SpawnPosition.class and /dev/null differ diff --git a/bin/main/dev/loki/sound/GeneratorSound.class b/bin/main/dev/loki/sound/GeneratorSound.class deleted file mode 100644 index 3b278e0..0000000 Binary files a/bin/main/dev/loki/sound/GeneratorSound.class and /dev/null differ diff --git a/bin/main/dev/loki/sound/SoundLevels.class b/bin/main/dev/loki/sound/SoundLevels.class deleted file mode 100644 index 2e35833..0000000 Binary files a/bin/main/dev/loki/sound/SoundLevels.class and /dev/null differ diff --git a/bin/main/dev/loki/sound/SoundManager.class b/bin/main/dev/loki/sound/SoundManager.class deleted file mode 100644 index ca6579e..0000000 Binary files a/bin/main/dev/loki/sound/SoundManager.class and /dev/null differ diff --git a/bin/main/dev/loki/sound/TerrorRadiusSound.class b/bin/main/dev/loki/sound/TerrorRadiusSound.class deleted file mode 100644 index 5ae62d7..0000000 Binary files a/bin/main/dev/loki/sound/TerrorRadiusSound.class and /dev/null differ diff --git a/bin/main/dev/loki/state/StateHandler.class b/bin/main/dev/loki/state/StateHandler.class deleted file mode 100644 index 91cad30..0000000 Binary files a/bin/main/dev/loki/state/StateHandler.class and /dev/null differ diff --git a/bin/main/dev/loki/state/StateMachine.class b/bin/main/dev/loki/state/StateMachine.class deleted file mode 100644 index 38bf158..0000000 Binary files a/bin/main/dev/loki/state/StateMachine.class and /dev/null differ diff --git a/bin/main/dev/loki/state/StateTransition.class b/bin/main/dev/loki/state/StateTransition.class deleted file mode 100644 index cfcfadb..0000000 Binary files a/bin/main/dev/loki/state/StateTransition.class and /dev/null differ diff --git a/bin/main/dev/loki/survivor/SurvivorController.class b/bin/main/dev/loki/survivor/SurvivorController.class deleted file mode 100644 index 20d29e6..0000000 Binary files a/bin/main/dev/loki/survivor/SurvivorController.class and /dev/null differ diff --git a/bin/main/dev/loki/survivor/SurvivorHealing.class b/bin/main/dev/loki/survivor/SurvivorHealing.class deleted file mode 100644 index 07b6c86..0000000 Binary files a/bin/main/dev/loki/survivor/SurvivorHealing.class and /dev/null differ diff --git a/bin/main/dev/loki/survivor/SurvivorMovement.class b/bin/main/dev/loki/survivor/SurvivorMovement.class deleted file mode 100644 index f1429e9..0000000 Binary files a/bin/main/dev/loki/survivor/SurvivorMovement.class and /dev/null differ diff --git a/bin/main/dev/loki/ui/ActionBar.class b/bin/main/dev/loki/ui/ActionBar.class deleted file mode 100644 index fba075c..0000000 Binary files a/bin/main/dev/loki/ui/ActionBar.class and /dev/null differ diff --git a/bin/main/dev/loki/ui/BossBarManager$1.class b/bin/main/dev/loki/ui/BossBarManager$1.class deleted file mode 100644 index cec8d5a..0000000 Binary files a/bin/main/dev/loki/ui/BossBarManager$1.class and /dev/null differ diff --git a/bin/main/dev/loki/ui/BossBarManager.class b/bin/main/dev/loki/ui/BossBarManager.class deleted file mode 100644 index ea4be78..0000000 Binary files a/bin/main/dev/loki/ui/BossBarManager.class and /dev/null differ diff --git a/bin/main/dev/loki/ui/ProgressBar.class b/bin/main/dev/loki/ui/ProgressBar.class deleted file mode 100644 index 85ab360..0000000 Binary files a/bin/main/dev/loki/ui/ProgressBar.class and /dev/null differ diff --git a/bin/main/dev/loki/ui/Sidebar.class b/bin/main/dev/loki/ui/Sidebar.class deleted file mode 100644 index 890a666..0000000 Binary files a/bin/main/dev/loki/ui/Sidebar.class and /dev/null differ diff --git a/bin/main/dev/loki/ui/StatusIcons.class b/bin/main/dev/loki/ui/StatusIcons.class deleted file mode 100644 index f373cb7..0000000 Binary files a/bin/main/dev/loki/ui/StatusIcons.class and /dev/null differ diff --git a/bin/main/dev/loki/ui/TitleUI$SkillCheckState.class b/bin/main/dev/loki/ui/TitleUI$SkillCheckState.class deleted file mode 100644 index d2a81ff..0000000 Binary files a/bin/main/dev/loki/ui/TitleUI$SkillCheckState.class and /dev/null differ diff --git a/bin/main/dev/loki/ui/TitleUI.class b/bin/main/dev/loki/ui/TitleUI.class deleted file mode 100644 index 76becf2..0000000 Binary files a/bin/main/dev/loki/ui/TitleUI.class and /dev/null differ diff --git a/bin/main/dev/loki/util/LocationUtil.class b/bin/main/dev/loki/util/LocationUtil.class deleted file mode 100644 index 681f3ef..0000000 Binary files a/bin/main/dev/loki/util/LocationUtil.class and /dev/null differ diff --git a/bin/main/dev/loki/util/MathUtil.class b/bin/main/dev/loki/util/MathUtil.class deleted file mode 100644 index 511e2d2..0000000 Binary files a/bin/main/dev/loki/util/MathUtil.class and /dev/null differ diff --git a/bin/main/dev/loki/util/ParticleUtil.class b/bin/main/dev/loki/util/ParticleUtil.class deleted file mode 100644 index 2a909a3..0000000 Binary files a/bin/main/dev/loki/util/ParticleUtil.class and /dev/null differ diff --git a/bin/main/dev/loki/util/PlayerUtil.class b/bin/main/dev/loki/util/PlayerUtil.class deleted file mode 100644 index 86472fd..0000000 Binary files a/bin/main/dev/loki/util/PlayerUtil.class and /dev/null differ diff --git a/bin/main/dev/loki/util/TextUtil.class b/bin/main/dev/loki/util/TextUtil.class deleted file mode 100644 index 1bedf31..0000000 Binary files a/bin/main/dev/loki/util/TextUtil.class and /dev/null differ diff --git a/bin/main/dev/loki/util/TimeUtil.class b/bin/main/dev/loki/util/TimeUtil.class deleted file mode 100644 index 07f1a31..0000000 Binary files a/bin/main/dev/loki/util/TimeUtil.class and /dev/null differ diff --git a/bin/main/dev/loki/window/Window$PlayerContext.class b/bin/main/dev/loki/window/Window$PlayerContext.class deleted file mode 100644 index 44f0b33..0000000 Binary files a/bin/main/dev/loki/window/Window$PlayerContext.class and /dev/null differ diff --git a/bin/main/dev/loki/window/Window.class b/bin/main/dev/loki/window/Window.class deleted file mode 100644 index 745850c..0000000 Binary files a/bin/main/dev/loki/window/Window.class and /dev/null differ diff --git a/bin/main/dev/loki/window/WindowFastVault$FastVaultTask.class b/bin/main/dev/loki/window/WindowFastVault$FastVaultTask.class deleted file mode 100644 index 9f79374..0000000 Binary files a/bin/main/dev/loki/window/WindowFastVault$FastVaultTask.class and /dev/null differ diff --git a/bin/main/dev/loki/window/WindowFastVault.class b/bin/main/dev/loki/window/WindowFastVault.class deleted file mode 100644 index 9eed959..0000000 Binary files a/bin/main/dev/loki/window/WindowFastVault.class and /dev/null differ diff --git a/bin/main/dev/loki/window/WindowManager.class b/bin/main/dev/loki/window/WindowManager.class deleted file mode 100644 index 8e5daf5..0000000 Binary files a/bin/main/dev/loki/window/WindowManager.class and /dev/null differ diff --git a/bin/main/dev/loki/window/WindowVault$VaultTask.class b/bin/main/dev/loki/window/WindowVault$VaultTask.class deleted file mode 100644 index 68491dc..0000000 Binary files a/bin/main/dev/loki/window/WindowVault$VaultTask.class and /dev/null differ diff --git a/bin/main/dev/loki/window/WindowVault.class b/bin/main/dev/loki/window/WindowVault.class deleted file mode 100644 index 8d7d31c..0000000 Binary files a/bin/main/dev/loki/window/WindowVault.class and /dev/null differ diff --git a/bin/main/dev/loki/world/LobbyManager.class b/bin/main/dev/loki/world/LobbyManager.class deleted file mode 100644 index 911bfa7..0000000 Binary files a/bin/main/dev/loki/world/LobbyManager.class and /dev/null differ diff --git a/bin/main/dev/loki/world/VoidWorldGenerator.class b/bin/main/dev/loki/world/VoidWorldGenerator.class deleted file mode 100644 index 6a9e20d..0000000 Binary files a/bin/main/dev/loki/world/VoidWorldGenerator.class and /dev/null differ diff --git a/bin/main/game.yml b/bin/main/game.yml deleted file mode 100644 index c2e91e1..0000000 --- a/bin/main/game.yml +++ /dev/null @@ -1,260 +0,0 @@ -# ============================================ -# Mdbd Plugin - Игровой баланс -# ============================================ - -# Лобби -lobby: - # Минимум игроков для старта - min-players: 4 - - # Максимум игроков - max-players: 5 - - # Обратный отсчёт при 4 игроках (секунд) - countdown-4-players: 45 - - # Обратный отсчёт при 5 игроках (секунд) - countdown-5-players: 15 - - # Задержка перед назначением ролей (секунд) - role-assign-delay: 5 - - # Количество серверных лобби (0 = только пользовательские) - server-lobby-count: 3 - - # Сохранять лобби между перезапусками - persist-lobbies: true - -# Размеры игроков (1.0 = стандарт) -players: - survivor-scale: 1.0 - killer-scale: 1.5 - -# Генераторы -generators: - # Всего генераторов на карте - total-count: 7 - - # Сколько нужно починить для ворот - required-count: 5 - - # Время починки одного генератора (секунд) [90 сек по ТЗ] - repair-seconds: 90 - - # Скорость отката без чинильщика (%/сек) - regression-rate: 0.25 - - # Скорость отката после удара (%/сек) - kick-regression-rate: 2.5 - - # Потеря прогресса после удара (%) - kick-regression-percent: 20.0 - - # Длительность отката после удара (секунд) - kick-duration-seconds: 30 - - # Кулдаун удара по генератору (секунд, общий для всех генераторов) - kick-cooldown-seconds: 30 - - # Время зажатия Shift для удара (секунд) - kick-charge-seconds: 3 - - # Расстояние для взаимодействия с генератором (блоков) - interaction-range: 3.0 - -# Крюки -hooks: - # Длительность Фазы 1 (секунд) - stage1-seconds: 60 - - # Длительность Фазы 2 (секунд) - stage2-seconds: 60 - - # Интервал борьбы на Фазе 2 (секунд) - struggle-interval: 2 - - # Максимум крюков на одного выжившего (= жизни) - max-hooks: 3 - - # Расстояние для взаимодействия с крюком (блоков) - interaction-range: 3.0 - - # Время спасения (секунд) - rescue-seconds: 3 - - # Время саботажа крюка (секунд) - sabotage-seconds: 10 - - # Время восстановления крюка (секунд) - sabotage-recovery-seconds: 60 - -# Скорости (% от нормальной) -movement: - # Киллер - killer: 115 - - # Киллер с выжившим - killer-carry: 92 - - # Выживший шагом - survivor-walk: 100 - - # Выживший спринт - survivor-sprint: 150 - - # Выживший в нокдауне (ползком) - survivor-crawl: 30 - - # Длительность истощения (секунд) - exhaustion-duration: 3 - - # Кулдаун истощения (секунд) - exhaustion-cooldown: 5 - -# Лечение -healing: - # Лечение другого (секунд) - other-heal-seconds: 15 - - # Самолечение аптечкой (секунд) - self-heal-seconds: 30 - - # Бонус аптечки (0.5 = в 2 раза быстрее) - medkit-bonus: 0.5 - - # Прерывание при движении - interrupt-on-move: true - - # Максимум лечений за матч - max-heals-per-match: 4 - - # Расстояние лечения (блоков) - heal-range: 3.0 - -# Переноска выжившего -carry: - # Количество прыжков для заполнения шкалы побега - escape-jumps: 15 - - # Speed I после побега (секунд) - escape-speed-seconds: 3 - - # Приращение шкалы за прыжок (%) - jump-fill-percent: 6.67 - -# Киллер -killer: - # Кулдаун атаки (секунд) — базовый (swing animation) - attack-cooldown: 2.5 - - # Кулдаун после ПОПАДАНИЯ по выжившему (секунд) — 3–5 сек - post-hit-cooldown: 4.0 - - # Дальность атаки (блоков) - attack-range: 3.0 - - # Длительность оглушения паллетом (секунд) - pallet-stun-seconds: 2.0 - - # Время ломания паллета (секунд) - pallet-break-seconds: 3.0 - - # Радиус страха (блоков) - terror-radius: 32 - - # Расстояние для взаимодействия с паллетом (блоков) - pallet-interaction-range: 3.0 - -# BossBar радиуса страха -bossbar: - # Включить BossBar - enabled: true - - # Название (поддерживает MiniMessage) - title: "☠ МАНЬЯК РЯДОМ ☠" - - # Цвет: PINK, BLUE, RED, GREEN, YELLOW, PURPLE, WHITE - color: RED - - # Стиль: SOLID, SEGMENTED_6, SEGMENTED_10, SEGMENTED_12, SEGMENTED_20 - style: SOLID - - # Расстояние начала появления (блоков) - appear-range: 32 - - # Расстояние полного заполнения (блоков) - full-range: 8 - -# Окна -window: - # Быстрый волт выжившего (секунд) - vault-survivor-fast: 1.0 - - # Медленный волт выжившего (секунд) - vault-survivor-slow: 1.5 - - # Волт киллера (секунд) - vault-killer: 1.5 - - # Блокировка после 3 волтов подряд (секунд) - vault-block-seconds: 15 - -# Эндгейм -endgame: - # Время открытия ворот (секунд) - gate-open-seconds: 20 - - # Время до конца после открытия ворот (секунд) - collapse-seconds: 120 - - # Открытие сундуков (секунд) - chest-open-seconds: 3 - - # Расстояние для взаимодействия с воротами (блоков) - gate-interaction-range: 3.0 - -# Уведомление маньяка о генераторе -killer-notification: - # Время действия компаса (секунд) - compass-duration-seconds: 5 - -# Скорборд -scoreboard: - # Показывать сердца (жизни) - show-hearts: true - - # Символ сердца - heart-symbol: "♥" - - # Цвет ника: HEALTHY, INJURED (красный), HEALING (зелёный), HOOKED (тёмно-красный) - color-healthy: "WHITE" - color-injured: "RED" - color-healing: "GREEN" - color-hooked: "DARK_RED" - -# Люк -hatch: - # Минимум живых для открытия люка - min-survivors-for-open: 1 - - # Условие: живых < починенных генераторов - require-more-gens-than-survivors: true - -# Лут сундуков -loot: - medkit: - weight: 55 - min-charges: 12 - max-charges: 16 - flashlight: - weight: 45 - min-charges: 6 - max-charges: 8 - -# Custom Model Data для ресурспака -# Задайте числа соответствующие overrides в вашем ресурспаке. -# 0 = не применять (использовать ванильную модель) -items: - model-data: - medkit: 1001 - flashlight: 1002 diff --git a/bin/main/locations.yml b/bin/main/locations.yml deleted file mode 100644 index d52f930..0000000 --- a/bin/main/locations.yml +++ /dev/null @@ -1,58 +0,0 @@ -# ============================================ -# Mdbd Plugin - Координаты объектов -# Устанавливайте через /dbdsetup set <тип> -# ============================================ - -# Лобби (спавн перед игрой) -lobby: - world: "world" - x: 0.0 - y: 100.0 - z: 0.0 - yaw: 0.0 - pitch: 0.0 - -# Генераторы — настраиваются через /dbdadmin create generator -# и сохраняются в data/generators.yml - -# Крюки (обычно 4-6) -hooks: [] -# Пример: -# - "world,120.5,65.0,180.5,0.0,0.0" - -# Паллеты (много) -pallets: [] -# Пример: -# - "world,110.5,64.0,190.5,0.0,0.0" - -# Окна (много) -windows: [] - -# Сундуки с лутом (3-4) -chests: [] - -# Тотемы (5) -totems: [] - -# Ворота выхода (2) -exitgates: [] - -# Спавн киллера (1 точка) -killer-spawn: [] - -# Спавны выживших (4 точки) -survivor-spawns: [] - -# ============================================ -# Команды для настройки: -# /dbdadmin create generator - Войти в режим выбора генератора (сломайте блок) -# /dbdadmin list - Список всех генераторов -# /dbdadmin remove - Удалить генератор по ID -# /dbdadmin clear - Очистить все генераторы -# /dbdsetup set hook - Установить крюк -# /dbdsetup set pallet - Установить паллет -# /dbdsetup set chest - Установить сундук -# /dbdsetup set totem - Установить тотем -# /dbdsetup set gate - Установить ворота -# /dbdsetup set lobby - Установить лобби -# ============================================ diff --git a/bin/main/messages.yml b/bin/main/messages.yml deleted file mode 100644 index d866caf..0000000 --- a/bin/main/messages.yml +++ /dev/null @@ -1,119 +0,0 @@ -# ============================================ -# Mdbd Plugin - Сообщения -# Поддерживаются цвета в формате &#rrggbb -# ============================================ - -# Префикс всех сообщений плагина -prefix: "&#FF6B00&l[DBD]&r " - -# Сообщения игры -game: - # Старт/конец - start: "�FF00Игра началась! Бегите от Киллера!" - end-victory-escape: "&#FFD700Выжившие сбежали! Победа!" - end-victory-killer: "&#DC143CКиллер победил!" - - # Ожидание - waiting: "&#FFFF00Ожидание игроков... (%s/%s)" - countdown: "&#FFA500До начала: %s секунд" - countdown-short: "&#FF0000%s..." - - # Роли - role-killer: "&#DC143CВы Киллер! Поймайте всех выживших." - role-survivor: "�CED1Вы Выживший! Почините генераторы и сбегите." - -# Генераторы -generator: - repair-start: "�CED1Вы начали чинить генератор..." - repair-stop: "WCEEBВы остановились." - repair-complete: "�FF00✓ Генератор активирован! (%s/%s)" - repair-progress: "&#FFD700Прогресс: %s%%" - - skillcheck-success: "�FF00Отлично! +%s%%" - skillcheck-super: "&#FFD700Великолепно! +%s%%" - skillcheck-fail: "&#FF0000✗ Провал! -%s%%" - - explosion: "&#FF4500⚠ Взрыв генератора!" - regression: "&#FF6347Генератор откатывается..." - kicked: "&#FF4500Киллер пнул генератор!" - -# Киллер -killer: - terror-near: "B0000Сердце колотится..." - terror-medium: "&#FF4500Опасно рядом..." - terror-far: "&#FFA500Киллер где-то рядом" - - chase-start: "&#FF0000⚠ Киллер преследует вас!" - chase-stop: "�FF00Вы ушли от погони." - - hit: "&#DC143CВы ранены! Ищите укрытие!" - down: "B0000Вы в нокдауне! Ползите!" - pickup: "󃕐Киллер поднял вас..." - hook: "B0082Вас повесили на крюк!" - -# Выжившие -survivor: - heal-self-start: "�FA9AЛечите себя..." - heal-self-complete: "�FF00Вылечены!" - heal-other-start: "�FA9AЛечите союзника..." - heal-other-complete: "�FF00Союзник вылечен!" - heal-interrupt: "&#FFD700Лечение прервано!" - - sprint-exhausted: "&#FF8C00Вы устали... (Истощение)" - sprint-ready: "�FF00Можно бежать!" - - pallet-drop: "&#DEB887Вы бросили паллет!" - pallet-stun: "&#FFD700Киллер оглушён! Уходите!" - - vault-fast: "�CED1Быстрый прыжок!" - vault-slow: "WCEEBМедленный прыжок..." - - rescue-start: "�CED1Спасаете с крюка..." - rescue-complete: "�FF00Союзник спасён!" - -# Крюки -hook: - stage1: "&#FFFF00Крюк: Фаза 1 — Не двигайтесь" - stage2: "&#FFA500Крюк: Фаза 2 — БОРИТЕСЬ!" - struggle-success: "�FF00Успех! Держитесь!" - struggle-fail: "&#FF0000Провал! Этап ускорен!" - sacrifice: "B0000☠ Вы жертвованы..." - -# Предметы -item: - found: "&#FFD700Найдено: %s" - - medkit-use: "�FA9AИспользуете аптечку..." - medkit-depleted: "&#FF6347Аптечка закончилась" - - flashlight-blind: "&#FFFF00Ослепили киллера! Уходите!" - flashlight-depleted: "&#FF6347Батарейка села" - - key-hatch: "�CED1Ключ открывает люк!" - map-reveal: "WCEEBОбъекты показаны на карте" - - chest-opening: "&#FFD700Открываете сундук..." - chest-found: "�FF00Нашли: %s" - -# Эндгейм -endgame: - gates-powered: "�FF00Ворота запитаны! Бегите!" - gates-opening: "&#FFD700Открываете ворота..." - - hatch-open: "B0082Люк открыт! Сбегите!" - hatch-escape: "�FF00✓ Вы сбежали через люк!" - - collapse-warning: "&#FF0000⚠ КОЛЛАПС ЧЕРЕЗ %s СЕК!" - collapse-start: "B0000КОЛЛАПС НАЧАЛСЯ! БЕГИТЕ!" - - escape: "�FF00✓ ПОБЕГ!" - -# Ошибки -error: - no-permission: "&#FF0000У вас нет прав!" - not-in-game: "&#FF6347Вы не в игре!" - already-in-game: "&#FF6347Вы уже в игре!" - game-full: "&#FF6347Игра заполнена!" - no-game: "&#FF6347Нет активной игры!" - not-survivor: "&#FF6347Только для выживших!" - not-killer: "&#FF6347Только для киллера!" diff --git a/bin/main/plugin.yml b/bin/main/plugin.yml deleted file mode 100644 index deb1a7d..0000000 --- a/bin/main/plugin.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Mdbd -version: '${version}' -main: dev.loki.mdbd -api-version: '1.21' - -commands: - dbd: - description: "Main DBD command" - usage: "/dbd " - permission: mdbd.player - dbdsetup: - description: "Map setup commands" - usage: "/dbdsetup " - permission: mdbd.admin - dbdadmin: - description: "Admin commands for generators and objects" - usage: "/dbdadmin " - permission: mdbd.admin - -permissions: - mdbd.admin: - description: "Full access to admin commands" - default: op - mdbd.player: - description: "Basic player commands" - default: true diff --git a/bin/main/skillcheck.yml b/bin/main/skillcheck.yml deleted file mode 100644 index 74cde8e..0000000 --- a/bin/main/skillcheck.yml +++ /dev/null @@ -1,120 +0,0 @@ -# ============================================ -# Mdbd Plugin - Настройки Skill Check (QTE) -# ============================================ - -# Цвета в формате &#rrggbb -colors: - # Зона успеха (зелёная) - zone-success: "�FF00" - - # Зона супер-успеха (золотая, бонус) - zone-super: "&#FFD700" - - # Стрелка (белая) - arrow: "&#FFFFFF" - - # Фон трека (тёмно-серый) - background: "񑘕" - - # Текст титра (жёлтый) - text-title: "&#FFFF00" - - # Подзаголовок (белый) - text-subtitle: "&#FFFFFF" - - # Цвет провала (красный) - fail: "&#FF0000" - -# Размеры зон (в градусах) -zones: - # Зона обычного успеха (±градусов от цели) - success-size: 15 - - # Зона супер-успеха (меньше = сложнее) - super-size: 5 - -# Вращение -rotation: - # Скорость вращения (оборотов в секунду) - speed: 3.0 - - # Направление: CLOCKWISE или COUNTER_CLOCKWISE - direction: "CLOCKWISE" - - # Случайное направление для каждого скиллчека - variation: true - - # Случайная начальная позиция - random-start: true - -# Тайминги -timing: - # Минимальный интервал между скиллчеками (в тиках, 20 = 1 сек) - min-interval: 80 - - # Максимальный интервал (в тиках) - max-interval: 200 - - # Время на реакцию (тиков после входа в зону) - reaction-window: 20 - - # Задержка перед показом скиллчека (тиков) - pre-delay: 5 - -# Награды и штрафы (в процентах от прогресса) -rewards: - # Обычный успех: +% к прогрессу - success-bonus: 5.0 - - # Супер-успех: +% к прогрессу - super-bonus: 10.0 - -penalties: - # Провал: -% от прогресса - fail-penalty: 10.0 - - # Радиус алерта киллера при провале (блоков) - fail-alert-range: 16 - - # Визуальный взрыв при провале - fail-explosion: true - - # Громкость звука взрыва (0.0 - 1.0) - explosion-volume: 0.8 - -# Визуальные эффекты -visual: - # Показывать трек движения - show-track: true - - # Показывать стрелку - show-arrow: true - - # Анимация при успехе - flash-on-success: true - - # Тряска экрана при провале - screenshake-on-fail: true - - # Альтернативный режим через BossBar (вместо Title) - use-boss-bar: false - -# Звуки -sounds: - # Звук при появлении скиллчека - appear: "BLOCK_NOTE_BLOCK_PLING" - appear-volume: 0.5 - - # Звук успеха - success: "ENTITY_PLAYER_LEVELUP" - success-volume: 0.6 - - # Звук супер-успеха - super: "ENTITY_PLAYER_LEVELUP" - super-volume: 1.0 - super-pitch: 1.5 - - # Звук провала - fail: "BLOCK_ANVIL_LAND" - fail-volume: 0.8 - fail-pitch: 0.8 diff --git a/src/main/java/dev/loki/chest/Chest.java b/src/main/java/dev/loki/chest/Chest.java index 9e15fad..69de308 100644 --- a/src/main/java/dev/loki/chest/Chest.java +++ b/src/main/java/dev/loki/chest/Chest.java @@ -15,6 +15,9 @@ public class Chest { private boolean opened; private boolean empty; private ItemStack loot; + private long lastEmptiedTime; + + private static final long COOLDOWN_MILLIS = 3 * 60 * 1000; // 3 minutes public Chest(Location location) { this.id = UUID.randomUUID(); @@ -22,6 +25,7 @@ public class Chest { this.opened = false; this.empty = false; this.loot = null; + this.lastEmptiedTime = 0; } public UUID getId() { @@ -52,6 +56,21 @@ public class Chest { return !opened && !empty && hasLoot(); } + public void onLooted() { + this.empty = true; + this.lastEmptiedTime = System.currentTimeMillis(); + } + + public long getSecondsUntilReset() { + long elapsed = (System.currentTimeMillis() - lastEmptiedTime) / 1000; + long remaining = (COOLDOWN_MILLIS / 1000) - elapsed; + return Math.max(0, remaining); + } + + public boolean isOnCooldown() { + return getSecondsUntilReset() > 0; + } + public ItemStack getLoot() { return loot; } diff --git a/src/main/java/dev/loki/chest/ChestHologram.java b/src/main/java/dev/loki/chest/ChestHologram.java index d61a787..a0975ba 100644 --- a/src/main/java/dev/loki/chest/ChestHologram.java +++ b/src/main/java/dev/loki/chest/ChestHologram.java @@ -73,7 +73,7 @@ public class ChestHologram { continue; } - Location holoLoc = loc.clone().add(0.5, 2.0 + heightOffset, 0.5); + Location holoLoc = loc.getBlock().getLocation().add(0.5, 2.0 + heightOffset, 0.5); try { if (useTextDisplay) { TextDisplay display = loc.getWorld().spawn(holoLoc, TextDisplay.class, d -> { @@ -118,14 +118,8 @@ public class ChestHologram { String status; if (chest.isEmpty()) { - long secs = manager.getSecondsUntilNextReset(); - if (secs > 60) { - status = "§7Пуст §8(" + (secs / 60) + "м)"; - } else if (secs > 0) { - status = "§7Пуст §8(" + secs + "с)"; - } else { - status = "§7Пуст"; - } + long secs = chest.getSecondsUntilReset(); + status = secs > 0 ? "§7Пусто §8(" + secs + "с)" : "§7Пусто"; } else if (chest.isOpened()) { status = "§6Открывается..."; } else if (chest.hasLoot()) { @@ -139,11 +133,11 @@ public class ChestHologram { if (display instanceof TextDisplay td) { td.text(TextUtil.color(text)); - Location base = chest.getLocation().clone().add(0.5, 2.0 + heightOffset, 0.5); + Location base = chest.getLocation().getBlock().getLocation().add(0.5, 2.0 + heightOffset, 0.5); td.teleport(base); } else if (display instanceof ArmorStand as) { as.setCustomName(text); - Location base = chest.getLocation().clone().add(0.5, 2.0 + heightOffset, 0.5); + Location base = chest.getLocation().getBlock().getLocation().add(0.5, 2.0 + heightOffset, 0.5); as.teleport(base); } } diff --git a/src/main/java/dev/loki/chest/ChestManager.java b/src/main/java/dev/loki/chest/ChestManager.java index f38b4e7..ec8866d 100644 --- a/src/main/java/dev/loki/chest/ChestManager.java +++ b/src/main/java/dev/loki/chest/ChestManager.java @@ -21,7 +21,6 @@ public class ChestManager { private final GameConfig gameConfig; private ChestLoot chestLoot; private BukkitTask lootResetTask; - private long lastResetTime = System.currentTimeMillis(); public ChestManager(JavaPlugin plugin, LocationsConfig locationsConfig, GameConfig gameConfig) { this.plugin = plugin; @@ -36,7 +35,6 @@ public class ChestManager { List locations = locationsConfig.getChests(); for (Location loc : locations) { Chest chest = new Chest(loc); - // Populate loot immediately so it's available chest.setLoot(chestLoot.rollLoot()); chests.put(chest.getId(), chest); } @@ -44,14 +42,12 @@ public class ChestManager { } /** - * Start periodic loot reset timer. - * Every N minutes (from config), all empty chests get new loot. + * Start per-chest cooldown timer. Each chest refills individually 3 min after being looted. */ public void startLootResetTimer() { if (lootResetTask != null) lootResetTask.cancel(); - long intervalTicks = (long) (gameConfig.getChestLootResetMinutes() * 60 * 20); - lootResetTask = plugin.getServer().getScheduler().runTaskTimer(plugin, this::resetLoot, intervalTicks, intervalTicks); - plugin.getLogger().info("Chest loot reset timer started (every " + gameConfig.getChestLootResetMinutes() + " min)"); + lootResetTask = plugin.getServer().getScheduler().runTaskTimer(plugin, this::tickCooldowns, 20L, 20L); + plugin.getLogger().info("Per-chest cooldown timer started (3 min per chest)"); } public void stopLootResetTimer() { @@ -62,20 +58,16 @@ public class ChestManager { } /** - * Reset loot: refill ALL chests with new random loot. + * Every second, check each chest — if its cooldown expired, refill it. */ - private void resetLoot() { - int refilled = 0; + private void tickCooldowns() { for (Chest chest : chests.values()) { - // Skip chests currently being opened to avoid race condition if (chest.isOpened()) continue; - chest.setLoot(chestLoot.rollLoot()); - chest.setOpened(false); - chest.setEmpty(false); - refilled++; + if (chest.isEmpty() && !chest.isOnCooldown()) { + chest.setLoot(chestLoot.rollLoot()); + chest.setEmpty(false); + } } - this.lastResetTime = System.currentTimeMillis(); - plugin.getLogger().info("[Chest] Loot reset — " + refilled + " chests refilled"); } public Chest getChest(UUID id) { @@ -117,7 +109,6 @@ public class ChestManager { chest.setEmpty(false); chest.setLoot(chestLoot.rollLoot()); } - this.lastResetTime = System.currentTimeMillis(); plugin.getLogger().info("Reset all chests"); } @@ -140,13 +131,7 @@ public class ChestManager { return (int) chests.values().stream().filter(Chest::isAvailable).count(); } - /** Seconds until the next automatic loot reset. */ - public long getSecondsUntilNextReset() { - long elapsed = (System.currentTimeMillis() - lastResetTime) / 1000; - long total = (long) (gameConfig.getChestLootResetMinutes() * 60); - long remaining = total - elapsed; - return Math.max(0, remaining); - } + public ChestLoot getChestLoot() { return chestLoot; diff --git a/src/main/java/dev/loki/chest/ChestOpening.java b/src/main/java/dev/loki/chest/ChestOpening.java index 32a4122..1618e92 100644 --- a/src/main/java/dev/loki/chest/ChestOpening.java +++ b/src/main/java/dev/loki/chest/ChestOpening.java @@ -47,15 +47,9 @@ public class ChestOpening { return false; } - // Ensure chest has loot; roll if missing - if (!chest.hasLoot()) { - ItemStack rolled = chestLoot.rollLoot(); - if (rolled == null) { - player.sendMessage(messages().get("chest.empty")); - return false; - } - chest.setLoot(rolled); - chest.setEmpty(false); + if (!chest.hasLoot() || chest.isEmpty()) { + player.sendMessage(messages().get("chest.empty")); + return false; } if (!chest.isAvailable()) { @@ -152,7 +146,7 @@ public class ChestOpening { } } chest.setOpened(false); - chest.setEmpty(true); + chest.onLooted(); chest.setLoot(null); // Clear loot after giving chest.closeVisual(); diff --git a/src/main/java/dev/loki/command/DBDAdminCommand.java b/src/main/java/dev/loki/command/DBDAdminCommand.java index 516f0c4..99d298f 100644 --- a/src/main/java/dev/loki/command/DBDAdminCommand.java +++ b/src/main/java/dev/loki/command/DBDAdminCommand.java @@ -43,6 +43,7 @@ public class DBDAdminCommand implements TabExecutor { return switch (args[0].toLowerCase()) { case "list" -> handleList(player, args); case "world" -> handleWorld(player, args); + case "repairgens" -> handleRepairGens(player); default -> { sendUsage(sender); yield true; @@ -57,6 +58,7 @@ public class DBDAdminCommand implements TabExecutor { if (args.length >= 2) { return switch (args[1].toLowerCase()) { case "generator" -> listType(player, "Генераторы", locations.getGenerators()); + case "computer" -> listType(player, "Компьютеры", plugin.getComputerData().getComputers()); case "hook" -> listType(player, "Крюки", locations.getHooks()); case "chest" -> listType(player, "Сундуки", locations.getChests()); case "killer-spawn", "killerspawn" -> listType(player, "Спавн киллера", locations.getKillerSpawns()); @@ -72,6 +74,7 @@ public class DBDAdminCommand implements TabExecutor { player.sendMessage(plugin.getConfigManager().getMessages().get("dbdadmin-list.header")); listType(player, "Генераторы", locations.getGenerators()); + listType(player, "Компьютеры", plugin.getComputerData().getComputers()); listType(player, "Крюки", locations.getHooks()); listType(player, "Сундуки", locations.getChests()); listType(player, "Спавн киллера", locations.getKillerSpawns()); @@ -124,12 +127,41 @@ public class DBDAdminCommand implements TabExecutor { return true; } + private boolean handleRepairGens(Player player) { + var gameManager = plugin.getGameManager(); + var session = gameManager.getCurrentSession(); + if (session == null) { + player.sendMessage("§cНет активной игры."); + return true; + } + + var genManager = session.getGeneratorManager(); + if (genManager == null) { + player.sendMessage("§cGeneratorManager не найден."); + return true; + } + + int count = 0; + for (var gen : genManager.getIncompleteGenerators()) { + gen.setProgress(100.0); + genManager.onGeneratorCompleted(gen); + count++; + } + + player.sendMessage("§aПочинено генераторов: §e" + count); + if (count > 0 && genManager.areAllRequiredCompleted()) { + player.sendMessage("§a✔ Все необходимые генераторы починены — ворота открываются!"); + } + return true; + } + private void sendUsage(CommandSender sender) { var messages = plugin.getConfigManager().getMessages(); sender.sendMessage(messages.get("usage.admin-list-header")); sender.sendMessage(messages.get("usage.admin-list-list")); sender.sendMessage(messages.get("usage.admin-list-type")); sender.sendMessage(messages.get("usage.admin-world")); + sender.sendMessage("§7/dbdadmin repairgens §8— §fПочинить все генераторы"); } @Override @@ -138,13 +170,13 @@ public class DBDAdminCommand implements TabExecutor { if (!sender.hasPermission("mdbd.admin")) return List.of(); if (args.length == 1) { - return Arrays.asList("list", "world").stream() + return Arrays.asList("list", "world", "repairgens").stream() .filter(s -> s.startsWith(args[0].toLowerCase())) .toList(); } if (args.length == 2 && args[0].equalsIgnoreCase("list")) { - return Arrays.asList("generator", "hook", "chest", "killer-spawn", "survivor-spawn").stream() + return Arrays.asList("generator", "computer", "hook", "chest", "killer-spawn", "survivor-spawn").stream() .filter(s -> s.startsWith(args[1].toLowerCase())) .toList(); } diff --git a/src/main/java/dev/loki/command/SetupCommand.java b/src/main/java/dev/loki/command/SetupCommand.java index 5e8d218..53b0b3b 100644 --- a/src/main/java/dev/loki/command/SetupCommand.java +++ b/src/main/java/dev/loki/command/SetupCommand.java @@ -1,8 +1,10 @@ package dev.loki.command; +import dev.loki.gate.ExitGate; import dev.loki.mdbd; import dev.loki.util.SpawnUtil; import dev.loki.util.TextUtil; +import dev.loki.config.LocationsConfig; import dev.loki.config.MessagesConfig; import org.bukkit.Location; import org.bukkit.Material; @@ -70,7 +72,8 @@ public class SetupCommand implements TabExecutor { return switch (args[0].toLowerCase()) { case "wand" -> giveWand(player); case "set" -> handleSet(player, args); - case "clear" -> clearMap(player); + case "clear" -> handleClear(player, args); + case "delete" -> handleDelete(player, args); case "clear-spawns" -> clearSpawns(player); case "lobby" -> setLobby(player); default -> { sendUsage(sender); yield true; } @@ -129,7 +132,7 @@ public class SetupCommand implements TabExecutor { default -> type; }; - if (!List.of("hook", "chest", "generator", "killer-spawn", "survivor-spawn").contains(mode)) { + if (!List.of("hook", "chest", "generator", "computer", "killer-spawn", "survivor-spawn").contains(mode)) { player.sendMessage(messages().get("setup.unknown-type", type)); return true; } @@ -201,8 +204,14 @@ public class SetupCommand implements TabExecutor { var locations = plugin.getConfigManager().getLocations(); locations.setGate(gateId, center); + locations.setGateZone(gateId, p1, p2); plugin.getConfigManager().saveConfig("locations.yml", locations.getConfig()); + ExitGate gate = new ExitGate(center); + gate.setZone(p1, p2); + gate.spawnGateStructure(); + gate.spawnGateBlock(); + player.sendMessage(messages().get("setup.gate-placed", gateId, formatLoc(center))); return true; } @@ -255,6 +264,17 @@ public class SetupCommand implements TabExecutor { plugin.getGeneratorData().addGenerator(gLoc); player.sendMessage(messages().get("setup.generator-placed", formatLoc(loc))); } + case "computer" -> { + Location cLoc = blockLoc.clone().add(0.5, 0, 0.5); + if (locations.hasLocation("computers", cLoc)) { + player.sendMessage(messages().get("setup.duplicate")); + break; + } + locations.addLocation("computers", cLoc); + plugin.getComputerData().addComputer(cLoc); + player.sendMessage(plugin.getConfigManager().getMessages().get("setup.computer-placed", + formatLoc(loc))); + } case "killer-spawn" -> { if (locations.getKillerSpawn() != null) { player.sendMessage(messages().get("setup.killer-spawn-exists")); @@ -330,6 +350,174 @@ public class SetupCommand implements TabExecutor { return true; } + // ── Clear ──────────────────────────────────────────────────────── + + private boolean handleClear(Player player, String[] args) { + var locations = plugin.getConfigManager().getLocations(); + + if (args.length >= 2) { + String type = args[1].toLowerCase(); + return clearObjectsByType(player, locations, type); + } + + // Clear ALL object data from config (no block destruction) + locations.clearLocationList("generators"); + locations.clearLocationList("computers"); + locations.clearLocationList("hooks"); + locations.clearLocationList("chests"); + locations.clearLocationList("killer-spawn"); + for (String id : List.of("a", "b")) { + locations.getConfig().set("gate-" + id, null); + locations.getConfig().set("gate-" + id + "-corner1", null); + locations.getConfig().set("gate-" + id + "-corner2", null); + } + locations.clearSurvivorSpawns(); + plugin.getGeneratorData().clearGenerators(); + plugin.getComputerData().clearComputers(); + plugin.getConfigManager().saveConfig("locations.yml", locations.getConfig()); + player.sendMessage(messages().get("setup.cleared")); + return true; + } + + private boolean clearObjectsByType(Player player, LocationsConfig locations, String type) { + String path = switch (type) { + case "generator" -> "generators"; + case "computer" -> "computers"; + case "hook" -> "hooks"; + case "chest" -> "chests"; + case "gate" -> null; + case "killer-spawn" -> "killer-spawn"; + case "survivor-spawn" -> null; + default -> { player.sendMessage(messages().get("setup.unknown-type", type)); yield null; } + }; + if (path == null) { + if (type.equals("gate")) { + for (String id : List.of("a", "b")) { + locations.getConfig().set("gate-" + id, null); + locations.getConfig().set("gate-" + id + "-corner1", null); + locations.getConfig().set("gate-" + id + "-corner2", null); + } + } else if (type.equals("survivor-spawn")) { + locations.clearSurvivorSpawns(); + } else { + return true; + } + } else { + locations.clearLocationList(path); + if (type.equals("generator")) { + plugin.getGeneratorData().clearGenerators(); + } else if (type.equals("computer")) { + plugin.getComputerData().clearComputers(); + } + } + plugin.getConfigManager().saveConfig("locations.yml", locations.getConfig()); + player.sendMessage(messages().get("setup.cleared-type", type)); + return true; + } + + // ── Delete ─────────────────────────────────────────────────────── + + private boolean handleDelete(Player player, String[] args) { + if (args.length < 3) { + player.sendMessage(messages().get("setup.delete-usage")); + return true; + } + + String type = args[1].toLowerCase(); + var locations = plugin.getConfigManager().getLocations(); + + switch (type) { + case "generator" -> { + int index = parseInt(args[2], player); + if (index < 0) return true; + var gens = locations.getGenerators(); + if (index >= gens.size()) { + player.sendMessage(messages().get("setup.out-of-range", index)); + return true; + } + locations.removeLocationAtIndex("generators", index); + plugin.getGeneratorData().removeGenerator(index); + player.sendMessage(messages().get("setup.deleted", "generator #" + index)); + } + case "computer" -> { + int index = parseInt(args[2], player); + if (index < 0) return true; + var comps = plugin.getComputerData().getComputers(); + if (index >= comps.size()) { + player.sendMessage(messages().get("setup.out-of-range", index)); + return true; + } + locations.removeLocationAtIndex("computers", index); + plugin.getComputerData().removeComputer(index); + player.sendMessage(messages().get("setup.deleted", "computer #" + index)); + } + case "hook" -> { + int index = parseInt(args[2], player); + if (index < 0) return true; + var hooks = locations.getHooks(); + if (index >= hooks.size()) { + player.sendMessage(messages().get("setup.out-of-range", index)); + return true; + } + locations.removeLocationAtIndex("hooks", index); + player.sendMessage(messages().get("setup.deleted", "hook #" + index)); + } + case "chest" -> { + int index = parseInt(args[2], player); + if (index < 0) return true; + var chests = locations.getChests(); + if (index >= chests.size()) { + player.sendMessage(messages().get("setup.out-of-range", index)); + return true; + } + locations.removeLocationAtIndex("chests", index); + player.sendMessage(messages().get("setup.deleted", "chest #" + index)); + } + case "killer-spawn" -> { + locations.clearLocationList("killer-spawn"); + player.sendMessage(messages().get("setup.deleted", "killer-spawn")); + } + case "survivor-spawn" -> { + int index = parseInt(args[2], player); + if (index < 0) return true; + if (index < 1 || index > 4) { + player.sendMessage(messages().get("setup.out-of-range", index)); + return true; + } + locations.clearSurvivorSpawn(index); + player.sendMessage(messages().get("setup.deleted", "survivor-spawn #" + index)); + } + case "gate" -> { + String raw = args[2].toLowerCase(); + String id = switch (raw) { case "a", "1" -> "a"; case "b", "2" -> "b"; default -> null; }; + if (id == null || locations.getGate(id) == null) { + player.sendMessage(messages().get("setup.out-of-range", raw)); + return true; + } + locations.getConfig().set("gate-" + id, null); + locations.getConfig().set("gate-" + id + "-corner1", null); + locations.getConfig().set("gate-" + id + "-corner2", null); + player.sendMessage(messages().get("setup.deleted", "gate-" + id)); + } + default -> { + player.sendMessage(messages().get("setup.unknown-type", type)); + return true; + } + } + + plugin.getConfigManager().saveConfig("locations.yml", locations.getConfig()); + return true; + } + + private int parseInt(String s, Player player) { + try { + return Integer.parseInt(s); + } catch (NumberFormatException e) { + player.sendMessage(messages().get("setup.invalid-number", s)); + return -1; + } + } + private boolean clearSpawns(Player player) { var locations = plugin.getConfigManager().getLocations(); locations.clearLocationList("killer-spawn"); @@ -355,9 +543,12 @@ public class SetupCommand implements TabExecutor { sender.sendMessage(messages().get("usage.setup-hook")); sender.sendMessage(messages().get("usage.setup-chest")); sender.sendMessage(messages().get("usage.setup-generator")); + sender.sendMessage(messages().get("usage.setup-computer")); sender.sendMessage(messages().get("usage.setup-killer-spawn")); sender.sendMessage(messages().get("usage.setup-survivor-spawn")); sender.sendMessage(messages().get("usage.setup-gate")); + sender.sendMessage(messages().get("usage.setup-clear")); + sender.sendMessage(messages().get("usage.setup-delete")); sender.sendMessage(messages().get("usage.setup-clear-spawns")); sender.sendMessage(messages().get("usage.setup-lobby")); } @@ -372,12 +563,14 @@ public class SetupCommand implements TabExecutor { @NotNull String label, @NotNull String[] args) { if (!sender.hasPermission("mdbd.admin")) return List.of(); if (args.length == 1) { - return List.of("wand", "set", "clear", "clear-spawns", "lobby").stream() + return List.of("wand", "set", "clear", "delete", "clear-spawns", "lobby").stream() .filter(s -> s.startsWith(args[0].toLowerCase())).toList(); } - if (args.length == 2 && args[0].equalsIgnoreCase("set")) { - return List.of("hook", "chest", "generator", "killer-spawn", "survivor-spawn", "gate").stream() - .filter(s -> s.startsWith(args[1].toLowerCase())).toList(); + if (args.length == 2) { + if (args[0].equalsIgnoreCase("set") || args[0].equalsIgnoreCase("clear") || args[0].equalsIgnoreCase("delete")) { + return List.of("computer", "generator", "hook", "chest", "killer-spawn", "survivor-spawn", "gate").stream() + .filter(s -> s.startsWith(args[1].toLowerCase())).toList(); + } } return List.of(); } diff --git a/src/main/java/dev/loki/config/GameConfig.java b/src/main/java/dev/loki/config/GameConfig.java index 9187ba4..2b62100 100644 --- a/src/main/java/dev/loki/config/GameConfig.java +++ b/src/main/java/dev/loki/config/GameConfig.java @@ -28,8 +28,6 @@ public class GameConfig { setDefault("generators.total-count", 7); setDefault("generators.required-count", 5); setDefault("generators.repair-seconds", 90); - setDefault("generators.regression-rate", 0.25); - setDefault("generators.kick-regression-rate", 2.5); setDefault("generators.kick-regression-percent", 20.0); setDefault("generators.kick-duration-seconds", 30); setDefault("generators.kick-cooldown-seconds", 30); @@ -37,6 +35,12 @@ public class GameConfig { setDefault("generators.interaction-range", 3.0); setDefault("generators.minigame", "gui"); + // Computers + setDefault("computers.total-count", 2); + setDefault("computers.required-count", 2); + setDefault("computers.repair-seconds", 90); + setDefault("computers.interaction-range", 3.0); + // Hooks setDefault("hooks.stage1-seconds", 60); setDefault("hooks.stage2-seconds", 60); @@ -84,8 +88,11 @@ public class GameConfig { // Endgame setDefault("endgame.gate-open-seconds", 20); + setDefault("endgame.pregame-seconds", 30); + setDefault("endgame.postgame-seconds", 60); setDefault("endgame.chest-open-seconds", 3); setDefault("endgame.gate-interaction-range", 3.0); + setDefault("endgame.escape-range", 2.0); // Killer notification setDefault("killer-notification.compass-duration-seconds", 5); @@ -106,12 +113,9 @@ public class GameConfig { setDefault("colors.progress-bar-incomplete", "&#FF0000"); setDefault("colors.progress-bar-background", "񑘕"); - // Hatch - setDefault("hatch.min-survivors-for-open", 1); - setDefault("hatch.require-more-gens-than-survivors", true); - // Block types for spawning setDefault("blocks.generator", "BLAST_FURNACE"); + setDefault("blocks.computer", "LECTERN"); setDefault("blocks.chest", "CHEST"); setDefault("blocks.hook", "IRON_BARS"); @@ -119,6 +123,8 @@ public class GameConfig { setDefault("holograms.enabled", true); setDefault("holograms.generator.enabled", true); setDefault("holograms.generator.height-offset", -1.0); + setDefault("holograms.computer.enabled", true); + setDefault("holograms.computer.height-offset", -1.0); setDefault("holograms.hook.enabled", true); setDefault("holograms.hook.height-offset", -1.0); setDefault("holograms.chest.enabled", true); @@ -134,6 +140,22 @@ public class GameConfig { // Chest loot setDefault("chest.loot-reset-minutes", 3.0); + + // Loot weights + setDefault("loot.medkit.weight", 25); + setDefault("loot.medkit.min-charges", 12); + setDefault("loot.medkit.max-charges", 16); + setDefault("loot.bandage.weight", 25); + setDefault("loot.full-medkit.weight", 10); + setDefault("loot.soda.weight", 20); + setDefault("loot.wrench.weight", 20); + + // Item model data + setDefault("items.model-data.medkit", 1001); + setDefault("items.model-data.bandage", 1004); + setDefault("items.model-data.fullmedkit", 1005); + setDefault("items.model-data.soda", 1006); + setDefault("items.model-data.wrench", 1007); } private void setDefault(String path, Object value) { @@ -177,6 +199,20 @@ public class GameConfig { public double getGeneratorInteractionRange() { return config.getDouble("generators.interaction-range", 3.0); } public String getGeneratorMinigame() { return config.getString("generators.minigame", "gui"); } + // ------------------------------------------------------------------------- + // Computers + // ------------------------------------------------------------------------- + public int getComputerTotal() { return config.getInt("computers.total-count", 5); } + public int getComputerRequired() { return config.getInt("computers.required-count", 3); } + public int getComputerRepairSeconds() { return config.getInt("computers.repair-seconds", 90); } + public double getComputerRegressionRate() { return config.getDouble("computers.regression-rate", 0.25); } + public double getComputerKickRegressionRate() { return config.getDouble("computers.kick-regression-rate", 2.5); } + public double getComputerKickRegressionPercent() { return config.getDouble("computers.kick-regression-percent", 20.0); } + public int getComputerKickDuration() { return config.getInt("computers.kick-duration-seconds", 30); } + public int getComputerKickCooldownSeconds() { return config.getInt("computers.kick-cooldown-seconds", 30); } + public int getComputerKickChargeSeconds() { return config.getInt("computers.kick-charge-seconds", 3); } + public double getComputerInteractionRange() { return config.getDouble("computers.interaction-range", 3.0); } + // ------------------------------------------------------------------------- // Hooks // ------------------------------------------------------------------------- @@ -230,6 +266,7 @@ public class GameConfig { // Block types (какие блоки спавнятся на местах объектов) // ------------------------------------------------------------------------- public String getGeneratorBlock() { return config.getString("blocks.generator", "BLAST_FURNACE"); } + public String getComputerBlock() { return config.getString("blocks.computer", "LECTERN"); } public String getChestBlock() { return config.getString("blocks.chest", "CHEST"); } public String getHookBlock() { return config.getString("blocks.hook", "IRON_BARS"); } @@ -239,6 +276,8 @@ public class GameConfig { public boolean isHologramsEnabled() { return config.getBoolean("holograms.enabled", true); } public boolean isGeneratorHologramEnabled() { return config.getBoolean("holograms.generator.enabled", true); } public double getGeneratorHologramHeightOffset() { return config.getDouble("holograms.generator.height-offset", -1.0); } + public boolean isComputerHologramEnabled() { return config.getBoolean("holograms.computer.enabled", true); } + public double getComputerHologramHeightOffset() { return config.getDouble("holograms.computer.height-offset", -1.0); } public boolean isHookHologramEnabled() { return config.getBoolean("holograms.hook.enabled", true); } public double getHookHologramHeightOffset() { return config.getDouble("holograms.hook.height-offset", 0.0); } public boolean isChestHologramEnabled() { return config.getBoolean("holograms.chest.enabled", true); } @@ -263,8 +302,11 @@ public class GameConfig { // Endgame // ------------------------------------------------------------------------- public int getGateOpenSeconds() { return config.getInt("endgame.gate-open-seconds", 20); } + public int getPregameSeconds() { return config.getInt("endgame.pregame-seconds", 30); } + public int getPostgameSeconds() { return config.getInt("endgame.postgame-seconds", 60); } public int getChestOpenSeconds() { return config.getInt("endgame.chest-open-seconds", 3); } public double getGateInteractionRange() { return config.getDouble("endgame.gate-interaction-range", 3.0); } + public double getEscapeRange() { return config.getDouble("endgame.escape-range", 2.0); } // ------------------------------------------------------------------------- // Killer notification diff --git a/src/main/java/dev/loki/config/LocationsConfig.java b/src/main/java/dev/loki/config/LocationsConfig.java index 054d24c..7bf719c 100644 --- a/src/main/java/dev/loki/config/LocationsConfig.java +++ b/src/main/java/dev/loki/config/LocationsConfig.java @@ -31,12 +31,12 @@ public class LocationsConfig { if (!config.contains("generators") || config.getList("generators") == null) { config.set("generators", new ArrayList<>()); } + if (!config.contains("computers") || config.getList("computers") == null) { + config.set("computers", new ArrayList<>()); + } if (!config.contains("hooks") || config.getList("hooks") == null) { config.set("hooks", new ArrayList<>()); } - if (!config.contains("windows") || config.getList("windows") == null) { - config.set("windows", new ArrayList<>()); - } if (!config.contains("chests") || config.getList("chests") == null) { config.set("chests", new ArrayList<>()); } @@ -79,18 +79,10 @@ public class LocationsConfig { return loadLocationList("hooks"); } - public List getWindows() { - return loadLocationList("windows"); - } - public List getChests() { return loadLocationList("chests"); } - public List getTotems() { - return loadLocationList("totems"); - } - // getExitGates() — определён в addLocation/setGate блоке public List getKillerSpawns() { @@ -130,6 +122,12 @@ public class LocationsConfig { return -1; } + /** Clear a specific survivor spawn slot (index 1-4). */ + public void clearSurvivorSpawn(int index) { + if (index < 1 || index > 4) throw new IllegalArgumentException("Index must be 1-4"); + config.set("survivor-spawn-" + index, null); + } + /** Clear all survivor spawn slots. */ public void clearSurvivorSpawns() { for (int i = 1; i <= 4; i++) { @@ -219,18 +217,39 @@ public class LocationsConfig { public void clearLocationList(String path) { config.set(path, new ArrayList<>()); } + /** Remove a location at a specific index from a list. */ + public void removeLocationAtIndex(String path, int index) { + List list = safeGetStringList(path); + if (index >= 0 && index < list.size()) { + list.remove(index); + config.set(path, list); + } + } + public void setSingleLocation(String path, Location loc) { List list = new ArrayList<>(); list.add(locationToString(loc)); config.set(path, list); } - /** Устанавливает ворота A или B (хранятся как gate-a / gate-b). */ + /** Устанавливает ворота A или B с зоной (хранятся как gate-a / gate-b + corner1/corner2). */ public void setGate(String id, Location loc) { String key = "gate-" + id.toLowerCase(); config.set(key, locationToString(loc)); } + public void setGateZone(String id, Location corner1, Location corner2) { + String prefix = "gate-" + id.toLowerCase(); + config.set(prefix + "-corner1", locationToString(corner1)); + config.set(prefix + "-corner2", locationToString(corner2)); + } + + public Location getGateCorner(String id, int num) { + String key = "gate-" + id.toLowerCase() + "-corner" + num; + String str = config.getString(key); + return str != null ? parseLocation(str) : null; + } + public Location getGate(String id) { String key = "gate-" + id.toLowerCase(); String str = config.getString(key); @@ -251,6 +270,15 @@ public class LocationsConfig { return gates; } + /** Get all gate IDs that exist in config. */ + public List getGateIds() { + List ids = new ArrayList<>(); + for (String id : List.of("a", "b")) { + if (getGate(id) != null) ids.add(id); + } + return ids; + } + private String locationToString(Location loc) { // Use | as separator instead of , to avoid issues with negative coordinates in YAML return String.format(Locale.US, "%s|%.2f|%.2f|%.2f|%.1f|%.1f", diff --git a/src/main/java/dev/loki/core/GameSession.java b/src/main/java/dev/loki/core/GameSession.java index 4a2b524..9960d4a 100644 --- a/src/main/java/dev/loki/core/GameSession.java +++ b/src/main/java/dev/loki/core/GameSession.java @@ -5,6 +5,7 @@ import dev.loki.gate.GateManager; import dev.loki.hook.HookManager; import dev.loki.mdbd; import dev.loki.objective.GeneratorManager; +import dev.loki.objective.ComputerManager; import dev.loki.phase.EndgamePhase; import dev.loki.phase.GameplayPhase; import dev.loki.phase.LobbyPhase; @@ -28,6 +29,7 @@ public class GameSession { private int ticksElapsed; private RoleAssigner roleAssigner; private GeneratorManager generatorManager; + private ComputerManager computerManager; private boolean forceStarted = false; private Runnable onStopped; // callback для LobbyRegistry @@ -42,7 +44,11 @@ public class GameSession { this.ticksElapsed = 0; this.roleAssigner = new RoleAssigner(plugin.getConfigManager().getLocations()); this.generatorManager = new GeneratorManager( - plugin.getGeneratorData(), plugin.getConfigManager().getGame().getGeneratorRequired()); + plugin.getGeneratorData(), plugin.getConfigManager().getGame().getGeneratorRequired(), + plugin.getConfigManager().getGame()); + this.computerManager = new ComputerManager( + plugin.getComputerData(), plugin.getConfigManager().getGame().getComputerRequired(), + plugin.getConfigManager().getGame()); } public void start() { @@ -109,12 +115,12 @@ public class GameSession { return new RoleAssignPhase(roleAssigner, skip); } if (currentPhase instanceof RoleAssignPhase) { - return new GameplayPhase(generatorManager); + return new GameplayPhase(generatorManager, computerManager); } if (currentPhase instanceof GameplayPhase gp) { // Всегда переходим в EndgamePhase для корректного завершения игры // (очистка скорборда, телепорт в лобби, показ результатов) - return new EndgamePhase(gateManager, null, null); + return new EndgamePhase(gateManager); } return null; } @@ -176,6 +182,10 @@ public class GameSession { return generatorManager; } + public ComputerManager getComputerManager() { + return computerManager; + } + public HookManager getHookManager() { return hookManager; } public void setHookManager(HookManager hm) { this.hookManager = hm; } diff --git a/src/main/java/dev/loki/data/ComputerDataManager.java b/src/main/java/dev/loki/data/ComputerDataManager.java new file mode 100644 index 0000000..4dce0e4 --- /dev/null +++ b/src/main/java/dev/loki/data/ComputerDataManager.java @@ -0,0 +1,148 @@ +package dev.loki.data; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class ComputerDataManager { + + private static final double ZONE_RADIUS = 3.0; + + private final JavaPlugin plugin; + private final File dataFile; + private FileConfiguration data; + + public ComputerDataManager(JavaPlugin plugin) { + this.plugin = plugin; + this.dataFile = new File(plugin.getDataFolder(), "computers.yml"); + load(); + } + + public void load() { + if (!dataFile.exists()) { + try { + dataFile.createNewFile(); + } catch (IOException e) { + plugin.getLogger().warning("Could not create computers.yml"); + } + } + this.data = YamlConfiguration.loadConfiguration(dataFile); + } + + public void save() { + try { + data.save(dataFile); + } catch (IOException e) { + plugin.getLogger().warning("Could not save computers.yml"); + } + } + + public void addComputer(Location location) { + List list = data.getStringList("computers"); + String entry = formatLocation(location); + if (!list.contains(entry)) { + list.add(entry); + data.set("computers", list); + save(); + } + } + + public void removeComputer(int index) { + List list = data.getStringList("computers"); + if (index >= 0 && index < list.size()) { + list.remove(index); + data.set("computers", list); + save(); + } + } + + public void clearComputers() { + data.set("computers", null); + save(); + } + + public List getComputers() { + List result = new ArrayList<>(); + List list = data.getStringList("computers"); + for (String entry : list) { + Location loc = parseLocation(entry); + if (loc != null) { + result.add(loc); + } + } + return result; + } + + public List getComputerEntries() { + return data.getStringList("computers"); + } + + public int getCount() { + return data.getStringList("computers").size(); + } + + public static String formatLocation(Location loc) { + return String.format("%s:%.0f|%.0f|%.0f", + loc.getWorld().getName(), + loc.getX(), + loc.getY(), + loc.getZ()); + } + + private Location parseLocation(String str) { + String[] parts = str.split(":"); + if (parts.length < 2) return null; + + World world = Bukkit.getWorld(parts[0].trim()); + if (world == null) return null; + + String[] coords = parts[1].split("\\|"); + if (coords.length < 3) return null; + + try { + double x = Double.parseDouble(coords[0].trim()); + double y = Double.parseDouble(coords[1].trim()); + double z = Double.parseDouble(coords[2].trim()); + return new Location(world, x, y, z); + } catch (NumberFormatException e) { + return null; + } + } + + public static double getZoneRadius() { + return ZONE_RADIUS; + } + + public boolean isInZone(Location location, List computers) { + for (Location compLoc : computers) { + if (!compLoc.getWorld().equals(location.getWorld())) continue; + if (compLoc.distance(location) <= ZONE_RADIUS) { + return true; + } + } + return false; + } + + public Location getNearestComputer(Location location, List computers) { + Location nearest = null; + double nearestDist = ZONE_RADIUS; + + for (Location compLoc : computers) { + if (!compLoc.getWorld().equals(location.getWorld())) continue; + double dist = compLoc.distance(location); + if (dist <= nearestDist) { + nearestDist = dist; + nearest = compLoc; + } + } + return nearest; + } +} diff --git a/src/main/java/dev/loki/gate/ExitGate.java b/src/main/java/dev/loki/gate/ExitGate.java index 14f143c..148b5a9 100644 --- a/src/main/java/dev/loki/gate/ExitGate.java +++ b/src/main/java/dev/loki/gate/ExitGate.java @@ -11,6 +11,10 @@ public class ExitGate { private double savedProgress; // сохраняется при уходе игрока private boolean powered; + private int zoneMinX, zoneMinY, zoneMinZ; + private int zoneMaxX, zoneMaxY, zoneMaxZ; + private boolean hasZone; + public enum GateState { CLOSED, OPENING, @@ -23,6 +27,22 @@ public class ExitGate { this.progress = 0.0; this.savedProgress = 0.0; this.powered = false; + this.hasZone = false; + } + + /** Set the gate zone from two corner locations. */ + public void setZone(Location corner1, Location corner2) { + zoneMinX = Math.min(corner1.getBlockX(), corner2.getBlockX()); + zoneMinY = Math.min(corner1.getBlockY(), corner2.getBlockY()); + zoneMinZ = Math.min(corner1.getBlockZ(), corner2.getBlockZ()); + zoneMaxX = Math.max(corner1.getBlockX(), corner2.getBlockX()); + zoneMaxY = Math.max(corner1.getBlockY(), corner2.getBlockY()); + zoneMaxZ = Math.max(corner1.getBlockZ(), corner2.getBlockZ()); + hasZone = true; + } + + private boolean hasZone() { + return hasZone; } public Location getLocation() { @@ -32,7 +52,48 @@ public class ExitGate { /** Place a HOPPER block at this gate's location for player interaction. */ public void spawnGateBlock() { if (location.getWorld() == null) return; - location.getBlock().setType(org.bukkit.Material.HOPPER, false); + location.getBlock().setType(org.bukkit.Material.HOPPER, true); + } + + /** Build the gate structure fills the entire designated zone. */ + public void spawnGateStructure() { + World world = location.getWorld(); + if (world == null) return; + + if (hasZone) { + for (int x = zoneMinX; x <= zoneMaxX; x++) { + for (int y = zoneMinY; y <= zoneMaxY; y++) { + for (int z = zoneMinZ; z <= zoneMaxZ; z++) { + world.getBlockAt(x, y, z).setType(org.bukkit.Material.IRON_BARS, true); + } + } + } + for (int x = zoneMinX; x <= zoneMaxX; x++) { + for (int y = zoneMinY; y <= zoneMaxY; y++) { + for (int z = zoneMinZ; z <= zoneMaxZ; z++) { + world.getBlockAt(x, y, z).getState().update(true, true); + } + } + } + } else { + int cx = location.getBlockX(), cy = location.getBlockY(), cz = location.getBlockZ(); + for (int x = cx - 1; x <= cx + 1; x++) { + for (int y = cy; y <= cy + 2; y++) { + for (int z = cz - 1; z <= cz + 1; z++) { + world.getBlockAt(x, y, z).setType(org.bukkit.Material.IRON_BARS, true); + } + } + } + for (int x = cx - 1; x <= cx + 1; x++) { + for (int y = cy; y <= cy + 2; y++) { + for (int z = cz - 1; z <= cz + 1; z++) { + world.getBlockAt(x, y, z).getState().update(true, true); + } + } + } + } + + world.getBlockAt(location.clone().add(1, 1, 1)).setType(org.bukkit.Material.STONE_BUTTON, true); } /** Remove the interaction block (e.g. when gate opens or on cleanup). */ @@ -41,6 +102,36 @@ public class ExitGate { location.getBlock().setType(org.bukkit.Material.AIR, false); } + /** Remove the entire gate zone + interaction block. */ + public void removeGateStructure() { + World world = location.getWorld(); + if (world == null) return; + + if (hasZone) { + for (int x = zoneMinX; x <= zoneMaxX; x++) { + for (int y = zoneMinY; y <= zoneMaxY; y++) { + for (int z = zoneMinZ; z <= zoneMaxZ; z++) { + world.getBlockAt(x, y, z).setType(org.bukkit.Material.AIR, false); + } + } + } + } else { + int cx = location.getBlockX(), cy = location.getBlockY(), cz = location.getBlockZ(); + for (int x = cx - 1; x <= cx + 1; x++) { + for (int y = cy; y <= cy + 2; y++) { + for (int z = cz - 1; z <= cz + 1; z++) { + world.getBlockAt(x, y, z).setType(org.bukkit.Material.AIR, false); + } + } + } + } + + // Remove button at (1, 1, 1) + world.getBlockAt(location.clone().add(1, 1, 1)).setType(org.bukkit.Material.AIR, false); + + removeGateBlock(); + } + public GateState getState() { return state; } @@ -71,9 +162,7 @@ public class ExitGate { public void setPowered(boolean powered) { this.powered = powered; - if (powered && state == GateState.CLOSED) { - state = GateState.OPENING; - } + // НЕ меняем статус на OPENING — ворота открываются только через взаимодействие игрока } public void startOpening() { @@ -95,6 +184,16 @@ public class ExitGate { } } + /** Принудительно открыть ворота (вызывается после таймера эндгейма). */ + public void forceOpen() { + this.state = GateState.OPEN; + this.progress = 1.0; + this.savedProgress = 1.0; + removeGateStructure(); + spawnOpenParticles(); + playOpenedSound(); + } + /** Прямая установка прогресса (для линейного расчёта в GateOpening). */ public void setProgressDirect(double value) { if (state != GateState.OPENING) return; @@ -102,7 +201,8 @@ public class ExitGate { if (progress >= 1.0) { progress = 1.0; state = GateState.OPEN; - removeGateBlock(); + removeGateStructure(); + spawnOpenParticles(); playOpenedSound(); } } @@ -116,7 +216,6 @@ public class ExitGate { public void restoreProgress() { progress = savedProgress; } public void reset() { - removeGateBlock(); this.state = GateState.CLOSED; this.progress = 0.0; this.savedProgress = 0.0; @@ -124,6 +223,14 @@ public class ExitGate { } public boolean isInRange(Location playerLoc, double radius) { + if (hasZone) { + int px = playerLoc.getBlockX(), py = playerLoc.getBlockY(), pz = playerLoc.getBlockZ(); + int closestX = Math.max(zoneMinX, Math.min(px, zoneMaxX)); + int closestY = Math.max(zoneMinY, Math.min(py, zoneMaxY)); + int closestZ = Math.max(zoneMinZ, Math.min(pz, zoneMaxZ)); + double dx = px - closestX, dy = py - closestY, dz = pz - closestZ; + return Math.sqrt(dx * dx + dy * dy + dz * dz) <= radius; + } return location.distance(playerLoc) <= radius; } diff --git a/src/main/java/dev/loki/gate/GateManager.java b/src/main/java/dev/loki/gate/GateManager.java index 074604e..69f3671 100644 --- a/src/main/java/dev/loki/gate/GateManager.java +++ b/src/main/java/dev/loki/gate/GateManager.java @@ -19,9 +19,17 @@ public class GateManager { } private void loadGates(LocationsConfig locations) { - List gateLocations = locations.getExitGates(); - for (int i = 0; i < Math.min(gateLocations.size(), GATE_COUNT); i++) { - gates.add(new ExitGate(gateLocations.get(i))); + for (String id : locations.getGateIds()) { + Location center = locations.getGate(id); + if (center == null) continue; + ExitGate gate = new ExitGate(center); + Location c1 = locations.getGateCorner(id, 1); + Location c2 = locations.getGateCorner(id, 2); + if (c1 != null && c2 != null) { + gate.setZone(c1, c2); + } + gates.add(gate); + if (gates.size() >= GATE_COUNT) break; } } @@ -102,6 +110,12 @@ public class GateManager { return (int) gates.stream().filter(ExitGate::isOpen).count(); } + public void openAllGates() { + for (ExitGate gate : gates) { + gate.forceOpen(); + } + } + public void resetAll() { endgameActive = false; for (ExitGate gate : gates) { diff --git a/src/main/java/dev/loki/gate/GateOpening.java b/src/main/java/dev/loki/gate/GateOpening.java index 5e2da93..673969d 100644 --- a/src/main/java/dev/loki/gate/GateOpening.java +++ b/src/main/java/dev/loki/gate/GateOpening.java @@ -38,17 +38,22 @@ public class GateOpening { ExitGate gate = gateManager.getGateAt(player.getLocation(), INTERACTION_RADIUS); if (gate == null) { - player.sendMessage(messages.get("chest.door-nearby")); + player.sendMessage(messages.get("gate.door-nearby")); return false; } if (!gate.isPowered()) { - player.sendMessage(messages.get("chest.door-no-power")); + player.sendMessage(messages.get("gate.door-no-power")); return false; } - if (!gate.isClosed() && !gate.isOpening()) { - player.sendMessage(messages.get("chest.door-already")); + if (gate.isOpening()) { + player.sendMessage(messages.get("gate.door-already")); + return false; + } + + if (gate.isOpen()) { + player.sendMessage(messages.get("gate.door-already")); return false; } @@ -61,7 +66,7 @@ public class GateOpening { if (progress != null) { Player player = survivor.getPlayer(); if (player != null) { - player.sendMessage(messages.get("chest.door-interrupted")); + player.sendMessage(messages.get("gate.door-interrupted")); } } } @@ -74,11 +79,14 @@ public class GateOpening { Player player = survivor.getPlayer(); if (player == null) return; + // Перевести ворота в режим открытия + gate.startOpening(); + // Восстановить сохранённый прогресс gate.restoreProgress(); openingPlayers.put(survivor.getUuid(), new OpeningProgress(gate, 0)); - player.sendMessage(messages.get("chest.door-open")); + player.sendMessage(messages.get("gate.door-open")); new OpeningTask(survivor, gate, plugin).runTaskTimer(plugin, 0L, 1L); } @@ -89,7 +97,7 @@ public class GateOpening { World world = loc.getWorld(); if (player != null) { - player.sendMessage(messages.get("chest.door-opened")); + player.sendMessage(messages.get("gate.door-opened")); if (world != null) { world.playSound(loc, Sound.BLOCK_IRON_DOOR_OPEN, 1.0f, 1.0f); // Big particle burst when opened diff --git a/src/main/java/dev/loki/hatch/Hatch.java b/src/main/java/dev/loki/hatch/Hatch.java deleted file mode 100644 index 806b299..0000000 --- a/src/main/java/dev/loki/hatch/Hatch.java +++ /dev/null @@ -1,125 +0,0 @@ -package dev.loki.hatch; - -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.Sound; -import org.bukkit.World; -import org.bukkit.block.Block; - -public class Hatch { - private final Location location; - private HatchState state; - private Block hatchBlock; - - public enum HatchState { - HIDDEN, - CLOSED, - OPEN - } - - public Hatch(Location location) { - this.location = location; - this.state = HatchState.HIDDEN; - this.hatchBlock = location.getBlock(); - } - - public Location getLocation() { - return location; - } - - public Location getJumpTarget() { - return location.clone().add(0.5, -2, 0.5); - } - - public HatchState getState() { - return state; - } - - public boolean isHidden() { - return state == HatchState.HIDDEN; - } - - public boolean isClosed() { - return state == HatchState.CLOSED; - } - - public boolean isOpen() { - return state == HatchState.OPEN; - } - - public void reveal() { - if (state == HatchState.HIDDEN) { - state = HatchState.CLOSED; - updateVisuals(); - playRevealEffect(); - } - } - - public void open() { - if (state == HatchState.CLOSED) { - state = HatchState.OPEN; - updateVisuals(); - playOpenEffect(); - } - } - - public void hide() { - state = HatchState.HIDDEN; - clearVisuals(); - } - - public void reset() { - state = HatchState.HIDDEN; - clearVisuals(); - } - - private void updateVisuals() { - switch (state) { - case CLOSED -> hatchBlock.setType(Material.IRON_TRAPDOOR); - case OPEN -> hatchBlock.setType(Material.AIR); - default -> hatchBlock.setType(Material.AIR); - } - } - - private void clearVisuals() { - hatchBlock.setType(Material.AIR); - } - - public boolean isInRange(Location playerLoc, double radius) { - return location.distance(playerLoc) <= radius; - } - - public boolean canEscape(Location playerLoc) { - return isOpen() && isInRange(playerLoc, 1.5); - } - - private void playRevealEffect() { - World world = location.getWorld(); - if (world == null) return; - - world.playSound(location, Sound.BLOCK_METAL_PLACE, 1.0f, 0.8f); - world.spawnParticle(org.bukkit.Particle.SMOKE, - location.clone().add(0.5, 0.5, 0.5), - 30, 0.3, 0.3, 0.3, 0.05); - } - - private void playOpenEffect() { - World world = location.getWorld(); - if (world == null) return; - - world.playSound(location, Sound.BLOCK_IRON_TRAPDOOR_OPEN, 1.0f, 1.0f); - world.spawnParticle(org.bukkit.Particle.END_ROD, - location.clone().add(0.5, 0.5, 0.5), - 20, 0.2, 0.2, 0.2, 0.02); - } - - public void playEscapeEffect() { - World world = location.getWorld(); - if (world == null) return; - - world.playSound(location, Sound.ENTITY_PLAYER_SPLASH, 1.0f, 1.0f); - world.spawnParticle(org.bukkit.Particle.PORTAL, - location.clone().add(0.5, 0, 0.5), - 100, 0.5, 0.5, 0.5, 0.5); - } -} diff --git a/src/main/java/dev/loki/hatch/HatchCondition.java b/src/main/java/dev/loki/hatch/HatchCondition.java deleted file mode 100644 index 1e9406d..0000000 --- a/src/main/java/dev/loki/hatch/HatchCondition.java +++ /dev/null @@ -1,76 +0,0 @@ -package dev.loki.hatch; - -import dev.loki.player.BasePlayer; -import dev.loki.player.PlayerState; - -import java.util.Collection; - -public class HatchCondition { - private final int minSurvivorsForOpen; - - public HatchCondition(int minSurvivorsForOpen) { - this.minSurvivorsForOpen = minSurvivorsForOpen; - } - - public boolean shouldSpawn(Collection players, int repairedGenerators) { - int aliveSurvivors = countAliveSurvivors(players); - return aliveSurvivors <= repairedGenerators; - } - - public boolean shouldOpen(Hatch hatch, Collection players, int repairedGenerators) { - if (hatch.isOpen()) return false; - if (!hatch.isClosed() && !hatch.isHidden()) return false; - - return shouldSpawn(players, repairedGenerators); - } - - public boolean canOpen(Hatch hatch, Collection players, int repairedGenerators) { - if (!hatch.isHidden() && !hatch.isClosed()) return false; - - int aliveSurvivors = countAliveSurvivors(players); - int requiredGens = Math.max(minSurvivorsForOpen, aliveSurvivors); - - return repairedGenerators >= requiredGens; - } - - public int countAliveSurvivors(Collection players) { - return (int) players.stream() - .filter(p -> !p.isKiller()) - .filter(this::isAlive) - .count(); - } - - private boolean isAlive(BasePlayer player) { - PlayerState state = player.getState(); - return state != PlayerState.DEAD - && state != PlayerState.ESCAPED - && state != PlayerState.HOOKED; - } - - public int getRemainingAlive(Collection players) { - return countAliveSurvivors(players); - } - - public boolean isLastSurvivor(Collection players) { - return countAliveSurvivors(players) == 1; - } - - public boolean checkAndReveal(Hatch hatch, Collection players, int repairedGenerators) { - if (hatch.isHidden() && shouldSpawn(players, repairedGenerators)) { - hatch.reveal(); - return true; - } - return false; - } - - public boolean checkAndOpen(Hatch hatch, Collection players, int repairedGenerators) { - if (canOpen(hatch, players, repairedGenerators)) { - if (hatch.isHidden()) { - hatch.reveal(); - } - hatch.open(); - return true; - } - return false; - } -} diff --git a/src/main/java/dev/loki/hatch/HatchEscape.java b/src/main/java/dev/loki/hatch/HatchEscape.java deleted file mode 100644 index 0dca00f..0000000 --- a/src/main/java/dev/loki/hatch/HatchEscape.java +++ /dev/null @@ -1,117 +0,0 @@ -package dev.loki.hatch; - -import dev.loki.config.MessagesConfig; -import dev.loki.mdbd; -import dev.loki.player.PlayerState; -import dev.loki.player.Survivor; -import org.bukkit.Location; -import org.bukkit.Sound; -import org.bukkit.World; -import org.bukkit.entity.Player; - -public class HatchEscape { - private static final double ESCAPE_RADIUS = 1.5; - - private final Hatch hatch; - private final MessagesConfig messages; - - public HatchEscape(Hatch hatch) { - this.hatch = hatch; - this.messages = mdbd.getInstance().getConfigManager().getMessages(); - } - - public boolean tryEscape(Survivor survivor) { - if (!hatch.isOpen()) { - return tryRevealAndCheck(survivor); - } - - Player player = survivor.getPlayer(); - if (player == null) return false; - - if (!isAtHatch(player.getLocation())) { - player.sendMessage(messages.get("hatch.too-far")); - return false; - } - - performEscape(survivor); - return true; - } - - public boolean canEscape(Survivor survivor) { - if (!hatch.isOpen()) return false; - - Player player = survivor.getPlayer(); - if (player == null) return false; - - return isAtHatch(player.getLocation()); - } - - private boolean tryRevealAndCheck(Survivor survivor) { - Player player = survivor.getPlayer(); - if (player == null) return false; - - if (hatch.isHidden()) { - player.sendMessage(messages.get("hatch.hidden")); - return false; - } - - if (hatch.isClosed()) { - player.sendMessage(messages.get("hatch.closed")); - return false; - } - - return false; - } - - private boolean isAtHatch(Location location) { - return hatch.isInRange(location, ESCAPE_RADIUS); - } - - private void performEscape(Survivor survivor) { - Player player = survivor.getPlayer(); - if (player == null) return; - - player.teleport(hatch.getJumpTarget()); - hatch.playEscapeEffect(); - - player.getWorld().playSound(player.getLocation(), - Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.0f); - player.sendTitle( - messages.getString("hatch.escape-title"), - messages.getString("hatch.escape-subtitle"), - 10, 60, 10); - - survivor.setState(PlayerState.ESCAPED); - - giveEscapeReward(survivor); - } - - private void giveEscapeReward(Survivor survivor) { - Player player = survivor.getPlayer(); - if (player == null) return; - - player.sendMessage(messages.get("hatch.reward")); - } - - public void forceOpenForEscape() { - if (hatch.isHidden()) { - hatch.reveal(); - } - if (!hatch.isOpen()) { - hatch.open(); - } - } - - public void notifyNearbyPlayers() { - Location hatchLoc = hatch.getLocation(); - World world = hatchLoc.getWorld(); - if (world == null) return; - - for (Player player : world.getPlayers()) { - if (player.getLocation().distance(hatchLoc) < 30) { - player.sendMessage(messages.get("hatch.open-notify")); - player.playSound(player.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 1.0f, 2.0f); - } - } - } -} diff --git a/src/main/java/dev/loki/hook/Hook.java b/src/main/java/dev/loki/hook/Hook.java index 7c60d85..d3f40a0 100644 --- a/src/main/java/dev/loki/hook/Hook.java +++ b/src/main/java/dev/loki/hook/Hook.java @@ -76,4 +76,6 @@ public class Hook { public boolean isBasement() { return basement; } public boolean isDisabled() { return disabled; } public void setDisabled(boolean disabled) { this.disabled = disabled; } + public int getStageProgress() { return stageProgress; } + public void addStageProgress(int delta) { stageProgress = Math.max(0, stageProgress + delta); } } diff --git a/src/main/java/dev/loki/hook/HookHologram.java b/src/main/java/dev/loki/hook/HookHologram.java index 9691a17..7bb2879 100644 --- a/src/main/java/dev/loki/hook/HookHologram.java +++ b/src/main/java/dev/loki/hook/HookHologram.java @@ -72,7 +72,7 @@ public class HookHologram { continue; } - Location holoLoc = loc.clone().add(0.5, 2.0 + heightOffset, 0.5); + Location holoLoc = loc.getBlock().getLocation().add(0.5, 2.0 + heightOffset, 0.5); try { if (useTextDisplay) { TextDisplay display = loc.getWorld().spawn(holoLoc, TextDisplay.class, d -> { @@ -128,7 +128,7 @@ public class HookHologram { String text = "§c⚓ Крюк §f#" + visibleIndex + "/" + total + "\n§7" + status; double heightOffset = plugin.getConfigManager().getGame().getHookHologramHeightOffset(); - Location base = hook.getLocation().clone().add(0.5, 2.0 + heightOffset, 0.5); + Location base = hook.getLocation().getBlock().getLocation().add(0.5, 2.0 + heightOffset, 0.5); if (display instanceof TextDisplay td) { td.text(TextUtil.color(text)); diff --git a/src/main/java/dev/loki/hook/HookManager.java b/src/main/java/dev/loki/hook/HookManager.java index 2da3ff5..21828b5 100644 --- a/src/main/java/dev/loki/hook/HookManager.java +++ b/src/main/java/dev/loki/hook/HookManager.java @@ -111,10 +111,7 @@ public class HookManager { return true; // "повешен" = завершён } - Hook nearest = findNearestUnoccupiedHook(killerLoc, 5.0); - if (nearest == null) { - nearest = findAvailableHook().orElse(null); - } + Hook nearest = findNearestUnoccupiedHook(killerLoc, 4.0); if (nearest != null) { nearest.hookSurvivor(survivor); survivor.setState(PlayerState.HOOKED); // Set state to HOOKED diff --git a/src/main/java/dev/loki/item/ChestLoot.java b/src/main/java/dev/loki/item/ChestLoot.java index f77c926..3489672 100644 --- a/src/main/java/dev/loki/item/ChestLoot.java +++ b/src/main/java/dev/loki/item/ChestLoot.java @@ -5,56 +5,66 @@ import org.bukkit.inventory.ItemStack; import java.util.Random; import java.util.concurrent.ThreadLocalRandom; -/** - * Генератор случайного лута для сундуков. - * Может выдать: Медкит (разное количество зарядов), Набор для срезания. - */ public class ChestLoot { private final Random random; - // Веса для разных типов лута (из конфига game.yml: loot.*) private final int medkitWeight; + private final int bandageWeight; + private final int fullMedkitWeight; + private final int sodaWeight; + private final int wrenchWeight; + private final int medkitMinCharges; private final int medkitMaxCharges; - private final int hookCutterWeight; - /** Config-aware constructor. */ public ChestLoot(dev.loki.config.GameConfig config) { this.random = ThreadLocalRandom.current(); var cfg = config.getConfig(); - this.medkitWeight = cfg.getInt("loot.medkit.weight", 60); - this.medkitMinCharges = cfg.getInt("loot.medkit.min-charges", 12); - this.medkitMaxCharges = cfg.getInt("loot.medkit.max-charges", 16); - this.hookCutterWeight = cfg.getInt("loot.hook-cutter.weight", 40); + this.medkitWeight = cfg.getInt("loot.medkit.weight", 30); + this.bandageWeight = cfg.getInt("loot.bandage.weight", 20); + this.fullMedkitWeight = cfg.getInt("loot.full-medkit.weight", 10); + this.sodaWeight = cfg.getInt("loot.soda.weight", 20); + this.wrenchWeight = cfg.getInt("loot.wrench.weight", 20); + this.medkitMinCharges = cfg.getInt("loot.medkit.min-charges", 12); + this.medkitMaxCharges = cfg.getInt("loot.medkit.max-charges", 16); } - /** Fallback — hardcoded defaults. */ public ChestLoot() { this.random = ThreadLocalRandom.current(); - this.medkitWeight = 60; + this.medkitWeight = 30; + this.bandageWeight = 20; + this.fullMedkitWeight = 10; + this.sodaWeight = 20; + this.wrenchWeight = 20; this.medkitMinCharges = 12; this.medkitMaxCharges = 16; - this.hookCutterWeight = 40; } - /** - * Бросает кубик и возвращает случайный предмет. - * Никогда не возвращает null. - */ public ItemStack rollLoot() { - int total = medkitWeight + hookCutterWeight; + int total = medkitWeight + bandageWeight + fullMedkitWeight + sodaWeight + wrenchWeight; if (total <= 0) { return DBDItems.medkit(12); } int roll = random.nextInt(total); + int cumulative = 0; - if (roll < medkitWeight) { + cumulative += medkitWeight; + if (roll < cumulative) { int charges = medkitMinCharges + random.nextInt(Math.max(1, medkitMaxCharges - medkitMinCharges + 1)); return DBDItems.medkit(charges); - } else { - return DBDItems.hookCutter(); } + + cumulative += bandageWeight; + if (roll < cumulative) return DBDItems.bandage(); + + cumulative += fullMedkitWeight; + if (roll < cumulative) return DBDItems.fullMedkit(); + + cumulative += sodaWeight; + if (roll < cumulative) return DBDItems.soda(); + + return DBDItems.wrench(); } -} +} \ No newline at end of file diff --git a/src/main/java/dev/loki/item/DBDItems.java b/src/main/java/dev/loki/item/DBDItems.java index ffe9b15..aa55252 100644 --- a/src/main/java/dev/loki/item/DBDItems.java +++ b/src/main/java/dev/loki/item/DBDItems.java @@ -20,14 +20,22 @@ public final class DBDItems { public static final NamespacedKey KEY_CHARGES = new NamespacedKey("mdbd", "charges"); // Custom Model Data IDs (соответствуют ресурспаку) - // Переопределяются через game.yml: items.model-data.medkit / hookcutter + // Переопределяются через game.yml: items.model-data.* private static int CMD_MEDKIT = 1001; private static int CMD_HOOKCUTTER = 1003; + private static int CMD_BANDAGE = 1004; + private static int CMD_FULLMEDKIT = 1005; + private static int CMD_SODA = 1006; + private static int CMD_WRENCH = 1007; /** Вызывать при загрузке плагина / reload, чтобы подхватить значения из конфига. */ public static void loadModelData(org.bukkit.configuration.file.FileConfiguration cfg) { CMD_MEDKIT = cfg.getInt("items.model-data.medkit", 1001); CMD_HOOKCUTTER = cfg.getInt("items.model-data.hookcutter", 1003); + CMD_BANDAGE = cfg.getInt("items.model-data.bandage", 1004); + CMD_FULLMEDKIT = cfg.getInt("items.model-data.fullmedkit", 1005); + CMD_SODA = cfg.getInt("items.model-data.soda", 1006); + CMD_WRENCH = cfg.getInt("items.model-data.wrench", 1007); } private DBDItems() {} @@ -70,6 +78,58 @@ public final class DBDItems { ); } + public static ItemStack bandage() { + return build( + Material.PAPER, CMD_BANDAGE, + "<#FFA07A>Бинт", + 1, 1, + List.of( + "Регенерирует здоровье в течение 30 секунд.", + "ПКМ — наложить бинт" + ), + "BANDAGE" + ); + } + + public static ItemStack fullMedkit() { + return build( + Material.PAPER, CMD_FULLMEDKIT, + "<#FF1493>Аптечка", + 1, 1, + List.of( + "Полностью восстанавливает здоровье.", + "ПКМ — воспользоваться аптечкой" + ), + "FULL_MEDKIT" + ); + } + + public static ItemStack soda() { + return build( + Material.PAPER, CMD_SODA, + "<#FFD700>Содовая", + 1, 1, + List.of( + "Даёт буст к скорости на 10 секунд.", + "ПКМ — выпить" + ), + "SODA" + ); + } + + public static ItemStack wrench() { + return build( + Material.IRON_PICKAXE, CMD_WRENCH, + "<#A0A0A0>Гаечный ключ", + 1, 1, + List.of( + "Даёт буст к починке на 10 секунд.", + "ПКМ — использовать" + ), + "WRENCH" + ); + } + // ── Helpers ─────────────────────────────────────────────────────────────── private static ItemStack build(Material mat, int customModelData, String name, @@ -126,11 +186,19 @@ public final class DBDItems { if (type == null) return item; return switch (type) { case "MEDKIT" -> medkit(newCharges); + case "BANDAGE" -> bandage(); + case "FULL_MEDKIT" -> fullMedkit(); + case "SODA" -> soda(); + case "WRENCH" -> wrench(); default -> item; }; } - public static boolean isMedkit(ItemStack item) { return "MEDKIT".equals(getType(item)); } - public static boolean isHookCutter(ItemStack item) { return "HOOK_CUTTER".equals(getType(item)); } - public static boolean isDBDItem(ItemStack item) { return getType(item) != null; } + public static boolean isMedkit(ItemStack item) { return "MEDKIT".equals(getType(item)); } + public static boolean isHookCutter(ItemStack item) { return "HOOK_CUTTER".equals(getType(item)); } + public static boolean isBandage(ItemStack item) { return "BANDAGE".equals(getType(item)); } + public static boolean isFullMedkit(ItemStack item) { return "FULL_MEDKIT".equals(getType(item)); } + public static boolean isSoda(ItemStack item) { return "SODA".equals(getType(item)); } + public static boolean isWrench(ItemStack item) { return "WRENCH".equals(getType(item)); } + public static boolean isDBDItem(ItemStack item) { return getType(item) != null; } } diff --git a/src/main/java/dev/loki/item/ItemMedkit.java b/src/main/java/dev/loki/item/ItemMedkit.java index 46ad11d..fefe9be 100644 --- a/src/main/java/dev/loki/item/ItemMedkit.java +++ b/src/main/java/dev/loki/item/ItemMedkit.java @@ -39,8 +39,8 @@ public class ItemMedkit implements BaseItem { meta.setDisplayName("§a" + getType().getDisplayName()); meta.setLore(Arrays.asList( "§7" + getType().getDescription(), - "§eCharges: §f" + charges + "/" + getType().getDefaultCharges(), - "§7Right-click to heal" + "§eЗаряды: §f" + charges + "/" + getType().getDefaultCharges(), + "§7ПКМ — лечиться" )); PersistentDataContainer pdc = meta.getPersistentDataContainer(); @@ -60,7 +60,7 @@ public class ItemMedkit implements BaseItem { @Override public void onUse(Player player) { if (charges > 0) { - player.sendMessage("§aYou start healing..."); + player.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.medkit.start")); } } diff --git a/src/main/java/dev/loki/item/ItemType.java b/src/main/java/dev/loki/item/ItemType.java index dd8ed48..7bdbcbe 100644 --- a/src/main/java/dev/loki/item/ItemType.java +++ b/src/main/java/dev/loki/item/ItemType.java @@ -1,8 +1,12 @@ package dev.loki.item; public enum ItemType { - MEDKIT("Medkit", "Heals one health state", 16), - HOOK_CUTTER("Hook Cutter", "Cuts a survivor off a hook", 1); + MEDKIT("Медкит", "Лечит одно состояние здоровья", 16), + HOOK_CUTTER("Крюкорез", "Срезает выжившего с крюка", 1), + BANDAGE("Бинт", "Регенерирует здоровье в течение 30 секунд", 1), + FULL_MEDKIT("Аптечка", "Полностью восстанавливает здоровье", 1), + SODA("Содовая", "Даёт буст к скорости на 10 секунд", 1), + WRENCH("Гаечный ключ", "Даёт буст к починке на 10 секунд", 1); private final String displayName; private final String description; diff --git a/src/main/java/dev/loki/item/ItemUseListener.java b/src/main/java/dev/loki/item/ItemUseListener.java index 18dcfb4..27b12d3 100644 --- a/src/main/java/dev/loki/item/ItemUseListener.java +++ b/src/main/java/dev/loki/item/ItemUseListener.java @@ -18,6 +18,8 @@ import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.Plugin; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; import org.bukkit.scheduler.BukkitRunnable; import org.bukkit.scheduler.BukkitTask; @@ -25,16 +27,15 @@ import java.util.HashMap; import java.util.Map; import java.util.UUID; -/** - * Handles right-click usage of DBD custom items (Medkit) during gameplay. - */ public class ItemUseListener implements Listener { private final Plugin plugin; private final GameSession session; private final Map activeHeals = new HashMap<>(); - private static final int MEDKIT_TICKS = 160; // 8 seconds + private static final int MEDKIT_TICKS = 160; + + private final Map repairBoostExpiry = new HashMap<>(); public ItemUseListener(Plugin plugin, GameSession session) { this.plugin = plugin; @@ -49,6 +50,17 @@ public class ItemUseListener implements Listener { HandlerList.unregisterAll(this); activeHeals.values().forEach(BukkitTask::cancel); activeHeals.clear(); + repairBoostExpiry.clear(); + } + + public boolean hasRepairBoost(UUID playerId) { + Long expiry = repairBoostExpiry.get(playerId); + if (expiry == null) return false; + if (System.currentTimeMillis() > expiry) { + repairBoostExpiry.remove(playerId); + return false; + } + return true; } @EventHandler(priority = EventPriority.HIGH) @@ -69,22 +81,24 @@ public class ItemUseListener implements Listener { event.setCancelled(true); String type = DBDItems.getType(hand); - if ("MEDKIT".equals(type)) { - // Медкит: нужен Shift (sneaking) + стоять на месте - if (!player.isSneaking()) { - player.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.medkit.need-shift")); - return; + switch (type) { + case "MEDKIT" -> { + if (!player.isSneaking()) { + player.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.medkit.need-shift")); + return; + } + useMedkit(player, (Survivor) bp, hand); } - useMedkit(player, (Survivor) bp, hand); + case "BANDAGE" -> useBandage(player, (Survivor) bp); + case "FULL_MEDKIT" -> useFullMedkit(player, (Survivor) bp); + case "SODA" -> useSoda(player); + case "WRENCH" -> useWrench(player); } } - // ── Медкит ──────────────────────────────────────────────────────────────── - private void useMedkit(Player player, Survivor survivor, ItemStack item) { UUID uuid = player.getUniqueId(); - // Повторный Shift+ПКМ — отмена лечения if (activeHeals.containsKey(uuid)) { cancelHeal(player); return; @@ -104,25 +118,21 @@ public class ItemUseListener implements Listener { player.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.medkit.start")); - // Запоминаем позицию для проверки движения final Location startLoc = player.getLocation().clone(); BukkitTask task = new BukkitRunnable() { int ticks = 0; - final int healTicks = MEDKIT_TICKS; @Override public void run() { if (!player.isOnline()) { cancelHeal(player); cancel(); return; } if (survivor.getState() != PlayerState.INJURED) { cancelHeal(player); cancel(); return; } - // Прерывание при движении или отпускании Shift if (!player.isSneaking()) { cancelHeal(player); cancel(); return; } - // Проверка движения (больше 0.5 блока от старта) if (player.getLocation().distanceSquared(startLoc) > 0.25) { cancelHeal(player); cancel(); @@ -131,10 +141,9 @@ public class ItemUseListener implements Listener { ticks++; - // Прогресс в action bar - int filled = (int) (8.0 * ticks / healTicks); + int filled = (int) (8.0 * ticks / MEDKIT_TICKS); int empty = 8 - filled; - int pct = (int) (100.0 * ticks / healTicks); + int pct = (int) (100.0 * ticks / MEDKIT_TICKS); String barStr = "█".repeat(filled) + "░".repeat(empty); player.sendActionBar(mdbd.getInstance().getConfigManager().getMessages().get("survivor.medkit.actionbar", barStr, pct)); @@ -142,12 +151,11 @@ public class ItemUseListener implements Listener { player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_HURT, 0.15f, 1.8f); } - if (ticks >= healTicks) { + if (ticks >= MEDKIT_TICKS) { survivor.setState(PlayerState.HEALTHY); player.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.medkit.done")); player.sendActionBar(Component.empty()); player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 0.6f, 1.5f); - // Полностью убираем медкит (один раз использован) player.getInventory().setItemInMainHand(null); activeHeals.remove(uuid); cancel(); @@ -167,4 +175,32 @@ public class ItemUseListener implements Listener { } } -} + private void useBandage(Player player, Survivor survivor) { + player.getInventory().setItemInMainHand(null); + player.addPotionEffect(new PotionEffect(PotionEffectType.REGENERATION, 30 * 20, 0)); + player.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.item.bandage")); + player.playSound(player.getLocation(), Sound.ENTITY_WITCH_DRINK, 0.6f, 1.2f); + } + + private void useFullMedkit(Player player, Survivor survivor) { + player.getInventory().setItemInMainHand(null); + survivor.setState(PlayerState.HEALTHY); + player.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.item.fullmedkit")); + player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 0.8f, 1.5f); + } + + private void useSoda(Player player) { + player.getInventory().setItemInMainHand(null); + player.addPotionEffect(new PotionEffect(PotionEffectType.SPEED, 10 * 20, 1)); + player.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.item.soda")); + player.playSound(player.getLocation(), Sound.ENTITY_GENERIC_DRINK, 0.6f, 1.0f); + } + + private void useWrench(Player player) { + player.getInventory().setItemInMainHand(null); + player.addPotionEffect(new PotionEffect(PotionEffectType.HASTE, 10 * 20, 2)); + repairBoostExpiry.put(player.getUniqueId(), System.currentTimeMillis() + 10_000); + player.sendMessage(mdbd.getInstance().getConfigManager().getMessages().get("survivor.item.wrench")); + player.playSound(player.getLocation(), Sound.BLOCK_ANVIL_USE, 0.4f, 1.5f); + } +} \ No newline at end of file diff --git a/src/main/java/dev/loki/killer/GeneratorKick.java b/src/main/java/dev/loki/killer/GeneratorKick.java index 110bf7c..82adc53 100644 --- a/src/main/java/dev/loki/killer/GeneratorKick.java +++ b/src/main/java/dev/loki/killer/GeneratorKick.java @@ -14,7 +14,9 @@ import org.bukkit.plugin.Plugin; import org.bukkit.scheduler.BukkitRunnable; import org.bukkit.scheduler.BukkitTask; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -39,6 +41,9 @@ public class GeneratorKick { private int globalCooldownTicks = 0; private BukkitTask cooldownTask; + /** Pending unblock tasks — cancelled on shutdown to avoid ghost tasks */ + private final List pendingUnblockTasks = new ArrayList<>(); + public GeneratorKick(Plugin plugin, GameConfig config, GeneratorManager generatorManager) { this.plugin = plugin; this.config = config; @@ -135,6 +140,8 @@ public class GeneratorKick { for (BukkitTask t : chargingTasks.values()) t.cancel(); chargingTasks.clear(); if (cooldownTask != null) { cooldownTask.cancel(); cooldownTask = null; } + for (BukkitTask t : pendingUnblockTasks) t.cancel(); + pendingUnblockTasks.clear(); } // ------------------------------------------------------------------------- @@ -164,9 +171,10 @@ public class GeneratorKick { // Unblock generator after kick-duration (separate from global cooldown) int unblockTicks = config.getKickDuration() * 20; - plugin.getServer().getScheduler().runTaskLater(plugin, () -> { + BukkitTask unblockTask = plugin.getServer().getScheduler().runTaskLater(plugin, () -> { gen.setBlocked(false); }, unblockTicks); + pendingUnblockTasks.add(unblockTask); // Start global kick cooldown startGlobalCooldown(); diff --git a/src/main/java/dev/loki/listener/PlayerInteractListener.java b/src/main/java/dev/loki/listener/PlayerInteractListener.java index 5b3b1ec..e236438 100644 --- a/src/main/java/dev/loki/listener/PlayerInteractListener.java +++ b/src/main/java/dev/loki/listener/PlayerInteractListener.java @@ -3,6 +3,7 @@ package dev.loki.listener; import dev.loki.command.SetupCommand; import dev.loki.core.GameManager; import dev.loki.objective.GeneratorManager; +import dev.loki.objective.ComputerManager; import dev.loki.player.BasePlayer; import dev.loki.player.PlayerState; import org.bukkit.Location; @@ -26,13 +27,15 @@ public class PlayerInteractListener implements Listener { private final Plugin plugin; private final GameManager gameManager; private GeneratorManager generatorManager; + private ComputerManager computerManager; private SetupCommand setupCommand; private BiConsumer onGeneratorInteract; + private BiConsumer onComputerInteract; private BiConsumer onAttack; private BiConsumer onRescueAttempt; private BiConsumer onChestInteract; - private BiConsumer onHatchInteract; + private BiConsumer onGateInteract; /** Called when killer toggles sneak: (player, isSneaking) */ private BiConsumer onKillerSneak; /** Called when a survivor sneaks near another injured/downed survivor: (healer, target) */ @@ -53,10 +56,11 @@ public class PlayerInteractListener implements Listener { } public void setOnGeneratorInteract(BiConsumer h) { this.onGeneratorInteract = h; } + public void setOnComputerInteract(BiConsumer h) { this.onComputerInteract = h; } public void setOnAttack(BiConsumer h) { this.onAttack = h; } public void setOnRescueAttempt(BiConsumer h) { this.onRescueAttempt = h; } public void setOnChestInteract(BiConsumer h) { this.onChestInteract = h; } - public void setOnHatchInteract(BiConsumer h) { this.onHatchInteract = h; } + public void setOnGateInteract(BiConsumer h) { this.onGateInteract = h; } public void setOnKillerSneak(BiConsumer h) { this.onKillerSneak = h; } public void setOnHealSneak(BiConsumer h) { this.onHealSneak = h; } private BiConsumer onCarriedShift; @@ -69,6 +73,10 @@ public class PlayerInteractListener implements Listener { this.generatorManager = manager; } + public void setComputerManager(ComputerManager manager) { + this.computerManager = manager; + } + @EventHandler public void onSneak(PlayerToggleSneakEvent event) { Player player = event.getPlayer(); @@ -96,8 +104,10 @@ public class PlayerInteractListener implements Listener { if (st != PlayerState.HEALTHY && st != PlayerState.INJURED) return; if (event.isSneaking()) { - // Generator repair takes priority over healing + // Generator repair takes priority if (tryGeneratorInteract(player)) return; + // Computer repair + if (tryComputerInteract(player)) return; // Healing: check if sneaking near an injured/downed survivor if (onHealSneak != null) { @@ -216,6 +226,19 @@ public class PlayerInteractListener implements Listener { return true; } + private boolean tryComputerInteract(Player player) { + if (onComputerInteract == null || computerManager == null) return false; + var session = gameManager.getPlayerSession(player.getUniqueId()); + if (session == null) return false; + BasePlayer bp = session.getPlayer(player.getUniqueId()); + if (bp == null || bp.isKiller()) return false; + var comp = computerManager.getNearestComputer( + player.getLocation(), session.getConfig().getComputerInteractionRange()); + if (comp == null) return false; + onComputerInteract.accept(player, Action.RIGHT_CLICK_AIR); + return true; + } + private boolean isOnCooldown(Player player) { long now = System.currentTimeMillis(); Long last = lastClick.get(player.getUniqueId()); @@ -228,13 +251,41 @@ public class PlayerInteractListener implements Listener { // #13: Запрет входа в печки для всех (и маньяка тоже) — MUST be FIRST before any other checks if (event.getClickedBlock() != null) { String type = event.getClickedBlock().getType().toString(); + if (type.contains("FURNACE") || type.contains("SMOKER") || type.contains("BLAST_FURNACE")) { event.setCancelled(true); return; } + + // Cancel right-click on LECTERN (computer block) to prevent book editing GUI + if (type.equals("LECTERN")) { + event.setCancelled(true); + } + + // Chest interaction - cancel vanilla GUI for ALL players (including killer) + if (type.contains("CHEST")) { + event.setCancelled(true); + if (onChestInteract != null && !bp.isKiller()) { + onChestInteract.accept(player, true); + } + return; + } + } + + // Gate opening — проверяем ДО генератора, чтобы ворота можно было открыть рядом с геной + if (!bp.isKiller() && onGateInteract != null) { + var s = gameManager.getPlayerSession(player.getUniqueId()); + if (s != null && s.getGateManager() != null) { + var gate = s.getGateManager().getGateAt(player.getLocation(), 3.0); + if (gate != null && !gate.isOpen()) { + onGateInteract.accept(player, true); + return; + } + } } if (!bp.isKiller() && tryGeneratorInteract(player)) return; + if (!bp.isKiller() && tryComputerInteract(player)) return; // Killer attack (hook/carry) — fires on both air AND block clicks if (bp.isKiller() && onAttack != null) { @@ -242,29 +293,15 @@ public class PlayerInteractListener implements Listener { return; } - // Gate opening via proximity for survivors — works on air clicks too - if (!bp.isKiller() && onHatchInteract != null) { - onHatchInteract.accept(player, true); - } - if (event.getClickedBlock() == null) return; String type = event.getClickedBlock().getType().toString(); - // Chest interaction - cancel vanilla GUI and use plugin chest system - if (type.contains("CHEST")) { - event.setCancelled(true); - if (onChestInteract != null && !bp.isKiller()) { - onChestInteract.accept(player, true); - } - return; - } // Hook rescue: if survivor clicks on a hook block, attempt rescue + // Hook rescue / block protection if (!bp.isKiller() && onRescueAttempt != null) { onRescueAttempt.accept(player, true); - if (type.contains("IRON_BARS") || type.contains("IRON")) event.setCancelled(true); } - // Cancel interaction with hook blocks to prevent block manipulation if (type.contains("IRON_BARS") || type.contains("IRON")) event.setCancelled(true); } diff --git a/src/main/java/dev/loki/listener/SetupBlockBreakListener.java b/src/main/java/dev/loki/listener/SetupBlockBreakListener.java index ae2b868..80597fe 100644 --- a/src/main/java/dev/loki/listener/SetupBlockBreakListener.java +++ b/src/main/java/dev/loki/listener/SetupBlockBreakListener.java @@ -49,6 +49,7 @@ public class SetupBlockBreakListener implements Listener { Material placeMat = switch (mode) { case "chest" -> Material.matchMaterial(cfg.getChestBlock()); case "generator" -> Material.matchMaterial(cfg.getGeneratorBlock()); + case "computer" -> Material.matchMaterial(cfg.getComputerBlock()); case "hook" -> Material.matchMaterial(cfg.getHookBlock()); default -> null; }; diff --git a/src/main/java/dev/loki/listener/SkillCheckListener.java b/src/main/java/dev/loki/listener/SkillCheckListener.java index b949356..1e30b03 100644 --- a/src/main/java/dev/loki/listener/SkillCheckListener.java +++ b/src/main/java/dev/loki/listener/SkillCheckListener.java @@ -22,28 +22,24 @@ public class SkillCheckListener implements Listener { @EventHandler public void onInventoryClick(InventoryClickEvent event) { - String title = event.getView().getTitle(); - if (!title.contains("Skill Check")) return; + if (!(event.getWhoClicked() instanceof Player player)) return; + if (!minigame.isActive(player.getUniqueId())) return; event.setCancelled(true); - if (!(event.getWhoClicked() instanceof Player player)) return; - minigame.handleClick(player.getUniqueId(), event.getRawSlot()); } @EventHandler public void onInventoryDrag(InventoryDragEvent event) { - String title = event.getView().getTitle(); - if (!title.contains("Skill Check")) return; + if (!(event.getWhoClicked() instanceof Player player)) return; + if (!minigame.isActive(player.getUniqueId())) return; event.setCancelled(true); } @EventHandler public void onInventoryClose(InventoryCloseEvent event) { - String title = event.getView().getTitle(); - if (!title.contains("Skill Check")) return; - + if (!minigame.isActive(event.getPlayer().getUniqueId())) return; minigame.handleClose(event.getPlayer().getUniqueId()); } diff --git a/src/main/java/dev/loki/map/MapBuilder.java b/src/main/java/dev/loki/map/MapBuilder.java index f09afeb..f91e8af 100644 --- a/src/main/java/dev/loki/map/MapBuilder.java +++ b/src/main/java/dev/loki/map/MapBuilder.java @@ -113,10 +113,6 @@ public class MapBuilder { return true; } - public void placeTotem(Location location) { - // Totems removed - no-op - } - public void placeHook(Location location) { if (templateManager.hasTemplate("hook")) { templateManager.placeObject("hook", location); diff --git a/src/main/java/dev/loki/map/MapConfig.java b/src/main/java/dev/loki/map/MapConfig.java index 7c2780b..1dea06e 100644 --- a/src/main/java/dev/loki/map/MapConfig.java +++ b/src/main/java/dev/loki/map/MapConfig.java @@ -95,7 +95,6 @@ public class MapConfig { applyList("generators", locs); applyList("hooks", locs); applyList("chests", locs); - applyList("windows", locs); } // ------------------------------------------------------------------------- diff --git a/src/main/java/dev/loki/map/TemplateManager.java b/src/main/java/dev/loki/map/TemplateManager.java index b5fe9f5..1666a32 100644 --- a/src/main/java/dev/loki/map/TemplateManager.java +++ b/src/main/java/dev/loki/map/TemplateManager.java @@ -83,10 +83,6 @@ public class TemplateManager { // Chest preset (1x1x1) presets.put("chest_default", new PresetMap("chest_default", "chest", new Location(null, 0, 0, 0))); - - // Hatch preset (2x1x2) - presets.put("hatch_default", new PresetMap("hatch_default", - "hatch", new Location(null, 1, 0, 1))); } public boolean placeObject(String templateName, Location location) { diff --git a/src/main/java/dev/loki/mdbd.java b/src/main/java/dev/loki/mdbd.java index d1b2d93..6799ed3 100644 --- a/src/main/java/dev/loki/mdbd.java +++ b/src/main/java/dev/loki/mdbd.java @@ -6,6 +6,7 @@ import dev.loki.command.SetupCommand; import dev.loki.config.ConfigManager; import dev.loki.core.GameManager; import dev.loki.data.GeneratorDataManager; +import dev.loki.data.ComputerDataManager; import dev.loki.listener.PlayerInteractListener; import dev.loki.listener.PlayerJoinListener; import dev.loki.listener.PlayerMoveListener; @@ -25,6 +26,7 @@ public final class mdbd extends JavaPlugin { private MapManager mapManager; private MapBuilder mapBuilder; private GeneratorDataManager generatorData; + private ComputerDataManager computerData; private LobbyRegistry lobbyRegistry; private DBDCommand dbdCommand; private DBDAdminCommand dbdAdminCommand; @@ -53,6 +55,7 @@ public final class mdbd extends JavaPlugin { this.mapBuilder = new MapBuilder(this, templateManager); this.generatorData = new GeneratorDataManager(this); + this.computerData = new ComputerDataManager(this); this.lobbyManager = new LobbyManager(this); this.lobbyManager.loadWorlds(); @@ -103,6 +106,9 @@ public final class mdbd extends JavaPlugin { if (generatorData != null) { generatorData.save(); } + if (computerData != null) { + computerData.save(); + } if (lobbyRegistry != null) { lobbyRegistry.save(); } @@ -137,6 +143,10 @@ public final class mdbd extends JavaPlugin { return generatorData; } + public ComputerDataManager getComputerData() { + return computerData; + } + public DBDAdminCommand getDbdAdminCommand() { return dbdAdminCommand; } @@ -162,6 +172,7 @@ public final class mdbd extends JavaPlugin { dev.loki.item.DBDItems.loadModelData(configManager.getGame().getConfig()); templateManager.loadAllTemplates(); generatorData.load(); + computerData.load(); getLogger().info("All configs reloaded!"); } } diff --git a/src/main/java/dev/loki/minigames/generator/GuiRouletteMinigame.java b/src/main/java/dev/loki/minigames/generator/GuiRouletteMinigame.java index da29b5c..34e7336 100644 --- a/src/main/java/dev/loki/minigames/generator/GuiRouletteMinigame.java +++ b/src/main/java/dev/loki/minigames/generator/GuiRouletteMinigame.java @@ -64,7 +64,7 @@ public class GuiRouletteMinigame implements GeneratorMinigame { RouletteSession session = new RouletteSession(player, callback); activeSessions.put(playerId, session); - String titleStr = plugin.getConfigManager().getMessages().getString("minigames.generator-roulette.title", "⚡ Skill Check!"); + String titleStr = plugin.getConfigManager().getMessages().getString("minigames.generator-roulette.title", "⚡ Проверка реакции!"); Inventory inv = Bukkit.createInventory(null, 9, Component.text(titleStr).color(NamedTextColor.YELLOW)); fillInventory(inv, session.successSlot); player.openInventory(inv); diff --git a/src/main/java/dev/loki/objective/Computer.java b/src/main/java/dev/loki/objective/Computer.java new file mode 100644 index 0000000..bf64c19 --- /dev/null +++ b/src/main/java/dev/loki/objective/Computer.java @@ -0,0 +1,146 @@ +package dev.loki.objective; + +import dev.loki.data.ComputerDataManager; +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; + +import java.util.UUID; + +public class Computer { + private final int id; + private final Location location; + private final double zoneRadius; + private double progress; + private boolean isCompleted; + private boolean isBlocked; + private UUID currentRepairer; + private int regressionTicks; + private boolean wasKicked; + + public Computer(int id, Location location) { + this(id, location, ComputerDataManager.getZoneRadius()); + } + + public Computer(int id, Location location, double zoneRadius) { + this.id = id; + this.location = location; + this.zoneRadius = zoneRadius; + this.progress = 0.0; + this.isCompleted = false; + this.isBlocked = false; + this.currentRepairer = null; + this.regressionTicks = 0; + this.wasKicked = false; + } + + public int getId() { + return id; + } + + public Location getLocation() { + return location; + } + + public double getZoneRadius() { + return zoneRadius; + } + + public boolean isPlayerInZone(Player player) { + if (!player.getWorld().equals(location.getWorld())) return false; + return player.getLocation().distance(location) <= zoneRadius; + } + + public boolean isLocationInZone(Location loc) { + if (!loc.getWorld().equals(location.getWorld())) return false; + return loc.distance(location) <= zoneRadius; + } + + public double getProgress() { + return progress; + } + + public void setProgress(double progress) { + this.progress = Math.max(0, Math.min(100, progress)); + if (this.progress >= 100) { + this.progress = 100; + this.isCompleted = true; + } + } + + public void addProgress(double amount) { + setProgress(progress + amount); + } + + public void removeProgress(double amount) { + setProgress(progress - amount); + } + + public boolean isCompleted() { + return isCompleted; + } + + public boolean isBlocked() { + return isBlocked; + } + + public void setBlocked(boolean blocked) { + this.isBlocked = blocked; + } + + public UUID getCurrentRepairer() { + return currentRepairer; + } + + public void setCurrentRepairer(UUID repairer) { + this.currentRepairer = repairer; + } + + public boolean isBeingRepaired() { + return currentRepairer != null; + } + + public int getRegressionTicks() { + return regressionTicks; + } + + public void setRegressionTicks(int ticks) { + this.regressionTicks = ticks; + } + + public void decrementRegression() { + if (regressionTicks > 0) { + regressionTicks--; + } + } + + public boolean isUnderRegression() { + return regressionTicks > 0 && !isCompleted && !isBeingRepaired(); + } + + public void setUnderRegression(boolean kicked, int durationTicks) { + this.wasKicked = kicked; + this.regressionTicks = durationTicks; + } + + public boolean wasKicked() { + return wasKicked; + } + + public void setKicked(boolean kicked) { + this.wasKicked = kicked; + } + + public boolean canBeRepaired() { + return !isCompleted && !isBlocked && !isUnderRegression(); + } + + @Deprecated + public Block getBlock() { + return location.getBlock(); + } + + public double getDistance(Location loc) { + return location.distance(loc); + } +} diff --git a/src/main/java/dev/loki/objective/ComputerHologram.java b/src/main/java/dev/loki/objective/ComputerHologram.java new file mode 100644 index 0000000..8dbf7a7 --- /dev/null +++ b/src/main/java/dev/loki/objective/ComputerHologram.java @@ -0,0 +1,134 @@ +package dev.loki.objective; + +import dev.loki.mdbd; +import dev.loki.util.TextUtil; +import org.bukkit.Location; +import org.bukkit.entity.ArmorStand; +import org.bukkit.entity.Display; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.TextDisplay; +import org.bukkit.scheduler.BukkitTask; + +import java.util.HashMap; +import java.util.Map; + +public class ComputerHologram { + + private final mdbd plugin; + private final ComputerManager manager; + private final Map displays = new HashMap<>(); + private BukkitTask task; + private boolean useTextDisplay; + + public ComputerHologram(mdbd plugin, ComputerManager manager) { + this.plugin = plugin; + this.manager = manager; + this.useTextDisplay = isTextDisplayAvailable(); + } + + private static boolean isTextDisplayAvailable() { + try { + EntityType.valueOf("TEXT_DISPLAY"); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + public void start() { + stop(); + spawnAll(); + task = plugin.getServer().getScheduler().runTaskTimer(plugin, this::refresh, 20L, 20L); + plugin.getLogger().info("[ComputerHologram] Started with " + displays.size() + " holograms (mode=" + (useTextDisplay ? "TextDisplay" : "ArmorStand") + ")"); + } + + public void stop() { + if (task != null) { + task.cancel(); + task = null; + } + for (Object display : displays.values()) { + if (display instanceof TextDisplay td && td.isValid()) { + td.remove(); + } else if (display instanceof ArmorStand as && as.isValid()) { + as.remove(); + } + } + displays.clear(); + } + + private void spawnAll() { + double heightOffset = plugin.getConfigManager().getGame().getComputerHologramHeightOffset(); + for (Computer comp : manager.getAllComputers()) { + Location loc = comp.getLocation(); + if (loc.getWorld() == null) continue; + + Location holoLoc = loc.getBlock().getLocation().add(0.5, 2.0 + heightOffset, 0.5); + try { + if (useTextDisplay) { + TextDisplay display = loc.getWorld().spawn(holoLoc, TextDisplay.class, d -> { + d.setBillboard(Display.Billboard.VERTICAL); + d.setSeeThrough(false); + d.setShadowed(true); + d.setDefaultBackground(false); + d.setPersistent(false); + d.setTextOpacity((byte) -1); + }); + displays.put(comp.getId(), display); + } else { + ArmorStand stand = loc.getWorld().spawn(holoLoc, ArmorStand.class, as -> { + as.setInvisible(true); + as.setInvulnerable(true); + as.setGravity(false); + as.setMarker(true); + as.setCustomNameVisible(true); + as.setPersistent(false); + }); + displays.put(comp.getId(), stand); + } + } catch (Exception e) { + plugin.getLogger().warning("[ComputerHologram] Failed to spawn hologram for computer " + comp.getId() + ": " + e.getMessage()); + } + } + } + + private void refresh() { + double heightOffset = plugin.getConfigManager().getGame().getComputerHologramHeightOffset(); + int total = manager.getTotalCount(); + for (Computer comp : manager.getAllComputers()) { + Object display = displays.get(comp.getId()); + if (display == null) continue; + + boolean valid = (display instanceof TextDisplay td && td.isValid()) || + (display instanceof ArmorStand as && as.isValid()); + if (!valid) continue; + + int pct = (int) comp.getProgress(); + String bar = buildBar(pct); + String text; + if (comp.isCompleted()) { + text = "§b🖥 Компьютер §f#" + (comp.getId() + 1) + "/" + total + "\n§a✔ Починен!"; + } else if (comp.isUnderRegression()) { + text = "§b🖥 Компьютер §f#" + (comp.getId() + 1) + "/" + total + "\n§c↓ " + bar + " §f" + pct + "%"; + } else { + text = "§b🖥 Компьютер §f#" + (comp.getId() + 1) + "/" + total + "\n§e" + bar + " §f" + pct + "%"; + } + + Location base = comp.getLocation().getBlock().getLocation().add(0.5, 2.0 + heightOffset, 0.5); + + if (display instanceof TextDisplay td) { + td.text(TextUtil.color(text)); + td.teleport(base); + } else if (display instanceof ArmorStand as) { + as.setCustomName(text); + as.teleport(base); + } + } + } + + private String buildBar(int pct) { + int filled = pct / 10; + int empty = 10 - filled; + return "§e" + "█".repeat(Math.max(0, filled)) + "§7" + "░".repeat(Math.max(0, empty)); + } +} diff --git a/src/main/java/dev/loki/objective/ComputerManager.java b/src/main/java/dev/loki/objective/ComputerManager.java new file mode 100644 index 0000000..e63eadf --- /dev/null +++ b/src/main/java/dev/loki/objective/ComputerManager.java @@ -0,0 +1,114 @@ +package dev.loki.objective; + +import dev.loki.data.ComputerDataManager; +import dev.loki.config.GameConfig; +import org.bukkit.Location; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ComputerManager { + private final Map computers; + private final int requiredCount; + private final double normalRegressionRate; + private final double kickRegressionRate; + + public ComputerManager(ComputerDataManager dataManager, int requiredCount, GameConfig config) { + this.computers = new HashMap<>(); + loadComputers(dataManager); + this.requiredCount = Math.min(requiredCount, computers.size()); + this.normalRegressionRate = config.getComputerRegressionRate() / 20.0; + this.kickRegressionRate = config.getComputerKickRegressionRate() / 20.0; + } + + private void loadComputers(ComputerDataManager dataManager) { + List locations = dataManager.getComputers(); + for (int i = 0; i < locations.size(); i++) { + computers.put(i, new Computer(i, locations.get(i))); + } + } + + public Computer getComputer(int id) { return computers.get(id); } + + public Computer getComputerAt(Location location) { + for (Computer comp : computers.values()) { + if (comp.isLocationInZone(location)) return comp; + } + return null; + } + + public Computer getNearestComputer(Location location, double maxDistance) { + Computer nearest = null; + double nearestDist = maxDistance; + for (Computer comp : computers.values()) { + if (!comp.getLocation().getWorld().equals(location.getWorld())) continue; + double dist = comp.getDistance(location); + if (dist < nearestDist) { nearestDist = dist; nearest = comp; } + } + return nearest; + } + + public boolean isInAnyZone(Player player) { + return getComputerAt(player.getLocation()) != null; + } + + public List getAllComputers() { return new ArrayList<>(computers.values()); } + + public List getIncompleteComputers() { + List incomplete = new ArrayList<>(); + for (Computer comp : computers.values()) { + if (!comp.isCompleted()) incomplete.add(comp); + } + return incomplete; + } + + public int getTotalCount() { return computers.size(); } + public int getRequiredCount() { return requiredCount; } + + public int getCompletedCount() { + int count = 0; + for (Computer comp : computers.values()) { + if (comp.isCompleted()) count++; + } + return count; + } + + public int getRemainingRequired() { + return Math.max(0, requiredCount - getCompletedCount()); + } + + public void onComputerCompleted(Computer computer) { + } + + public boolean areAllRequiredCompleted() { + return getCompletedCount() >= requiredCount; + } + + public double getAverageProgress() { + if (computers.isEmpty()) return 0; + double total = 0; + for (Computer comp : computers.values()) total += comp.getProgress(); + return total / computers.size(); + } + + public int getActiveRepairCount() { + int count = 0; + for (Computer comp : computers.values()) { + if (comp.isBeingRepaired()) count++; + } + return count; + } + + public void tick() { + for (Computer comp : computers.values()) { + if (comp.isUnderRegression()) { + double rate = comp.wasKicked() ? kickRegressionRate : normalRegressionRate; + comp.removeProgress(rate); + } + comp.decrementRegression(); + } + } +} diff --git a/src/main/java/dev/loki/objective/ComputerRepair.java b/src/main/java/dev/loki/objective/ComputerRepair.java new file mode 100644 index 0000000..bf193fd --- /dev/null +++ b/src/main/java/dev/loki/objective/ComputerRepair.java @@ -0,0 +1,195 @@ +package dev.loki.objective; + +import dev.loki.mdbd; +import dev.loki.minigames.GeneratorMinigame; +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class ComputerRepair { + private final mdbd plugin; + private final ComputerManager manager; + private final Map sessions; + private final GeneratorMinigame minigame; + + private dev.loki.item.ItemUseListener itemUseListener; + + private static final double REPAIR_PER_TICK = 100.0 / 1600.0; + private static final double REPAIR_BOOST_MULTIPLIER = 2.0; + private static final double REPAIR_RANGE = 3.0; + + public void setItemUseListener(dev.loki.item.ItemUseListener listener) { + this.itemUseListener = listener; + } + + public ComputerRepair(mdbd plugin, ComputerManager manager, GeneratorMinigame minigame) { + this.plugin = plugin; + this.manager = manager; + this.minigame = minigame; + this.sessions = new HashMap<>(); + } + + public void startRepair(Player player, Computer computer) { + UUID uuid = player.getUniqueId(); + + if (sessions.containsKey(uuid)) { + RepairSession existing = sessions.get(uuid); + if (existing.computer.getId() == computer.getId()) { + return; + } else { + stopRepair(player); + } + } + + if (!computer.canBeRepaired()) { + player.sendMessage(plugin.getConfigManager().getMessages().get("computer.repair.cannot")); + return; + } + + if (computer.isBeingRepaired() && !computer.getCurrentRepairer().equals(uuid)) { + player.sendMessage(plugin.getConfigManager().getMessages().get("computer.repair.busy")); + return; + } + + computer.setCurrentRepairer(uuid); + RepairSession session = new RepairSession(player, computer); + sessions.put(uuid, session); + } + + public void stopRepair(Player player) { + UUID uuid = player.getUniqueId(); + RepairSession session = sessions.remove(uuid); + + if (session != null) { + session.computer.setCurrentRepairer(null); + if (!session.computer.isUnderRegression() && !session.computer.isBlocked()) { + session.computer.setBlocked(false); + } + player.sendActionBar(Component.empty()); + } + } + + public void tick() { + for (RepairSession session : new HashMap<>(sessions).values()) { + if (!updateRepair(session)) { + stopRepair(session.player); + } + } + } + + private boolean updateRepair(RepairSession session) { + Player player = session.player; + Computer computer = session.computer; + + if (!player.isOnline()) return false; + if (!player.isSneaking()) return false; + if (computer.getDistance(player.getLocation()) > REPAIR_RANGE) return false; + if (computer.isCompleted()) return false; + + if (minigame.isActive(player.getUniqueId())) return true; + + if (session.skillCheckCooldown > 0) { + session.skillCheckCooldown--; + } else if (shouldTriggerSkillCheck()) { + minigame.start(player, (p, success) -> { + Computer c = session.computer; + if (success) { + double winPercent = plugin.getConfigManager().getGame().getSkillCheckWinPercent(); + c.addProgress(winPercent); + p.sendActionBar(plugin.getConfigManager().getMessages().get("computer.repair.skillcheck-success", (int)winPercent)); + p.playSound(p.getLocation(), org.bukkit.Sound.ENTITY_PLAYER_LEVELUP, 0.8f, 1.2f); + } else { + double lossPercent = plugin.getConfigManager().getGame().getSkillCheckLossPercent(); + c.removeProgress(lossPercent); + p.sendActionBar(plugin.getConfigManager().getMessages().get("computer.repair.skillcheck-fail", (int)lossPercent)); + p.playSound(p.getLocation(), org.bukkit.Sound.ENTITY_BLAZE_HURT, 1.0f, 0.5f); + } + }); + session.skillCheckCooldown = 100 + (int)(Math.random() * 200); + return true; + } + + double rate = REPAIR_PER_TICK; + if (itemUseListener != null && itemUseListener.hasRepairBoost(player.getUniqueId())) { + rate *= REPAIR_BOOST_MULTIPLIER; + } + computer.addProgress(rate); + showProgressBar(player, session); + + if (computer.isCompleted()) { + onComputerComplete(computer, player); + return false; + } + + return true; + } + + private void showProgressBar(Player player, RepairSession session) { + double progress = session.computer.getProgress(); + String bar = renderProgressBar(progress); + player.sendActionBar(plugin.getConfigManager().getMessages().get("computer.repair.actionbar", bar)); + } + + private String renderProgressBar(double progress) { + int filled = (int) (10 * progress / 100.0); + int empty = 10 - filled; + StringBuilder bar = new StringBuilder(""); + bar.append("█".repeat(Math.max(0, filled))); + bar.append(""); + bar.append("░".repeat(Math.max(0, empty))); + bar.append(" ").append((int)progress).append("%"); + return bar.toString(); + } + + private void onComputerComplete(Computer computer, Player player) { + manager.onComputerCompleted(computer); + Bukkit.broadcast(plugin.getConfigManager().getMessages().get( + "computer.repair.broadcast", manager.getCompletedCount(), manager.getRequiredCount())); + } + + private boolean shouldTriggerSkillCheck() { + double chance = plugin.getConfigManager().getGame().getSkillCheckChance(); + return Math.random() < chance; + } + + public boolean isRepairing(UUID uuid) { + return sessions.containsKey(uuid); + } + + public GeneratorMinigame getMinigame() { + return minigame; + } + + public boolean isMinigameActive(UUID playerId) { + return minigame.isActive(playerId); + } + + public void handleMinigameClick(Player player) { + minigame.handleRightClick(player); + } + + public void handleMinigameSneak(Player player) { + minigame.handleSneak(player); + } + + public void shutdown() { + minigame.cleanup(); + sessions.clear(); + } + + public static class RepairSession { + final Player player; + final Computer computer; + int skillCheckCooldown; + + RepairSession(Player player, Computer computer) { + this.player = player; + this.computer = computer; + this.skillCheckCooldown = 50; + } + } +} diff --git a/src/main/java/dev/loki/objective/GeneratorHologram.java b/src/main/java/dev/loki/objective/GeneratorHologram.java index 1ea0a91..b0c56ae 100644 --- a/src/main/java/dev/loki/objective/GeneratorHologram.java +++ b/src/main/java/dev/loki/objective/GeneratorHologram.java @@ -67,7 +67,7 @@ public class GeneratorHologram { Location loc = gen.getLocation(); if (loc.getWorld() == null) continue; - Location holoLoc = loc.clone().add(0.5, 2.0 + heightOffset, 0.5); + Location holoLoc = loc.getBlock().getLocation().add(0.5, 2.0 + heightOffset, 0.5); try { if (useTextDisplay) { TextDisplay display = loc.getWorld().spawn(holoLoc, TextDisplay.class, d -> { @@ -118,7 +118,7 @@ public class GeneratorHologram { text = "§6⚙ Генератор §f#" + (gen.getId() + 1) + "/" + total + "\n§e" + bar + " §f" + pct + "%"; } - Location base = gen.getLocation().clone().add(0.5, 2.0 + heightOffset, 0.5); + Location base = gen.getLocation().getBlock().getLocation().add(0.5, 2.0 + heightOffset, 0.5); if (display instanceof TextDisplay td) { td.text(TextUtil.color(text)); diff --git a/src/main/java/dev/loki/objective/GeneratorManager.java b/src/main/java/dev/loki/objective/GeneratorManager.java index f8ac492..de128b0 100644 --- a/src/main/java/dev/loki/objective/GeneratorManager.java +++ b/src/main/java/dev/loki/objective/GeneratorManager.java @@ -1,6 +1,7 @@ package dev.loki.objective; import dev.loki.data.GeneratorDataManager; +import dev.loki.config.GameConfig; import org.bukkit.Location; import org.bukkit.entity.Player; @@ -12,11 +13,15 @@ import java.util.Map; public class GeneratorManager { private final Map generators; private final int requiredCount; + private final double normalRegressionRate; + private final double kickRegressionRate; - public GeneratorManager(GeneratorDataManager dataManager, int requiredCount) { + public GeneratorManager(GeneratorDataManager dataManager, int requiredCount, GameConfig config) { this.generators = new HashMap<>(); - this.requiredCount = requiredCount; loadGenerators(dataManager); + this.requiredCount = Math.min(requiredCount, generators.size()); + this.normalRegressionRate = config.getRegressionRate() / 20.0; + this.kickRegressionRate = config.getKickRegressionRate() / 20.0; } private void loadGenerators(GeneratorDataManager dataManager) { @@ -103,7 +108,7 @@ public class GeneratorManager { public void tick() { for (Generator gen : generators.values()) { if (gen.isUnderRegression()) { - double rate = gen.wasKicked() ? 0.125 : 0.0125; + double rate = gen.wasKicked() ? kickRegressionRate : normalRegressionRate; gen.removeProgress(rate); } gen.decrementRegression(); diff --git a/src/main/java/dev/loki/objective/GeneratorRegression.java b/src/main/java/dev/loki/objective/GeneratorRegression.java deleted file mode 100644 index 012a28d..0000000 --- a/src/main/java/dev/loki/objective/GeneratorRegression.java +++ /dev/null @@ -1,52 +0,0 @@ -package dev.loki.objective; - -import dev.loki.config.GameConfig; - -public class GeneratorRegression { - private final GameConfig config; - - public GeneratorRegression(GameConfig config) { - this.config = config; - } - - public void applyRegression(Generator generator) { - if (generator.isCompleted() || generator.isBeingRepaired()) return; - double rate = generator.wasKicked() ? getKickRegressionRate() : getNormalRegressionRate(); - generator.removeProgress(rate); - } - - public void onGeneratorKicked(Generator generator) { - generator.setUnderRegression(true, config.getKickDuration() * 20); - } - - public void tick(Generator generator) { - if (!generator.isUnderRegression()) return; - applyRegression(generator); - generator.decrementRegression(); - if (generator.getRegressionTicks() <= 0) { - generator.setKicked(false); - } - } - - public void tickAll(GeneratorManager manager) { - for (Generator gen : manager.getAllGenerators()) { - if (!gen.isCompleted() && !gen.isBeingRepaired()) { - tick(gen); - } - } - } - - private double getNormalRegressionRate() { - return config.getRegressionRate() / 20.0; - } - - private double getKickRegressionRate() { - return config.getKickRegressionRate() / 20.0; - } - - public static boolean shouldStartRegression(Generator generator) { - return !generator.isCompleted() - && !generator.isBeingRepaired() - && generator.getProgress() > 0; - } -} diff --git a/src/main/java/dev/loki/objective/GeneratorRepair.java b/src/main/java/dev/loki/objective/GeneratorRepair.java index 18c75d9..de3ef9a 100644 --- a/src/main/java/dev/loki/objective/GeneratorRepair.java +++ b/src/main/java/dev/loki/objective/GeneratorRepair.java @@ -2,6 +2,7 @@ package dev.loki.objective; import dev.loki.mdbd; import dev.loki.minigames.GeneratorMinigame; +import dev.loki.player.BasePlayer; import net.kyori.adventure.text.Component; import org.bukkit.Bukkit; import org.bukkit.Location; @@ -19,10 +20,19 @@ public class GeneratorRepair { private final GeneratorManager manager; private final Map sessions; private final GeneratorMinigame minigame; + private final Map compassTaskIds = new HashMap<>(); + private final Map compassPrevItems = new HashMap<>(); + + private dev.loki.item.ItemUseListener itemUseListener; private static final double REPAIR_PER_TICK = 100.0 / 1600.0; + private static final double REPAIR_BOOST_MULTIPLIER = 2.0; private static final double REPAIR_RANGE = 3.0; + public void setItemUseListener(dev.loki.item.ItemUseListener listener) { + this.itemUseListener = listener; + } + public GeneratorRepair(mdbd plugin, GeneratorManager manager, GeneratorMinigame minigame) { this.plugin = plugin; this.manager = manager; @@ -71,8 +81,8 @@ public class GeneratorRepair { if (session != null) { session.generator.setCurrentRepairer(null); // Progress is PRESERVED - generator keeps its progress value - // Only unblock if it was blocked due to an ongoing repair (not a kick) - if (!session.generator.isUnderRegression()) { + // Only unblock if it was blocked due to an ongoing repair (not a kick or block) + if (!session.generator.isUnderRegression() && !session.generator.isBlocked()) { session.generator.setBlocked(false); } player.sendActionBar(Component.empty()); @@ -120,7 +130,11 @@ public class GeneratorRepair { return true; } - generator.addProgress(REPAIR_PER_TICK); + double rate = REPAIR_PER_TICK; + if (itemUseListener != null && itemUseListener.hasRepairBoost(player.getUniqueId())) { + rate *= REPAIR_BOOST_MULTIPLIER; + } + generator.addProgress(rate); showProgressBar(player, session); if (generator.isCompleted()) { @@ -176,12 +190,18 @@ public class GeneratorRepair { } } - /** Give every online killer a compass pointing to the failed generator for compass-duration-seconds. */ + /** Give only killers a compass pointing to the failed generator for compass-duration-seconds. */ private void notifyKillerWithCompass(Player failedPlayer, Generator generator) { + var session = plugin.getGameManager().getCurrentSession(); + if (session == null) return; + int durationTicks = plugin.getConfigManager().getGame().getCompassDurationSeconds() * 20; Location compassTarget = generator.getLocation(); - for (Player online : Bukkit.getOnlinePlayers()) { + for (BasePlayer bp : session.getPlayers().values()) { + if (!bp.isKiller()) continue; + Player online = bp.getPlayer(); + if (online == null || !online.isOnline()) continue; if (online.getUniqueId().equals(failedPlayer.getUniqueId())) continue; online.sendMessage(plugin.getConfigManager().getMessages().get("generator.kicked.notification")); @@ -194,15 +214,47 @@ public class GeneratorRepair { compass.setItemMeta(meta); } - ItemStack prev = online.getInventory().getItemInOffHand(); + UUID killerId = online.getUniqueId(); + + Integer prevTaskId = compassTaskIds.remove(killerId); + if (prevTaskId != null) { + Bukkit.getScheduler().cancelTask(prevTaskId); + } + + compassPrevItems.put(killerId, online.getInventory().getItemInOffHand()); online.getInventory().setItemInOffHand(compass); - // Восстанавливаем старый предмет только если в offhand всё ещё компас - plugin.getServer().getScheduler().runTaskLater(plugin, () -> { - ItemStack current = online.getInventory().getItemInOffHand(); - if (current.getType() == Material.COMPASS) { - online.getInventory().setItemInOffHand(prev); + + int taskId = Bukkit.getScheduler().runTaskLater(plugin, () -> { + if (!online.isOnline()) return; + + ItemStack offHand = online.getInventory().getItemInOffHand(); + if (offHand.getType() == Material.COMPASS) { + online.getInventory().setItemInOffHand(compassPrevItems.get(killerId)); + compassTaskIds.remove(killerId); + compassPrevItems.remove(killerId); + return; } - }, durationTicks); + + ItemStack mainHand = online.getInventory().getItemInMainHand(); + if (mainHand.getType() == Material.COMPASS) { + online.getInventory().setItemInMainHand(compassPrevItems.get(killerId)); + compassTaskIds.remove(killerId); + compassPrevItems.remove(killerId); + return; + } + + for (int i = 0; i < online.getInventory().getSize(); i++) { + ItemStack item = online.getInventory().getItem(i); + if (item != null && item.getType() == Material.COMPASS) { + online.getInventory().setItem(i, null); + break; + } + } + compassTaskIds.remove(killerId); + compassPrevItems.remove(killerId); + }, durationTicks).getTaskId(); + + compassTaskIds.put(killerId, taskId); } } diff --git a/src/main/java/dev/loki/objective/SkillCheck.java b/src/main/java/dev/loki/objective/SkillCheck.java deleted file mode 100644 index eff6f52..0000000 --- a/src/main/java/dev/loki/objective/SkillCheck.java +++ /dev/null @@ -1,142 +0,0 @@ -package dev.loki.objective; - -import dev.loki.mdbd; -import org.bukkit.Bukkit; - -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; - -public class SkillCheck { - private final mdbd plugin; - private final GeneratorRepair generatorRepair; - private final Map activeChecks; - - private static final double ZONE_SIZE = 0.15; - private static final double ROTATION_SPEED = 3.0; - - public SkillCheck(mdbd plugin, GeneratorRepair generatorRepair) { - this.plugin = plugin; - this.generatorRepair = generatorRepair; - this.activeChecks = new HashMap<>(); - } - - public void start(UUID playerId, Generator generator) { - double zoneStart = Math.random() * 0.7; - double zoneEnd = zoneStart + ZONE_SIZE; - - CheckState state = new CheckState( - playerId, - generator, - zoneStart, - zoneEnd, - 0.0, - ROTATION_SPEED - ); - activeChecks.put(playerId, state); - } - - public void tick() { - for (CheckState state : new HashMap<>(activeChecks).values()) { - state.update(); - - if (state.isExpired()) { - onFail(state); - } - } - } - - public boolean tryComplete(UUID playerId, boolean isSuccessZone) { - CheckState state = activeChecks.get(playerId); - if (state == null) return false; - - if (isSuccessZone) { - onSuccess(state); - } else { - onFail(state); - } - return true; - } - - private void onSuccess(CheckState state) { - activeChecks.remove(state.playerId); - Bukkit.getScheduler().runTask(plugin, () -> - generatorRepair.onSkillCheckResult(state.playerId, true, state.generator) - ); - } - - private void onFail(CheckState state) { - activeChecks.remove(state.playerId); - Bukkit.getScheduler().runTask(plugin, () -> - generatorRepair.onSkillCheckResult(state.playerId, false, state.generator) - ); - } - - public boolean isActive(UUID playerId) { - return activeChecks.containsKey(playerId); - } - - public CheckState getState(UUID playerId) { - return activeChecks.get(playerId); - } - - public static class CheckState { - final UUID playerId; - final Generator generator; - final double zoneStart; - final double zoneEnd; - double pointerPosition; - final double rotationSpeed; - int ticks; - - CheckState(UUID playerId, Generator generator, double zoneStart, double zoneEnd, - double pointerPosition, double rotationSpeed) { - this.playerId = playerId; - this.generator = generator; - this.zoneStart = zoneStart; - this.zoneEnd = zoneEnd; - this.pointerPosition = pointerPosition; - this.rotationSpeed = rotationSpeed; - this.ticks = 0; - } - - void update() { - pointerPosition += rotationSpeed * 0.01; - if (pointerPosition >= 1.0) { - pointerPosition = 0.0; - } - ticks++; - } - - boolean isExpired() { - return ticks > 100; - } - - boolean isInSuccessZone() { - return pointerPosition >= zoneStart && pointerPosition <= zoneEnd; - } - - public String getVisualRepresentation() { - int zoneStartPos = (int) (zoneStart * 20); - int zoneEndPos = (int) (zoneEnd * 20); - int pointerPos = (int) (pointerPosition * 20); - - StringBuilder sb = new StringBuilder("["); - for (int i = 0; i < 20; i++) { - if (i == pointerPos) { - sb.append("◆"); - } else if (i >= zoneStartPos && i <= zoneEndPos) { - sb.append("█"); - } else { - sb.append("░"); - } - } - sb.append("]"); - return sb.toString(); - } - - public double getPointerPosition() { - return pointerPosition; - } - } -} diff --git a/src/main/java/dev/loki/objective/SkillCheckInventory.java b/src/main/java/dev/loki/objective/SkillCheckInventory.java deleted file mode 100644 index 3ffe8c6..0000000 --- a/src/main/java/dev/loki/objective/SkillCheckInventory.java +++ /dev/null @@ -1,245 +0,0 @@ -package dev.loki.objective; - -import dev.loki.config.GameConfig; -import dev.loki.mdbd; -import dev.loki.objective.Generator; -import dev.loki.objective.GeneratorRepair; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.Sound; -import org.bukkit.entity.Player; -import org.bukkit.event.inventory.InventoryClickEvent; -import org.bukkit.event.inventory.InventoryCloseEvent; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; - -import java.util.*; - -/** - * GUI-based Skill Check minigame — ROULETTE style. - * - * Green block moves sequentially left-to-right (0→1→2→...→8→0→...). - * Starts FAST (2-3 ticks), gradually slows down (8-12 ticks). - * ~12-18 switches, then stops on final slot. - * Player must click the GREEN block. Clicking gray = fail. Timeout = fail. - */ -public class SkillCheckInventory { - private final mdbd plugin; - private final Map activeSessions = new HashMap<>(); - private static final int TIMEOUT_TICKS = 60; // 3 seconds — не выбрал = проигрыш -10% - private static final int MIN_SWITCHES = 5; - private static final int MAX_SWITCHES = 8; - - private final Material successMaterial; - private final Material failMaterial; - private final Material neutralMaterial; - - public SkillCheckInventory(mdbd plugin) { - this.plugin = plugin; - GameConfig cfg = plugin.getConfigManager().getGame(); - this.successMaterial = getMaterial(cfg.getSkillCheckSuccessBlock(), Material.GREEN_STAINED_GLASS_PANE); - this.failMaterial = getMaterial(cfg.getSkillCheckFailBlock(), Material.RED_STAINED_GLASS_PANE); - this.neutralMaterial = getMaterial(cfg.getSkillCheckNeutralBlock(), Material.GRAY_STAINED_GLASS_PANE); - } - - private static Material getMaterial(String name, Material fallback) { - Material m = Material.getMaterial(name); - return m != null ? m : fallback; - } - - public void startSkillCheck(Player player, UUID playerId, SkillCheck skillCheck, Generator generator, GeneratorRepair generatorRepair) { - SkillCheckSession session = new SkillCheckSession(player, playerId, skillCheck, generator, generatorRepair); - activeSessions.put(playerId, session); - - Inventory inv = Bukkit.createInventory(null, 9, Component.text("⚡ Skill Check!").color(NamedTextColor.YELLOW)); - fillInventory(inv, session.successSlot); - player.openInventory(inv); - - // Schedule timeout - plugin.getServer().getScheduler().runTaskLater(plugin, () -> { - if (activeSessions.containsKey(playerId)) { - onTimeout(playerId); - } - }, TIMEOUT_TICKS); - - // Start roulette animation - runRoulette(playerId); - } - - /** - * Roulette animation: green block moves left-to-right sequentially, - * slowing down over time like a real roulette wheel. - */ - private void runRoulette(UUID playerId) { - SkillCheckSession session = activeSessions.get(playerId); - if (session == null || !session.player.isOnline()) { - activeSessions.remove(playerId); - return; - } - - // Check if timeout happened - if (session.totalTicks >= TIMEOUT_TICKS) { - onTimeout(playerId); - return; - } - - // Move green sequentially to next slot - session.lastSuccessSlot = session.successSlot; - session.successSlot = (session.successSlot + 1) % 9; - session.switchesDone++; - - // Update inventory visually - Inventory inv = session.player.getOpenInventory().getTopInventory(); - if (inv != null && inv.getSize() == 9) { - inv.setItem(session.lastSuccessSlot, createNeutralGlass()); - inv.setItem(session.successSlot, createSuccessGlass()); - // Play tick sound for roulette feel - session.player.playSound(session.player.getLocation(), Sound.BLOCK_NOTE_BLOCK_HAT, 0.5f, 1.5f); - } - - // Calculate next delay — roulette slows down - int nextDelay; - int done = session.switchesDone; - int total = session.totalSwitches; - - if (done < total * 0.4) { - // Fast phase: 40% of switches - nextDelay = 2 + (int)(Math.random() * 2); // 2-3 ticks - } else if (done < total * 0.8) { - // Medium phase: 40% of switches - nextDelay = 4 + (int)(Math.random() * 3); // 4-6 ticks - } else { - // Slow phase: final 20% of switches - nextDelay = 8 + (int)(Math.random() * 5); // 8-12 ticks - } - - session.totalTicks += nextDelay; - - // Stop roulette after totalSwitches — green stays on final slot - if (done >= total) { - // Roulette stopped — player must click now (timeout still ticking) - return; - } - - // Continue roulette - plugin.getServer().getScheduler().runTaskLater(plugin, () -> runRoulette(playerId), nextDelay); - } - - /** - * Called when the skill check inventory is closed (e.g. player presses ESC). - * Auto-fail: treats close as timeout. - */ - public void onInventoryClose(InventoryCloseEvent event) { - if (!(event.getPlayer() instanceof Player player)) return; - UUID playerId = player.getUniqueId(); - if (activeSessions.containsKey(playerId)) { - onTimeout(playerId); - } - } - - public void handleClick(InventoryClickEvent event, UUID playerId) { - // event.setCancelled(true) is already called in SkillCheckListener before this - SkillCheckSession session = activeSessions.remove(playerId); - if (session == null) return; - - Player player = event.getWhoClicked() instanceof Player ? (Player) event.getWhoClicked() : null; - if (player == null) return; - - player.closeInventory(); - - int clickedSlot = event.getRawSlot(); - boolean success = (clickedSlot == session.successSlot); - - if (success) { - player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.5f); - } else { - player.playSound(player.getLocation(), Sound.ENTITY_BLAZE_HURT, 1.0f, 0.5f); - } - - session.generatorRepair.onSkillCheckResult(playerId, success, session.generator); - } - - private void onTimeout(UUID playerId) { - SkillCheckSession session = activeSessions.remove(playerId); - if (session == null) return; - - Player player = session.player; - if (player != null && player.isOnline()) { - player.closeInventory(); - player.playSound(player.getLocation(), Sound.ENTITY_BLAZE_HURT, 1.0f, 0.5f); - } - - // Timeout = failure - session.generatorRepair.onSkillCheckResult(playerId, false, session.generator); - } - - private void fillInventory(Inventory inv, int greenSlot) { - for (int i = 0; i < 9; i++) { - inv.setItem(i, i == greenSlot ? createSuccessGlass() : createNeutralGlass()); - } - } - - private ItemStack createSuccessGlass() { - ItemStack glass = new ItemStack(successMaterial); - ItemMeta meta = glass.getItemMeta(); - if (meta != null) { - meta.displayName(Component.text("✓ Нажми!").color(NamedTextColor.GREEN)); - glass.setItemMeta(meta); - } - return glass; - } - - private ItemStack createFailGlass() { - ItemStack glass = new ItemStack(failMaterial); - ItemMeta meta = glass.getItemMeta(); - if (meta != null) { - meta.displayName(Component.text("✗").color(NamedTextColor.RED)); - glass.setItemMeta(meta); - } - return glass; - } - - private ItemStack createNeutralGlass() { - ItemStack glass = new ItemStack(neutralMaterial); - ItemMeta meta = glass.getItemMeta(); - if (meta != null) { - meta.displayName(Component.text("-").color(NamedTextColor.GRAY)); - glass.setItemMeta(meta); - } - return glass; - } - - public void clear() { - for (UUID uuid : new HashSet<>(activeSessions.keySet())) { - SkillCheckSession session = activeSessions.remove(uuid); - if (session.player != null && session.player.isOnline()) { - session.player.closeInventory(); - } - } - activeSessions.clear(); - } - - private static class SkillCheckSession { - final Player player; - final UUID playerId; - final SkillCheck skillCheck; - final Generator generator; - final GeneratorRepair generatorRepair; - int successSlot = 0; - int lastSuccessSlot = -1; - int switchesDone = 0; - final int totalSwitches = MIN_SWITCHES + new Random().nextInt(MAX_SWITCHES - MIN_SWITCHES + 1); - int totalTicks = 0; - - SkillCheckSession(Player player, UUID playerId, SkillCheck skillCheck, Generator generator, GeneratorRepair generatorRepair) { - this.player = player; - this.playerId = playerId; - this.skillCheck = skillCheck; - this.generator = generator; - this.generatorRepair = generatorRepair; - } - } -} diff --git a/src/main/java/dev/loki/objective/SkillCheckUI.java b/src/main/java/dev/loki/objective/SkillCheckUI.java deleted file mode 100644 index 468caf1..0000000 --- a/src/main/java/dev/loki/objective/SkillCheckUI.java +++ /dev/null @@ -1,120 +0,0 @@ -package dev.loki.objective; - -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.minimessage.MiniMessage; -import net.kyori.adventure.title.Title; -import org.bukkit.entity.Player; - -import java.util.UUID; - -public class SkillCheckUI { - private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage(); - - public static void showSkillCheck(Player player, SkillCheck.CheckState state) { - String visual = buildVisualDisplay(state); - Component title = MINI_MESSAGE.deserialize(visual); - Component subtitle = MINI_MESSAGE.deserialize("Нажмите ПКМ в зеленой зоне!"); - - player.showTitle(Title.title(title, subtitle)); - } - - public static void showActionBarSkillCheck(Player player, SkillCheck.CheckState state) { - String visual = buildCompactVisual(state); - player.sendActionBar(MiniMessage.miniMessage().deserialize(visual)); - } - - public static void showSuccess(Player player) { - player.showTitle(Title.title( - MINI_MESSAGE.deserialize("✓ Отлично!"), - Component.empty(), - Title.Times.times(java.time.Duration.ZERO, java.time.Duration.ofMillis(500), java.time.Duration.ZERO) - )); - } - - public static void showFail(Player player) { - player.showTitle(Title.title( - MINI_MESSAGE.deserialize("✗ Провал!"), - MINI_MESSAGE.deserialize("Потеря прогресса..."), - Title.Times.times(java.time.Duration.ZERO, java.time.Duration.ofMillis(800), java.time.Duration.ZERO) - )); - } - - public static void clear(Player player) { - player.clearTitle(); - player.sendActionBar(Component.empty()); - } - - private static String buildVisualDisplay(SkillCheck.CheckState state) { - double zoneStart = state.zoneStart; - double zoneEnd = state.zoneEnd; - double pointer = state.getPointerPosition(); - - int displayWidth = 15; - int zoneStartPos = (int) (zoneStart * displayWidth); - int zoneEndPos = (int) (zoneEnd * displayWidth); - int pointerPos = (int) (pointer * displayWidth); - - StringBuilder sb = new StringBuilder(); - sb.append(""); - - for (int i = 0; i < displayWidth; i++) { - if (i == pointerPos) { - sb.append("◆"); - } else if (i >= zoneStartPos && i <= zoneEndPos) { - sb.append("█"); - } else { - sb.append("▒"); - } - } - - sb.append(""); - return sb.toString(); - } - - private static String buildCompactVisual(SkillCheck.CheckState state) { - double zoneStart = state.zoneStart; - double zoneEnd = state.zoneEnd; - double pointer = state.getPointerPosition(); - - int displayWidth = 10; - int zoneStartPos = (int) (zoneStart * displayWidth); - int zoneEndPos = (int) (zoneEnd * displayWidth); - int pointerPos = (int) (pointer * displayWidth); - - StringBuilder sb = new StringBuilder(); - sb.append("QTE: ["); - - for (int i = 0; i < displayWidth; i++) { - if (i == pointerPos) { - sb.append("♦"); - } else if (i >= zoneStartPos && i <= zoneEndPos) { - sb.append("█"); - } else { - sb.append("░"); - } - } - - sb.append("]"); - return sb.toString(); - } - - public static String getSkillCheckText(UUID playerId, double pointer, double zoneStart, double zoneEnd) { - int width = 12; - int pPos = (int) (pointer * width); - int zStart = (int) (zoneStart * width); - int zEnd = (int) (zoneEnd * width); - - StringBuilder sb = new StringBuilder("["); - for (int i = 0; i < width; i++) { - if (i == pPos) { - sb.append("◆"); - } else if (i >= zStart && i <= zEnd) { - sb.append("█"); - } else { - sb.append("░"); - } - } - sb.append("]"); - return sb.toString(); - } -} diff --git a/src/main/java/dev/loki/phase/EndgamePhase.java b/src/main/java/dev/loki/phase/EndgamePhase.java index 3b1d45a..220757d 100644 --- a/src/main/java/dev/loki/phase/EndgamePhase.java +++ b/src/main/java/dev/loki/phase/EndgamePhase.java @@ -2,12 +2,8 @@ package dev.loki.phase; import dev.loki.core.GameSession; import dev.loki.gate.GateManager; -import dev.loki.hatch.Hatch; -import dev.loki.hatch.HatchCondition; -import dev.loki.hatch.HatchEscape; import dev.loki.player.BasePlayer; import dev.loki.player.PlayerState; -import dev.loki.player.Survivor; import dev.loki.ui.Sidebar; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; @@ -27,22 +23,16 @@ public class EndgamePhase implements Phase { private boolean gatesActivated; private boolean killerWin; // true если маньяк убил всех — пропускаем геймплей эндгейма private GateManager gateManager; - private Hatch hatch; - private HatchCondition hatchCondition; - private HatchEscape hatchEscape; private Sidebar sidebar; + private int endgameTimer = -1; // секунд до конца эндгейма (-1 = не активно) public EndgamePhase() { this.openTicks = 20 * 20; } - public EndgamePhase(GateManager gateManager, - Hatch hatch, HatchCondition hatchCondition) { + public EndgamePhase(GateManager gateManager) { this.openTicks = 20 * 20; this.gateManager = gateManager; - this.hatch = hatch; - this.hatchCondition = hatchCondition; - this.hatchEscape = new HatchEscape(hatch); } // ------------------------------------------------------------------------- @@ -69,7 +59,7 @@ public class EndgamePhase implements Phase { this.complete = true; // Показываем результаты и телепортируем всех в лобби announceEndgameEnd(session); - teleportAllToLobby(session); + teleportAllToLobbyDelayed(session); return; } @@ -78,6 +68,15 @@ public class EndgamePhase implements Phase { gateManager = session.getGateManager(); } + // Если ворота уже открыты — игра завершилась по таймеру эндгейма, + // показываем результаты сразу + if (gateManager != null && gateManager.getOpenGateCount() > 0) { + this.complete = true; + announceEndgameEnd(session); + teleportAllToLobbyDelayed(session); + return; + } + // Set up sidebar sidebar = new Sidebar(); for (BasePlayer bp : session.getPlayers().values()) { @@ -91,18 +90,26 @@ public class EndgamePhase implements Phase { @Override public void tick(GameSession session) { + updateGates(session); + + if (endgameTimer > 0 && session.getTicksElapsed() % 20 == 0) { + endgameTimer--; + if (sidebar != null) { + sidebar.setEndgameTimer(endgameTimer); + } + } + // Update sidebar stats every 40 ticks if (session.getTicksElapsed() % 40 == 0 && sidebar != null) { for (BasePlayer bp : session.getPlayers().values()) { Player p = bp.getPlayer(); if (p != null && p.isOnline()) { - sidebar.updateGameStats(p, session, session.getGeneratorManager()); + sidebar.updateGameStats(p, session, session.getGeneratorManager(), session.getComputerManager()); } } } updateGates(session); - updateHatch(session); checkWinConditions(session); } @@ -119,8 +126,8 @@ public class EndgamePhase implements Phase { sidebar = null; } - if (hatch != null) { - hatch.reset(); + if (gateManager != null) { + gateManager.resetAll(); } // exit() НЕ вызывает session.stop() — это делает transitionToNextPhase() @@ -128,11 +135,23 @@ public class EndgamePhase implements Phase { } /** - * Телепортирует всех игроков в лобби, сбрасывает атрибуты и останавливает сессию. + * Телепортирует всех игроков в лобби и сбрасывает атрибуты (синхронно). */ private void teleportAllToLobby(GameSession session) { - List playersToTeleport = new ArrayList<>(session.getPlayers().values()); + for (BasePlayer bp : session.getPlayers().values()) { + Player p = bp.getPlayer(); + if (p != null && p.isOnline()) { + resetPlayerAttributes(p); + session.getPlugin().getLobbyManager().teleportToLobby(p); + } + } + } + /** + * Отложенный телепорт в лобби (для красивого перехода из enter). + */ + private void teleportAllToLobbyDelayed(GameSession session) { + List playersToTeleport = new ArrayList<>(session.getPlayers().values()); new BukkitRunnable() { @Override public void run() { @@ -143,7 +162,6 @@ public class EndgamePhase implements Phase { session.getPlugin().getLobbyManager().teleportToLobby(p); } } - session.stop(); } }.runTaskLater(session.getPlugin(), 60L); } @@ -186,33 +204,19 @@ public class EndgamePhase implements Phase { // ------------------------------------------------------------------------- private void activateEndgame(GameSession session) { + if (endgameTimer < 0) { + endgameTimer = session.getConfig().getPostgameSeconds(); + } + if (sidebar != null) { + sidebar.setEndgameTimer(endgameTimer); + } if (gateManager != null && !gatesActivated) { gateManager.activateEndgame(); gatesActivated = true; } - checkHatchConditions(session); session.getPlugin().getLogger().info("Endgame started!"); } - private void checkHatchConditions(GameSession session) { - if (hatch == null || hatchCondition == null) return; - int repairedGens = countRepairedGenerators(session); - hatchCondition.checkAndReveal(hatch, session.getPlayers().values(), repairedGens); - if (hatchCondition.isLastSurvivor(session.getPlayers().values())) { - hatchCondition.checkAndOpen(hatch, session.getPlayers().values(), repairedGens); - if (hatch.isOpen() && hatchEscape != null) { - hatchEscape.notifyNearbyPlayers(); - } - } - } - - private int countRepairedGenerators(GameSession session) { - if (session.getGeneratorManager() != null) { - return session.getGeneratorManager().getCompletedCount(); - } - return 0; - } - private void announceEndgameStart(GameSession session) { for (BasePlayer bp : session.getPlayers().values()) { Player p = bp.getPlayer(); @@ -232,19 +236,8 @@ public class EndgamePhase implements Phase { gateManager.updateAllGates(1.0 / openTicks); } - private void updateHatch(GameSession session) { - if (hatch == null || !hatch.isOpen() || hatchEscape == null) return; - for (BasePlayer player : session.getPlayers().values()) { - if (player.isKiller()) continue; - PlayerState st = player.getState(); - if (st != PlayerState.HEALTHY && st != PlayerState.INJURED) continue; - if (hatchEscape.canEscape((Survivor) player)) { - hatchEscape.tryEscape((Survivor) player); - } - } - } - private void checkWinConditions(GameSession session) { + if (complete) return; int survivorsAlive = 0; for (BasePlayer player : session.getPlayers().values()) { if (player.isKiller()) continue; @@ -256,26 +249,40 @@ public class EndgamePhase implements Phase { } if (survivorsAlive == 0) { complete = true; + announceEndgameEnd(session); + teleportAllToLobby(session); } } private void announceEndgameEnd(GameSession session) { List results = new ArrayList<>(); - results.add("§6§l=== РЕЗУЛЬТАТЫ ==="); - int escaped = 0, died = 0; + int escaped = 0, died = 0, sacrificed = 0, leftBehind = 0; for (BasePlayer player : session.getPlayers().values()) { if (player.isKiller()) continue; Player p = player.getPlayer(); String name = p != null ? p.getName() : "Unknown"; - if (player.getState() == PlayerState.ESCAPED) { - results.add("§a✔ " + name + " — Сбежал"); - escaped++; - } else { - results.add("§c✘ " + name + " — Погиб"); - died++; + switch (player.getState()) { + case ESCAPED: + results.add("§a✔ " + name + " — Сбежал"); + escaped++; + break; + case DEAD: + results.add("§c✘ " + name + " — Погиб"); + died++; + break; + case HOOKED: + case CARRIED: + results.add("§4✘ " + name + " — Принесён в жертву"); + sacrificed++; + break; + default: + results.add("§7✘ " + name + " — Оставлен"); + leftBehind++; + break; } } - results.add("§7Выживших: §a" + escaped + " §7/ Погибших: §c" + died); + results.add("§7Выживших: §a" + escaped + " §7/ Погибших: §c" + died + + " §7/ В жертву: §4" + sacrificed + " §7/ Оставлено: §7" + leftBehind); String msg = String.join("\n", results); for (BasePlayer player : session.getPlayers().values()) { Player p = player.getPlayer(); diff --git a/src/main/java/dev/loki/phase/GameplayDamageSystem.java b/src/main/java/dev/loki/phase/GameplayDamageSystem.java index dcc226a..9c9bbe4 100644 --- a/src/main/java/dev/loki/phase/GameplayDamageSystem.java +++ b/src/main/java/dev/loki/phase/GameplayDamageSystem.java @@ -42,6 +42,7 @@ public class GameplayDamageSystem { private Listener vanillaDamageBlocker; private ItemUseListener itemUseListener; private PlayerJumpListener jumpListener; + private org.bukkit.scheduler.BukkitTask swimmingTask; /** * Initialize damage and combat system. @@ -83,18 +84,21 @@ public class GameplayDamageSystem { p.playSound(p.getLocation(), Sound.ENTITY_PLAYER_HURT, 1.0f, 0.5f); p.setHealth(20.0); p.setSwimming(true); - Bukkit.getScheduler().runTaskTimer(plugin, task -> { - if (!p.isOnline()) { - task.cancel(); - return; + swimmingTask = new org.bukkit.scheduler.BukkitRunnable() { + @Override + public void run() { + if (!p.isOnline()) { + cancel(); + return; + } + if (survivor.getState() != PlayerState.DOWNED) { + p.setSwimming(false); + cancel(); + return; + } + if (!p.isSwimming()) p.setSwimming(true); } - if (survivor.getState() != PlayerState.DOWNED) { - p.setSwimming(false); - task.cancel(); - return; - } - if (!p.isSwimming()) p.setSwimming(true); - }, 1L, 5L); + }.runTaskTimer(plugin, 1L, 5L); }); killerController = new KillerController(plugin, playerManager, damageHandler, cfg); @@ -134,10 +138,18 @@ public class GameplayDamageSystem { return playerManager; } + public ItemUseListener getItemUseListener() { + return itemUseListener; + } + /** * Clear all damage system resources. */ public void clear() { + if (swimmingTask != null) { + swimmingTask.cancel(); + swimmingTask = null; + } if (jumpListener != null) { jumpListener.clearInGamePlayers(); jumpListener.unregister(); @@ -156,7 +168,15 @@ public class GameplayDamageSystem { */ public void clearManagers() { if (killerController != null) killerController.shutdown(); - if (playerManager != null) playerManager.clear(); + if (playerManager != null) { + for (BasePlayer bp : playerManager.getAllPlayers()) { + Player p = bp.getPlayer(); + if (p != null && p.isOnline()) { + p.setSwimming(false); + } + } + playerManager.clear(); + } } // ========================================================================= diff --git a/src/main/java/dev/loki/phase/GameplayHookHandler.java b/src/main/java/dev/loki/phase/GameplayHookHandler.java index 2c1ede6..55df515 100644 --- a/src/main/java/dev/loki/phase/GameplayHookHandler.java +++ b/src/main/java/dev/loki/phase/GameplayHookHandler.java @@ -4,8 +4,10 @@ import dev.loki.config.GameConfig; import dev.loki.core.GameSession; import dev.loki.hook.*; import dev.loki.player.BasePlayer; +import dev.loki.player.PlayerState; import dev.loki.player.Survivor; import org.bukkit.Bukkit; +import org.bukkit.Location; import org.bukkit.entity.Player; /** @@ -18,6 +20,10 @@ public class GameplayHookHandler { private final HookSacrifice hookSacrifice; private final HookStruggle hookStruggle; private final HookRecovery hookRecovery; + private GameSession session; + + private static final double CAMP_REGRESS_RANGE = 7.0; + private static final double CAMP_STALL_RANGE = 13.0; public GameplayHookHandler(HookManager hookManager, HookSacrifice hookSacrifice, HookStruggle hookStruggle, HookRecovery hookRecovery) { @@ -27,12 +33,17 @@ public class GameplayHookHandler { this.hookRecovery = hookRecovery; } + public void setSession(GameSession session) { + this.session = session; + } + private int hookTpTick; /** - * Called every tick - checks hook stage progression and teleports hooked players back. + * Called every tick - checks hook stage progression, killer proximity, and teleports hooked players back. */ public void tick() { + applyKillerProximity(); checkHookStages(); hookTpTick++; if (hookTpTick >= 20) { @@ -41,6 +52,45 @@ public class GameplayHookHandler { } } + /** + * If the killer is near a hook, slow or regress the hook progression. + * < 7 blocks → regress (goes negative) + * 7-13 blocks → stall (no progression) + * > 13 blocks → normal + */ + private void applyKillerProximity() { + if (session == null) return; + + Location killerLoc = null; + for (BasePlayer bp : session.getPlayers().values()) { + if (bp.isKiller()) { + Player p = bp.getPlayer(); + if (p != null && p.isOnline()) { + killerLoc = p.getLocation(); + } + break; + } + } + if (killerLoc == null) return; + + for (Hook hook : hookManager.getAllHooks()) { + if (!hook.isOccupied()) continue; + if (!killerLoc.getWorld().equals(hook.getLocation().getWorld())) continue; + + double dist = killerLoc.distance(hook.getLocation()); + + if (dist < CAMP_REGRESS_RANGE) { + // Regress: -2 per tick (faster than normal +1) + hook.addStageProgress(-2); + // If STAGE_1 and progress goes to 0, that's as low as it gets + } else if (dist < CAMP_STALL_RANGE) { + // Stall: cancel the normal +1 by adding -1 + hook.addStageProgress(-1); + } + // else: normal progression (Hook.tick already adds +1) + } + } + /** * Teleport hooked survivors back to their hook if they've moved too far. */ diff --git a/src/main/java/dev/loki/phase/GameplayInteraction.java b/src/main/java/dev/loki/phase/GameplayInteraction.java index f13a92a..bd2ed50 100644 --- a/src/main/java/dev/loki/phase/GameplayInteraction.java +++ b/src/main/java/dev/loki/phase/GameplayInteraction.java @@ -45,6 +45,7 @@ public class GameplayInteraction { private dev.loki.chest.ChestOpening chestOpening; private GateOpening gateOpening; private dev.loki.objective.GeneratorRepair generatorRepair; + private dev.loki.objective.ComputerRepair computerRepair; private boolean allGensDone; private final Map killerLastSneakMs = new HashMap<>(); private final Map survivorControllers = new HashMap<>(); @@ -92,7 +93,8 @@ public class GameplayInteraction { dev.loki.chest.ChestManager chestManager, dev.loki.chest.ChestOpening chestOpening, GateOpening gateOpening, - dev.loki.objective.GeneratorRepair generatorRepair) { + dev.loki.objective.GeneratorRepair generatorRepair, + dev.loki.objective.ComputerRepair computerRepair) { this.gateManager = gateManager; this.hookManager = hookManager; this.hookRescue = hookRescue; @@ -103,6 +105,7 @@ public class GameplayInteraction { this.chestOpening = chestOpening; this.gateOpening = gateOpening; this.generatorRepair = generatorRepair; + this.computerRepair = computerRepair; } public void setAllGensDone(boolean allGensDone) { @@ -110,21 +113,7 @@ public class GameplayInteraction { } private void wireMoveListener(GameSession session) { - if (moveListener == null) return; - moveListener.setOnSurvivorMove((survivor, loc) -> { - if (!allGensDone) return; - if (survivor.getState() == PlayerState.ESCAPED) return; - ExitGate gate = gateManager.getGateAt(loc, 3.0); - if (gate != null && gate.isOpen()) { - survivor.setState(PlayerState.ESCAPED); - Player p = survivor.getPlayer(); - if (p != null) { - p.playSound(loc, Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.2f); - Bukkit.broadcast(Component.text( - "[DBD] " + p.getName() + " сбежал через ворота!", NamedTextColor.GOLD)); - } - } - }); + // Escape check is handled in GameplayPhase.checkEscapes() every tick during POSTGAME } private void wireInteractListener(GameSession session) { @@ -156,10 +145,13 @@ public class GameplayInteraction { Survivor carried = (Survivor) other; if (hookManager.tryHookSurvivor(carried, player.getLocation())) { killer.setCarrying(false); - Hook nearest = hookManager.findNearestOccupiedHook(player.getLocation(), 5.0); - if (nearest != null) { - Player vp = carried.getPlayer(); - if (vp != null) vp.teleport(nearest.getLocation()); + // Only teleport if actually hooked (not dead from exceeding hook limit) + if (carried.getState() == PlayerState.HOOKED) { + Hook nearest = hookManager.findNearestOccupiedHook(player.getLocation(), 5.0); + if (nearest != null) { + Player vp = carried.getPlayer(); + if (vp != null) vp.teleport(nearest.getLocation()); + } } } break; @@ -190,10 +182,12 @@ public class GameplayInteraction { Survivor carried = (Survivor) other; if (hookManager.tryHookSurvivor(carried, player.getLocation())) { killer.setCarrying(false); - Hook nearest = hookManager.findNearestOccupiedHook(player.getLocation(), 5.0); - if (nearest != null) { - Player vp = carried.getPlayer(); - if (vp != null) vp.teleport(nearest.getLocation()); + if (carried.getState() == PlayerState.HOOKED) { + Hook nearest = hookManager.findNearestOccupiedHook(player.getLocation(), 5.0); + if (nearest != null) { + Player vp = carried.getPlayer(); + if (vp != null) vp.teleport(nearest.getLocation()); + } } return; } @@ -262,6 +256,9 @@ public class GameplayInteraction { if (generatorRepair.isMinigameActive(player.getUniqueId())) { generatorRepair.handleMinigameSneak(player); } + if (computerRepair != null && computerRepair.isMinigameActive(player.getUniqueId())) { + computerRepair.handleMinigameSneak(player); + } }); // Generator repair + right‑click for text skill check @@ -284,6 +281,26 @@ public class GameplayInteraction { } }); + // Computer repair + right‑click for text skill check + interactListener.setOnComputerInteract((player, action) -> { + BasePlayer bp = session.getPlayer(player.getUniqueId()); + if (bp == null || bp.isKiller()) return; + if (computerRepair == null) return; + + if (computerRepair.isMinigameActive(player.getUniqueId())) { + computerRepair.handleMinigameClick(player); + return; + } + + var compManager = session.getComputerManager(); + if (compManager == null) return; + double range = session.getConfig().getComputerInteractionRange(); + var computer = compManager.getNearestComputer(player.getLocation(), range); + if (computer != null) { + computerRepair.startRepair(player, computer); + } + }); + // Chest interactListener.setOnChestInteract((player, ignored) -> { BasePlayer bp = session.getPlayer(player.getUniqueId()); @@ -310,8 +327,8 @@ public class GameplayInteraction { damageSystem.getKillerController().getCarry().onSurvivorShift(survivor.getUuid()); }); - // Gate (uses hatch callback - triggered on PORTAL/HOPPER interaction) - interactListener.setOnHatchInteract((player, ignored) -> { + // Gate - triggered on HOPPER interaction + interactListener.setOnGateInteract((player, ignored) -> { BasePlayer bp = session.getPlayer(player.getUniqueId()); if (bp == null || bp.isKiller()) return; Survivor survivor = (Survivor) bp; @@ -346,7 +363,8 @@ public class GameplayInteraction { interactListener.setOnCarriedShift(null); interactListener.setOnGeneratorSkillCheckSneak(null); interactListener.setOnGeneratorInteract(null); - interactListener.setOnHatchInteract(null); + interactListener.setOnComputerInteract(null); + interactListener.setOnGateInteract(null); } } } diff --git a/src/main/java/dev/loki/phase/GameplayPhase.java b/src/main/java/dev/loki/phase/GameplayPhase.java index f46f335..a7b8e97 100644 --- a/src/main/java/dev/loki/phase/GameplayPhase.java +++ b/src/main/java/dev/loki/phase/GameplayPhase.java @@ -19,12 +19,15 @@ import dev.loki.mdbd; import dev.loki.minigames.GeneratorMinigame; import dev.loki.minigames.generator.GuiRouletteMinigame; import dev.loki.objective.GeneratorManager; +import dev.loki.objective.ComputerManager; import dev.loki.player.BasePlayer; import dev.loki.player.PlayerState; import dev.loki.player.Survivor; import dev.loki.ui.ActionBar; import dev.loki.ui.TitleUI; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.title.Title; import org.bukkit.Bukkit; import org.bukkit.GameMode; import org.bukkit.Location; @@ -33,15 +36,22 @@ import org.bukkit.Sound; import org.bukkit.block.Block; import org.bukkit.entity.Player; +import java.time.Duration; import java.util.ArrayList; import java.util.List; public class GameplayPhase implements Phase { + private enum EndgameState { NONE, PREGAME, POSTGAME } + private final GeneratorManager generatorManager; + private final ComputerManager computerManager; private int tickCounter; private boolean shouldEnd; private boolean allGensDone; + private boolean allCompsDone; + private EndgameState endgameState = EndgameState.NONE; + private int endgameTimer; // секунд до смены состояния // Sub-handlers private GameplayHookHandler hookHandler; @@ -62,12 +72,15 @@ public class GameplayPhase implements Phase { private ChestOpening chestOpening; private GeneratorKick generatorKick; private dev.loki.objective.GeneratorRepair generatorRepair; + private dev.loki.objective.ComputerRepair computerRepair; private dev.loki.objective.GeneratorHologram generatorHologram; + private dev.loki.objective.ComputerHologram computerHologram; private dev.loki.hook.HookHologram hookHologram; private dev.loki.chest.ChestHologram chestHologram; - public GameplayPhase(GeneratorManager generatorManager) { + public GameplayPhase(GeneratorManager generatorManager, ComputerManager computerManager) { this.generatorManager = generatorManager; + this.computerManager = computerManager; } // ========================================================================= @@ -81,6 +94,9 @@ public class GameplayPhase implements Phase { tickCounter = 0; shouldEnd = false; allGensDone = false; + allCompsDone = false; + endgameState = EndgameState.NONE; + endgameTimer = 0; winConditions = new GameplayWinConditions(); initManagers(session, plugin, cfg); @@ -101,37 +117,63 @@ public class GameplayPhase implements Phase { public void tick(GameSession session) { tickCounter++; generatorManager.tick(); + computerManager.tick(); if (generatorRepair != null) generatorRepair.tick(); + if (computerRepair != null) computerRepair.tick(); if (interaction != null) interaction.tickSurvivors(); hookHandler.tick(); - boolean timeExpired = ui.tickMatchTimer(session); + if (ui.tickMatchTimer(session)) { + shouldEnd = true; + } - // Particle effects around generators (every 10 ticks) - if (tickCounter % 10 == 0 && generatorManager != null) { - for (var gen : generatorManager.getAllGenerators()) { - if (gen.isCompleted()) continue; - Location gloc = gen.getLocation().clone().add(0.5, 0.5, 0.5); - gloc.getWorld().spawnParticle(org.bukkit.Particle.ELECTRIC_SPARK, - gloc, 2, 0.8, 0.5, 0.8, 0.01); - gloc.getWorld().spawnParticle(org.bukkit.Particle.END_ROD, - gloc, 1, 0.6, 0.3, 0.6, 0.01); + // Particle effects around generators and computers (every 10 ticks) + if (tickCounter % 10 == 0) { + if (generatorManager != null) { + for (var gen : generatorManager.getAllGenerators()) { + if (gen.isCompleted()) continue; + Location gloc = gen.getLocation().clone().add(0.5, 0.5, 0.5); + gloc.getWorld().spawnParticle(org.bukkit.Particle.ELECTRIC_SPARK, + gloc, 2, 0.8, 0.5, 0.8, 0.01); + gloc.getWorld().spawnParticle(org.bukkit.Particle.END_ROD, + gloc, 1, 0.6, 0.3, 0.6, 0.01); + } + } + if (computerManager != null) { + for (var comp : computerManager.getAllComputers()) { + if (comp.isCompleted()) continue; + Location cloc = comp.getLocation().clone().add(0.5, 0.5, 0.5); + cloc.getWorld().spawnParticle(org.bukkit.Particle.ELECTRIC_SPARK, + cloc, 2, 0.8, 0.5, 0.8, 0.01); + cloc.getWorld().spawnParticle(org.bukkit.Particle.END_ROD, + cloc, 1, 0.6, 0.3, 0.6, 0.01); + } } } if (tickCounter % 20 == 0) { + tickEndgame(session); checkGeneratorProgress(session); - winConditions.checkWinConditions(session); - if (winConditions.isShouldEnd() || timeExpired) { - shouldEnd = true; + if (endgameState == EndgameState.NONE) { + winConditions.checkWinConditions(session); + if (winConditions.isShouldEnd()) { + shouldEnd = true; + } + } else { + // Во время эндгейма игра заканчивается когда все выжившие мертвы/сбежали + if (allSurvivorsIncapacitated(session)) { + shouldEnd = true; + } + // Или когда истёк таймер после открытия ворот + if (endgameState == EndgameState.POSTGAME && endgameTimer <= 0) { + shouldEnd = true; + } } - ui.updateSidebar(session, generatorManager); + ui.updateSidebar(session, generatorManager, computerManager); } - if (timeExpired) { - showGameResults(session); - teleportAllToLobby(session); - session.stop(); - return; + // Escape check — каждый тик для мгновенной реакции + if (endgameState == EndgameState.POSTGAME) { + checkEscapes(session); } } @@ -143,8 +185,6 @@ public class GameplayPhase implements Phase { ui.resetPlayers(session); damageSystem.clear(); damageSystem.clearManagers(); - - Bukkit.broadcast(session.getPlugin().getConfigManager().getMessages().get("game.end")); } @Override @@ -193,6 +233,7 @@ public class GameplayPhase implements Phase { hookRescue = new HookRescue(plugin, ab, cfg); hookHandler = new GameplayHookHandler(hookManager, hookSacrifice, hookStruggle, hookRecovery); + hookHandler.setSession(session); hookRescue.setOnRescueComplete((rescuer, victim) -> hookHandler.onRescued(victim)); chestManager = new ChestManager(plugin, plugin.getConfigManager().getLocations(), cfg); @@ -205,6 +246,7 @@ public class GameplayPhase implements Phase { for (var h : hookManager.getAllHooks()) h.getLocation().setWorld(gameWorld); for (var c : chestManager.getAllChests()) c.getLocation().setWorld(gameWorld); for (var g : generatorManager.getAllGenerators()) g.getLocation().setWorld(gameWorld); + for (var c : computerManager.getAllComputers()) c.getLocation().setWorld(gameWorld); } spawnGameBlocks(plugin, cfg); @@ -212,11 +254,16 @@ public class GameplayPhase implements Phase { generatorKick = new GeneratorKick(plugin, cfg, generatorManager); generatorRepair = new dev.loki.objective.GeneratorRepair(plugin, generatorManager, genMinigame); + computerRepair = new dev.loki.objective.ComputerRepair(plugin, computerManager, genMinigame); // Голограммы ВСЕГДА включены generatorHologram = new dev.loki.objective.GeneratorHologram(plugin, generatorManager); generatorHologram.start(); plugin.getLogger().info("[GameplayPhase] Generator holograms started"); + computerHologram = new dev.loki.objective.ComputerHologram(plugin, computerManager); + computerHologram.start(); + plugin.getLogger().info("[GameplayPhase] Computer holograms started"); + hookHologram = new dev.loki.hook.HookHologram(plugin, hookManager); hookHologram.start(); plugin.getLogger().info("[GameplayPhase] Hook holograms: " + hookManager.getAllHooks().size()); @@ -233,6 +280,13 @@ public class GameplayPhase implements Phase { damageSystem = new GameplayDamageSystem(); damageSystem.init(session, plugin, cfg); + if (generatorRepair != null) { + generatorRepair.setItemUseListener(damageSystem.getItemUseListener()); + } + if (computerRepair != null) { + computerRepair.setItemUseListener(damageSystem.getItemUseListener()); + } + ui = new GameplayUI(); ui.init(session, plugin, cfg); ui.setWinConditions(winConditions); @@ -242,9 +296,10 @@ public class GameplayPhase implements Phase { interaction.setInteractListener(plugin.getInteractListener()); // ВАЖНО: переустановить generatorManager, поскольку он может быть null после exit() plugin.getInteractListener().setGeneratorManager(generatorManager); + plugin.getInteractListener().setComputerManager(computerManager); interaction.setManagers(gateManager, hookManager, hookRescue, hookSabotage, generatorKick, damageSystem, - chestManager, chestOpening, gateOpening, generatorRepair); + chestManager, chestOpening, gateOpening, generatorRepair, computerRepair); } private void initListeners(GameSession session, mdbd plugin) { @@ -267,13 +322,43 @@ public class GameplayPhase implements Phase { // Tick helpers // ========================================================================= + /** Все выжившие мертвы или сбежали? */ + private boolean allSurvivorsIncapacitated(GameSession session) { + boolean hasSurvivor = false; + for (BasePlayer bp : session.getPlayers().values()) { + if (bp.isKiller()) continue; + hasSurvivor = true; + PlayerState st = bp.getState(); + if (st != PlayerState.DEAD && st != PlayerState.ESCAPED + && st != PlayerState.DOWNED && st != PlayerState.HOOKED + && st != PlayerState.CARRIED) { + return false; + } + } + return hasSurvivor; + } + private void checkGeneratorProgress(GameSession session) { - if (allGensDone) return; - if (generatorManager.areAllRequiredCompleted()) { + if (allGensDone && allCompsDone) return; + + boolean gensDone = generatorManager.areAllRequiredCompleted(); + boolean compsDone = computerManager.areAllRequiredCompleted(); + + if (gensDone && !allGensDone) { allGensDone = true; + Bukkit.broadcast(session.getPlugin().getConfigManager().getMessages().get("generator.repair.all-done")); + } + if (compsDone && !allCompsDone) { + allCompsDone = true; + Bukkit.broadcast(session.getPlugin().getConfigManager().getMessages().get("computer.repair.all-done")); + } + + if (gensDone && compsDone) { interaction.setAllGensDone(true); gateManager.activateEndgame(); - Bukkit.broadcast(session.getPlugin().getConfigManager().getMessages().get("generator.repair.all-done")); + endgameState = EndgameState.PREGAME; + endgameTimer = session.getConfig().getPregameSeconds(); + Bukkit.broadcast(session.getPlugin().getConfigManager().getMessages().get("generator.repair.all-objectives-done")); for (BasePlayer bp : session.getPlayers().values()) { Player p = bp.getPlayer(); if (p != null) p.playSound(p.getLocation(), Sound.ENTITY_FIREWORK_ROCKET_BLAST, 1.0f, 1.0f); @@ -281,6 +366,66 @@ public class GameplayPhase implements Phase { } } + private void tickEndgame(GameSession session) { + if (endgameState == EndgameState.NONE) return; + + endgameTimer--; + + if (endgameState == EndgameState.PREGAME) { + // Каждую секунду шлём сообщение о начале эндгейма + if (endgameTimer > 0) { + for (BasePlayer bp : session.getPlayers().values()) { + Player p = bp.getPlayer(); + if (p == null) continue; + p.sendActionBar(net.kyori.adventure.text.Component.text( + "§c⚡ Эндгейм! Ворота откроются через §e" + endgameTimer + "с")); + } + } else { + // Таймер истёк — открываем ворота + gateManager.openAllGates(); + endgameState = EndgameState.POSTGAME; + endgameTimer = session.getConfig().getPostgameSeconds(); + for (BasePlayer bp : session.getPlayers().values()) { + Player p = bp.getPlayer(); + if (p == null) continue; + Title title = Title.title( + Component.text("ВОРОТА ОТКРЫТЫ!").color(NamedTextColor.GREEN), + Component.text("Бегите к выходу! (" + endgameTimer + "с)").color(NamedTextColor.YELLOW), + Title.Times.times(Duration.ofMillis(500), Duration.ofSeconds(3), Duration.ofMillis(500)) + ); + p.showTitle(title); + p.playSound(p.getLocation(), Sound.BLOCK_IRON_DOOR_OPEN, 1.0f, 1.0f); + } + } + } else if (endgameState == EndgameState.POSTGAME) { + if (endgameTimer > 0 && endgameTimer % 10 == 0 && endgameTimer <= 30) { + Bukkit.broadcast(Component.text("§c⚠ До конца игры: §e" + endgameTimer + "с")); + } + } + } + + private void checkEscapes(GameSession session) { + double escapeRange = session.getConfig().getEscapeRange(); + for (BasePlayer bp : session.getPlayers().values()) { + if (bp.isKiller()) continue; + if (bp.getState() == PlayerState.ESCAPED) continue; + if (bp.getState() != PlayerState.HEALTHY && bp.getState() != PlayerState.INJURED) continue; + Survivor survivor = (Survivor) bp; + Player p = survivor.getPlayer(); + if (p == null || !p.isOnline()) continue; + ExitGate gate = gateManager.getGateAt(p.getLocation(), escapeRange); + if (gate != null && gate.isOpen()) { + survivor.setState(PlayerState.ESCAPED); + p.playSound(p.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.2f); + Bukkit.broadcast(Component.text( + "§6[DBD] §a" + p.getName() + " §6сбежал через ворота!")); + p.setGameMode(GameMode.SPECTATOR); + p.getInventory().clear(); + session.getPlugin().getLobbyManager().teleportToLobby(p); + } + } + } + private void spawnGameBlocks(mdbd plugin, GameConfig cfg) { // Спавн блоков на местах генераторов Material genMat = Material.matchMaterial(cfg.getGeneratorBlock()); @@ -321,8 +466,20 @@ public class GameplayPhase implements Phase { } plugin.getLogger().info("[GameplayPhase] Spawned blocks for " + hookManager.getAllHooks().size() + " hooks"); - // Спавн блоков на местах ворот (Hopper — для открытия через ПКМ) + // Спавн блоков на местах компьютеров + Material compMat = Material.matchMaterial(cfg.getComputerBlock()); + if (compMat == null) compMat = Material.LECTERN; + for (var comp : computerManager.getAllComputers()) { + Location loc = comp.getLocation(); + if (loc.getWorld() == null) continue; + Block b = loc.getBlock(); + if (b.getType() != compMat) b.setType(compMat, false); + } + plugin.getLogger().info("[GameplayPhase] Spawned blocks for " + computerManager.getTotalCount() + " computers"); + + // Спавн структуры ворот (рамка + кнопка + Hopper для взаимодействия) for (ExitGate gate : gateManager.getAllGates()) { + gate.spawnGateStructure(); gate.spawnGateBlock(); } plugin.getLogger().info("[GameplayPhase] Spawned blocks for " + gateManager.getAllGates().size() + " gates"); @@ -332,35 +489,6 @@ public class GameplayPhase implements Phase { // Cleanup // ========================================================================= - private void showGameResults(GameSession session) { - List results = new ArrayList<>(); - results.add("§6§l=== РЕЗУЛЬТАТЫ ==="); - int escaped = 0, died = 0; - for (BasePlayer player : session.getPlayers().values()) { - if (player.isKiller()) { - Player p = player.getPlayer(); - String name = p != null ? p.getName() : "Unknown"; - results.add("§c☠ " + name + " — Маньяк"); - continue; - } - Player p = player.getPlayer(); - String name = p != null ? p.getName() : "Unknown"; - if (player.getState() == PlayerState.ESCAPED) { - results.add("§a✔ " + name + " — Сбежал"); - escaped++; - } else { - results.add("§c✘ " + name + " — Погиб"); - died++; - } - } - results.add("§7Выживших: §a" + escaped + " §7/ Погибших: §c" + died); - String msg = String.join("\n", results); - for (BasePlayer player : session.getPlayers().values()) { - Player p = player.getPlayer(); - if (p != null) p.sendMessage(msg); - } - } - private void teleportAllToLobby(GameSession session) { for (BasePlayer bp : session.getPlayers().values()) { Player p = bp.getPlayer(); @@ -388,19 +516,21 @@ public class GameplayPhase implements Phase { if (chestOpening != null) chestOpening.cancelAll(); if (chestManager != null) chestManager.resetAllChests(); if (generatorKick != null) generatorKick.shutdown(); + if (computerRepair != null) computerRepair.shutdown(); if (generatorHologram != null) generatorHologram.stop(); + if (computerHologram != null) computerHologram.stop(); if (hookHologram != null) hookHologram.stop(); if (titleUI != null) titleUI.clearSkillChecks(); if (hookStruggle != null) hookStruggle.clear(); if (chestManager != null) chestManager.stopLootResetTimer(); if (chestHologram != null) chestHologram.stop(); - // Clean up gate blocks + // Clean up gate blocks — don't reset gate state (EndgamePhase needs to know gates were open) if (gateManager != null) { for (ExitGate gate : gateManager.getAllGates()) { gate.removeGateBlock(); } - gateManager.resetAll(); } session.getPlugin().getInteractListener().setGeneratorManager(null); + session.getPlugin().getInteractListener().setComputerManager(null); } } \ No newline at end of file diff --git a/src/main/java/dev/loki/phase/GameplayUI.java b/src/main/java/dev/loki/phase/GameplayUI.java index ba19952..74cde24 100644 --- a/src/main/java/dev/loki/phase/GameplayUI.java +++ b/src/main/java/dev/loki/phase/GameplayUI.java @@ -4,6 +4,7 @@ import dev.loki.config.GameConfig; import dev.loki.core.GameSession; import dev.loki.mdbd; import dev.loki.objective.GeneratorManager; +import dev.loki.objective.ComputerManager; import dev.loki.player.BasePlayer; import dev.loki.player.Killer; import dev.loki.player.Survivor; @@ -114,11 +115,11 @@ public class GameplayUI { /** * Update sidebar stats. */ - public void updateSidebar(GameSession session, GeneratorManager generatorManager) { + public void updateSidebar(GameSession session, GeneratorManager generatorManager, ComputerManager computerManager) { if (sidebar == null) return; for (BasePlayer bp : session.getPlayers().values()) { Player p = bp.getPlayer(); - if (p != null && p.isOnline()) sidebar.updateGameStats(p, session, generatorManager); + if (p != null && p.isOnline()) sidebar.updateGameStats(p, session, generatorManager, computerManager); } } diff --git a/src/main/java/dev/loki/phase/GameplayWinConditions.java b/src/main/java/dev/loki/phase/GameplayWinConditions.java index 2f91504..a2d96c7 100644 --- a/src/main/java/dev/loki/phase/GameplayWinConditions.java +++ b/src/main/java/dev/loki/phase/GameplayWinConditions.java @@ -57,24 +57,24 @@ public class GameplayWinConditions { return; } - // Killer wins: all survivors are downed/hooked/carried/dead - if (allSurvivorsIncapacitated(session)) { - winReason = "killer_win"; - Bukkit.broadcast(session.getPlugin().getConfigManager().getMessages().get("game.all-incapacitated")); - announceKillerWin(session); - shouldEnd = true; - return; - } - // Survivors win: all generators done and at least one escaped if (hasEscapedSurvivor(session)) { winReason = "survivors_win"; announceSurvivorsWin(session); shouldEnd = true; + return; + } + + // Killer wins: all survivors are downed/hooked/carried/dead (ESCAPED excluded) + if (allSurvivorsIncapacitated(session)) { + winReason = "killer_win"; + Bukkit.broadcast(session.getPlugin().getConfigManager().getMessages().get("game.all-incapacitated")); + announceKillerWin(session); + shouldEnd = true; } } - /** True if NO survivor can act (all are DOWNED, HOOKED, CARRIED, DEAD, or ESCAPED). */ + /** True if NO survivor can act (all are DOWNED, HOOKED, CARRIED, DEAD). */ private boolean allSurvivorsIncapacitated(GameSession session) { boolean hasSurvivors = false; for (BasePlayer bp : session.getPlayers().values()) { @@ -82,8 +82,7 @@ public class GameplayWinConditions { hasSurvivors = true; PlayerState st = bp.getState(); if (st != PlayerState.DOWNED && st != PlayerState.HOOKED - && st != PlayerState.CARRIED && st != PlayerState.DEAD - && st != PlayerState.ESCAPED) { + && st != PlayerState.CARRIED && st != PlayerState.DEAD) { return false; // at least one survivor can still act } } @@ -121,7 +120,7 @@ public class GameplayWinConditions { for (BasePlayer bp : session.getPlayers().values()) { Player p = bp.getPlayer(); if (p == null || !p.isOnline()) continue; - if (!bp.isKiller() && bp.getState() == PlayerState.DEAD) { + if (!bp.isKiller() && (bp.getState() == PlayerState.DEAD || bp.getState() == PlayerState.ESCAPED)) { p.setGameMode(GameMode.SPECTATOR); } else { p.setGameMode(GameMode.SURVIVAL); diff --git a/src/main/java/dev/loki/survivor/SurvivorHealing.java b/src/main/java/dev/loki/survivor/SurvivorHealing.java index efaa516..b34ae8c 100644 --- a/src/main/java/dev/loki/survivor/SurvivorHealing.java +++ b/src/main/java/dev/loki/survivor/SurvivorHealing.java @@ -4,8 +4,8 @@ import dev.loki.config.GameConfig; import dev.loki.mdbd; import dev.loki.player.PlayerState; import dev.loki.player.Survivor; +import dev.loki.util.TextUtil; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.entity.Player; import java.util.UUID; @@ -98,6 +98,10 @@ public class SurvivorHealing { if (targetSurvivor == null) { cancelHealing(); return; } PlayerState targetState = targetSurvivor.getState(); + if (targetState == PlayerState.DEAD || targetState == PlayerState.ESCAPED) { + cancelHealing(); + return; + } if (targetState != PlayerState.INJURED && targetState != PlayerState.DOWNED) { completeHealing(targetSurvivor); return; @@ -166,7 +170,7 @@ public class SurvivorHealing { String bar = renderProgressBar(progress); String template = mdbd.getInstance().getConfigManager().getMessages().getString( isSelfHeal ? "survivor.heal.self-start" : "survivor.heal.other-start", bar); - Component message = MiniMessage.miniMessage().deserialize(template); + Component message = TextUtil.color(template); healingTarget.sendActionBar(message); // Also show on healer Player healer = controller.getPlayer(); diff --git a/src/main/java/dev/loki/survivor/SurvivorMovement.java b/src/main/java/dev/loki/survivor/SurvivorMovement.java index c4c4e2a..8f5830e 100644 --- a/src/main/java/dev/loki/survivor/SurvivorMovement.java +++ b/src/main/java/dev/loki/survivor/SurvivorMovement.java @@ -77,7 +77,7 @@ public class SurvivorMovement { if (player == null) return; if (isCrawling) { - player.addPotionEffect(new PotionEffect(PotionEffectType.SLOWNESS, 5, 2, false, false)); + player.addPotionEffect(new PotionEffect(PotionEffectType.SLOW_FALLING, 10, 0, false, false)); } } diff --git a/src/main/java/dev/loki/ui/ActionBar.java b/src/main/java/dev/loki/ui/ActionBar.java index daf40d6..d942064 100644 --- a/src/main/java/dev/loki/ui/ActionBar.java +++ b/src/main/java/dev/loki/ui/ActionBar.java @@ -4,6 +4,7 @@ import dev.loki.hook.Hook; import dev.loki.hook.HookStage; import dev.loki.player.PlayerState; import dev.loki.player.Survivor; +import dev.loki.util.TextUtil; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.format.NamedTextColor; @@ -12,7 +13,7 @@ import org.bukkit.entity.Player; public class ActionBar { public void sendMessage(Player player, String message) { - player.sendActionBar(Component.text(message)); + player.sendActionBar(TextUtil.color(message)); } public void sendComponent(Player player, Component component) { @@ -26,20 +27,20 @@ public class ActionBar { public void showRepairProgress(Player player, double percent, boolean skillCheckActive) { Component bar = ProgressBar.create(percent); - TextComponent message = Component.text("Repairing ") + TextComponent message = Component.text("Починка: ") .color(NamedTextColor.WHITE) .append(bar) .append(Component.text(" " + (int) percent + "%").color(NamedTextColor.GRAY)); if (skillCheckActive) { - message = message.append(Component.text(" ⚠ SKILL CHECK!").color(NamedTextColor.YELLOW)); + message = message.append(Component.text(" ⚠ СКИЛЛ-ЧЕК!").color(NamedTextColor.YELLOW)); } player.sendActionBar(message); } public void showHealProgress(Player player, double percent, boolean selfHeal) { - String label = selfHeal ? "Healing self" : "Healing ally"; + String label = selfHeal ? "Лечение себя" : "Лечение союзника"; Component bar = ProgressBar.create(percent); Component message = Component.text(label + " ") .color(NamedTextColor.WHITE) @@ -50,7 +51,7 @@ public class ActionBar { public void showRescueProgress(Player player, double percent) { Component bar = ProgressBar.create(percent); - Component message = Component.text("Rescuing ") + Component message = Component.text("Спасение: ") .color(NamedTextColor.WHITE) .append(bar) .append(Component.text(" " + (int) percent + "%").color(NamedTextColor.GRAY)); @@ -71,15 +72,15 @@ public class ActionBar { if (stage == HookStage.STAGE_1) { stageIcon = StatusIcons.HOOK; stageColor = NamedTextColor.YELLOW; - stageName = "Stage 1"; + stageName = "Стадия 1"; } else if (stage == HookStage.STAGE_2) { stageIcon = StatusIcons.HOOK; stageColor = NamedTextColor.RED; - stageName = "Stage 2 - STRUGGLE!"; + stageName = "Стадия 2 — ВЫРВИСЬ!"; } else { stageIcon = StatusIcons.DEATH_SKULL; stageColor = NamedTextColor.DARK_RED; - stageName = "SACRIFICE IMMINENT"; + stageName = "ЖЕРТВОПРИНОШЕНИЕ"; } Component bar = stage == HookStage.STAGE_2 @@ -91,7 +92,7 @@ public class ActionBar { .append(bar); if (stage == HookStage.STAGE_2) { - message = message.append(Component.text(" CROUCH to struggle!").color(NamedTextColor.YELLOW)); + message = message.append(Component.text(" ПРИСЯДЬ чтобы вырваться!").color(NamedTextColor.YELLOW)); } player.sendActionBar(message); @@ -108,22 +109,22 @@ public class ActionBar { private Component formatStatus(PlayerState state) { return switch (state) { case HEALTHY -> StatusIcons.HEALTHY_HEART - .append(Component.text(" Healthy").color(NamedTextColor.GREEN)); + .append(Component.text(" Здоров").color(NamedTextColor.GREEN)); case INJURED -> StatusIcons.INJURED_HEART - .append(Component.text(" Injured").color(NamedTextColor.YELLOW)); - case DOWNED -> Component.text("🆘 Downed - need help!").color(NamedTextColor.RED); + .append(Component.text(" Ранен").color(NamedTextColor.YELLOW)); + case DOWNED -> Component.text("🆘 В нокдауне — нужна помощь!").color(NamedTextColor.RED); case HOOKED -> StatusIcons.HOOK - .append(Component.text(" Hooked!").color(NamedTextColor.DARK_RED)); - case CARRIED -> Component.text("🎒 Being carried").color(NamedTextColor.RED); + .append(Component.text(" На крюке!").color(NamedTextColor.DARK_RED)); + case CARRIED -> Component.text("🎒 Унесен маньяком").color(NamedTextColor.RED); default -> Component.text(""); }; } public void showCarrying(Player killer, String survivorName) { - Component message = Component.text("Carrying: ") + Component message = Component.text("Несет: ") .color(NamedTextColor.WHITE) .append(Component.text(survivorName).color(NamedTextColor.YELLOW)) - .append(Component.text(" - Find a hook!").color(NamedTextColor.GRAY)); + .append(Component.text(" — Найди крюк!").color(NamedTextColor.GRAY)); killer.sendActionBar(message); } diff --git a/src/main/java/dev/loki/ui/Sidebar.java b/src/main/java/dev/loki/ui/Sidebar.java index 96f3579..506155c 100644 --- a/src/main/java/dev/loki/ui/Sidebar.java +++ b/src/main/java/dev/loki/ui/Sidebar.java @@ -3,6 +3,7 @@ package dev.loki.ui; import dev.loki.config.GameConfig; import dev.loki.core.GameSession; import dev.loki.objective.GeneratorManager; +import dev.loki.objective.ComputerManager; import dev.loki.player.BasePlayer; import dev.loki.player.PlayerState; import dev.loki.player.Survivor; @@ -94,14 +95,14 @@ public class Sidebar { // Clear ALL old entries in this objective first time only if (lastSeconds == null) { for (String entry : new ArrayList<>(board.getEntries())) { - if (timerObj.getScore(entry).getScore() == 0 || entry.contains("⏱") || entry.contains("До конца")) { + if (timerObj.getScore(entry).getScore() == 0 || entry.contains("⏱") || entry.contains("Time left")) { board.resetScores(entry); } } } // Use a FIXED entry key that never changes - only the score updates - String timerEntry = "§6⏱ До конца: §c" + seconds + " сек"; + String timerEntry = "§6⏱ Осталось: §c" + seconds + "с"; // Simply update the score - the entry persists Score score = timerObj.getScore(timerEntry); @@ -124,7 +125,7 @@ public class Sidebar { // Clear entries and hide for (String entry : board.getEntries()) { - if (entry.contains("⏱") || entry.contains("До конца")) { + if (entry.contains("⏱") || entry.contains("Осталось")) { board.resetScores(entry); } } @@ -138,6 +139,10 @@ public class Sidebar { } public void updateGameStats(Player player, GameSession session, GeneratorManager generatorManager) { + updateGameStats(player, session, generatorManager, null); + } + + public void updateGameStats(Player player, GameSession session, GeneratorManager generatorManager, ComputerManager computerManager) { Scoreboard board = scoreboards.get(player.getUniqueId()); if (board == null) return; @@ -155,19 +160,25 @@ public class Sidebar { if (endgameSecondsLeft >= 0) { String timerColor = endgameSecondsLeft <= 30 ? "§c" : "§e"; setScore(objective, "§8─────────────────", line--); - setScore(objective, timerColor + "⏱ До конца: " + endgameSecondsLeft + " сек", line--); + setScore(objective, timerColor + "⏱ Осталось: " + endgameSecondsLeft + "с", line--); setScore(objective, "§8─────────────────", line--); } // Generator progress int completedGens = generatorManager != null ? generatorManager.getCompletedCount() : 0; - int requiredGens = cfg.getGeneratorRequired(); - int totalGens = cfg.getGeneratorTotal(); + int totalGens = generatorManager != null ? generatorManager.getTotalCount() : cfg.getGeneratorRequired(); setScore(objective, "§eГенераторы:", line--); - String genBar = buildGenBar(completedGens, requiredGens); - setScore(objective, " §f" + completedGens + "/" + requiredGens + " " + genBar, line--); - setScore(objective, " §7(всего: " + totalGens + ")", line--); + String genBar = buildGenBar(completedGens, totalGens); + setScore(objective, " §f" + completedGens + "/" + totalGens + " " + genBar, line--); + + // Computer progress + int completedComps = computerManager != null ? computerManager.getCompletedCount() : 0; + int totalComps = computerManager != null ? computerManager.getTotalCount() : cfg.getComputerRequired(); + + setScore(objective, "§bКомпьютеры:", line--); + String compBar = buildGenBar(completedComps, totalComps); + setScore(objective, " §f" + completedComps + "/" + totalComps + " " + compBar, line--); setScore(objective, " ", line--); // Survivor list with hearts and colors diff --git a/src/main/java/dev/loki/ui/TitleUI.java b/src/main/java/dev/loki/ui/TitleUI.java index 22791bc..13c8c95 100644 --- a/src/main/java/dev/loki/ui/TitleUI.java +++ b/src/main/java/dev/loki/ui/TitleUI.java @@ -33,16 +33,16 @@ public class TitleUI { Component subtitle; if (isKiller) { - title = Component.text("KILLER") + title = Component.text("МАНЬЯК") .color(NamedTextColor.DARK_RED) .decoration(TextDecoration.BOLD, true); - subtitle = Component.text("Sacrifice them all") + subtitle = Component.text("Принеси их в жертву") .color(NamedTextColor.RED); } else { - title = Component.text("SURVIVOR") + title = Component.text("ВЫЖИВШИЙ") .color(NamedTextColor.DARK_GREEN) .decoration(TextDecoration.BOLD, true); - subtitle = Component.text("Repair generators and escape") + subtitle = Component.text("Чини генераторы и выбирайся") .color(NamedTextColor.GREEN); } @@ -57,17 +57,17 @@ public class TitleUI { ); Component title = escaped - ? Component.text("ESCAPED!") + ? Component.text("СБЕЖАЛ!") .color(NamedTextColor.GREEN) .decoration(TextDecoration.BOLD, true) - : Component.text("VICTORY!") + : Component.text("ПОБЕДА!") .color(NamedTextColor.DARK_RED) .decoration(TextDecoration.BOLD, true); Component subtitle = escaped - ? Component.text("You survived the night") + ? Component.text("Ты пережил ночь") .color(NamedTextColor.DARK_GREEN) - : Component.text("All survivors sacrificed") + : Component.text("Все выжившие принесены в жертву") .color(NamedTextColor.RED); player.showTitle(Title.title(title, subtitle, times)); @@ -80,11 +80,11 @@ public class TitleUI { Duration.ofSeconds(1) ); - Component title = Component.text("DEFEAT") + Component title = Component.text("ПОРАЖЕНИЕ") .color(NamedTextColor.DARK_RED) .decoration(TextDecoration.BOLD, true); - Component subtitle = Component.text("The Entity claims all") + Component subtitle = Component.text("Сущность забирает всех") .color(NamedTextColor.RED); player.showTitle(Title.title(title, subtitle, times)); diff --git a/src/main/resources/game.yml b/src/main/resources/game.yml index 05bf4f6..59f58d7 100644 --- a/src/main/resources/game.yml +++ b/src/main/resources/game.yml @@ -19,12 +19,6 @@ lobby: # Задержка перед назначением ролей (секунд) role-assign-delay: 5 - # Количество серверных лобби (0 = только пользовательские) - server-lobby-count: 3 - - # Сохранять лобби между перезапусками - persist-lobbies: true - # Размеры игроков (1.0 = стандарт) players: survivor-scale: 1.0 @@ -41,12 +35,6 @@ generators: # Время починки одного генератора (секунд) [90 сек по ТЗ] repair-seconds: 90 - # Скорость отката без чинильщика (%/сек) - regression-rate: 0.25 - - # Скорость отката после удара (%/сек) - kick-regression-rate: 2.5 - # Потеря прогресса после удара (%) kick-regression-percent: 20.0 @@ -62,6 +50,20 @@ generators: # Расстояние для взаимодействия с генератором (блоков) interaction-range: 3.0 +# Компьютеры (аналог генераторов — нужно чинить и их тоже для победы) +computers: + # Всего компьютеров на карте + total-count: 2 + + # Сколько нужно починить (должно быть = total-count для победы) + required-count: 2 + + # Время починки одного компьютера (секунд) + repair-seconds: 90 + + # Расстояние для взаимодействия с компьютером (блоков) + interaction-range: 3.0 + # Крюки hooks: # Длительность Фазы 1 (секунд) @@ -176,31 +178,26 @@ bossbar: # Расстояние полного заполнения (блоков) full-range: 8 -# Окна -window: - # Быстрый волт выжившего (секунд) - vault-survivor-fast: 1.0 - - # Медленный волт выжившего (секунд) - vault-survivor-slow: 1.5 - - # Волт киллера (секунд) - vault-killer: 1.5 - - # Блокировка после 3 волтов подряд (секунд) - vault-block-seconds: 15 - # Эндгейм endgame: # Время открытия ворот (секунд) gate-open-seconds: 20 + # Секунд до открытия ворот (после починки всех ген) + pregame-seconds: 30 + + # Секунд после открытия ворот до конца игры + postgame-seconds: 60 + # Открытие сундуков (секунд) chest-open-seconds: 3 # Расстояние для взаимодействия с воротами (блоков) gate-interaction-range: 3.0 + # Расстояние до ворот для побега (блоков) + escape-range: 2.0 + # Уведомление маньяка о генераторе killer-notification: # Время действия компаса (секунд) @@ -220,18 +217,12 @@ scoreboard: color-healing: "GREEN" color-hooked: "DARK_RED" -# Люк -hatch: - # Минимум живых для открытия люка - min-survivors-for-open: 1 - - # Условие: живых < починенных генераторов - require-more-gens-than-survivors: true - # Блоки для объектов (спавнятся на местах при старте игры) blocks: # Блок на месте генератора generator: "BLAST_FURNACE" + # Блок на месте компьютера + computer: "LECTERN" # Блок на месте сундука chest: "CHEST" # Блок на месте крюка @@ -240,13 +231,17 @@ blocks: # Лут сундуков loot: medkit: - weight: 55 + weight: 25 min-charges: 12 max-charges: 16 - flashlight: - weight: 45 - min-charges: 6 - max-charges: 8 + bandage: + weight: 25 + full-medkit: + weight: 10 + soda: + weight: 20 + wrench: + weight: 20 # Custom Model Data для ресурспака # Задайте числа соответствующие overrides в вашем ресурспаке. @@ -255,6 +250,10 @@ items: model-data: medkit: 1001 flashlight: 1002 + bandage: 1004 + fullmedkit: 1005 + soda: 1006 + wrench: 1007 # Голограммы holograms: @@ -262,6 +261,9 @@ holograms: generator: enabled: true height-offset: -1.0 + computer: + enabled: true + height-offset: -1.0 hook: enabled: true height-offset: 0.0 diff --git a/src/main/resources/locations.yml b/src/main/resources/locations.yml index 5d9f037..0e5bcbf 100644 --- a/src/main/resources/locations.yml +++ b/src/main/resources/locations.yml @@ -15,20 +15,17 @@ lobby: # Генераторы — настраиваются через /dbdadmin create generator # и сохраняются в data/generators.yml +# Компьютеры — настраиваются через /dbdsetup set computer +# и сохраняются в data/computers.yml + # Крюки (обычно 4-6) hooks: [] # Пример: # - "world,120.5,65.0,180.5,0.0,0.0" -# Окна (много) -windows: [] - # Сундуки с лутом (3-4) chests: [] -# Тотемы (5) -totems: [] - # Ворота выхода (2) exitgates: [] @@ -46,7 +43,6 @@ survivor-spawns: [] # /dbdadmin clear - Очистить все генераторы # /dbdsetup set hook - Установить крюк # /dbdsetup set chest - Установить сундук -# /dbdsetup set totem - Установить тотем # /dbdsetup set gate - Установить ворота # /dbdsetup set lobby - Установить лобби # ============================================ diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml index 2db8982..bc2f9e1 100644 --- a/src/main/resources/messages.yml +++ b/src/main/resources/messages.yml @@ -64,7 +64,7 @@ dbdadmin: hooks: usage: "%admin-prefix%&c/dbd admin hooks <игрок> <0-3>" sender: "%admin-prefix%&a%s hooks=%d" - invalid: "%admin-prefix%&cЧисло 0–3." + invalid: "%admin-prefix%&cЧисло 0-3." kill: sender: "%admin-prefix%&c%s → DEAD" resolve: @@ -110,12 +110,18 @@ setup: hook-placed: "%setup-prefix%&aКрюк установлен: %s" chest-placed: "%setup-prefix%&aСундук добавлен: %s" generator-placed: "%setup-prefix%&aГенератор добавлен: %s" + computer-placed: "%setup-prefix%&bКомпьютер установлен: %s" killer-spawn-exists: "%setup-prefix%&cСпавн киллера уже установлен! Используй /dbdsetup clear-spawns для сброса." killer-spawn-placed: "&#FF4444Спавн киллера установлен: %s" survivor-spawn-limit: "%setup-prefix%&cДостигнут лимит спавнов выживших (4)! Используй /dbdsetup clear-spawns для сброса." survivor-spawn-placed: "%setup-prefix%&aСпавн для &6%s &aвыжившего установлен! (всего: %d/4) %s" wand-point: "%setup-prefix%&a[Wand] Точка %d: %s" map-cleared: "%setup-prefix%&aКарта очищена!" + cleared: "%setup-prefix%&aВсе объекты удалены из конфига!" + cleared-type: "%setup-prefix%&aОбъекты &6%s &aудалены из конфига!" + delete-usage: "%setup-prefix%&cИспользование: /dbdsetup delete <тип> <номер>" + out-of-range: "%setup-prefix%&cИндекс &6%d &cвне диапазона!" + deleted: "%setup-prefix%&aОбъект &6%s &aудалён!" spawns-cleared: "%setup-prefix%&aВсе спавны (киллер + выжившие) очищены!" lobby-set: "%setup-prefix%&aЛобби установлено: %s" @@ -147,8 +153,23 @@ lobby: # ── Роли ─────────────────────────────────────── role: - killer: "%prefix%&4Вы — Маньяк! Поймайте всех выживших!" - survivor: "%prefix%&bВы — Выживший! Почините генераторы и сбегите!" + killer: "%prefix%&4Вы - Маньяк! Поймайте всех выживших!" + survivor: "%prefix%&bВы - Выживший! Почините генераторы и сбегите!" + +# ── Компьютеры ──────────────────────────────── + +computer: + repair: + start: "&bВы начали чинить компьютер..." + stop: "&eВы остановились." + complete: "&a✔ Компьютер активирован!" + broadcast: "&bКомпьютер починен! &7(%d/%d)" + cannot: "&cКомпьютер не может быть починен!" + busy: "&cКто-то уже чинит этот компьютер!" + actionbar: "&7Починка компьютера: %s" + skillcheck-success: "&a✓ Успех! +%d%%" + skillcheck-fail: "&c✗ Промах! -%d%%. Чини дальше." + all-done: "%prefix%&bВсе компьютеры починены!" # ── Генераторы ───────────────────────────────── @@ -161,9 +182,11 @@ generator: cannot: "&cГенератор не может быть починен!" busy: "&cКто-то уже чинит этот генератор!" actionbar: "&7Починка: %s" - skillcheck-title: "&eSkill Check! %s" - skillcheck-success: "&a✓ Отлично! +%d%%" - skillcheck-fail: "&c✗ Ошибка! -%d%%. Продолжай чинить." + skillcheck-title: "&eПроверка реакции! %s" + skillcheck-success: "&a✓ Успех! +%d%%" + skillcheck-fail: "&c✗ Промах! -%d%%. Чини дальше." + all-done: "%prefix%&aВсе генераторы починены!" + all-objectives-done: "%prefix%&6Все генераторы и компьютеры починены! Ворота открываются!" kicked: begin: "%prefix%&cДержите Shift для разрушения генератора..." cooldown: "%prefix%&cКулдаун удара: %d сек" @@ -171,8 +194,6 @@ generator: actionbar: "&c⚡ Разрушение: %d%%" success: "%prefix%&cГенератор поврежден! -%d%% прогресса" broadcast: "%prefix%&4⚡ Маньяк разрушил генератор! -%d%%" - complete: "&aГенератор починен!\n%e" - all-done: "%prefix%&aВсе генераторы починены! Открывайте ворота!" notification: "&4[DBD] ⚡ Выживший допустил ошибку! Компас укажет на генератор!" # ── Маньяк ───────────────────────────────────── @@ -200,7 +221,12 @@ survivor: actionbar: "&a⚕ %s %d%%" done: "%prefix%&a✔ Вы вылечились!" cancelled: "%prefix%&eЛечение прервано." - depleted: "&cYour medkit has been depleted!" + depleted: "&cМедкит закончился!" + item: + bandage: "%prefix%&aВы наложили бинт. Здоровье регенерирует!" + fullmedkit: "%prefix%&aАптечка использована. Здоровье полностью восстановлено!" + soda: "%prefix%&eСодовая выпита. Скорость увеличена!" + wrench: "%prefix%&aГаечный ключ использован. Починка ускорена!" # ── Крюки ────────────────────────────────────── @@ -228,18 +254,22 @@ hook: sacrifice: message: "%prefix%&4Вы были принесены в жертву. Наблюдайте за матчем." title: "☠" - subtitle: "&cYou have been sacrificed" - broadcast: "%s has been sacrificed to the Entity" + subtitle: "&cВы были принесены в жертву" + broadcast: "%s принесен в жертву Сущности" # ── Сундуки ──────────────────────────────────── chest: - empty: "&cThis chest is empty!" - opening: "&eOpening chest..." - cancelled: "&cOpening cancelled!" + empty: "&cСундук пуст!" + opening: "&eОткрытие сундука..." + cancelled: "&cОткрытие прервано!" too-far: "%prefix%&cСлишком далеко!" actionbar: "&6Открытие: %s %d%%" found: "%prefix%&aВы нашли: %s" + +# ── Ворота ───────────────────────────────────── + +gate: door-open: "&aОткрываем ворота... (зажмите ПКМ)" door-opened: "&aВорота открыты!" door-nearby: "&cНет ворот поблизости!" @@ -248,17 +278,6 @@ chest: door-interrupted: "&7Открытие прервано!" door-actionbar: "&aОткрываем ворота... (зажмите ПКМ)" -# ── Люк ──────────────────────────────────────── - -hatch: - too-far: "&cПодойдите ближе к люку!" - hidden: "&7Люк скрыт..." - closed: "&cЛюк закрыт!" - open-notify: "&e&lЛюк открыт!" - escape-title: "&a&lПОБЕГ!" - escape-subtitle: "&7Вы сбежали через люк!" - reward: "&a+5000 Очков — Побег через люк!" - # ── Игрок вышел ──────────────────────────────── quit: @@ -269,7 +288,7 @@ quit: minigames: generator-roulette: - title: "⚡ Skill Check!" + title: "⚡ Проверка реакции!" success-label: "✓ Нажми!" text-skillcheck: subtitle: "&eНажми ПКМ или отпусти шифт в зоне!" @@ -278,19 +297,19 @@ minigames: usage: dbd-header: "&6=== DBD Команды ===" - dbd-join: "&7/dbd join [name] — присоединиться к игре/лобби" - dbd-leave: "&7/dbd leave — покинуть игру" - dbd-lobby: "&7/dbd lobby — телепорт в лобби" - dbd-create: "&7/dbd create — создать лобби" - dbd-list: "&7/dbd list — список лобби" - dbd-delete: "&7/dbd delete — удалить лобби" - dbd-admin: "&e/dbd admin — управление игроками в матче" - dbd-forcestart: "&e/dbd forcestart — форс старт" - dbd-stop: "&e/dbd stop — остановить игру" - dbd-reload: "&e/dbd reload — перезагрузить конфиги" - dbd-setlobby: "&e/dbd setlobby — установить точку лобби" - dbd-save: "&e/dbd save — сохранить карту" - dbd-load: "&e/dbd load — загрузить карту" + dbd-join: "&7/dbd join [name] - присоединиться к игре/лобби" + dbd-leave: "&7/dbd leave - покинуть игру" + dbd-lobby: "&7/dbd lobby - телепорт в лобби" + dbd-create: "&7/dbd create - создать лобби" + dbd-list: "&7/dbd list - список лобби" + dbd-delete: "&7/dbd delete - удалить лобби" + dbd-admin: "&e/dbd admin - управление игроками в матче" + dbd-forcestart: "&e/dbd forcestart - форс старт" + dbd-stop: "&e/dbd stop - остановить игру" + dbd-reload: "&e/dbd reload - перезагрузить конфиги" + dbd-setlobby: "&e/dbd setlobby - установить точку лобби" + dbd-save: "&e/dbd save - сохранить карту" + dbd-load: "&e/dbd load - загрузить карту" admin-header: "&6=== /dbd admin ===" admin-heal: "&6/dbd admin heal [игрок]" admin-down: "&6/dbd admin down [игрок]" @@ -298,18 +317,21 @@ usage: admin-state: "&6/dbd admin state <игрок> <статус>" admin-hooks: "&6/dbd admin hooks <игрок> <0-3>" admin-kill: "&6/dbd admin kill [игрок]" - admin-jump: "&6/dbd admin jump [on/off] — вкл/выкл прыжки" + admin-jump: "&6/dbd admin jump [on/off] - вкл/выкл прыжки" setup-header: "&6=== DBD Setup ===" - setup-wand: "&6/dbdsetup wand — Получить ванд (ЛКМ/ПКМ для ворот)" - setup-hook: "&6/dbdsetup set hook — Установить крюк (ломай блок)" - setup-chest: "&6/dbdsetup set chest — Установить сундук" - setup-generator: "&6/dbdsetup set generator — Установить генератор" - setup-killer-spawn: "&6/dbdsetup set killer-spawn — Спавн киллера (только 1)" - setup-survivor-spawn: "&6/dbdsetup set survivor-spawn — Спавн выжившего (до 4)" - setup-gate: "&6/dbdsetup set gate A|B — Установить ворота (из точек ванда)" - setup-clear-spawns: "&6/dbdsetup clear-spawns — Очистить все спавны" - setup-lobby: "&6/dbdsetup lobby — Установить точку лобби" + setup-wand: "&6/dbdsetup wand - Получить ванд (ЛКМ/ПКМ для ворот)" + setup-hook: "&6/dbdsetup set hook - Установить крюк (ломай блок)" + setup-chest: "&6/dbdsetup set chest - Установить сундук" + setup-generator: "&6/dbdsetup set generator - Установить генератор" + setup-computer: "&6/dbdsetup set computer - Установить компьютер" + setup-killer-spawn: "&6/dbdsetup set killer-spawn - Спавн киллера (только 1)" + setup-survivor-spawn: "&6/dbdsetup set survivor-spawn - Спавн выжившего (до 4)" + setup-gate: "&6/dbdsetup set gate A|B - Установить ворота (из точек ванда)" + setup-clear: "&6/dbdsetup clear [тип] - Очистить объекты (без разрушения блоков)" + setup-delete: "&6/dbdsetup delete <тип> <#> - Удалить конкретный объект" + setup-clear-spawns: "&6/dbdsetup clear-spawns - Очистить все спавны" + setup-lobby: "&6/dbdsetup lobby - Установить точку лобби" admin-list-header: "&6=== DBD Admin ===" - admin-list-list: "&6/dbdadmin list — Список всех объектов" - admin-list-type: "&6/dbdadmin list <тип> — Список по типу" - admin-world: "&6/dbdadmin world — Телепорт в мир" + admin-list-list: "&6/dbdadmin list - Список всех объектов" + admin-list-type: "&6/dbdadmin list <тип> - Список по типу" + admin-world: "&6/dbdadmin world - Телепорт в мир" diff --git a/src/test/java/dev/loki/gate/ExitGateTest.java b/src/test/java/dev/loki/gate/ExitGateTest.java index 55e4242..81c288e 100644 --- a/src/test/java/dev/loki/gate/ExitGateTest.java +++ b/src/test/java/dev/loki/gate/ExitGateTest.java @@ -26,18 +26,17 @@ class ExitGateTest { } @Test - void setPowered_shouldTransitionToOpening() { + void setPowered_shouldStayClosed() { gate.setPowered(true); - assertEquals(ExitGate.GateState.OPENING, gate.getState()); + assertEquals(ExitGate.GateState.CLOSED, gate.getState()); assertTrue(gate.isPowered()); - assertTrue(gate.isOpening()); - assertFalse(gate.isClosed()); - assertFalse(gate.isOpen()); + assertTrue(gate.isClosed()); } @Test void setProgressDirect_shouldIncreaseProgress() { gate.setPowered(true); + gate.startOpening(); gate.setProgressDirect(0.5); assertEquals(0.5, gate.getProgress()); assertEquals(50, gate.getProgressPercent()); @@ -47,6 +46,7 @@ class ExitGateTest { @Test void setProgressDirect_shouldCompleteWhenReachingOne() { gate.setPowered(true); + gate.startOpening(); gate.setProgressDirect(1.0); assertEquals(1.0, gate.getProgress()); assertEquals(ExitGate.GateState.OPEN, gate.getState()); @@ -56,6 +56,7 @@ class ExitGateTest { @Test void setProgressDirect_shouldClampToMax() { gate.setPowered(true); + gate.startOpening(); gate.setProgressDirect(2.0); assertEquals(1.0, gate.getProgress()); assertTrue(gate.isOpen()); @@ -64,6 +65,7 @@ class ExitGateTest { @Test void setProgressDirect_shouldNotExceedMin() { gate.setPowered(true); + gate.startOpening(); gate.setProgressDirect(-0.5); assertEquals(0.0, gate.getProgress()); } @@ -79,6 +81,7 @@ class ExitGateTest { @Test void updateProgress_shouldWorkIncrementally() { gate.setPowered(true); + gate.startOpening(); gate.updateProgress(0.3); assertEquals(0.3, gate.getProgress()); gate.updateProgress(0.3); @@ -101,6 +104,7 @@ class ExitGateTest { @Test void saveAndRestoreProgress_shouldPersist() { gate.setPowered(true); + gate.startOpening(); gate.setProgressDirect(0.4); assertEquals(0.4, gate.getProgress()); diff --git a/src/test/java/dev/loki/phase/GameplayWinConditionsTest.java b/src/test/java/dev/loki/phase/GameplayWinConditionsTest.java index 2b0cbac..bf89cf8 100644 --- a/src/test/java/dev/loki/phase/GameplayWinConditionsTest.java +++ b/src/test/java/dev/loki/phase/GameplayWinConditionsTest.java @@ -98,14 +98,14 @@ class GameplayWinConditionsTest { // ========================================================================= @Test - void incapacitated_withEscapedAndRestDisabled_shouldReturnTrue() { + void incapacitated_withEscapedAndRestDisabled_shouldReturnFalse() { Map 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)"); + assertFalse(allIncapacitated(sessionWith(players)), + "ESCAPED survivors should NOT count as incapacitated for killer win"); } @Test @@ -120,13 +120,14 @@ class GameplayWinConditionsTest { } @Test - void incapacitated_withAllTerminalStates_shouldReturnTrue() { + void incapacitated_withEscapedAndDead_shouldReturnFalse() { Map 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))); + assertFalse(allIncapacitated(sessionWith(players)), + "ESCAPED should not count as incapacitated"); } // =========================================================================