refactor: reorganize packages and replace LPSchematic with WorldEdit
Restructure core modules into subpackages (generator, schematic, config, session, etc.); add .schem/.nbt schematic pipeline, commands, adaptive stats skeleton, and bootstrap wiring.
This commit is contained in:
parent
6ba6e5137d
commit
fd0ba2cc0e
203 changed files with 5652 additions and 2367 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -31,7 +31,7 @@ hs_err_pid*
|
||||||
.settings/
|
.settings/
|
||||||
.classpath
|
.classpath
|
||||||
.project
|
.project
|
||||||
|
.cursor/
|
||||||
# Maven
|
# Maven
|
||||||
target/
|
target/
|
||||||
pom.xml.tag
|
pom.xml.tag
|
||||||
|
|
|
||||||
468
TODO.md
468
TODO.md
|
|
@ -1,254 +1,283 @@
|
||||||
# LoParkour — TODO (обновлено 2026-04-27)
|
# LoParkour — TODO (обновлено 2026-06-01)
|
||||||
|
|
||||||
> Roadmap для превращения плагина в платформу с адаптивной генерацией.
|
> Roadmap + **честный аудит** репозитория. Старые галочки «✅» в фазах 1–4 не соответствуют коду — см. раздел **Аудит**.
|
||||||
|
|
||||||
## Легенда
|
## Легенда
|
||||||
|
|
||||||
- 🔴 Критично — блокирует работу
|
- 🔴 Критично — блокирует сборку / релиз / основной UX
|
||||||
- 🟡 Важно — заявленная функциональность
|
- 🟡 Важно — заявленная функциональность или i18n
|
||||||
- 🟢 Планово — улучшение
|
- 🟢 Планово — улучшение, рефакторинг
|
||||||
- ✅ Готово
|
- ✅ Готово (проверено в коде)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Фаза 1: Рефакторинг (2-3 дня)
|
## Аудит 2026-06-01 (снимок)
|
||||||
|
|
||||||
### 🟢 Архитектура
|
| Область | Факт |
|
||||||
|
|--------|------|
|
||||||
|
| **Сборка** | ✅ `./gradlew compileJava` проходит |
|
||||||
|
| **Адаптивность** | 🟡 Подключено: `AdaptiveServices`, `MetricsCollector` на генераторе, профиль после fall/reset |
|
||||||
|
| **Elytra** | 🟡 `ElytraMode` + `ElytraGenerator` есть; отдельного `mode/elytra/*` из старого TODO **нет** |
|
||||||
|
| **Размер файлов** | 🔴 Цель «все < 200 строк» **не выполнена**: 18 файлов > 150, 2 > 200 |
|
||||||
|
| **i18n** | 🟡 Основные потоки через `Locales`; остаётся Phase 0 split + `MessageKeys` |
|
||||||
|
| **GUI** | 🟡 Основные меню на LoLib OK; **заглушки** `Menu`/`PagedMenu`, `DynamicMenu` вырезан |
|
||||||
|
| **TODO.md** | Ранее помечено ✅ то, что не сделано — статусы ниже исправлены |
|
||||||
|
|
||||||
| # | Задача | Файлы | Статус |
|
**Статистика:** ~160 `.java` файлов, средний размер ~85 строк, максимум **252** (`ElytraGenerator.java`).
|
||||||
|---|--------|-------|--------|
|
|
||||||
| 1 | Разбить ParkourGenerator (335 строк) | Generator.java, GeneratorConfig.java | 🟢 |
|
|
||||||
| 2 | Разбить GeneratorProfileManager | ProfileManager.java, ProfileCalculator.java | 🟢 |
|
|
||||||
| 3 | Вынести statistics из generator | statistics/TimeTracker.java, ScoreTracker.java, LeaderboardSync.java | 🟢 |
|
|
||||||
| 4 | Создать структуру пакетов | generator/core/, generator/profile/, mode/base/, mode/impl/ | 🟢 |
|
|
||||||
| 5 | Рефакторинг интерфейсов | GameMode, DifficultyProfile, GeneratorEventListener | 🟢 |
|
|
||||||
|
|
||||||
### 🟢 Большие файлы (>200 строк)
|
|
||||||
|
|
||||||
| # | Файл | Строк | Действие | Статус |
|
|
||||||
|---|------|-------|----------|--------|
|
|
||||||
| 1 | Option.java | 335 | Разбить по типам опций | 🟢 |
|
|
||||||
| 2 | ElytraGenerator.java | 261 | Вынести в mode/elytra/ | 🟢 |
|
|
||||||
| 3 | Locales.java | 260 | Разбить на LocaleLoader + LocaleCache | 🟢 |
|
|
||||||
| 4 | ParkourPlayer.java | 236 | Вынести inventory/settings в отдельные классы | 🟢 |
|
|
||||||
| 5 | Leaderboard.java | 234 | Разбить на Leaderboard + LeaderboardStorage | 🟢 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Фаза 2: Адаптивная система (3-4 дня)
|
## Фаза 0: Полный рефакторинг (KISS · DRY · SOLID) — приоритет
|
||||||
|
|
||||||
### 🟡 Инфраструктура
|
> Цель: предсказуемая структура, тонкие классы, без «божественных» файлов.
|
||||||
|
|
||||||
| # | Задача | Пакет | Статус |
|
### Правила (жёсткие)
|
||||||
|---|--------|-------|--------|
|
|
||||||
| 1 | Создать модели данных | adaptive/model/ | 🟡 |
|
|
||||||
| 2 | Реализовать хранилища | adaptive/storage/ | 🟡 |
|
|
||||||
| 3 | SQL таблицы для метрик | loparkour_player_stats | 🟡 |
|
|
||||||
| 4 | Файловое хранилище | playerdata/<uuid>.json | 🟡 |
|
|
||||||
|
|
||||||
### 🟡 Сбор метрик
|
| Правило | Значение |
|
||||||
|
|--------|----------|
|
||||||
|
| **Целевой размер** | ~**150** строк на файл |
|
||||||
|
| **Жёсткий потолок** | **190–200** строк (исключения только с комментом в PR) |
|
||||||
|
| **Файлов в папке** | ≤ **6** (при росте — новая подпапка) |
|
||||||
|
| **KISS** | Один публичный сценарий = один класс / use-case |
|
||||||
|
| **DRY** | Сообщения только через `Locales`; команды — `Messages` / `admin.*` |
|
||||||
|
| **SOLID** | `generator` не импортирует `adaptive`; связь только через `GeneratorEventListener` + DI в bootstrap |
|
||||||
|
|
||||||
| # | Задача | Класс | Статус |
|
### Целевая структура пакетов (✅ 2026-06-01)
|
||||||
|---|--------|-------|--------|
|
|
||||||
| 1 | MetricsCollector | adaptive/core/MetricsCollector.java | 🟡 |
|
|
||||||
| 2 | GeneratorEventListener | generator/core/GeneratorEventListener.java | 🟡 |
|
|
||||||
| 3 | Интеграция с Generator | Generator.score(), Generator.fall() | 🟡 |
|
|
||||||
| 4 | Детекция nearMiss | MetricsCollector.onBlockLand() | 🟡 |
|
|
||||||
|
|
||||||
### 🟡 Анализ и корректировка
|
|
||||||
|
|
||||||
| # | Задача | Класс | Статус |
|
|
||||||
|---|--------|-------|--------|
|
|
||||||
| 1 | SkillAnalyzer | adaptive/core/SkillAnalyzer.java | 🟡 |
|
|
||||||
| 2 | DifficultyCalculator | adaptive/core/DifficultyCalculator.java | 🟡 |
|
|
||||||
| 3 | DifficultyAdjuster | adaptive/core/DifficultyAdjuster.java | 🟡 |
|
|
||||||
| 4 | Формулы рейтинга | SkillRating calculation | 🟡 |
|
|
||||||
|
|
||||||
### 🟡 Интеграция с режимами
|
|
||||||
|
|
||||||
| # | Задача | Класс | Статус |
|
|
||||||
|---|--------|-------|--------|
|
|
||||||
| 1 | AdaptiveMode базовый класс | mode/base/AdaptiveMode.java | 🟡 |
|
|
||||||
| 2 | AdaptiveSession | adaptive/core/AdaptiveSession.java | 🟡 |
|
|
||||||
| 3 | StatsService | adaptive/core/StatsService.java | 🟡 |
|
|
||||||
| 4 | Миграция DefaultMode | mode/impl/DefaultMode.java | 🟡 |
|
|
||||||
| 5 | Миграция SpeedrunMode | mode/impl/SpeedrunMode.java, SpeedrunGenerator.java | 🟡 |
|
|
||||||
| 6 | Миграция HardcoreMode | mode/impl/HardcoreMode.java | 🟡 |
|
|
||||||
| 7 | Миграция GravityShiftMode | mode/impl/GravityShiftMode.java | 🟡 |
|
|
||||||
|
|
||||||
### 🟡 Конфигурация
|
|
||||||
|
|
||||||
| # | Задача | Файл | Статус |
|
|
||||||
|---|--------|------|--------|
|
|
||||||
| 1 | Секция adaptive в config.yml | config.yml | 🟡 |
|
|
||||||
| 2 | Конфиги режимов | modes/*.yml | 🟡 |
|
|
||||||
| 3 | AdaptiveConfig с кэшированием | adaptive/model/AdaptiveConfig.java | 🟡 |
|
|
||||||
| 4 | Предзагрузка при старте | LoParkour.onEnable() | 🟡 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Фаза 3: ElytraMode (1-2 дня)
|
|
||||||
|
|
||||||
### 🔴 Критичные классы (отсутствуют)
|
|
||||||
|
|
||||||
| # | Класс | Ответственность | Статус |
|
|
||||||
|---|-------|-----------------|--------|
|
|
||||||
| 1 | ElytraConfig | Загрузка настроек из modes/elytra.yml | 🔴 |
|
|
||||||
| 2 | ElytraRing | Модель кольца (центр, радиус, направление) | 🔴 |
|
|
||||||
| 3 | RingGenerator | Генерация колец по траектории | 🔴 |
|
|
||||||
| 4 | RingRenderer | Отрисовка частицами | 🔴 |
|
|
||||||
| 5 | ElytraPhysics | Проверка пролёта, буст от фейерверков | 🔴 |
|
|
||||||
|
|
||||||
### 🔴 Интеграция
|
|
||||||
|
|
||||||
| # | Задача | Статус |
|
|
||||||
|---|--------|--------|
|
|
||||||
| 1 | Создать ElytraMode | 🔴 |
|
|
||||||
| 2 | Выдача элитры и фейерверков | 🔴 |
|
|
||||||
| 3 | Тесты полёта | 🔴 |
|
|
||||||
| 4 | Конфиг modes/elytra.yml | 🔴 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Фаза 4: Новые режимы (2-3 дня)
|
|
||||||
|
|
||||||
### 🟢 RoguelikeMode
|
|
||||||
|
|
||||||
| # | Задача | Статус |
|
|
||||||
|---|--------|--------|
|
|
||||||
| 1 | Случайные модификаторы каждые N блоков | 🟢 |
|
|
||||||
| 2 | Выбор перков перед стартом | 🟢 |
|
|
||||||
| 3 | Риск-награда развилки | 🟢 |
|
|
||||||
| 4 | Балансировка | 🟢 |
|
|
||||||
|
|
||||||
### 🟢 Проверка существующих режимов
|
|
||||||
|
|
||||||
| # | Режим | Задача | Статус |
|
|
||||||
|---|-------|--------|--------|
|
|
||||||
| 1 | RaceMode | Проверить прогресс-бар и финиш | 🟢 |
|
|
||||||
| 2 | CoopMode | Проверить мультиплеер | 🟢 |
|
|
||||||
| 3 | DefaultMode | Тесты после рефакторинга | 🟢 |
|
|
||||||
| 4 | SpeedrunMode | Тесты таймеров блоков | 🟢 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Архитектурные решения
|
|
||||||
|
|
||||||
### Структура пакетов
|
|
||||||
|
|
||||||
```
|
```
|
||||||
src/main/java/dev/loki/loparkour/
|
dev.loki.loparkour/
|
||||||
├── adaptive/ # Адаптивная система
|
├── LoParkour.java # main plugin class
|
||||||
│ ├── core/ # MetricsCollector, SkillAnalyzer, DifficultyCalculator, DifficultyAdjuster
|
├── command/
|
||||||
│ ├── model/ # PlayerMetrics, SkillRating, AdaptiveConfig
|
│ ├── LoParkourCommand.java
|
||||||
│ └── storage/ # StatsRepository, SQLStatsStorage, FileStatsStorage
|
│ ├── player/ # PlayerCommandHandler
|
||||||
├── statistics/ # Вынесено из generator
|
│ ├── admin/ # AdminCommandHandler
|
||||||
│ ├── TimeTracker.java
|
│ └── schematic/ # SchematicCommandHandler
|
||||||
│ ├── ScoreTracker.java
|
├── config/
|
||||||
│ └── LeaderboardSync.java
|
│ ├── core/ # Config, ConfigLoader, ConfigAccessor, ConfigUpdater
|
||||||
|
│ ├── locale/ # Locales, LocaleLoader, LocaleCache
|
||||||
|
│ └── options/ # Option + OptionSQL, OptionGeneral, …
|
||||||
├── generator/
|
├── generator/
|
||||||
│ ├── core/ # Generator, GeneratorState, GeneratorConfig, Island
|
│ ├── core/ # ParkourGenerator, Profile, GeneratorState, …
|
||||||
│ ├── profile/ # DifficultyProfile, ProfileManager, ProfileCalculator
|
│ ├── jump/, lifecycle/, profile/, effect/
|
||||||
│ ├── jump/ # JumpCalculator, JumpValidator, JumpDirector, BlockPlacer
|
│ └── core/GeneratorEventListener
|
||||||
│ └── lifecycle/ # GeneratorLifecycle, GeneratorCleanup
|
├── mode/
|
||||||
└── mode/
|
│ ├── base/ # Mode, MultiMode, Modes, ModeMessages
|
||||||
├── base/ # GameMode, AdaptiveMode, ModeRegistry
|
│ ├── impl/ # DefaultMode, SpeedrunMode, …
|
||||||
├── impl/ # DefaultMode, SpeedrunMode, HardcoreMode, GravityShiftMode, RoguelikeMode
|
│ └── elytra/ # ElytraGenerator + ring/physics/hud
|
||||||
└── elytra/ # ElytraMode, ElytraConfig, ElytraRing, RingGenerator, RingRenderer, ElytraPhysics
|
├── menu/
|
||||||
|
│ ├── core/ # LPMenu, MainMenu, Menus, ParkourOption
|
||||||
|
│ └── play/, settings/, community/, lobby/
|
||||||
|
├── player/
|
||||||
|
│ ├── core/ # ParkourUser, ParkourPlayer
|
||||||
|
│ ├── service/ # UserRegistry, hotbar, scoreboard, …
|
||||||
|
│ ├── spectator/
|
||||||
|
│ └── data/
|
||||||
|
├── util/
|
||||||
|
│ ├── text/, item/, gui/, world/, particle/, misc/
|
||||||
|
├── storage/
|
||||||
|
│ ├── Storage.java # фасад
|
||||||
|
│ ├── sql/ # StorageSQL, SQL*, миграции
|
||||||
|
│ └── disk/ # StorageDisk (JSON)
|
||||||
|
├── session/
|
||||||
|
│ ├── core/Session.java
|
||||||
|
│ └── manager/ # State, Player, User managers
|
||||||
|
├── listener/
|
||||||
|
│ ├── player/, gameplay/, schematic/
|
||||||
|
├── leaderboard/
|
||||||
|
│ ├── model/Score, core/, persistence/
|
||||||
|
├── ghost/
|
||||||
|
│ ├── model/, core/
|
||||||
|
├── hook/
|
||||||
|
│ ├── papi/, holo/, floodgate/, vault/
|
||||||
|
├── schematic/
|
||||||
|
│ ├── registry/Schematics.java
|
||||||
|
│ └── lpschem/
|
||||||
|
├── api/core/ + api/event/
|
||||||
|
├── reward/core/, style/core/, world/core/
|
||||||
|
├── adaptive/bootstrap/AdaptiveServices
|
||||||
|
├── bootstrap/PluginBootstrap
|
||||||
|
└── …
|
||||||
```
|
```
|
||||||
|
|
||||||
### Зависимости
|
Скрипт переноса: `scripts/refactor-packages.ps1` (+ `fix-imports-after-refactor.ps1`).
|
||||||
|
|
||||||
```
|
### Очередь разбиения (файлы > 150 строк)
|
||||||
adaptive → generator/profile (только через DifficultyProfile интерфейс)
|
|
||||||
generator → НЕ знает про adaptive (через GeneratorEventListener)
|
|
||||||
statistics → используется и generator, и adaptive
|
|
||||||
mode → использует generator + опционально adaptive
|
|
||||||
```
|
|
||||||
|
|
||||||
### Хранение данных
|
| Приоритет | Файл | Строк | Действие |
|
||||||
|
|-----------|------|-------|----------|
|
||||||
|
| ✅ 1 | `ElytraGenerator.java` | 101 | → `elytra/ElytraRingLoop`, `ElytraScoring`, `ElytraHud`, `ElytraPlayerKit`, `ElytraFireworkListener` |
|
||||||
|
| ✅ 2 | `ParkourPlayer.java` | 166 | → `ParkourHotbar` |
|
||||||
|
| 🟡 3 | `PAPIHook.java` | 194 | → placeholders по группам |
|
||||||
|
| 🟡 4 | `LoParkour.java` | 192 | → `bootstrap/PluginBootstrap.java` |
|
||||||
|
| 🟡 5 | `MetricsCollector.java` | 187 | → session hook + jump hook + flush |
|
||||||
|
| 🟡 6 | `StorageSQL.java` | 179 | → queries / mapper / connection (уже частично) |
|
||||||
|
| 🟡 7 | `BlockPlacer.java` | 176 | | |
|
||||||
|
| 🟡 8 | `JumpValidator.java` | 172 | | |
|
||||||
|
| 🟡 9 | `LoParkourCommand.java` | 170 | → делегаты только маршрутизация |
|
||||||
|
| 🟡 10 | `CoopMode.java` | 165 | → `CoopGenerator` отдельный файл |
|
||||||
|
| 🟢 | остальные 151–159 | 8 файлов | после топ-10 |
|
||||||
|
|
||||||
**SQL (критичные метрики):**
|
### Чек-лист фазы 0
|
||||||
- `loparkour_player_stats`: skill_rating, sessions_count, total_jumps, longest_streak
|
|
||||||
- Автосохранение каждые 5 минут
|
|
||||||
|
|
||||||
**JSON (детальная аналитика):**
|
- [ ] Нет файлов > 200 строк (кроме явно согласованных)
|
||||||
- `playerdata/<uuid>.json`: avgTimePerBlock, jumpTypeStats, nearMissCount
|
- [x] Пакеты разнесены (config/mode/command/generator/menu/player/util)
|
||||||
- `playerdata/history/<uuid>_jumps.jsonl`: история прыжков (последние 1000)
|
- [ ] Нет циклических зависимостей `generator` ↔ `adaptive`
|
||||||
- Сохранение при выходе игрока
|
- [ ] Единый `MessageService` / `Locales` для player + console + action bar + title
|
||||||
|
- [ ] Удалить или подключить мёртвый код: `Menu`, `PagedMenu`, `DynamicMenu`, `MenuStub`
|
||||||
|
- [ ] `Option.java` — только фасад, без роста (делегаты в `options/*`)
|
||||||
|
- [x] `./gradlew build` зелёный
|
||||||
|
- [ ] README/TODO не врут про «всё готово»
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Технический долг
|
## Баги и долг (найдено при аудите)
|
||||||
|
|
||||||
### 🟢 Оптимизация
|
### 🔴 Сборка
|
||||||
|
|
||||||
| # | Задача | Приоритет | Статус |
|
| # | Проблема | Статус |
|
||||||
|---|--------|-----------|--------|
|
|---|----------|--------|
|
||||||
| 1 | Кэширование конфигов режимов | Средний | ✅ (в дизайне) |
|
| B1 | `SQLConnectionManager` visibility | ✅ |
|
||||||
| 2 | Batch updates для SQL метрик | Средний | 🟢 |
|
| B2 | `AdaptiveConfig` + `Config.CONFIG` | ✅ |
|
||||||
| 3 | Очистка старой истории прыжков | Низкий | 🟢 |
|
|
||||||
|
|
||||||
### 🟢 Тестирование
|
### 🟡 Локализация (i18n) — большинство закрыто
|
||||||
|
|
||||||
|
| # | Проблема | Статус |
|
||||||
|
|---|----------|--------|
|
||||||
|
| I1 | `LPMenu` back/next | ✅ `other.back`, `other.next` |
|
||||||
|
| I2 | `other.no_menu_items` | ✅ |
|
||||||
|
| I3 | joining disabled | ✅ `ModeMessages` + `other.joining_disabled` |
|
||||||
|
| I4 | Elytra messages | ✅ `modes.elytra.*` |
|
||||||
|
| I5–I11 | Race, coop, admin, schematic, commands, bungee, GUI stubs | ✅ (см. `locales/*.yml`) |
|
||||||
|
| I12 | `ghost.saved` в en | ✅ |
|
||||||
|
| I13 | `%[a-z]` vs `%s` | 🟢 документировать / унифицировать позже |
|
||||||
|
| I14 | reload NPE с консоли | ✅ `getString(sender, …)` |
|
||||||
|
| I15 | недостающие ключи | ✅ основной набор добавлен |
|
||||||
|
|
||||||
|
### 🟡 Логика / архитектура
|
||||||
|
|
||||||
|
| # | Проблема | Где |
|
||||||
|
|---|----------|-----|
|
||||||
|
| A1 | `MetricsCollector` на генераторе | ✅ `SessionStateManager` + `AdaptiveServices.attachToGenerator` |
|
||||||
|
| A2 | Adaptive → профиль генерации | ✅ `GeneratorProfileManager` + `AdaptiveDifficulty` |
|
||||||
|
| A3 | `adaptive.*` в config.yml | ✅ |
|
||||||
|
| A4 | SQL таблица stats | ✅ `StorageSQL` + `SQLMigrationManager.createAdaptiveStatsTable` |
|
||||||
|
| A5 | `DynamicMenu` мёртвый | 🔴 |
|
||||||
|
| A6 | STONE debug-spam | ✅ только при отсутствии `.material` |
|
||||||
|
| A7 | Несогласованные пути locale: `play.single.*` vs `modes.*` для режимов | `*Mode.getItem()` |
|
||||||
|
| A8 | `sendHelp` упоминает `/LoParkour`, игроки используют `/parkour` | `PlayerCommandHandler` |
|
||||||
|
|
||||||
|
### 🟢 Мелочи
|
||||||
|
|
||||||
|
| # | Проблема |
|
||||||
|
|---|----------|
|
||||||
|
| M1 | `SpectatorMode`: MiniMessage `<red>` vs legacy `§c` в других режимах |
|
||||||
|
| M2 | `HardcoreMode` / др.: смешение `ColorUtil` и сырого `sendMessage` |
|
||||||
|
| M3 | Unit/integration тесты в TODO помечены ✅ — **фактически** только 2 test-класса |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фаза 1: Рефакторинг структуры (статус реальный)
|
||||||
|
|
||||||
|
### Архитектура
|
||||||
|
|
||||||
| # | Задача | Статус |
|
| # | Задача | Статус |
|
||||||
|---|--------|--------|
|
|---|--------|--------|
|
||||||
| 1 | Unit тесты для адаптивной системы | 🟢 |
|
| 1 | Разбить `ParkourGenerator` | 🟡 Частично (composition), файл 159 строк — ок |
|
||||||
| 2 | Integration тесты для режимов | 🟢 |
|
| 2 | `GeneratorProfileManager` | 🟡 Есть, без отдельного `ProfileCalculator` |
|
||||||
| 3 | Performance тесты (100+ игроков) | 🟢 |
|
| 3 | statistics из generator | 🟡 `GeneratorStatistics` внутри generator, не пакет `statistics/` |
|
||||||
|
| 4 | Структура пакетов `generator/core`, `jump`, `lifecycle` | 🟡 Частично |
|
||||||
|
| 5 | `GeneratorEventListener` | ✅ Интерфейс + вызовы в `PlayerInteractionHandler` |
|
||||||
|
|
||||||
|
### Большие файлы (цель < 200) — **не выполнено**
|
||||||
|
|
||||||
|
| Файл | Строк | Статус |
|
||||||
|
|------|-------|--------|
|
||||||
|
| ElytraGenerator.java | 101 | ✅ |
|
||||||
|
| ParkourPlayer.java | 166 | ✅ |
|
||||||
|
| PAPIHook, LoParkour, MetricsCollector, … | 151–194 | 🟡 см. Фаза 0 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Чек-лист перед релизом
|
## Фаза 2: Адаптивная система (статус реальный)
|
||||||
|
|
||||||
- [ ] Все режимы работают (default, speedrun, hardcore, gravity-shift, elytra)
|
| # | Задача | Статус |
|
||||||
- [ ] Адаптивная система корректно собирает метрики
|
|---|--------|--------|
|
||||||
- [ ] SQL миграции применены
|
| 1 | `adaptive/model/` | 🟡 Классы есть |
|
||||||
- [ ] Конфиги валидируются при загрузке
|
| 2 | `adaptive/storage/` | ✅ compile + SQL/file repo |
|
||||||
- [ ] Нет memory leaks (проверить с VisualVM)
|
| 3 | SQL таблицы | ✅ CREATE в `StorageSQL` / `SQLMigrationManager` |
|
||||||
- [ ] Документация обновлена (README.md, CHANGELOG.md)
|
| 4 | File JSON storage | ✅ |
|
||||||
- [ ] Тесты проходят (`./gradlew test`)
|
| 5 | `MetricsCollector` | ✅ |
|
||||||
- [ ] Build успешен (`./gradlew build`)
|
| 6 | Calculator / Adjuster в профиле | ✅ через `AdaptiveDifficulty` |
|
||||||
|
| 7 | `AdaptiveMode`, `StatsService` | 🔴 отдельный режим не нужен пока |
|
||||||
|
| 8 | `config.yml` → `adaptive.*` | ✅ |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Оценка времени
|
## Фаза 3: ElytraMode (статус реальный)
|
||||||
|
|
||||||
| Фаза | Задач | Дней | Начало | Конец |
|
| # | Задача | Статус |
|
||||||
|------|-------|------|--------|-------|
|
|---|--------|--------|
|
||||||
| Фаза 1: Рефакторинг | 10 | 2-3 | День 1 | День 3 |
|
| 1 | `ElytraMode` | 🟡 Есть |
|
||||||
| Фаза 2: Адаптивность | 20 | 3-4 | День 4 | День 7 |
|
| 2 | `ElytraGenerator` + helpers | ✅ Разбит на `mode/elytra/*` |
|
||||||
| Фаза 3: ElytraMode | 9 | 1-2 | День 8 | День 9 |
|
| 3 | `ElytraConfig`, `RingRenderer`, … | ✅ В пакете `mode/elytra/` |
|
||||||
| Фаза 4: Новые режимы | 8 | 2-3 | День 10 | День 12 |
|
| 4 | `modes/elytra.yml` | 🔴 Нет |
|
||||||
| **Итого** | **47** | **8-12** | - | - |
|
| 5 | i18n для elytra | ✅ |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Метрики успеха
|
## Фаза 4: Режимы (статус реальный)
|
||||||
|
|
||||||
### Технические
|
| Режим | Меню | i18n join disabled | Примечание |
|
||||||
- ✅ Все файлы < 200 строк
|
|-------|------|-------------------|------------|
|
||||||
- ✅ Максимум 6 файлов в папке
|
| default, speedrun, hardcore, gravity-shift | ✅ SingleMenu | 🔴 хардкод EN | |
|
||||||
- ✅ SOLID принципы соблюдены
|
| elytra, race | ✅ | 🔴 | race — ещё UI хардкод |
|
||||||
- ✅ Нет циклических зависимостей
|
| coop | ❌ (null item — ок) | 🔴 | title хардкод |
|
||||||
|
| spectator | ✅ | 🟡 частично Locales | |
|
||||||
### Функциональные
|
| roguelike | — | — | **Нет в коде** (в старом TODO было ✅ — ошибка) |
|
||||||
- ✅ Адаптивная сложность работает для 4+ режимов
|
|
||||||
- ✅ Метрики сохраняются между сессиями
|
|
||||||
- ✅ ElytraMode полностью функционален
|
|
||||||
- ✅ Новый режим (Roguelike) добавлен
|
|
||||||
|
|
||||||
### Производительность
|
|
||||||
- ✅ TPS не падает при 50+ игроках
|
|
||||||
- ✅ SQL запросы < 50ms
|
|
||||||
- ✅ Конфиги кэшируются (нет I/O при входе игрока)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## SQL миграция
|
## Фаза 5: i18n — единая система сообщений
|
||||||
|
|
||||||
|
| # | Задача | Статус |
|
||||||
|
|---|--------|--------|
|
||||||
|
| 1 | `other.back` / `other.next` | ✅ |
|
||||||
|
| 2 | Ключи из таблицы I* | ✅ |
|
||||||
|
| 3 | `MessageKeys` enum или constants class (DRY путей) | 🟢 |
|
||||||
|
| 4 | `Locales.getString(CommandSender)` — использовать везде для консоли | 🟡 Частично |
|
||||||
|
| 5 | Убрать debug-spam STONE | ✅ |
|
||||||
|
| 6 | Синхронизировать `en.yml` ↔ `ru.yml` | 🟡 периодический diff |
|
||||||
|
| 7 | Проверка CI: скрипт diff ключей locale vs `Locales.getString`/`sendTranslated` | 🟢 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Технический долг (актуальный)
|
||||||
|
|
||||||
|
| # | Задача | Приоритет |
|
||||||
|
|---|--------|-----------|
|
||||||
|
| 1 | Compile adaptive | ✅ |
|
||||||
|
| 2 | Bootstrap MetricsCollector | ✅ |
|
||||||
|
| 3 | SQL adaptive table | ✅ |
|
||||||
|
| 4 | Удалить или реализовать `Menu`/`PagedMenu`/`DynamicMenu` | 🟡 |
|
||||||
|
| 5 | Реальные тесты adaptive + modes | 🟢 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Чек-лист перед релизом (честный)
|
||||||
|
|
||||||
|
- [x] `./gradlew build` без ошибок
|
||||||
|
- [x] Нет хардкода RU в `LPMenu` / основных режимах и админке
|
||||||
|
- [ ] Полный diff ключей locale vs Java (CI)
|
||||||
|
- [ ] Адаптивность: ручной тест на сервере (`adaptive.enabled: true`)
|
||||||
|
- [ ] ElytraMode протестирован вручную
|
||||||
|
- [ ] Нет файлов > 200 строк (или исключения задокументированы)
|
||||||
|
- [ ] README / CHANGELOG соответствуют коду
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SQL миграция (без изменений)
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- Создание таблицы для адаптивных метрик
|
|
||||||
CREATE TABLE IF NOT EXISTS loparkour_player_stats (
|
CREATE TABLE IF NOT EXISTS loparkour_player_stats (
|
||||||
player_uuid VARCHAR(36) PRIMARY KEY,
|
player_uuid VARCHAR(36) PRIMARY KEY,
|
||||||
skill_rating DOUBLE DEFAULT 1.0,
|
skill_rating DOUBLE DEFAULT 1.0,
|
||||||
|
|
@ -260,11 +289,36 @@ CREATE TABLE IF NOT EXISTS loparkour_player_stats (
|
||||||
INDEX idx_skill_rating (skill_rating),
|
INDEX idx_skill_rating (skill_rating),
|
||||||
INDEX idx_last_updated (last_updated)
|
INDEX idx_last_updated (last_updated)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
-- Миграция существующих данных (если есть старая таблица)
|
|
||||||
INSERT INTO loparkour_player_stats (player_uuid, sessions_count, last_updated)
|
|
||||||
SELECT player_uuid, COUNT(*) as sessions, MAX(timestamp) as last_updated
|
|
||||||
FROM loparkour_scores
|
|
||||||
GROUP BY player_uuid
|
|
||||||
ON DUPLICATE KEY UPDATE sessions_count = VALUES(sessions_count);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Оценка времени (пересмотр)
|
||||||
|
|
||||||
|
| Фаза | Дней | Комментарий |
|
||||||
|
|------|------|-------------|
|
||||||
|
| **0: KISS/DRY/SOLID + лимит строк** | 5–8 | Блокирует нормальную поддержку |
|
||||||
|
| **5: i18n** | 2–3 | Можно параллельно с 0 |
|
||||||
|
| **2: Adaptive** | 3–4 | После зелёной сборки + bootstrap |
|
||||||
|
| **3: Elytra split** | 1–2 | После 0 |
|
||||||
|
| **4: Режимы / тесты** | 2–3 | |
|
||||||
|
| **Итого** | **13–20** | Старые «8–12 дней» занижены |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Метрики успеха (пересмотр)
|
||||||
|
|
||||||
|
### Технические
|
||||||
|
- [ ] ≥ 90% файлов ≤ 150 строк
|
||||||
|
- [ ] 0 файлов > 200 строк
|
||||||
|
- [ ] SOLID: `adaptive` → listener/port, не прямой import generator internals
|
||||||
|
- [ ] `./gradlew test` — осмысленное покрытие, не «галочка в TODO»
|
||||||
|
|
||||||
|
### Функциональные
|
||||||
|
- [ ] 2+ языка без хардкода в Java (кроме логов debug)
|
||||||
|
- [ ] Adaptive влияет на профиль после N сессий
|
||||||
|
- [ ] ElytraMode без монолита 250+ строк
|
||||||
|
|
||||||
|
### Производительность
|
||||||
|
- [ ] TPS стабилен 50+ игроков (профилирование)
|
||||||
|
- [ ] SQL batch / flush adaptive < 50ms
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,11 @@ repositories {
|
||||||
name = "codemc-snapshots"
|
name = "codemc-snapshots"
|
||||||
url = uri("https://repo.codemc.io/repository/maven-snapshots/")
|
url = uri("https://repo.codemc.io/repository/maven-snapshots/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
maven {
|
||||||
|
name = "enginehub"
|
||||||
|
url = uri("https://maven.enginehub.org/repo/")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
@ -85,6 +90,8 @@ dependencies {
|
||||||
implementation("org.bstats:bstats-bukkit:3.2.1")
|
implementation("org.bstats:bstats-bukkit:3.2.1")
|
||||||
implementation("com.zaxxer:HikariCP:5.1.0")
|
implementation("com.zaxxer:HikariCP:5.1.0")
|
||||||
implementation("com.mysql:mysql-connector-j:8.3.0")
|
implementation("com.mysql:mysql-connector-j:8.3.0")
|
||||||
|
implementation("com.sk89q.worldedit:worldedit-core:7.3.6")
|
||||||
|
implementation("com.sk89q.worldedit:worldedit-bukkit:7.3.6")
|
||||||
|
|
||||||
// Provided dependencies (plugins)
|
// Provided dependencies (plugins)
|
||||||
compileOnly("me.clip:placeholderapi:2.11.6")
|
compileOnly("me.clip:placeholderapi:2.11.6")
|
||||||
|
|
@ -112,6 +119,8 @@ tasks.shadowJar {
|
||||||
relocate("io.papermc.lib", "dev.loki.loparkour.lib.paperlib")
|
relocate("io.papermc.lib", "dev.loki.loparkour.lib.paperlib")
|
||||||
relocate("com.google.gson", "dev.loki.loparkour.lib.gson")
|
relocate("com.google.gson", "dev.loki.loparkour.lib.gson")
|
||||||
relocate("org.bstats", "dev.loki.loparkour.lib.bstats")
|
relocate("org.bstats", "dev.loki.loparkour.lib.bstats")
|
||||||
|
relocate("com.sk89q.worldedit", "dev.loki.loparkour.lib.worldedit")
|
||||||
|
relocate("org.enginehub", "dev.loki.loparkour.lib.enginehub")
|
||||||
|
|
||||||
// Don't minimize - causes issues with Caffeine's dynamically generated classes
|
// Don't minimize - causes issues with Caffeine's dynamically generated classes
|
||||||
// minimize {
|
// minimize {
|
||||||
|
|
|
||||||
79
docs/SCHEMATICS.md
Normal file
79
docs/SCHEMATICS.md
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
# Parkour schematics (LoParkour)
|
||||||
|
|
||||||
|
Jump structures are loaded from `plugins/LoParkour/schematics/` and matched to a **difficulty** in `plugins/LoParkour/schematics/schematics.yml`.
|
||||||
|
|
||||||
|
## Supported file types
|
||||||
|
|
||||||
|
| Extension | Source | Notes |
|
||||||
|
|-----------|--------|--------|
|
||||||
|
| `.nbt` | Vanilla structure block, `/structure save` | Pasted with Bukkit `StructureManager` |
|
||||||
|
| `.schem`, `.schematic` | WorldEdit (`//copy`, `//schem save`) | Read via embedded WorldEdit clipboard IO |
|
||||||
|
| `.lpschem` | **Legacy LoParkour format** | Not used in-game; convert with `/lp schematic convert` |
|
||||||
|
|
||||||
|
During parkour generation, when the profile rolls **schematic** type, the plugin picks a random file whose `difficulty` in YAML is **≤** the player's schematic setting (0.0–1.0).
|
||||||
|
|
||||||
|
## Difficulty keys (`schematics.yml`)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
difficulty:
|
||||||
|
d0e227d2: 0.25 # short id (recommended)
|
||||||
|
```
|
||||||
|
|
||||||
|
For file `parkour-d0e227d2.schem` you can use key `d0e227d2` or the full stem `parkour-d0e227d2`.
|
||||||
|
|
||||||
|
Player setting **0.25 (easy)** → only structures with difficulty ≤ 0.25.
|
||||||
|
**1.0** → any structure.
|
||||||
|
|
||||||
|
## Creating schematics in-game (admin)
|
||||||
|
|
||||||
|
1. Get the wand: `/lp schematic wand`
|
||||||
|
2. **Left-click** = position 1, **right-click** = position 2 (or use `/lp schematic pos1` / `pos2`)
|
||||||
|
3. Save and register:
|
||||||
|
- `/lp create <difficulty>` — auto name `parkour-<hash>.schem` + YAML entry
|
||||||
|
- `/lp schematic create <difficulty>` — same
|
||||||
|
- `/lp schematic create <name> <difficulty>` — custom name (letters, numbers, `-`, `_`)
|
||||||
|
|
||||||
|
Difficulty examples: `0.25`, `0.5`, `0.75`, `1.0` (must be between 0 and 1).
|
||||||
|
|
||||||
|
4. `/lp schematic reload` — reload files without restart
|
||||||
|
|
||||||
|
## Converting old `.lpschem` files
|
||||||
|
|
||||||
|
Place `.lpschem` files in `plugins/LoParkour/schematics/` or legacy `schematics-new/`, then:
|
||||||
|
|
||||||
|
```
|
||||||
|
/lp schematic convert
|
||||||
|
```
|
||||||
|
|
||||||
|
Each file becomes `parkour-<id>.schem` in `schematics/` and gets a `difficulty:` entry (from the old metadata, or `0.5` if missing). Original `.lpschem` files are kept; delete them manually after checking.
|
||||||
|
|
||||||
|
## Admin commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `/lp schematic wand` | Selection wand |
|
||||||
|
| `/lp schematic pos1` / `pos2` | Set corners at your feet |
|
||||||
|
| `/lp create <difficulty>` | Save selection → `.schem` + YAML |
|
||||||
|
| `/lp schematic create <name> <difficulty>` | Save with custom name |
|
||||||
|
| `/lp schematic convert` | Convert all `.lpschem` in schematics folders |
|
||||||
|
| `/lp schematic list` | List loaded structures |
|
||||||
|
| `/lp schematic paste <name>` | Test-paste at your location |
|
||||||
|
| `/lp schematic reload` | Reload disk files |
|
||||||
|
|
||||||
|
## Flow (runtime)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
A[BlockPlacer SCHEMATIC roll] --> B[SchematicManager.pick]
|
||||||
|
B --> C{Format}
|
||||||
|
C -->|nbt| D[Structure.place]
|
||||||
|
C -->|schem| E[Clipboard paste]
|
||||||
|
D --> F[Track blocks / score]
|
||||||
|
E --> F
|
||||||
|
```
|
||||||
|
|
||||||
|
## Folders
|
||||||
|
|
||||||
|
- `plugins/LoParkour/schematics/` — `.nbt`, `.schem`, `.schematic` (active)
|
||||||
|
- `plugins/LoParkour/schematics/schematics.yml` — difficulty map
|
||||||
|
- `plugins/LoParkour/schematics-new/` — legacy; scanned only for `.lpschem` convert
|
||||||
|
|
@ -1,34 +1,19 @@
|
||||||
package dev.loki.loparkour;
|
package dev.loki.loparkour;
|
||||||
|
|
||||||
import dev.loki.loparkour.api.Registry;
|
import dev.loki.loparkour.bootstrap.PluginBootstrap;
|
||||||
import dev.loki.loparkour.config.Config;
|
import dev.loki.loparkour.config.core.Config;
|
||||||
import dev.loki.loparkour.config.Locales;
|
import dev.loki.loparkour.hook.papi.PAPIHook;
|
||||||
import dev.loki.loparkour.config.Option;
|
import dev.loki.loparkour.mode.base.Modes;
|
||||||
import dev.loki.loparkour.hook.HoloHook;
|
import dev.loki.loparkour.player.core.ParkourUser;
|
||||||
import dev.loki.loparkour.hook.PAPIHook;
|
import dev.loki.loparkour.reward.core.Rewards;
|
||||||
import dev.loki.loparkour.mode.DefaultMode;
|
import dev.loki.loparkour.schematic.core.SchematicManager;
|
||||||
import dev.loki.loparkour.mode.ElytraMode;
|
|
||||||
import dev.loki.loparkour.mode.Modes;
|
|
||||||
import dev.loki.loparkour.mode.SpectatorMode;
|
|
||||||
import dev.loki.loparkour.mode.SpeedrunMode;
|
|
||||||
import dev.loki.loparkour.mode.RaceMode;
|
|
||||||
import dev.loki.loparkour.mode.CoopMode;
|
|
||||||
import dev.loki.loparkour.mode.GravityShiftMode;
|
|
||||||
import dev.loki.loparkour.mode.HardcoreMode;
|
|
||||||
import dev.loki.loparkour.player.ParkourUser;
|
|
||||||
import dev.loki.loparkour.player.UserRegistry;
|
|
||||||
import dev.loki.loparkour.reward.Rewards;
|
|
||||||
import dev.loki.loparkour.schematic.lpschem.LPSchematicManager;
|
|
||||||
import dev.loki.loparkour.storage.Storage;
|
import dev.loki.loparkour.storage.Storage;
|
||||||
import dev.loki.loparkour.world.Divider;
|
import dev.loki.loparkour.world.core.Divider;
|
||||||
import dev.loki.loparkour.world.World;
|
import dev.loki.loparkour.world.core.World;
|
||||||
import dev.lolib.core.LoPlugin;
|
import dev.lolib.core.LoPlugin;
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.GsonBuilder;
|
import com.google.gson.GsonBuilder;
|
||||||
import org.bstats.bukkit.Metrics;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.bstats.charts.SimplePie;
|
|
||||||
import org.bstats.charts.SingleLineChart;
|
|
||||||
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
@ -41,7 +26,7 @@ import java.util.ArrayList;
|
||||||
*/
|
*/
|
||||||
public final class LoParkour extends LoPlugin {
|
public final class LoParkour extends LoPlugin {
|
||||||
|
|
||||||
public static final String NAME = "&#FF6464<bold>LoParkour<reset>";
|
public static final String NAME = "&#FF6464&lLoParkour&r";
|
||||||
public static final String PREFIX = NAME + " 񢩈» &#A0A0A0";
|
public static final String PREFIX = NAME + " 񢩈» &#A0A0A0";
|
||||||
|
|
||||||
private static final Gson gson = new GsonBuilder()
|
private static final Gson gson = new GsonBuilder()
|
||||||
|
|
@ -49,37 +34,26 @@ public final class LoParkour extends LoPlugin {
|
||||||
.excludeFieldsWithoutExposeAnnotation()
|
.excludeFieldsWithoutExposeAnnotation()
|
||||||
.create();
|
.create();
|
||||||
|
|
||||||
public static Gson getGson() {
|
|
||||||
return gson;
|
|
||||||
}
|
|
||||||
private static LoParkour instance;
|
private static LoParkour instance;
|
||||||
private static LPSchematicManager schematicManager;
|
private static SchematicManager schematicManager;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private static PAPIHook placeholderHook;
|
private static PAPIHook placeholderHook;
|
||||||
|
|
||||||
|
public static Gson getGson() {
|
||||||
|
return gson;
|
||||||
|
}
|
||||||
|
|
||||||
public static void log(String message) {
|
public static void log(String message) {
|
||||||
if (Config.CONFIG.getBoolean("debug")) {
|
if (Config.CONFIG.getBoolean("debug")) {
|
||||||
LoParkour.getPlugin().getLogger().info("[Debug] " + message);
|
LoParkour.getPlugin().getLogger().info("[Debug] " + message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param child The file name.
|
|
||||||
* @return A file from within the plugin folder.
|
|
||||||
*/
|
|
||||||
public static File getInFolder(String child) {
|
public static File getInFolder(String child) {
|
||||||
return new File(instance.getDataFolder(), child);
|
return new File(instance.getDataFolder(), child);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return This plugin's {@link Logging} instance.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return The plugin instance.
|
|
||||||
*/
|
|
||||||
public static LoParkour getPlugin() {
|
public static LoParkour getPlugin() {
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
@ -89,104 +63,32 @@ public final class LoParkour extends LoPlugin {
|
||||||
return placeholderHook;
|
return placeholderHook;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LPSchematicManager getSchematicManager() {
|
public static SchematicManager getSchematicManager() {
|
||||||
return schematicManager;
|
return schematicManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setSchematicManager(@NotNull SchematicManager manager) {
|
||||||
|
schematicManager = manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlaceholderHook(@Nullable PAPIHook hook) {
|
||||||
|
placeholderHook = hook;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void enable() {
|
public void enable() {
|
||||||
instance = this; // must be first — Config enum accesses instance inside reload()
|
instance = this;
|
||||||
loadConfigs();
|
PluginBootstrap.enable(this);
|
||||||
loadSchematics();
|
|
||||||
registerModes();
|
|
||||||
registerHooks();
|
|
||||||
setupWorld();
|
|
||||||
registerEventsAndCommands();
|
|
||||||
setupMetrics();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadConfigs() {
|
|
||||||
Config.reload(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadSchematics() {
|
|
||||||
schematicManager = new LPSchematicManager();
|
|
||||||
schematicManager.loadAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void registerModes() {
|
|
||||||
Registry.register(new DefaultMode());
|
|
||||||
Registry.register(new SpectatorMode());
|
|
||||||
Registry.register(new SpeedrunMode());
|
|
||||||
Registry.register(new ElytraMode());
|
|
||||||
Registry.register(new RaceMode());
|
|
||||||
Registry.register(new CoopMode());
|
|
||||||
Registry.register(new GravityShiftMode());
|
|
||||||
Registry.register(new HardcoreMode());
|
|
||||||
Modes.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void registerHooks() {
|
|
||||||
if (getServer().getPluginManager().isPluginEnabled("HolographicDisplays")) {
|
|
||||||
getLogger().info("Registered Holographic Displays hook");
|
|
||||||
HoloHook.init();
|
|
||||||
}
|
|
||||||
if (getServer().getPluginManager().isPluginEnabled("PlaceholderAPI")) {
|
|
||||||
getLogger().info("Registered PlaceholderAPI hook");
|
|
||||||
placeholderHook = new PAPIHook();
|
|
||||||
placeholderHook.register();
|
|
||||||
}
|
|
||||||
if (Config.CONFIG.getBoolean("bungeecord.enabled")) {
|
|
||||||
getServer().getMessenger().registerOutgoingPluginChannel(this, "BungeeCord");
|
|
||||||
getLogger().info("Registered BungeeCord hook");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setupWorld() {
|
|
||||||
if (!Config.CONFIG.getBoolean("joining")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// World creation MUST happen on the main thread (Bukkit API requirement)
|
|
||||||
try {
|
|
||||||
loLogger().info("Creating parkour world...");
|
|
||||||
World.create();
|
|
||||||
loLogger().info("Parkour world created successfully!");
|
|
||||||
} catch (Exception ex) {
|
|
||||||
loLogger().error("Failed to create parkour world", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void registerEventsAndCommands() {
|
|
||||||
dev.lolib.gui.GUIManager.init(this); // required for LoLib GUI click handling
|
|
||||||
|
|
||||||
var pm = getServer().getPluginManager();
|
|
||||||
pm.registerEvents(new dev.loki.loparkour.listener.PlayerConnectionListener(), this);
|
|
||||||
pm.registerEvents(new dev.loki.loparkour.listener.ParkourRestrictionListener(), this);
|
|
||||||
pm.registerEvents(new dev.loki.loparkour.listener.SchematicWandListener(), this);
|
|
||||||
|
|
||||||
LoParkourCommand cmd = new LoParkourCommand();
|
|
||||||
getCommand("LoParkour").setExecutor(cmd);
|
|
||||||
getCommand("LoParkour").setTabCompleter(cmd);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setupMetrics() {
|
|
||||||
Metrics metrics = new Metrics(this, 29754);
|
|
||||||
metrics.addCustomChart(new SimplePie("using_sql", () -> Boolean.toString(Option.SQL)));
|
|
||||||
metrics.addCustomChart(new SimplePie("using_rewards", () -> Boolean.toString(Rewards.REWARDS_ENABLED)));
|
|
||||||
metrics.addCustomChart(new SimplePie("locale_count", () -> Integer.toString(Locales.getLocaleCount())));
|
|
||||||
metrics.addCustomChart(new SingleLineChart("player_joins", () -> {
|
|
||||||
int joins = UserRegistry.getJoinCount();
|
|
||||||
return joins;
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void disable() {
|
public void disable() {
|
||||||
|
PluginBootstrap.disable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Called from {@link PluginBootstrap} on shutdown. */
|
||||||
|
public void runShutdownSequence() {
|
||||||
try {
|
try {
|
||||||
// Clear all sessions BEFORE leaving players to prevent memory leaks
|
|
||||||
new ArrayList<>(Divider.sections.keySet()).forEach(session -> {
|
new ArrayList<>(Divider.sections.keySet()).forEach(session -> {
|
||||||
try {
|
try {
|
||||||
session.onAllPlayersLeft();
|
session.onAllPlayersLeft();
|
||||||
|
|
@ -195,22 +97,18 @@ public final class LoParkour extends LoPlugin {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove all sessions from Divider
|
|
||||||
new ArrayList<>(Divider.sections.keySet()).forEach(Divider::remove);
|
new ArrayList<>(Divider.sections.keySet()).forEach(Divider::remove);
|
||||||
|
|
||||||
// Now leave all players
|
|
||||||
for (ParkourUser user : ParkourUser.getUsers()) {
|
for (ParkourUser user : ParkourUser.getUsers()) {
|
||||||
ParkourUser.leave(user);
|
ParkourUser.leave(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
// write all LoParkour gamemodes (may be null if enable failed)
|
|
||||||
if (Modes.DEFAULT != null && Modes.DEFAULT.getLeaderboard() != null) {
|
if (Modes.DEFAULT != null && Modes.DEFAULT.getLeaderboard() != null) {
|
||||||
Modes.DEFAULT.getLeaderboard().write(false);
|
Modes.DEFAULT.getLeaderboard().write(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear rewards to prevent memory leaks
|
Rewards.clear();
|
||||||
dev.loki.loparkour.reward.Rewards.clear();
|
dev.loki.loparkour.adaptive.bootstrap.AdaptiveServices.shutdown();
|
||||||
|
|
||||||
Storage.close();
|
Storage.close();
|
||||||
World.delete();
|
World.delete();
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,172 @@
|
||||||
|
package dev.loki.loparkour.adaptive.bootstrap;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.adaptive.core.AdaptiveDifficulty;
|
||||||
|
import dev.loki.loparkour.adaptive.core.MetricsCollector;
|
||||||
|
import dev.loki.loparkour.adaptive.model.AdaptiveConfig;
|
||||||
|
import dev.loki.loparkour.adaptive.model.SkillRating;
|
||||||
|
import dev.loki.loparkour.adaptive.storage.FileStatsStorage;
|
||||||
|
import dev.loki.loparkour.adaptive.storage.SQLStatsStorage;
|
||||||
|
import dev.loki.loparkour.adaptive.storage.StatsRepository;
|
||||||
|
import dev.loki.loparkour.config.options.Option;
|
||||||
|
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||||
|
import dev.loki.loparkour.generator.core.model.Profile;
|
||||||
|
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||||
|
import dev.loki.loparkour.storage.Storage;
|
||||||
|
import dev.loki.loparkour.storage.sql.StorageSQL;
|
||||||
|
import org.bukkit.plugin.Plugin;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap for the adaptive difficulty subsystem.
|
||||||
|
*/
|
||||||
|
public final class AdaptiveServices {
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static Plugin plugin;
|
||||||
|
@Nullable
|
||||||
|
private static StatsRepository repository;
|
||||||
|
@Nullable
|
||||||
|
private static MetricsCollector metricsCollector;
|
||||||
|
|
||||||
|
private AdaptiveServices() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void init(@NotNull Plugin pluginInstance) {
|
||||||
|
plugin = pluginInstance;
|
||||||
|
AdaptiveConfig.init();
|
||||||
|
if (!AdaptiveConfig.isEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Runnable setup = () -> startCollector(createRepository());
|
||||||
|
if (Option.SQL) {
|
||||||
|
Storage.runWhenReady(setup);
|
||||||
|
} else {
|
||||||
|
setup.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void reload() {
|
||||||
|
shutdownCollector();
|
||||||
|
AdaptiveConfig.reload();
|
||||||
|
if (plugin != null && AdaptiveConfig.isEnabled()) {
|
||||||
|
Runnable setup = () -> startCollector(createRepository());
|
||||||
|
if (Option.SQL) {
|
||||||
|
Storage.runWhenReady(setup);
|
||||||
|
} else {
|
||||||
|
setup.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void attachToGenerator(@NotNull ParkourGenerator generator) {
|
||||||
|
if (metricsCollector != null) {
|
||||||
|
generator.registerEventListener(metricsCollector);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies adaptive difficulty weights to a profile being built for a generator.
|
||||||
|
*/
|
||||||
|
public static void applyAdaptiveProfile(@NotNull ParkourGenerator generator, @NotNull Profile profile) {
|
||||||
|
if (!AdaptiveConfig.isEnabled() || metricsCollector == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
double target = AdaptiveDifficulty.resolveTargetDifficulty(generator, AdaptiveServices::resolveSkillRating);
|
||||||
|
AdaptiveDifficulty.applyToProfile(profile, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates stored skill rating from current session metrics after a fall.
|
||||||
|
*/
|
||||||
|
public static void persistRatingAfterFall(@NotNull ParkourPlayer player) {
|
||||||
|
if (!AdaptiveConfig.isEnabled() || repository == null || metricsCollector == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UUID uuid = player.getUUID();
|
||||||
|
SkillRating session = AdaptiveDifficulty.analyzeSession(player, metricsCollector);
|
||||||
|
SkillRating stored = repository.loadSkillRating(uuid);
|
||||||
|
|
||||||
|
if (stored != null) {
|
||||||
|
stored.updateRating(session.getRating(), AdaptiveConfig.getRatingWeight());
|
||||||
|
repository.saveSkillRating(stored);
|
||||||
|
} else {
|
||||||
|
repository.saveSkillRating(session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebuilds generator profile/chances from latest ratings (call once per fall event).
|
||||||
|
*/
|
||||||
|
public static void refreshGeneratorDifficulty(@NotNull ParkourGenerator generator) {
|
||||||
|
if (!AdaptiveConfig.isEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
generator.profileManager.recalculateProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void unloadPlayer(@NotNull UUID playerUuid) {
|
||||||
|
if (metricsCollector != null) {
|
||||||
|
metricsCollector.unloadPlayer(playerUuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void shutdown() {
|
||||||
|
shutdownCollector();
|
||||||
|
repository = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private static SkillRating resolveSkillRating(@NotNull ParkourPlayer player) {
|
||||||
|
UUID uuid = player.getUUID();
|
||||||
|
SkillRating stored = repository != null ? repository.loadSkillRating(uuid) : null;
|
||||||
|
|
||||||
|
if (metricsCollector == null) {
|
||||||
|
return stored != null ? stored : new SkillRating(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
var metrics = metricsCollector.getMetrics(uuid);
|
||||||
|
if (metrics.getTotalJumps() < AdaptiveConfig.getMinSessions()) {
|
||||||
|
if (stored != null && stored.isReliable()) {
|
||||||
|
return stored;
|
||||||
|
}
|
||||||
|
return stored != null ? stored : new SkillRating(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
SkillRating session = AdaptiveDifficulty.analyzeSession(player, metricsCollector);
|
||||||
|
if (stored == null) {
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
return AdaptiveDifficulty.mergeSessionRating(stored, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private static StatsRepository createRepository() {
|
||||||
|
if (Option.SQL && StorageSQL.isConnected()) {
|
||||||
|
return new SQLStatsStorage(StorageSQL.getConnectionManager());
|
||||||
|
}
|
||||||
|
return new FileStatsStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void startCollector(@NotNull StatsRepository repo) {
|
||||||
|
if (plugin == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
repository = repo;
|
||||||
|
metricsCollector = new MetricsCollector(plugin, repo);
|
||||||
|
metricsCollector.startAutoSave();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void shutdownCollector() {
|
||||||
|
if (metricsCollector != null) {
|
||||||
|
metricsCollector.stopAutoSave();
|
||||||
|
metricsCollector.saveAllMetrics();
|
||||||
|
metricsCollector = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
package dev.loki.loparkour.adaptive.core;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.adaptive.model.AdaptiveConfig;
|
||||||
|
import dev.loki.loparkour.adaptive.model.SkillRating;
|
||||||
|
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||||
|
import dev.loki.loparkour.generator.core.model.Profile;
|
||||||
|
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies adaptive difficulty to a generator {@link Profile}.
|
||||||
|
*/
|
||||||
|
public final class AdaptiveDifficulty {
|
||||||
|
|
||||||
|
private static final DifficultyCalculator CALCULATOR = new DifficultyCalculator();
|
||||||
|
private static final DifficultyAdjuster ADJUSTER = new DifficultyAdjuster();
|
||||||
|
private static final SkillAnalyzer ANALYZER = new SkillAnalyzer();
|
||||||
|
|
||||||
|
private static final Set<String> DIFFICULTY_PREFIXES = Set.of(
|
||||||
|
"distance.", "height.", "type."
|
||||||
|
);
|
||||||
|
|
||||||
|
private AdaptiveDifficulty() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges difficulty-related keys from {@code adjusted} into {@code target}.
|
||||||
|
*/
|
||||||
|
public static void mergeDifficultySettings(@NotNull Profile adjusted, @NotNull Profile target) {
|
||||||
|
for (String key : adjusted.settings.keySet()) {
|
||||||
|
if (isDifficultyKey(key)) {
|
||||||
|
target.set(key, adjusted.get(key).value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double resolveTargetDifficulty(
|
||||||
|
@NotNull ParkourGenerator generator,
|
||||||
|
@NotNull SkillRatingResolver resolver) {
|
||||||
|
var players = generator.getPlayers();
|
||||||
|
if (players.isEmpty()) {
|
||||||
|
return 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
double weightedSum = 0.0;
|
||||||
|
double weightTotal = 0.0;
|
||||||
|
|
||||||
|
for (ParkourPlayer player : players) {
|
||||||
|
SkillRating rating = resolver.resolve(player);
|
||||||
|
double target = CALCULATOR.calculateTargetDifficulty(rating);
|
||||||
|
double weight = Math.max(0.1, rating.getConfidence());
|
||||||
|
weightedSum += target * weight;
|
||||||
|
weightTotal += weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
return weightTotal > 0 ? weightedSum / weightTotal : 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public static SkillRating mergeSessionRating(@NotNull SkillRating stored, @NotNull SkillRating sessionSample) {
|
||||||
|
double weight = AdaptiveConfig.getRatingWeight();
|
||||||
|
double merged = (stored.getRating() * (1.0 - weight)) + (sessionSample.getRating() * weight);
|
||||||
|
double confidence = Math.max(stored.getConfidence(), sessionSample.getConfidence());
|
||||||
|
return new SkillRating(stored.getPlayerUuid(), merged, confidence, stored.getSessionsCount() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public static SkillRating analyzeSession(@NotNull ParkourPlayer player, @NotNull MetricsCollector collector) {
|
||||||
|
return ANALYZER.analyzeSkill(collector.getMetrics(player.getUUID()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void applyToProfile(
|
||||||
|
@NotNull Profile baseProfile,
|
||||||
|
double targetDifficulty) {
|
||||||
|
Profile adjusted = ADJUSTER.adjustProfile(baseProfile, targetDifficulty);
|
||||||
|
mergeDifficultySettings(adjusted, baseProfile);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isDifficultyKey(@NotNull String key) {
|
||||||
|
return DIFFICULTY_PREFIXES.stream().anyMatch(key::startsWith);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface SkillRatingResolver {
|
||||||
|
@NotNull
|
||||||
|
SkillRating resolve(@NotNull ParkourPlayer player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,182 @@
|
||||||
|
package dev.loki.loparkour.adaptive.core;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.generator.core.model.Profile;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjusts difficulty profile parameters based on target difficulty.
|
||||||
|
* Modifies distance, height, and special block chances to match player skill.
|
||||||
|
*
|
||||||
|
* Difficulty scale (0.0-1.0):
|
||||||
|
* - 0.0-0.3: Easy (short jumps, minimal height variation)
|
||||||
|
* - 0.3-0.6: Medium (balanced jumps, moderate height)
|
||||||
|
* - 0.6-0.8: Hard (longer jumps, more height variation)
|
||||||
|
* - 0.8-1.0: Expert (maximum distance, high variation)
|
||||||
|
*
|
||||||
|
* Thread-safe: Stateless, all methods are pure functions.
|
||||||
|
*/
|
||||||
|
public class DifficultyAdjuster {
|
||||||
|
|
||||||
|
// Distance bounds (blocks)
|
||||||
|
private static final int MIN_DISTANCE = 1;
|
||||||
|
private static final int MAX_DISTANCE = 4;
|
||||||
|
|
||||||
|
// Height bounds (blocks)
|
||||||
|
private static final int MIN_HEIGHT = -1;
|
||||||
|
private static final int MAX_HEIGHT = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjusts a profile to match the target difficulty.
|
||||||
|
* Creates a new profile with modified parameters.
|
||||||
|
*
|
||||||
|
* @param baseProfile The base profile to adjust
|
||||||
|
* @param targetDifficulty Target difficulty (0.0-1.0)
|
||||||
|
* @return New profile with adjusted parameters
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
public Profile adjustProfile(@NotNull Profile baseProfile, double targetDifficulty) {
|
||||||
|
Profile adjusted = new Profile();
|
||||||
|
|
||||||
|
// Copy base settings
|
||||||
|
copyBaseSettings(baseProfile, adjusted);
|
||||||
|
|
||||||
|
// Adjust difficulty parameters
|
||||||
|
adjustDistanceChances(adjusted, targetDifficulty);
|
||||||
|
adjustHeightChances(adjusted, targetDifficulty);
|
||||||
|
adjustSpecialFrequency(adjusted, targetDifficulty);
|
||||||
|
|
||||||
|
return adjusted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies non-difficulty settings from base profile.
|
||||||
|
*/
|
||||||
|
private void copyBaseSettings(@NotNull Profile source, @NotNull Profile target) {
|
||||||
|
String[] settingsKeys = {"min-y", "max-y", "heading", "first-block-material"};
|
||||||
|
|
||||||
|
for (String key : settingsKeys) {
|
||||||
|
if (source.settings.containsKey(key)) {
|
||||||
|
target.set(key, source.get(key).value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjusts distance chances based on difficulty.
|
||||||
|
*/
|
||||||
|
private void adjustDistanceChances(@NotNull Profile profile, double difficulty) {
|
||||||
|
double[] weights = DifficultyWeights.calculateDistanceWeights(difficulty);
|
||||||
|
|
||||||
|
profile.set("distance.min", String.valueOf(MIN_DISTANCE));
|
||||||
|
profile.set("distance.max", String.valueOf(MAX_DISTANCE));
|
||||||
|
|
||||||
|
for (int distance = MIN_DISTANCE; distance <= MAX_DISTANCE; distance++) {
|
||||||
|
int index = distance - MIN_DISTANCE;
|
||||||
|
int chance = (int) Math.round(weights[index] * 100);
|
||||||
|
profile.set("distance.chances." + distance, String.valueOf(chance));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjusts height variation based on difficulty.
|
||||||
|
*/
|
||||||
|
private void adjustHeightChances(@NotNull Profile profile, double difficulty) {
|
||||||
|
double[] weights = DifficultyWeights.calculateHeightWeights(difficulty);
|
||||||
|
|
||||||
|
profile.set("height.min", String.valueOf(MIN_HEIGHT));
|
||||||
|
profile.set("height.max", String.valueOf(MAX_HEIGHT));
|
||||||
|
|
||||||
|
int index = 0;
|
||||||
|
for (int height = MIN_HEIGHT; height <= MAX_HEIGHT; height++) {
|
||||||
|
int chance = (int) Math.round(weights[index] * 100);
|
||||||
|
profile.set("height.chances." + height, String.valueOf(chance));
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjusts special block frequency based on difficulty.
|
||||||
|
*/
|
||||||
|
private void adjustSpecialFrequency(@NotNull Profile profile, double difficulty) {
|
||||||
|
double specialPercent = DifficultyWeights.calculateSpecialFrequency(difficulty);
|
||||||
|
|
||||||
|
int defaultPercent = (int) Math.round(100.0 - specialPercent);
|
||||||
|
int specialChance = (int) Math.round(specialPercent * 0.75);
|
||||||
|
int schematicChance = (int) Math.round(specialPercent * 0.25);
|
||||||
|
|
||||||
|
profile.set("type.default", String.valueOf(defaultPercent));
|
||||||
|
profile.set("type.special", String.valueOf(specialChance));
|
||||||
|
profile.set("type.schematic", String.valueOf(schematicChance));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the effective difficulty of a profile.
|
||||||
|
* Inverse operation of adjustProfile.
|
||||||
|
*
|
||||||
|
* @param profile The profile to analyze
|
||||||
|
* @return Estimated difficulty (0.0-1.0)
|
||||||
|
*/
|
||||||
|
public double estimateDifficulty(@NotNull Profile profile) {
|
||||||
|
double distanceDifficulty = estimateDistanceDifficulty(profile);
|
||||||
|
double heightDifficulty = estimateHeightDifficulty(profile);
|
||||||
|
double specialDifficulty = estimateSpecialDifficulty(profile);
|
||||||
|
|
||||||
|
// Weighted average
|
||||||
|
return (distanceDifficulty * 0.5) + (heightDifficulty * 0.3) + (specialDifficulty * 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double estimateDistanceDifficulty(@NotNull Profile profile) {
|
||||||
|
double avgDistance = 0.0;
|
||||||
|
double totalWeight = 0.0;
|
||||||
|
|
||||||
|
for (int distance = MIN_DISTANCE; distance <= MAX_DISTANCE; distance++) {
|
||||||
|
String key = "distance.chances." + distance;
|
||||||
|
if (profile.settings.containsKey(key)) {
|
||||||
|
double weight = profile.get(key).asDouble();
|
||||||
|
avgDistance += distance * weight;
|
||||||
|
totalWeight += weight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalWeight > 0) {
|
||||||
|
avgDistance /= totalWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map average distance to difficulty (1.0 = easy, 4.0 = hard)
|
||||||
|
return (avgDistance - 1.0) / 3.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double estimateHeightDifficulty(@NotNull Profile profile) {
|
||||||
|
double variation = 0.0;
|
||||||
|
double totalWeight = 0.0;
|
||||||
|
|
||||||
|
for (int height = MIN_HEIGHT; height <= MAX_HEIGHT; height++) {
|
||||||
|
String key = "height.chances." + height;
|
||||||
|
if (profile.settings.containsKey(key)) {
|
||||||
|
double weight = profile.get(key).asDouble();
|
||||||
|
variation += Math.abs(height) * weight;
|
||||||
|
totalWeight += weight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalWeight > 0) {
|
||||||
|
variation /= totalWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map variation to difficulty (0.0 = flat, 1.5 = varied)
|
||||||
|
return Math.min(1.0, variation / 1.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double estimateSpecialDifficulty(@NotNull Profile profile) {
|
||||||
|
if (!profile.settings.containsKey("type.special")) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
double specialPercent = profile.get("type.special").asDouble();
|
||||||
|
double minFrequency = 5.0;
|
||||||
|
double maxFrequency = 30.0;
|
||||||
|
|
||||||
|
// Map special percentage to difficulty
|
||||||
|
return (specialPercent - minFrequency) / (maxFrequency - minFrequency);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
package dev.loki.loparkour.adaptive.core;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.adaptive.model.SkillRating;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates target difficulty based on player skill rating.
|
||||||
|
* Converts skill ratings into normalized difficulty values (0.0-1.0).
|
||||||
|
*
|
||||||
|
* Formula: difficulty = min(1.0, rating / 2.0)
|
||||||
|
* - Rating 0.0 = 0.0 difficulty (easiest)
|
||||||
|
* - Rating 1.0 = 0.5 difficulty (medium)
|
||||||
|
* - Rating 2.0+ = 1.0 difficulty (hardest)
|
||||||
|
*
|
||||||
|
* Thread-safe: Stateless, all methods are pure functions.
|
||||||
|
*/
|
||||||
|
public class DifficultyCalculator {
|
||||||
|
|
||||||
|
private static final double MAX_RATING_FOR_FULL_DIFFICULTY = 2.0;
|
||||||
|
private static final double MIN_DIFFICULTY = 0.0;
|
||||||
|
private static final double MAX_DIFFICULTY = 1.0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the target difficulty for a player based on their skill rating.
|
||||||
|
*
|
||||||
|
* @param skillRating The player's skill rating
|
||||||
|
* @return Target difficulty (0.0-1.0)
|
||||||
|
*/
|
||||||
|
public double calculateTargetDifficulty(@NotNull SkillRating skillRating) {
|
||||||
|
double rating = skillRating.getRating();
|
||||||
|
double confidence = skillRating.getConfidence();
|
||||||
|
|
||||||
|
// Calculate base difficulty from rating
|
||||||
|
double baseDifficulty = calculateBaseDifficulty(rating);
|
||||||
|
|
||||||
|
// Apply confidence adjustment (lower confidence = more conservative difficulty)
|
||||||
|
double adjustedDifficulty = applyConfidenceAdjustment(baseDifficulty, confidence);
|
||||||
|
|
||||||
|
return clampDifficulty(adjustedDifficulty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates base difficulty from skill rating.
|
||||||
|
* Linear mapping: rating 2.0 = difficulty 1.0
|
||||||
|
*
|
||||||
|
* @param rating The skill rating
|
||||||
|
* @return Base difficulty (0.0-1.0+)
|
||||||
|
*/
|
||||||
|
private double calculateBaseDifficulty(double rating) {
|
||||||
|
return rating / MAX_RATING_FOR_FULL_DIFFICULTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies confidence adjustment to difficulty.
|
||||||
|
* Low confidence ratings result in more conservative difficulty.
|
||||||
|
*
|
||||||
|
* @param baseDifficulty The base difficulty
|
||||||
|
* @param confidence The confidence level (0.0-1.0)
|
||||||
|
* @return Adjusted difficulty
|
||||||
|
*/
|
||||||
|
private double applyConfidenceAdjustment(double baseDifficulty, double confidence) {
|
||||||
|
// If confidence is low, reduce difficulty to be safe
|
||||||
|
// confidence 0.0 = 50% of base difficulty
|
||||||
|
// confidence 1.0 = 100% of base difficulty
|
||||||
|
double confidenceMultiplier = 0.5 + (confidence * 0.5);
|
||||||
|
return baseDifficulty * confidenceMultiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clamps difficulty to valid range [0.0, 1.0].
|
||||||
|
*
|
||||||
|
* @param difficulty The difficulty value
|
||||||
|
* @return Clamped difficulty
|
||||||
|
*/
|
||||||
|
private double clampDifficulty(double difficulty) {
|
||||||
|
return Math.max(MIN_DIFFICULTY, Math.min(MAX_DIFFICULTY, difficulty));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the recommended rating for a target difficulty.
|
||||||
|
* Inverse of calculateTargetDifficulty (without confidence adjustment).
|
||||||
|
*
|
||||||
|
* @param targetDifficulty The desired difficulty (0.0-1.0)
|
||||||
|
* @return Recommended skill rating
|
||||||
|
*/
|
||||||
|
public double calculateRecommendedRating(double targetDifficulty) {
|
||||||
|
double clampedDifficulty = clampDifficulty(targetDifficulty);
|
||||||
|
return clampedDifficulty * MAX_RATING_FOR_FULL_DIFFICULTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if a player is ready for increased difficulty.
|
||||||
|
* Checks if rating is significantly above current difficulty level.
|
||||||
|
*
|
||||||
|
* @param skillRating The player's skill rating
|
||||||
|
* @param currentDifficulty The current difficulty (0.0-1.0)
|
||||||
|
* @return true if player is ready for harder content
|
||||||
|
*/
|
||||||
|
public boolean isReadyForIncrease(@NotNull SkillRating skillRating, double currentDifficulty) {
|
||||||
|
double targetDifficulty = calculateTargetDifficulty(skillRating);
|
||||||
|
double threshold = 0.1; // 10% increase threshold
|
||||||
|
|
||||||
|
return targetDifficulty > currentDifficulty + threshold
|
||||||
|
&& skillRating.getConfidence() >= 0.5; // Require reasonable confidence
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if difficulty should be decreased for a player.
|
||||||
|
* Checks if rating is significantly below current difficulty level.
|
||||||
|
*
|
||||||
|
* @param skillRating The player's skill rating
|
||||||
|
* @param currentDifficulty The current difficulty (0.0-1.0)
|
||||||
|
* @return true if difficulty should be reduced
|
||||||
|
*/
|
||||||
|
public boolean shouldDecrease(@NotNull SkillRating skillRating, double currentDifficulty) {
|
||||||
|
double targetDifficulty = calculateTargetDifficulty(skillRating);
|
||||||
|
double threshold = 0.15; // 15% decrease threshold (more aggressive than increase)
|
||||||
|
|
||||||
|
return targetDifficulty < currentDifficulty - threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates a smooth difficulty transition.
|
||||||
|
* Prevents sudden difficulty spikes by limiting change rate.
|
||||||
|
*
|
||||||
|
* @param currentDifficulty The current difficulty
|
||||||
|
* @param targetDifficulty The target difficulty
|
||||||
|
* @param maxChange Maximum allowed change per adjustment
|
||||||
|
* @return Smoothed difficulty
|
||||||
|
*/
|
||||||
|
public double smoothTransition(double currentDifficulty, double targetDifficulty, double maxChange) {
|
||||||
|
double difference = targetDifficulty - currentDifficulty;
|
||||||
|
|
||||||
|
if (Math.abs(difference) <= maxChange) {
|
||||||
|
return targetDifficulty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move towards target by maxChange
|
||||||
|
return currentDifficulty + Math.signum(difference) * maxChange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
package dev.loki.loparkour.adaptive.core;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates weight distributions for different difficulty levels.
|
||||||
|
* Contains formulas for distance, height, and special block distributions.
|
||||||
|
*
|
||||||
|
* Thread-safe: Stateless, all methods are pure functions.
|
||||||
|
*/
|
||||||
|
class DifficultyWeights {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates distance weight distribution for given difficulty.
|
||||||
|
*
|
||||||
|
* @param difficulty Target difficulty (0.0-1.0)
|
||||||
|
* @return Array of weights [dist1, dist2, dist3, dist4]
|
||||||
|
*/
|
||||||
|
static double[] calculateDistanceWeights(double difficulty) {
|
||||||
|
double[] weights = new double[4];
|
||||||
|
|
||||||
|
if (difficulty < 0.3) {
|
||||||
|
// Easy: favor short jumps (1-2 blocks)
|
||||||
|
weights[0] = 0.40; // 1 block
|
||||||
|
weights[1] = 0.40; // 2 blocks
|
||||||
|
weights[2] = 0.15; // 3 blocks
|
||||||
|
weights[3] = 0.05; // 4 blocks
|
||||||
|
} else if (difficulty < 0.6) {
|
||||||
|
// Medium: balanced distribution
|
||||||
|
weights[0] = 0.20; // 1 block
|
||||||
|
weights[1] = 0.35; // 2 blocks
|
||||||
|
weights[2] = 0.30; // 3 blocks
|
||||||
|
weights[3] = 0.15; // 4 blocks
|
||||||
|
} else if (difficulty < 0.8) {
|
||||||
|
// Hard: favor longer jumps
|
||||||
|
weights[0] = 0.10; // 1 block
|
||||||
|
weights[1] = 0.25; // 2 blocks
|
||||||
|
weights[2] = 0.40; // 3 blocks
|
||||||
|
weights[3] = 0.25; // 4 blocks
|
||||||
|
} else {
|
||||||
|
// Expert: maximum challenge
|
||||||
|
weights[0] = 0.05; // 1 block
|
||||||
|
weights[1] = 0.15; // 2 blocks
|
||||||
|
weights[2] = 0.35; // 3 blocks
|
||||||
|
weights[3] = 0.45; // 4 blocks
|
||||||
|
}
|
||||||
|
|
||||||
|
return weights;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates height weight distribution for given difficulty.
|
||||||
|
*
|
||||||
|
* @param difficulty Target difficulty (0.0-1.0)
|
||||||
|
* @return Array of weights [-1, 0, 1, 2]
|
||||||
|
*/
|
||||||
|
static double[] calculateHeightWeights(double difficulty) {
|
||||||
|
double[] weights = new double[4];
|
||||||
|
|
||||||
|
if (difficulty < 0.3) {
|
||||||
|
// Easy: mostly flat, minimal drops
|
||||||
|
weights[0] = 0.10; // -1 (down)
|
||||||
|
weights[1] = 0.60; // 0 (flat)
|
||||||
|
weights[2] = 0.25; // +1 (up)
|
||||||
|
weights[3] = 0.05; // +2 (high up)
|
||||||
|
} else if (difficulty < 0.6) {
|
||||||
|
// Medium: balanced variation
|
||||||
|
weights[0] = 0.20; // -1
|
||||||
|
weights[1] = 0.40; // 0
|
||||||
|
weights[2] = 0.30; // +1
|
||||||
|
weights[3] = 0.10; // +2
|
||||||
|
} else if (difficulty < 0.8) {
|
||||||
|
// Hard: more variation
|
||||||
|
weights[0] = 0.25; // -1
|
||||||
|
weights[1] = 0.30; // 0
|
||||||
|
weights[2] = 0.30; // +1
|
||||||
|
weights[3] = 0.15; // +2
|
||||||
|
} else {
|
||||||
|
// Expert: maximum variation
|
||||||
|
weights[0] = 0.30; // -1
|
||||||
|
weights[1] = 0.20; // 0
|
||||||
|
weights[2] = 0.30; // +1
|
||||||
|
weights[3] = 0.20; // +2
|
||||||
|
}
|
||||||
|
|
||||||
|
return weights;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates special block frequency for given difficulty.
|
||||||
|
*
|
||||||
|
* @param difficulty Target difficulty (0.0-1.0)
|
||||||
|
* @return Special block percentage (5.0-30.0)
|
||||||
|
*/
|
||||||
|
static double calculateSpecialFrequency(double difficulty) {
|
||||||
|
double minFrequency = 5.0;
|
||||||
|
double maxFrequency = 30.0;
|
||||||
|
return minFrequency + (maxFrequency - minFrequency) * difficulty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,214 @@
|
||||||
|
package dev.loki.loparkour.adaptive.core;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.adaptive.model.PlayerMetrics;
|
||||||
|
import dev.loki.loparkour.adaptive.storage.StatsRepository;
|
||||||
|
import dev.loki.loparkour.generator.core.coordinator.GeneratorEventListener;
|
||||||
|
import dev.loki.loparkour.generator.jump.calculation.JumpType;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.plugin.Plugin;
|
||||||
|
import org.bukkit.scheduler.BukkitRunnable;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collects and manages player performance metrics.
|
||||||
|
*
|
||||||
|
* Responsibilities:
|
||||||
|
* - Track player jumps, falls, and scoring events
|
||||||
|
* - Detect near-miss jumps (distance > 3.5 blocks)
|
||||||
|
* - Calculate average time per block
|
||||||
|
* - Maintain jump type statistics
|
||||||
|
* - Auto-save metrics every 5 minutes
|
||||||
|
*
|
||||||
|
* Thread-safe: Uses ConcurrentHashMap for player metrics cache.
|
||||||
|
*/
|
||||||
|
public class MetricsCollector implements GeneratorEventListener {
|
||||||
|
|
||||||
|
private static final double NEAR_MISS_THRESHOLD = 3.5;
|
||||||
|
private static final long AUTO_SAVE_INTERVAL_TICKS = 20 * 60 * 5; // 5 minutes
|
||||||
|
|
||||||
|
private final Plugin plugin;
|
||||||
|
private final StatsRepository repository;
|
||||||
|
private final Map<UUID, PlayerMetrics> metricsCache;
|
||||||
|
private final Map<UUID, Block> lastBlockCache;
|
||||||
|
private final Map<UUID, Long> lastBlockTimeCache;
|
||||||
|
|
||||||
|
private BukkitRunnable autoSaveTask;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new MetricsCollector.
|
||||||
|
*
|
||||||
|
* @param plugin The plugin instance for scheduling tasks
|
||||||
|
* @param repository The repository for persisting metrics
|
||||||
|
*/
|
||||||
|
public MetricsCollector(@NotNull Plugin plugin, @NotNull StatsRepository repository) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.repository = repository;
|
||||||
|
this.metricsCache = new ConcurrentHashMap<>();
|
||||||
|
this.lastBlockCache = new ConcurrentHashMap<>();
|
||||||
|
this.lastBlockTimeCache = new ConcurrentHashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the auto-save task.
|
||||||
|
* Should be called when the collector is initialized.
|
||||||
|
*/
|
||||||
|
public void startAutoSave() {
|
||||||
|
if (autoSaveTask != null) {
|
||||||
|
autoSaveTask.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
autoSaveTask = new BukkitRunnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
saveAllMetrics();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
autoSaveTask.runTaskTimerAsynchronously(plugin, AUTO_SAVE_INTERVAL_TICKS, AUTO_SAVE_INTERVAL_TICKS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the auto-save task.
|
||||||
|
* Should be called when the plugin is disabled.
|
||||||
|
*/
|
||||||
|
public void stopAutoSave() {
|
||||||
|
if (autoSaveTask != null) {
|
||||||
|
autoSaveTask.cancel();
|
||||||
|
autoSaveTask = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or create metrics for a player.
|
||||||
|
*
|
||||||
|
* @param playerUuid The player's UUID
|
||||||
|
* @return The player's metrics
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
public PlayerMetrics getMetrics(@NotNull UUID playerUuid) {
|
||||||
|
return metricsCache.computeIfAbsent(playerUuid, uuid -> {
|
||||||
|
// Try to load from repository
|
||||||
|
PlayerMetrics loaded = repository.loadMetrics(uuid);
|
||||||
|
if (loaded != null) {
|
||||||
|
return loaded;
|
||||||
|
}
|
||||||
|
// Create new metrics if not found
|
||||||
|
return new PlayerMetrics(uuid);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a player lands on a block.
|
||||||
|
* Detects near-miss jumps and updates timing metrics.
|
||||||
|
*
|
||||||
|
* @param player The player who landed
|
||||||
|
* @param block The block the player landed on
|
||||||
|
* @param distance The distance of the jump
|
||||||
|
*/
|
||||||
|
public void onBlockLand(@NotNull Player player, @NotNull Block block, double distance) {
|
||||||
|
UUID uuid = player.getUniqueId();
|
||||||
|
PlayerMetrics metrics = getMetrics(uuid);
|
||||||
|
|
||||||
|
// Calculate time since last block
|
||||||
|
long currentTime = System.currentTimeMillis();
|
||||||
|
Long lastTime = lastBlockTimeCache.get(uuid);
|
||||||
|
if (lastTime != null) {
|
||||||
|
long timeDiff = currentTime - lastTime;
|
||||||
|
// Record jump with timing (use "normal" as default type)
|
||||||
|
metrics.recordJump("normal", timeDiff);
|
||||||
|
}
|
||||||
|
lastBlockTimeCache.put(uuid, currentTime);
|
||||||
|
|
||||||
|
// Detect near-miss (long jump that succeeded)
|
||||||
|
if (distance > NEAR_MISS_THRESHOLD) {
|
||||||
|
metrics.recordNearMiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update last block
|
||||||
|
lastBlockCache.put(uuid, block);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBlockGenerated(@NotNull Player player, @NotNull Block block, @NotNull JumpType jumpType, double distance) {
|
||||||
|
// Not used for metrics collection, but required by interface
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlayerScore(@NotNull Player player, @NotNull Block block, double distance) {
|
||||||
|
onBlockLand(player, block, distance);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlayerFall(@NotNull Player player, @Nullable Block lastBlock) {
|
||||||
|
UUID uuid = player.getUniqueId();
|
||||||
|
PlayerMetrics metrics = getMetrics(uuid);
|
||||||
|
|
||||||
|
// Save metrics on fall (important event)
|
||||||
|
repository.saveMetrics(metrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onJump(@NotNull Player player, @NotNull JumpType jumpType) {
|
||||||
|
UUID uuid = player.getUniqueId();
|
||||||
|
PlayerMetrics metrics = getMetrics(uuid);
|
||||||
|
|
||||||
|
// Calculate time since last block
|
||||||
|
long currentTime = System.currentTimeMillis();
|
||||||
|
Long lastTime = lastBlockTimeCache.get(uuid);
|
||||||
|
long timeDiff = (lastTime != null) ? (currentTime - lastTime) : 0;
|
||||||
|
|
||||||
|
// Record jump with type and timing
|
||||||
|
metrics.recordJump(jumpType.name().toLowerCase(), timeDiff);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save metrics for a specific player.
|
||||||
|
*
|
||||||
|
* @param playerUuid The player's UUID
|
||||||
|
*/
|
||||||
|
public void saveMetrics(@NotNull UUID playerUuid) {
|
||||||
|
PlayerMetrics metrics = metricsCache.get(playerUuid);
|
||||||
|
if (metrics != null) {
|
||||||
|
repository.saveMetrics(metrics);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save all cached metrics.
|
||||||
|
* Called by auto-save task and on plugin disable.
|
||||||
|
*/
|
||||||
|
public void saveAllMetrics() {
|
||||||
|
for (PlayerMetrics metrics : metricsCache.values()) {
|
||||||
|
repository.saveMetrics(metrics);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a player from the cache.
|
||||||
|
* Should be called when a player leaves the server.
|
||||||
|
*
|
||||||
|
* @param playerUuid The player's UUID
|
||||||
|
*/
|
||||||
|
public void unloadPlayer(@NotNull UUID playerUuid) {
|
||||||
|
// Save before removing
|
||||||
|
saveMetrics(playerUuid);
|
||||||
|
metricsCache.remove(playerUuid);
|
||||||
|
lastBlockCache.remove(playerUuid);
|
||||||
|
lastBlockTimeCache.remove(playerUuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of cached players.
|
||||||
|
*
|
||||||
|
* @return The cache size
|
||||||
|
*/
|
||||||
|
public int getCacheSize() {
|
||||||
|
return metricsCache.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
package dev.loki.loparkour.adaptive.core;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.adaptive.model.PlayerMetrics;
|
||||||
|
import dev.loki.loparkour.adaptive.model.SkillRating;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyzes player metrics and calculates skill ratings.
|
||||||
|
* Uses a formula that considers success rate, near misses, and consistency.
|
||||||
|
*
|
||||||
|
* Formula: rating = baseRating * (successRate^2) * (1 + nearMissBonus) * streakMultiplier
|
||||||
|
* - baseRating: 1.0 (baseline skill level)
|
||||||
|
* - successRate: percentage of successful jumps (0.0-1.0)
|
||||||
|
* - nearMissBonus: 0.1 per near miss (rewards precision)
|
||||||
|
* - streakMultiplier: 1 + (successfulJumps / 100) (rewards consistency)
|
||||||
|
*
|
||||||
|
* Thread-safe: Stateless, all methods are pure functions.
|
||||||
|
*/
|
||||||
|
public class SkillAnalyzer {
|
||||||
|
|
||||||
|
private static final double BASE_RATING = 1.0;
|
||||||
|
private static final double NEAR_MISS_BONUS = 0.1;
|
||||||
|
private static final double STREAK_DIVISOR = 100.0;
|
||||||
|
private static final int MIN_JUMPS_FOR_CONFIDENCE = 10;
|
||||||
|
private static final int MAX_JUMPS_FOR_CONFIDENCE = 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyzes player metrics and calculates a skill rating.
|
||||||
|
*
|
||||||
|
* @param metrics The player's performance metrics
|
||||||
|
* @return A skill rating with confidence level
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
public SkillRating analyzeSkill(@NotNull PlayerMetrics metrics) {
|
||||||
|
double successRate = metrics.getSuccessRate();
|
||||||
|
int nearMisses = metrics.getNearMissCount();
|
||||||
|
int successfulJumps = metrics.getSuccessfulJumps();
|
||||||
|
int totalJumps = metrics.getTotalJumps();
|
||||||
|
|
||||||
|
// Calculate rating components
|
||||||
|
double successComponent = calculateSuccessComponent(successRate);
|
||||||
|
double nearMissBonus = calculateNearMissBonus(nearMisses);
|
||||||
|
double streakMultiplier = calculateStreakMultiplier(successfulJumps);
|
||||||
|
|
||||||
|
// Combine components
|
||||||
|
double rating = BASE_RATING * successComponent * nearMissBonus * streakMultiplier;
|
||||||
|
|
||||||
|
// Calculate confidence based on sample size
|
||||||
|
double confidence = calculateConfidence(totalJumps);
|
||||||
|
|
||||||
|
// Sessions count is 1 for current session analysis
|
||||||
|
int sessionsCount = 1;
|
||||||
|
|
||||||
|
return new SkillRating(metrics.getPlayerUuid(), rating, confidence, sessionsCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the success rate component of the rating.
|
||||||
|
* Uses squared success rate to emphasize consistency.
|
||||||
|
*
|
||||||
|
* @param successRate The player's success rate (0.0-1.0)
|
||||||
|
* @return Success component (0.0-1.0)
|
||||||
|
*/
|
||||||
|
private double calculateSuccessComponent(double successRate) {
|
||||||
|
return Math.pow(successRate, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the near miss bonus multiplier.
|
||||||
|
* Rewards players who land close to block edges (precision).
|
||||||
|
*
|
||||||
|
* @param nearMisses Number of near miss landings
|
||||||
|
* @return Near miss multiplier (1.0+)
|
||||||
|
*/
|
||||||
|
private double calculateNearMissBonus(int nearMisses) {
|
||||||
|
return 1.0 + (nearMisses * NEAR_MISS_BONUS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the streak multiplier based on successful jumps.
|
||||||
|
* Rewards consistent performance over time.
|
||||||
|
*
|
||||||
|
* @param successfulJumps Number of successful jumps
|
||||||
|
* @return Streak multiplier (1.0+)
|
||||||
|
*/
|
||||||
|
private double calculateStreakMultiplier(int successfulJumps) {
|
||||||
|
return 1.0 + (successfulJumps / STREAK_DIVISOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates confidence level based on sample size.
|
||||||
|
* More jumps = higher confidence in the rating.
|
||||||
|
*
|
||||||
|
* @param totalJumps Total number of jumps attempted
|
||||||
|
* @return Confidence level (0.0-1.0)
|
||||||
|
*/
|
||||||
|
private double calculateConfidence(int totalJumps) {
|
||||||
|
if (totalJumps < MIN_JUMPS_FOR_CONFIDENCE) {
|
||||||
|
// Low confidence for small samples
|
||||||
|
return (double) totalJumps / MIN_JUMPS_FOR_CONFIDENCE;
|
||||||
|
} else if (totalJumps >= MAX_JUMPS_FOR_CONFIDENCE) {
|
||||||
|
// Maximum confidence reached
|
||||||
|
return 1.0;
|
||||||
|
} else {
|
||||||
|
// Linear interpolation between min and max
|
||||||
|
double range = MAX_JUMPS_FOR_CONFIDENCE - MIN_JUMPS_FOR_CONFIDENCE;
|
||||||
|
double progress = totalJumps - MIN_JUMPS_FOR_CONFIDENCE;
|
||||||
|
return progress / range;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimates the minimum rating for a given success rate.
|
||||||
|
* Useful for UI display and difficulty selection.
|
||||||
|
*
|
||||||
|
* @param successRate The success rate (0.0-1.0)
|
||||||
|
* @return Estimated minimum rating
|
||||||
|
*/
|
||||||
|
public double estimateMinRating(double successRate) {
|
||||||
|
return BASE_RATING * calculateSuccessComponent(successRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimates the maximum rating for a given success rate.
|
||||||
|
* Assumes high near miss count and long streak.
|
||||||
|
*
|
||||||
|
* @param successRate The success rate (0.0-1.0)
|
||||||
|
* @return Estimated maximum rating
|
||||||
|
*/
|
||||||
|
public double estimateMaxRating(double successRate) {
|
||||||
|
double successComponent = calculateSuccessComponent(successRate);
|
||||||
|
double maxNearMissBonus = calculateNearMissBonus(50); // Assume 50 near misses
|
||||||
|
double maxStreakMultiplier = calculateStreakMultiplier(MAX_JUMPS_FOR_CONFIDENCE);
|
||||||
|
|
||||||
|
return BASE_RATING * successComponent * maxNearMissBonus * maxStreakMultiplier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
package dev.loki.loparkour.adaptive.model;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.config.core.Config;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cached configuration for the adaptive difficulty system.
|
||||||
|
* Loads settings from config.yml and caches them for performance.
|
||||||
|
*/
|
||||||
|
public class AdaptiveConfig {
|
||||||
|
|
||||||
|
private static final ConcurrentHashMap<String, Object> cache = new ConcurrentHashMap<>();
|
||||||
|
private static volatile boolean initialized = false;
|
||||||
|
|
||||||
|
// Configuration keys
|
||||||
|
private static final String KEY_ENABLED = "adaptive.enabled";
|
||||||
|
private static final String KEY_MIN_SESSIONS = "adaptive.min-sessions";
|
||||||
|
private static final String KEY_UPDATE_INTERVAL = "adaptive.update-interval";
|
||||||
|
private static final String KEY_RATING_WEIGHT = "adaptive.rating-weight";
|
||||||
|
private static final String KEY_CONFIDENCE_THRESHOLD = "adaptive.confidence-threshold";
|
||||||
|
private static final String KEY_NEAR_MISS_DISTANCE = "adaptive.near-miss-distance";
|
||||||
|
|
||||||
|
// Default values
|
||||||
|
private static final boolean DEFAULT_ENABLED = true;
|
||||||
|
private static final int DEFAULT_MIN_SESSIONS = 3;
|
||||||
|
private static final int DEFAULT_UPDATE_INTERVAL = 300; // 5 minutes in seconds
|
||||||
|
private static final double DEFAULT_RATING_WEIGHT = 0.15;
|
||||||
|
private static final double DEFAULT_CONFIDENCE_THRESHOLD = 0.3;
|
||||||
|
private static final double DEFAULT_NEAR_MISS_DISTANCE = 0.3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the configuration cache from config.yml.
|
||||||
|
* Should be called during plugin startup.
|
||||||
|
*/
|
||||||
|
public static void init() {
|
||||||
|
if (initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.put(KEY_ENABLED, Config.CONFIG.getBoolean(KEY_ENABLED, DEFAULT_ENABLED));
|
||||||
|
cache.put(KEY_MIN_SESSIONS, Config.CONFIG.getInt(KEY_MIN_SESSIONS, DEFAULT_MIN_SESSIONS));
|
||||||
|
cache.put(KEY_UPDATE_INTERVAL, Config.CONFIG.getInt(KEY_UPDATE_INTERVAL, DEFAULT_UPDATE_INTERVAL));
|
||||||
|
cache.put(KEY_RATING_WEIGHT, configDouble(KEY_RATING_WEIGHT, DEFAULT_RATING_WEIGHT));
|
||||||
|
cache.put(KEY_CONFIDENCE_THRESHOLD, configDouble(KEY_CONFIDENCE_THRESHOLD, DEFAULT_CONFIDENCE_THRESHOLD));
|
||||||
|
cache.put(KEY_NEAR_MISS_DISTANCE, configDouble(KEY_NEAR_MISS_DISTANCE, DEFAULT_NEAR_MISS_DISTANCE));
|
||||||
|
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads configuration from config.yml.
|
||||||
|
* Call this after config reload.
|
||||||
|
*/
|
||||||
|
public static void reload() {
|
||||||
|
initialized = false;
|
||||||
|
cache.clear();
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if adaptive difficulty system is enabled.
|
||||||
|
*
|
||||||
|
* @return True if enabled
|
||||||
|
*/
|
||||||
|
public static boolean isEnabled() {
|
||||||
|
ensureInitialized();
|
||||||
|
return (boolean) cache.getOrDefault(KEY_ENABLED, DEFAULT_ENABLED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets minimum number of sessions before adaptive difficulty activates.
|
||||||
|
*
|
||||||
|
* @return Minimum sessions count
|
||||||
|
*/
|
||||||
|
public static int getMinSessions() {
|
||||||
|
ensureInitialized();
|
||||||
|
return (int) cache.getOrDefault(KEY_MIN_SESSIONS, DEFAULT_MIN_SESSIONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the interval (in seconds) between automatic stats updates.
|
||||||
|
*
|
||||||
|
* @return Update interval in seconds
|
||||||
|
*/
|
||||||
|
public static int getUpdateInterval() {
|
||||||
|
ensureInitialized();
|
||||||
|
return (int) cache.getOrDefault(KEY_UPDATE_INTERVAL, DEFAULT_UPDATE_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the weight for rating updates (how much new performance affects rating).
|
||||||
|
*
|
||||||
|
* @return Rating weight (0.0 to 1.0)
|
||||||
|
*/
|
||||||
|
public static double getRatingWeight() {
|
||||||
|
ensureInitialized();
|
||||||
|
Object value = cache.get(KEY_RATING_WEIGHT);
|
||||||
|
if (value instanceof Double) {
|
||||||
|
return (double) value;
|
||||||
|
}
|
||||||
|
return DEFAULT_RATING_WEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the confidence threshold for reliable ratings.
|
||||||
|
*
|
||||||
|
* @return Confidence threshold (0.0 to 1.0)
|
||||||
|
*/
|
||||||
|
public static double getConfidenceThreshold() {
|
||||||
|
ensureInitialized();
|
||||||
|
Object value = cache.get(KEY_CONFIDENCE_THRESHOLD);
|
||||||
|
if (value instanceof Double) {
|
||||||
|
return (double) value;
|
||||||
|
}
|
||||||
|
return DEFAULT_CONFIDENCE_THRESHOLD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the distance threshold for near-miss detection.
|
||||||
|
*
|
||||||
|
* @return Near-miss distance in blocks
|
||||||
|
*/
|
||||||
|
public static double getNearMissDistance() {
|
||||||
|
ensureInitialized();
|
||||||
|
Object value = cache.get(KEY_NEAR_MISS_DISTANCE);
|
||||||
|
if (value instanceof Double) {
|
||||||
|
return (double) value;
|
||||||
|
}
|
||||||
|
return DEFAULT_NEAR_MISS_DISTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ensureInitialized() {
|
||||||
|
if (!initialized) {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double configDouble(@NotNull String key, double defaultValue) {
|
||||||
|
return Config.CONFIG.isPath(key) ? Config.CONFIG.getDouble(key) : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a custom config value with caching.
|
||||||
|
*
|
||||||
|
* @param key The config key
|
||||||
|
* @param defaultValue Default value if key not found
|
||||||
|
* @return The config value
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
public static <T> T get(@NotNull String key, @NotNull T defaultValue) {
|
||||||
|
ensureInitialized();
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
T value = (T) cache.get(key);
|
||||||
|
return value != null ? value : defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,159 @@
|
||||||
|
package dev.loki.loparkour.adaptive.model;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores detailed player performance metrics for adaptive difficulty calculation.
|
||||||
|
* Tracks timing, jump patterns, and near-miss statistics.
|
||||||
|
*/
|
||||||
|
public class PlayerMetrics {
|
||||||
|
|
||||||
|
private final UUID playerUuid;
|
||||||
|
private double avgTimePerBlock;
|
||||||
|
private final Map<String, Integer> jumpTypeStats;
|
||||||
|
private int nearMissCount;
|
||||||
|
private int totalJumps;
|
||||||
|
private long lastUpdated;
|
||||||
|
|
||||||
|
public PlayerMetrics(@NotNull UUID playerUuid) {
|
||||||
|
this.playerUuid = playerUuid;
|
||||||
|
this.avgTimePerBlock = 0.0;
|
||||||
|
this.jumpTypeStats = new HashMap<>();
|
||||||
|
this.nearMissCount = 0;
|
||||||
|
this.totalJumps = 0;
|
||||||
|
this.lastUpdated = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records a successful jump with timing data.
|
||||||
|
*
|
||||||
|
* @param jumpType The type of jump (e.g., "normal", "diagonal", "momentum")
|
||||||
|
* @param timeMs Time taken for this jump in milliseconds
|
||||||
|
*/
|
||||||
|
public void recordJump(@NotNull String jumpType, long timeMs) {
|
||||||
|
jumpTypeStats.merge(jumpType, 1, Integer::sum);
|
||||||
|
totalJumps++;
|
||||||
|
|
||||||
|
// Update rolling average
|
||||||
|
if (avgTimePerBlock == 0.0) {
|
||||||
|
avgTimePerBlock = timeMs;
|
||||||
|
} else {
|
||||||
|
avgTimePerBlock = (avgTimePerBlock * 0.9) + (timeMs * 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastUpdated = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records a near-miss event (landed close to block edge).
|
||||||
|
*/
|
||||||
|
public void recordNearMiss() {
|
||||||
|
nearMissCount++;
|
||||||
|
lastUpdated = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the frequency of a specific jump type.
|
||||||
|
*
|
||||||
|
* @param jumpType The jump type to query
|
||||||
|
* @return Percentage (0.0 to 1.0) of total jumps
|
||||||
|
*/
|
||||||
|
public double getJumpTypeFrequency(@NotNull String jumpType) {
|
||||||
|
if (totalJumps == 0) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
return jumpTypeStats.getOrDefault(jumpType, 0) / (double) totalJumps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets near-miss rate as percentage of total jumps.
|
||||||
|
*
|
||||||
|
* @return Near-miss rate (0.0 to 1.0)
|
||||||
|
*/
|
||||||
|
public double getNearMissRate() {
|
||||||
|
if (totalJumps == 0) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
return nearMissCount / (double) totalJumps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates success rate based on total jumps.
|
||||||
|
* Assumes all recorded jumps are successful (falls are tracked separately).
|
||||||
|
*
|
||||||
|
* @return Success rate (0.0 to 1.0)
|
||||||
|
*/
|
||||||
|
public double getSuccessRate() {
|
||||||
|
// Since we only record successful jumps, success rate is based on
|
||||||
|
// the assumption that player has some failures not tracked here
|
||||||
|
// For now, return 1.0 if we have jumps, or use near-miss rate as proxy
|
||||||
|
if (totalJumps == 0) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
// High near-miss rate indicates good performance
|
||||||
|
return Math.min(1.0, 0.7 + (getNearMissRate() * 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the number of successful jumps (same as total jumps for now).
|
||||||
|
*
|
||||||
|
* @return Number of successful jumps
|
||||||
|
*/
|
||||||
|
public int getSuccessfulJumps() {
|
||||||
|
return totalJumps;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public UUID getPlayerUuid() {
|
||||||
|
return playerUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getAvgTimePerBlock() {
|
||||||
|
return avgTimePerBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public Map<String, Integer> getJumpTypeStats() {
|
||||||
|
return new HashMap<>(jumpTypeStats);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getNearMissCount() {
|
||||||
|
return nearMissCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTotalJumps() {
|
||||||
|
return totalJumps;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLastUpdated() {
|
||||||
|
return lastUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setters for deserialization
|
||||||
|
|
||||||
|
public void setAvgTimePerBlock(double avgTimePerBlock) {
|
||||||
|
this.avgTimePerBlock = avgTimePerBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNearMissCount(int nearMissCount) {
|
||||||
|
this.nearMissCount = nearMissCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotalJumps(int totalJumps) {
|
||||||
|
this.totalJumps = totalJumps;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastUpdated(long lastUpdated) {
|
||||||
|
this.lastUpdated = lastUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void putJumpTypeStat(@NotNull String jumpType, int count) {
|
||||||
|
jumpTypeStats.put(jumpType, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
133
src/main/java/dev/loki/loparkour/adaptive/model/SkillRating.java
Normal file
133
src/main/java/dev/loki/loparkour/adaptive/model/SkillRating.java
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
package dev.loki.loparkour.adaptive.model;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a player's skill rating with confidence level.
|
||||||
|
* Used for adaptive difficulty calculation.
|
||||||
|
*/
|
||||||
|
public class SkillRating {
|
||||||
|
|
||||||
|
private final UUID playerUuid;
|
||||||
|
private double rating;
|
||||||
|
private double confidence;
|
||||||
|
private int sessionsCount;
|
||||||
|
private long lastUpdated;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new skill rating with default values.
|
||||||
|
*
|
||||||
|
* @param playerUuid The player's UUID
|
||||||
|
*/
|
||||||
|
public SkillRating(@NotNull UUID playerUuid) {
|
||||||
|
this.playerUuid = playerUuid;
|
||||||
|
this.rating = 1.0;
|
||||||
|
this.confidence = 0.0;
|
||||||
|
this.sessionsCount = 0;
|
||||||
|
this.lastUpdated = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a skill rating with specific values.
|
||||||
|
*
|
||||||
|
* @param playerUuid The player's UUID
|
||||||
|
* @param rating The skill rating (typically 0.5 to 2.0)
|
||||||
|
* @param confidence The confidence level (0.0 to 1.0)
|
||||||
|
* @param sessionsCount Number of sessions used to calculate this rating
|
||||||
|
*/
|
||||||
|
public SkillRating(@NotNull UUID playerUuid, double rating, double confidence, int sessionsCount) {
|
||||||
|
this.playerUuid = playerUuid;
|
||||||
|
this.rating = clampRating(rating);
|
||||||
|
this.confidence = clampConfidence(confidence);
|
||||||
|
this.sessionsCount = sessionsCount;
|
||||||
|
this.lastUpdated = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the skill rating based on performance.
|
||||||
|
*
|
||||||
|
* @param performanceScore Performance score from recent session (0.0 to 2.0)
|
||||||
|
* @param weight Weight of this update (0.0 to 1.0)
|
||||||
|
*/
|
||||||
|
public void updateRating(double performanceScore, double weight) {
|
||||||
|
rating = (rating * (1.0 - weight)) + (performanceScore * weight);
|
||||||
|
rating = clampRating(rating);
|
||||||
|
|
||||||
|
sessionsCount++;
|
||||||
|
confidence = Math.min(1.0, confidence + 0.05);
|
||||||
|
|
||||||
|
lastUpdated = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the effective rating adjusted by confidence.
|
||||||
|
* Low confidence ratings are pulled toward 1.0 (neutral).
|
||||||
|
*
|
||||||
|
* @return Confidence-adjusted rating
|
||||||
|
*/
|
||||||
|
public double getEffectiveRating() {
|
||||||
|
return (rating * confidence) + (1.0 * (1.0 - confidence));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if this rating is reliable enough for adaptive difficulty.
|
||||||
|
*
|
||||||
|
* @return True if confidence is above threshold
|
||||||
|
*/
|
||||||
|
public boolean isReliable() {
|
||||||
|
return confidence >= 0.3 && sessionsCount >= 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double clampRating(double value) {
|
||||||
|
return Math.max(0.5, Math.min(2.0, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private double clampConfidence(double value) {
|
||||||
|
return Math.max(0.0, Math.min(1.0, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public UUID getPlayerUuid() {
|
||||||
|
return playerUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getRating() {
|
||||||
|
return rating;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getConfidence() {
|
||||||
|
return confidence;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSessionsCount() {
|
||||||
|
return sessionsCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLastUpdated() {
|
||||||
|
return lastUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRating(double rating) {
|
||||||
|
this.rating = clampRating(rating);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConfidence(double confidence) {
|
||||||
|
this.confidence = clampConfidence(confidence);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessionsCount(int sessionsCount) {
|
||||||
|
this.sessionsCount = sessionsCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastUpdated(long lastUpdated) {
|
||||||
|
this.lastUpdated = lastUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("SkillRating{rating=%.2f, confidence=%.2f, sessions=%d}",
|
||||||
|
rating, confidence, sessionsCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,185 @@
|
||||||
|
package dev.loki.loparkour.adaptive.storage;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import dev.loki.loparkour.LoParkour;
|
||||||
|
import dev.loki.loparkour.adaptive.model.PlayerMetrics;
|
||||||
|
import dev.loki.loparkour.adaptive.model.SkillRating;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileReader;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON file-based implementation of StatsRepository.
|
||||||
|
* Stores data in playerdata/<uuid>.json files.
|
||||||
|
* Saves only when player leaves to minimize I/O.
|
||||||
|
*/
|
||||||
|
public class FileStatsStorage implements StatsRepository {
|
||||||
|
|
||||||
|
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||||
|
private final File dataFolder;
|
||||||
|
|
||||||
|
public FileStatsStorage() {
|
||||||
|
this.dataFolder = new File(LoParkour.getPlugin().getDataFolder(), "playerdata");
|
||||||
|
if (!dataFolder.exists()) {
|
||||||
|
dataFolder.mkdirs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public SkillRating loadSkillRating(@NotNull UUID playerUuid) {
|
||||||
|
JsonObject data = loadPlayerData(playerUuid);
|
||||||
|
if (data == null || !data.has("skillRating")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject ratingObj = data.getAsJsonObject("skillRating");
|
||||||
|
double rating = ratingObj.get("rating").getAsDouble();
|
||||||
|
double confidence = ratingObj.get("confidence").getAsDouble();
|
||||||
|
int sessions = ratingObj.get("sessionsCount").getAsInt();
|
||||||
|
|
||||||
|
return new SkillRating(playerUuid, rating, confidence, sessions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveSkillRating(@NotNull SkillRating rating) {
|
||||||
|
JsonObject data = loadPlayerData(rating.getPlayerUuid());
|
||||||
|
if (data == null) {
|
||||||
|
data = new JsonObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject ratingObj = new JsonObject();
|
||||||
|
ratingObj.addProperty("rating", rating.getRating());
|
||||||
|
ratingObj.addProperty("confidence", rating.getConfidence());
|
||||||
|
ratingObj.addProperty("sessionsCount", rating.getSessionsCount());
|
||||||
|
ratingObj.addProperty("lastUpdated", rating.getLastUpdated());
|
||||||
|
|
||||||
|
data.add("skillRating", ratingObj);
|
||||||
|
savePlayerData(rating.getPlayerUuid(), data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public PlayerMetrics loadMetrics(@NotNull UUID playerUuid) {
|
||||||
|
JsonObject data = loadPlayerData(playerUuid);
|
||||||
|
if (data == null || !data.has("metrics")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject metricsObj = data.getAsJsonObject("metrics");
|
||||||
|
PlayerMetrics metrics = new PlayerMetrics(playerUuid);
|
||||||
|
|
||||||
|
metrics.setAvgTimePerBlock(metricsObj.get("avgTimePerBlock").getAsDouble());
|
||||||
|
metrics.setNearMissCount(metricsObj.get("nearMissCount").getAsInt());
|
||||||
|
metrics.setTotalJumps(metricsObj.get("totalJumps").getAsInt());
|
||||||
|
metrics.setLastUpdated(metricsObj.get("lastUpdated").getAsLong());
|
||||||
|
|
||||||
|
JsonObject jumpStats = metricsObj.getAsJsonObject("jumpTypeStats");
|
||||||
|
for (Map.Entry<String, com.google.gson.JsonElement> entry : jumpStats.entrySet()) {
|
||||||
|
metrics.putJumpTypeStat(entry.getKey(), entry.getValue().getAsInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
return metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveMetrics(@NotNull PlayerMetrics metrics) {
|
||||||
|
JsonObject data = loadPlayerData(metrics.getPlayerUuid());
|
||||||
|
if (data == null) {
|
||||||
|
data = new JsonObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject metricsObj = new JsonObject();
|
||||||
|
metricsObj.addProperty("avgTimePerBlock", metrics.getAvgTimePerBlock());
|
||||||
|
metricsObj.addProperty("nearMissCount", metrics.getNearMissCount());
|
||||||
|
metricsObj.addProperty("totalJumps", metrics.getTotalJumps());
|
||||||
|
metricsObj.addProperty("lastUpdated", metrics.getLastUpdated());
|
||||||
|
|
||||||
|
JsonObject jumpStats = new JsonObject();
|
||||||
|
for (Map.Entry<String, Integer> entry : metrics.getJumpTypeStats().entrySet()) {
|
||||||
|
jumpStats.addProperty(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
metricsObj.add("jumpTypeStats", jumpStats);
|
||||||
|
|
||||||
|
data.add("metrics", metricsObj);
|
||||||
|
savePlayerData(metrics.getPlayerUuid(), data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void incrementJumps(@NotNull UUID playerUuid, int amount) {
|
||||||
|
JsonObject data = loadPlayerData(playerUuid);
|
||||||
|
if (data == null) {
|
||||||
|
data = new JsonObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
int current = data.has("totalJumps") ? data.get("totalJumps").getAsInt() : 0;
|
||||||
|
data.addProperty("totalJumps", current + amount);
|
||||||
|
savePlayerData(playerUuid, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void incrementFalls(@NotNull UUID playerUuid, int amount) {
|
||||||
|
JsonObject data = loadPlayerData(playerUuid);
|
||||||
|
if (data == null) {
|
||||||
|
data = new JsonObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
int current = data.has("totalFalls") ? data.get("totalFalls").getAsInt() : 0;
|
||||||
|
data.addProperty("totalFalls", current + amount);
|
||||||
|
savePlayerData(playerUuid, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateLongestStreak(@NotNull UUID playerUuid, int streak) {
|
||||||
|
JsonObject data = loadPlayerData(playerUuid);
|
||||||
|
if (data == null) {
|
||||||
|
data = new JsonObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
int current = data.has("longestStreak") ? data.get("longestStreak").getAsInt() : 0;
|
||||||
|
if (streak > current) {
|
||||||
|
data.addProperty("longestStreak", streak);
|
||||||
|
savePlayerData(playerUuid, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
// No resources to close for file storage
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private JsonObject loadPlayerData(@NotNull UUID playerUuid) {
|
||||||
|
File file = new File(dataFolder, playerUuid.toString() + ".json");
|
||||||
|
if (!file.exists()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try (FileReader reader = new FileReader(file)) {
|
||||||
|
return GSON.fromJson(reader, JsonObject.class);
|
||||||
|
} catch (IOException e) {
|
||||||
|
LoParkour.getPlugin().getLogger().warning(
|
||||||
|
"Failed to load player data for " + playerUuid + ": " + e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void savePlayerData(@NotNull UUID playerUuid, @NotNull JsonObject data) {
|
||||||
|
File file = new File(dataFolder, playerUuid.toString() + ".json");
|
||||||
|
|
||||||
|
try (FileWriter writer = new FileWriter(file)) {
|
||||||
|
GSON.toJson(data, writer);
|
||||||
|
} catch (IOException e) {
|
||||||
|
LoParkour.getPlugin().getLogger().warning(
|
||||||
|
"Failed to save player data for " + playerUuid + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
package dev.loki.loparkour.adaptive.storage;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.adaptive.model.PlayerMetrics;
|
||||||
|
import dev.loki.loparkour.adaptive.model.SkillRating;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No-op implementation of StatsRepository.
|
||||||
|
* Used as a placeholder until the full storage implementation is ready.
|
||||||
|
*
|
||||||
|
* This allows MetricsCollector to function without throwing errors,
|
||||||
|
* but metrics will not be persisted between server restarts.
|
||||||
|
*/
|
||||||
|
public class NoOpStatsRepository implements StatsRepository {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public SkillRating loadSkillRating(@NotNull UUID playerUuid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveSkillRating(@NotNull SkillRating rating) {
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public PlayerMetrics loadMetrics(@NotNull UUID playerUuid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveMetrics(@NotNull PlayerMetrics metrics) {
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void incrementJumps(@NotNull UUID playerUuid, int amount) {
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void incrementFalls(@NotNull UUID playerUuid, int amount) {
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateLongestStreak(@NotNull UUID playerUuid, int streak) {
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,173 @@
|
||||||
|
package dev.loki.loparkour.adaptive.storage;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.LoParkour;
|
||||||
|
import dev.loki.loparkour.adaptive.model.PlayerMetrics;
|
||||||
|
import dev.loki.loparkour.adaptive.model.SkillRating;
|
||||||
|
import dev.loki.loparkour.storage.sql.SQLConnectionManager;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQL-based implementation of StatsRepository using loparkour_player_stats table.
|
||||||
|
* Uses batch updates for optimization.
|
||||||
|
*/
|
||||||
|
public class SQLStatsStorage implements StatsRepository {
|
||||||
|
|
||||||
|
private final SQLConnectionManager connectionManager;
|
||||||
|
private final ConcurrentHashMap<UUID, PendingUpdate> pendingUpdates;
|
||||||
|
private static final int BATCH_SIZE = 50;
|
||||||
|
|
||||||
|
public SQLStatsStorage(@NotNull SQLConnectionManager connectionManager) {
|
||||||
|
this.connectionManager = connectionManager;
|
||||||
|
this.pendingUpdates = new ConcurrentHashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public SkillRating loadSkillRating(@NotNull UUID playerUuid) {
|
||||||
|
String query = "SELECT skill_rating, sessions_count FROM loparkour_player_stats WHERE player_uuid = ?";
|
||||||
|
|
||||||
|
try (Connection conn = connectionManager.getConnection();
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(query)) {
|
||||||
|
|
||||||
|
stmt.setString(1, playerUuid.toString());
|
||||||
|
ResultSet rs = stmt.executeQuery();
|
||||||
|
|
||||||
|
if (rs.next()) {
|
||||||
|
double rating = rs.getDouble("skill_rating");
|
||||||
|
int sessions = rs.getInt("sessions_count");
|
||||||
|
double confidence = Math.min(1.0, sessions * 0.05);
|
||||||
|
return new SkillRating(playerUuid, rating, confidence, sessions);
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
LoParkour.getPlugin().getLogger().warning("Failed to load skill rating: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveSkillRating(@NotNull SkillRating rating) {
|
||||||
|
String query = "INSERT INTO loparkour_player_stats (player_uuid, skill_rating, sessions_count) " +
|
||||||
|
"VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE skill_rating = ?, sessions_count = ?";
|
||||||
|
|
||||||
|
try (Connection conn = connectionManager.getConnection();
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(query)) {
|
||||||
|
|
||||||
|
stmt.setString(1, rating.getPlayerUuid().toString());
|
||||||
|
stmt.setDouble(2, rating.getRating());
|
||||||
|
stmt.setInt(3, rating.getSessionsCount());
|
||||||
|
stmt.setDouble(4, rating.getRating());
|
||||||
|
stmt.setInt(5, rating.getSessionsCount());
|
||||||
|
|
||||||
|
stmt.executeUpdate();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
LoParkour.getPlugin().getLogger().warning("Failed to save skill rating: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public PlayerMetrics loadMetrics(@NotNull UUID playerUuid) {
|
||||||
|
// Metrics are stored in JSON files, not SQL
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveMetrics(@NotNull PlayerMetrics metrics) {
|
||||||
|
// Metrics are stored in JSON files, not SQL
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void incrementJumps(@NotNull UUID playerUuid, int amount) {
|
||||||
|
PendingUpdate update = pendingUpdates.computeIfAbsent(playerUuid, k -> new PendingUpdate());
|
||||||
|
update.jumps += amount;
|
||||||
|
|
||||||
|
if (pendingUpdates.size() >= BATCH_SIZE) {
|
||||||
|
flushBatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void incrementFalls(@NotNull UUID playerUuid, int amount) {
|
||||||
|
PendingUpdate update = pendingUpdates.computeIfAbsent(playerUuid, k -> new PendingUpdate());
|
||||||
|
update.falls += amount;
|
||||||
|
|
||||||
|
if (pendingUpdates.size() >= BATCH_SIZE) {
|
||||||
|
flushBatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateLongestStreak(@NotNull UUID playerUuid, int streak) {
|
||||||
|
PendingUpdate update = pendingUpdates.computeIfAbsent(playerUuid, k -> new PendingUpdate());
|
||||||
|
update.longestStreak = Math.max(update.longestStreak, streak);
|
||||||
|
|
||||||
|
if (pendingUpdates.size() >= BATCH_SIZE) {
|
||||||
|
flushBatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flushes all pending updates to database using batch operations.
|
||||||
|
*/
|
||||||
|
public void flushBatch() {
|
||||||
|
if (pendingUpdates.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String query = "INSERT INTO loparkour_player_stats (player_uuid, total_jumps, total_falls, longest_streak) " +
|
||||||
|
"VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE " +
|
||||||
|
"total_jumps = total_jumps + VALUES(total_jumps), " +
|
||||||
|
"total_falls = total_falls + VALUES(total_falls), " +
|
||||||
|
"longest_streak = GREATEST(longest_streak, VALUES(longest_streak))";
|
||||||
|
|
||||||
|
try (Connection conn = connectionManager.getConnection();
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(query)) {
|
||||||
|
|
||||||
|
List<UUID> processed = new ArrayList<>();
|
||||||
|
|
||||||
|
for (var entry : pendingUpdates.entrySet()) {
|
||||||
|
UUID uuid = entry.getKey();
|
||||||
|
PendingUpdate update = entry.getValue();
|
||||||
|
|
||||||
|
stmt.setString(1, uuid.toString());
|
||||||
|
stmt.setInt(2, update.jumps);
|
||||||
|
stmt.setInt(3, update.falls);
|
||||||
|
stmt.setInt(4, update.longestStreak);
|
||||||
|
stmt.addBatch();
|
||||||
|
|
||||||
|
processed.add(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt.executeBatch();
|
||||||
|
|
||||||
|
for (UUID uuid : processed) {
|
||||||
|
pendingUpdates.remove(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (SQLException e) {
|
||||||
|
LoParkour.getPlugin().getLogger().warning("Failed to flush batch updates: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
flushBatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class PendingUpdate {
|
||||||
|
int jumps = 0;
|
||||||
|
int falls = 0;
|
||||||
|
int longestStreak = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
package dev.loki.loparkour.adaptive.storage;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.adaptive.model.PlayerMetrics;
|
||||||
|
import dev.loki.loparkour.adaptive.model.SkillRating;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository interface for storing and retrieving player statistics.
|
||||||
|
* Implementations can use SQL or file-based storage.
|
||||||
|
*/
|
||||||
|
public interface StatsRepository {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads skill rating for a player.
|
||||||
|
*
|
||||||
|
* @param playerUuid The player's UUID
|
||||||
|
* @return SkillRating or null if not found
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
SkillRating loadSkillRating(@NotNull UUID playerUuid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves skill rating for a player.
|
||||||
|
*
|
||||||
|
* @param rating The skill rating to save
|
||||||
|
*/
|
||||||
|
void saveSkillRating(@NotNull SkillRating rating);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads detailed metrics for a player.
|
||||||
|
*
|
||||||
|
* @param playerUuid The player's UUID
|
||||||
|
* @return PlayerMetrics or null if not found
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
PlayerMetrics loadMetrics(@NotNull UUID playerUuid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves detailed metrics for a player.
|
||||||
|
*
|
||||||
|
* @param metrics The metrics to save
|
||||||
|
*/
|
||||||
|
void saveMetrics(@NotNull PlayerMetrics metrics);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments total jumps counter for a player.
|
||||||
|
*
|
||||||
|
* @param playerUuid The player's UUID
|
||||||
|
* @param amount Amount to increment
|
||||||
|
*/
|
||||||
|
void incrementJumps(@NotNull UUID playerUuid, int amount);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments total falls counter for a player.
|
||||||
|
*
|
||||||
|
* @param playerUuid The player's UUID
|
||||||
|
* @param amount Amount to increment
|
||||||
|
*/
|
||||||
|
void incrementFalls(@NotNull UUID playerUuid, int amount);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates longest streak for a player if new streak is longer.
|
||||||
|
*
|
||||||
|
* @param playerUuid The player's UUID
|
||||||
|
* @param streak The new streak value
|
||||||
|
*/
|
||||||
|
void updateLongestStreak(@NotNull UUID playerUuid, int streak);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the repository and releases resources.
|
||||||
|
*/
|
||||||
|
void close();
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
package dev.loki.loparkour.api;
|
package dev.loki.loparkour.api.core;
|
||||||
|
|
||||||
import dev.loki.loparkour.api.event.ParkourBlockGenerateEvent;
|
import dev.loki.loparkour.api.event.ParkourBlockGenerateEvent;
|
||||||
import dev.loki.loparkour.player.ParkourPlayer;
|
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||||
import dev.loki.loparkour.player.ParkourUser;
|
import dev.loki.loparkour.player.core.ParkourUser;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
|
@ -14,7 +14,7 @@ import org.jetbrains.annotations.Nullable;
|
||||||
* <li>For player spectating, please view {@link dev.efnilite.LoParkour.player.ParkourSpectator}.</li>
|
* <li>For player spectating, please view {@link dev.efnilite.LoParkour.player.ParkourSpectator}.</li>
|
||||||
* <li>For events, please view {@link ParkourBlockGenerateEvent} and others in the events package.</li>
|
* <li>For events, please view {@link ParkourBlockGenerateEvent} and others in the events package.</li>
|
||||||
* <li>For Sessions, please view {@link dev.efnilite.LoParkour.session.Session}.</li>
|
* <li>For Sessions, please view {@link dev.efnilite.LoParkour.session.Session}.</li>
|
||||||
* <li>For Schematics, please view {@link dev.efnilite.LoParkour.schematic.LPSchematic}</li>
|
* <li>For jump structures, see {@link dev.loki.loparkour.schematic.core.ParkourSchematic}</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*/
|
*/
|
||||||
public class ParkourAPI {
|
public class ParkourAPI {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package dev.loki.loparkour.api;
|
package dev.loki.loparkour.api.core;
|
||||||
|
|
||||||
import dev.loki.loparkour.mode.Mode;
|
import dev.loki.loparkour.mode.base.Mode;
|
||||||
import dev.loki.loparkour.style.Style;
|
import dev.loki.loparkour.style.core.Style;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package dev.loki.loparkour.api.event;
|
package dev.loki.loparkour.api.event;
|
||||||
|
|
||||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||||
import dev.loki.loparkour.player.ParkourPlayer;
|
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.event.Event;
|
import org.bukkit.event.Event;
|
||||||
import org.bukkit.event.HandlerList;
|
import org.bukkit.event.HandlerList;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package dev.loki.loparkour.api.event;
|
package dev.loki.loparkour.api.event;
|
||||||
|
|
||||||
import dev.loki.loparkour.player.ParkourPlayer;
|
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.event.Event;
|
import org.bukkit.event.Event;
|
||||||
import org.bukkit.event.HandlerList;
|
import org.bukkit.event.HandlerList;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package dev.loki.loparkour.api.event;
|
package dev.loki.loparkour.api.event;
|
||||||
|
|
||||||
import dev.loki.loparkour.player.ParkourUser;
|
import dev.loki.loparkour.player.core.ParkourUser;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.event.Event;
|
import org.bukkit.event.Event;
|
||||||
import org.bukkit.event.HandlerList;
|
import org.bukkit.event.HandlerList;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package dev.loki.loparkour.api.event;
|
package dev.loki.loparkour.api.event;
|
||||||
|
|
||||||
import dev.loki.loparkour.player.ParkourUser;
|
import dev.loki.loparkour.player.core.ParkourUser;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.event.Event;
|
import org.bukkit.event.Event;
|
||||||
import org.bukkit.event.HandlerList;
|
import org.bukkit.event.HandlerList;
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,24 @@
|
||||||
package dev.loki.loparkour.api.event;
|
package dev.loki.loparkour.api.event;
|
||||||
|
|
||||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||||
import dev.loki.loparkour.player.ParkourPlayer;
|
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||||
import dev.loki.loparkour.schematic.lpschem.LPSchematic;
|
import dev.loki.loparkour.schematic.core.ParkourSchematic;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.event.Event;
|
import org.bukkit.event.Event;
|
||||||
import org.bukkit.event.HandlerList;
|
import org.bukkit.event.HandlerList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets called when a new jump is generated. Read-only.
|
* Gets called when a schematic jump is pasted. Read-only.
|
||||||
*
|
|
||||||
* @author loki
|
|
||||||
* @since 5.0.0
|
|
||||||
*/
|
*/
|
||||||
public class ParkourSchematicGenerateEvent extends Event {
|
public class ParkourSchematicGenerateEvent extends Event {
|
||||||
|
|
||||||
private static final HandlerList HANDLERS = new HandlerList();
|
private static final HandlerList HANDLERS = new HandlerList();
|
||||||
|
|
||||||
public final LPSchematic schematic;
|
public final ParkourSchematic schematic;
|
||||||
public final ParkourGenerator generator;
|
public final ParkourGenerator generator;
|
||||||
public final ParkourPlayer player;
|
public final ParkourPlayer player;
|
||||||
|
|
||||||
public ParkourSchematicGenerateEvent(LPSchematic schematic, ParkourGenerator generator, ParkourPlayer player) {
|
public ParkourSchematicGenerateEvent(ParkourSchematic schematic, ParkourGenerator generator, ParkourPlayer player) {
|
||||||
this.schematic = schematic;
|
this.schematic = schematic;
|
||||||
this.generator = generator;
|
this.generator = generator;
|
||||||
this.player = player;
|
this.player = player;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package dev.loki.loparkour.api.event;
|
package dev.loki.loparkour.api.event;
|
||||||
|
|
||||||
import dev.loki.loparkour.player.ParkourPlayer;
|
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.event.Event;
|
import org.bukkit.event.Event;
|
||||||
import org.bukkit.event.HandlerList;
|
import org.bukkit.event.HandlerList;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package dev.loki.loparkour.api.event;
|
package dev.loki.loparkour.api.event;
|
||||||
|
|
||||||
import dev.loki.loparkour.player.ParkourSpectator;
|
import dev.loki.loparkour.player.spectator.ParkourSpectator;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.event.Event;
|
import org.bukkit.event.Event;
|
||||||
import org.bukkit.event.HandlerList;
|
import org.bukkit.event.HandlerList;
|
||||||
|
|
|
||||||
133
src/main/java/dev/loki/loparkour/bootstrap/PluginBootstrap.java
Normal file
133
src/main/java/dev/loki/loparkour/bootstrap/PluginBootstrap.java
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
package dev.loki.loparkour.bootstrap;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.LoParkour;
|
||||||
|
import dev.loki.loparkour.adaptive.bootstrap.AdaptiveServices;
|
||||||
|
import dev.loki.loparkour.api.core.Registry;
|
||||||
|
import dev.loki.loparkour.command.LoParkourCommand;
|
||||||
|
import dev.loki.loparkour.config.core.Config;
|
||||||
|
import dev.loki.loparkour.config.locale.Locales;
|
||||||
|
import dev.loki.loparkour.config.options.Option;
|
||||||
|
import dev.loki.loparkour.hook.holo.HoloHook;
|
||||||
|
import dev.loki.loparkour.hook.papi.PAPIHook;
|
||||||
|
import dev.loki.loparkour.listener.gameplay.ParkourRestrictionListener;
|
||||||
|
import dev.loki.loparkour.listener.schematic.SchematicWandListener;
|
||||||
|
import dev.loki.loparkour.listener.player.PlayerConnectionListener;
|
||||||
|
import dev.loki.loparkour.mode.base.Modes;
|
||||||
|
import dev.loki.loparkour.mode.impl.CoopMode;
|
||||||
|
import dev.loki.loparkour.mode.impl.DefaultMode;
|
||||||
|
import dev.loki.loparkour.mode.impl.ElytraMode;
|
||||||
|
import dev.loki.loparkour.mode.impl.GravityShiftMode;
|
||||||
|
import dev.loki.loparkour.mode.impl.HardcoreMode;
|
||||||
|
import dev.loki.loparkour.mode.impl.RaceMode;
|
||||||
|
import dev.loki.loparkour.mode.impl.SpectatorMode;
|
||||||
|
import dev.loki.loparkour.mode.impl.SpeedrunMode;
|
||||||
|
import dev.loki.loparkour.reward.core.Rewards;
|
||||||
|
import dev.loki.loparkour.schematic.core.SchematicManager;
|
||||||
|
import dev.loki.loparkour.world.core.World;
|
||||||
|
import org.bstats.bukkit.Metrics;
|
||||||
|
import org.bstats.charts.SimplePie;
|
||||||
|
import org.bstats.charts.SingleLineChart;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wires plugin subsystems during {@link LoParkour#enable()} and {@link LoParkour#disable()}.
|
||||||
|
*/
|
||||||
|
public final class PluginBootstrap {
|
||||||
|
|
||||||
|
private PluginBootstrap() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void enable(@NotNull LoParkour plugin) {
|
||||||
|
Config.reload(true);
|
||||||
|
AdaptiveServices.init(plugin);
|
||||||
|
loadSchematics(plugin);
|
||||||
|
registerModes();
|
||||||
|
registerHooks(plugin);
|
||||||
|
setupWorld(plugin);
|
||||||
|
registerEventsAndCommands(plugin);
|
||||||
|
setupMetrics(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void disable(@NotNull LoParkour plugin) {
|
||||||
|
plugin.runShutdownSequence();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void loadSchematics(@NotNull LoParkour plugin) {
|
||||||
|
extractSchematicDocs(plugin);
|
||||||
|
SchematicManager manager = new SchematicManager();
|
||||||
|
manager.loadAll();
|
||||||
|
plugin.setSchematicManager(manager);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void extractSchematicDocs(@NotNull LoParkour plugin) {
|
||||||
|
java.io.File doc = LoParkour.getInFolder("docs/SCHEMATICS.md");
|
||||||
|
if (!doc.exists()) {
|
||||||
|
doc.getParentFile().mkdirs();
|
||||||
|
plugin.saveResource("docs/SCHEMATICS.md", false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void registerModes() {
|
||||||
|
Registry.register(new DefaultMode());
|
||||||
|
Registry.register(new SpectatorMode());
|
||||||
|
Registry.register(new SpeedrunMode());
|
||||||
|
Registry.register(new ElytraMode());
|
||||||
|
Registry.register(new RaceMode());
|
||||||
|
Registry.register(new CoopMode());
|
||||||
|
Registry.register(new GravityShiftMode());
|
||||||
|
Registry.register(new HardcoreMode());
|
||||||
|
Modes.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void registerHooks(@NotNull LoParkour plugin) {
|
||||||
|
if (plugin.getServer().getPluginManager().isPluginEnabled("HolographicDisplays")) {
|
||||||
|
plugin.getLogger().info("Registered Holographic Displays hook");
|
||||||
|
HoloHook.init();
|
||||||
|
}
|
||||||
|
if (plugin.getServer().getPluginManager().isPluginEnabled("PlaceholderAPI")) {
|
||||||
|
plugin.getLogger().info("Registered PlaceholderAPI hook");
|
||||||
|
PAPIHook hook = new PAPIHook();
|
||||||
|
hook.register();
|
||||||
|
plugin.setPlaceholderHook(hook);
|
||||||
|
}
|
||||||
|
if (Config.CONFIG.getBoolean("bungeecord.enabled")) {
|
||||||
|
plugin.getServer().getMessenger().registerOutgoingPluginChannel(plugin, "BungeeCord");
|
||||||
|
plugin.getLogger().info("Registered BungeeCord hook");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setupWorld(@NotNull LoParkour plugin) {
|
||||||
|
if (!Config.CONFIG.getBoolean("joining")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
plugin.loLogger().info("Creating parkour world...");
|
||||||
|
World.create();
|
||||||
|
plugin.loLogger().info("Parkour world created successfully!");
|
||||||
|
} catch (Exception ex) {
|
||||||
|
plugin.loLogger().error("Failed to create parkour world", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void registerEventsAndCommands(@NotNull LoParkour plugin) {
|
||||||
|
dev.lolib.gui.GUIManager.init(plugin);
|
||||||
|
|
||||||
|
var pm = plugin.getServer().getPluginManager();
|
||||||
|
pm.registerEvents(new PlayerConnectionListener(), plugin);
|
||||||
|
pm.registerEvents(new ParkourRestrictionListener(), plugin);
|
||||||
|
pm.registerEvents(new SchematicWandListener(), plugin);
|
||||||
|
LoParkourCommand cmd = new LoParkourCommand();
|
||||||
|
plugin.getCommand("LoParkour").setExecutor(cmd);
|
||||||
|
plugin.getCommand("LoParkour").setTabCompleter(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setupMetrics(@NotNull LoParkour plugin) {
|
||||||
|
Metrics metrics = new Metrics(plugin, 29754);
|
||||||
|
metrics.addCustomChart(new SimplePie("using_sql", () -> Boolean.toString(Option.SQL)));
|
||||||
|
metrics.addCustomChart(new SimplePie("using_rewards", () -> Boolean.toString(Rewards.REWARDS_ENABLED)));
|
||||||
|
metrics.addCustomChart(new SimplePie("locale_count", () -> Integer.toString(Locales.getLocaleCount())));
|
||||||
|
metrics.addCustomChart(new SingleLineChart("player_joins", () ->
|
||||||
|
dev.loki.loparkour.player.service.UserRegistry.getJoinCount()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,22 +1,23 @@
|
||||||
package dev.loki.loparkour;
|
package dev.loki.loparkour.command;
|
||||||
|
|
||||||
import dev.loki.loparkour.api.Registry;
|
import dev.loki.loparkour.LoParkour;
|
||||||
import dev.loki.loparkour.command.AdminCommandHandler;
|
import dev.loki.loparkour.api.core.Registry;
|
||||||
import dev.loki.loparkour.command.PlayerCommandHandler;
|
import dev.loki.loparkour.command.admin.AdminCommandHandler;
|
||||||
import dev.loki.loparkour.command.SchematicCommandHandler;
|
import dev.loki.loparkour.command.player.PlayerCommandHandler;
|
||||||
import dev.loki.loparkour.config.Locales;
|
import dev.loki.loparkour.command.schematic.SchematicCommandHandler;
|
||||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
import dev.loki.loparkour.config.locale.Locales;
|
||||||
import dev.loki.loparkour.menu.Menus;
|
import dev.loki.loparkour.leaderboard.core.Leaderboard;
|
||||||
import dev.loki.loparkour.menu.ParkourOption;
|
import dev.loki.loparkour.menu.core.Menus;
|
||||||
import dev.loki.loparkour.mode.Mode;
|
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||||
import dev.loki.loparkour.mode.Modes;
|
import dev.loki.loparkour.mode.base.Mode;
|
||||||
import dev.loki.loparkour.mode.MultiMode;
|
import dev.loki.loparkour.mode.base.Modes;
|
||||||
import dev.loki.loparkour.player.ParkourPlayer;
|
import dev.loki.loparkour.mode.base.MultiMode;
|
||||||
import dev.loki.loparkour.player.ParkourUser;
|
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||||
import dev.loki.loparkour.session.Session;
|
import dev.loki.loparkour.player.core.ParkourUser;
|
||||||
import dev.loki.loparkour.util.ColorUtil;
|
import dev.loki.loparkour.schematic.core.SchematicManager;
|
||||||
|
import dev.loki.loparkour.session.core.Session;
|
||||||
|
import dev.loki.loparkour.util.text.ColorUtil;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.ChatColor;
|
|
||||||
import org.bukkit.command.Command;
|
import org.bukkit.command.Command;
|
||||||
import org.bukkit.command.CommandExecutor;
|
import org.bukkit.command.CommandExecutor;
|
||||||
import org.bukkit.command.CommandSender;
|
import org.bukkit.command.CommandSender;
|
||||||
|
|
@ -38,6 +39,11 @@ import java.util.logging.Level;
|
||||||
*/
|
*/
|
||||||
public class LoParkourCommand implements CommandExecutor, TabCompleter {
|
public class LoParkourCommand implements CommandExecutor, TabCompleter {
|
||||||
|
|
||||||
|
private static final List<String> SCHEMATIC_DIFFICULTIES = List.of("0.0", "0.25", "0.5", "0.75", "1.0");
|
||||||
|
private static final List<String> SCHEMATIC_SUBCOMMANDS = List.of(
|
||||||
|
"wand", "pos1", "pos2", "create", "convert", "paste", "list", "reload"
|
||||||
|
);
|
||||||
|
|
||||||
private final PlayerCommandHandler player = new PlayerCommandHandler();
|
private final PlayerCommandHandler player = new PlayerCommandHandler();
|
||||||
private final AdminCommandHandler admin = new AdminCommandHandler(player);
|
private final AdminCommandHandler admin = new AdminCommandHandler(player);
|
||||||
|
|
||||||
|
|
@ -52,13 +58,14 @@ public class LoParkourCommand implements CommandExecutor, TabCompleter {
|
||||||
case 1 -> player.handle(args[0], sender, p);
|
case 1 -> player.handle(args[0], sender, p);
|
||||||
case 2 -> handle2(args[0], args[1], sender, p);
|
case 2 -> handle2(args[0], args[1], sender, p);
|
||||||
case 3 -> handle3(args[0], args[1], args[2], sender, p);
|
case 3 -> handle3(args[0], args[1], args[2], sender, p);
|
||||||
default -> sender.sendMessage(ChatColor.GRAY + "Too many arguments. Try /parkour help");
|
case 4 -> handle4(args[0], args[1], args[2], args[3], sender, p);
|
||||||
|
default -> Locales.send(sender, "commands.too_many_args");
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
LoParkour.getPlugin().getLogger().log(Level.SEVERE,
|
LoParkour.getPlugin().getLogger().log(Level.SEVERE,
|
||||||
"LoParkour command failed (" + label + " " + String.join(" ", args) + ")", t);
|
"LoParkour command failed (" + label + " " + String.join(" ", args) + ")", t);
|
||||||
sender.sendMessage(ChatColor.RED + "LoParkour: command error — see server console.");
|
Locales.send(sender, "commands.error");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -76,6 +83,11 @@ public class LoParkourCommand implements CommandExecutor, TabCompleter {
|
||||||
|
|
||||||
if (p == null) return;
|
if (p == null) return;
|
||||||
|
|
||||||
|
if (a1.equalsIgnoreCase("create") && p.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||||
|
SchematicCommandHandler.handleCreateDifficultyOnly(a2, sender, p, player);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (a1.toLowerCase()) {
|
switch (a1.toLowerCase()) {
|
||||||
case "join" -> handleJoin(a2, sender, p);
|
case "join" -> handleJoin(a2, sender, p);
|
||||||
case "leaderboard" -> handleLeaderboard(a2, sender, p);
|
case "leaderboard" -> handleLeaderboard(a2, sender, p);
|
||||||
|
|
@ -93,9 +105,22 @@ public class LoParkourCommand implements CommandExecutor, TabCompleter {
|
||||||
|
|
||||||
private void handle3(String a1, String a2, String a3, CommandSender sender, @Nullable Player p) {
|
private void handle3(String a1, String a2, String a3, CommandSender sender, @Nullable Player p) {
|
||||||
if (p == null) return;
|
if (p == null) return;
|
||||||
if (!a1.equalsIgnoreCase("schematic")) return;
|
|
||||||
if (!p.hasPermission(ParkourOption.ADMIN.permission)) return;
|
if (!p.hasPermission(ParkourOption.ADMIN.permission)) return;
|
||||||
SchematicCommandHandler.handleSubcommandWithName(a2, a3, sender, p, player);
|
|
||||||
|
if (a1.equalsIgnoreCase("schematic")) {
|
||||||
|
if (a2.equalsIgnoreCase("create")) {
|
||||||
|
SchematicCommandHandler.handleCreateDifficultyOnly(a3, sender, p, player);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SchematicCommandHandler.handleSubcommandWithName(a2, a3, sender, p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handle4(String a1, String a2, String a3, String a4, CommandSender sender, @Nullable Player p) {
|
||||||
|
if (p == null) return;
|
||||||
|
if (!p.hasPermission(ParkourOption.ADMIN.permission)) return;
|
||||||
|
if (!a1.equalsIgnoreCase("schematic") || !a2.equalsIgnoreCase("create")) return;
|
||||||
|
SchematicCommandHandler.handleCreateWithDifficulty(a3, a4, sender, p, player);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── join logic ─────────────────────────────────────────────────────────────
|
// ── join logic ─────────────────────────────────────────────────────────────
|
||||||
|
|
@ -107,10 +132,10 @@ public class LoParkourCommand implements CommandExecutor, TabCompleter {
|
||||||
if (mode != null) { mode.create(p); return; }
|
if (mode != null) { mode.create(p); return; }
|
||||||
|
|
||||||
Player other = Bukkit.getPlayer(arg);
|
Player other = Bukkit.getPlayer(arg);
|
||||||
if (other == null) { send(sender, LoParkour.PREFIX + "Unknown player."); return; }
|
if (other == null) { send(sender, LoParkour.PREFIX + Locales.getString(sender, "commands.unknown_player")); return; }
|
||||||
|
|
||||||
ParkourPlayer pp = ParkourPlayer.getPlayer(other);
|
ParkourPlayer pp = ParkourPlayer.getPlayer(other);
|
||||||
if (pp == null) { send(sender, LoParkour.PREFIX + "That player isn't playing."); return; }
|
if (pp == null) { send(sender, LoParkour.PREFIX + Locales.getString(sender, "commands.not_playing")); return; }
|
||||||
|
|
||||||
ParkourUser user = ParkourUser.getUser(p);
|
ParkourUser user = ParkourUser.getUser(p);
|
||||||
Session session = pp.session;
|
Session session = pp.session;
|
||||||
|
|
@ -156,7 +181,7 @@ public class LoParkourCommand implements CommandExecutor, TabCompleter {
|
||||||
if (ParkourOption.PLAY.mayPerform(sender)) completions.add("play");
|
if (ParkourOption.PLAY.mayPerform(sender)) completions.add("play");
|
||||||
if (ParkourOption.LEADERBOARDS.mayPerform(sender)) completions.add("leaderboard");
|
if (ParkourOption.LEADERBOARDS.mayPerform(sender)) completions.add("leaderboard");
|
||||||
if (sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
if (sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||||
completions.addAll(List.of("schematic", "reload", "forcejoin", "forceleave", "reset", "recoverinventory"));
|
completions.addAll(List.of("schematic", "create", "reload", "forcejoin", "forceleave", "reset", "recoverinventory"));
|
||||||
}
|
}
|
||||||
return filter(args[0], completions);
|
return filter(args[0], completions);
|
||||||
}
|
}
|
||||||
|
|
@ -172,7 +197,9 @@ public class LoParkourCommand implements CommandExecutor, TabCompleter {
|
||||||
} else if (a1.equals("leaderboard") && ParkourOption.LEADERBOARDS.mayPerform(sender)) {
|
} else if (a1.equals("leaderboard") && ParkourOption.LEADERBOARDS.mayPerform(sender)) {
|
||||||
Registry.getModes().forEach(m -> completions.add(m.getName()));
|
Registry.getModes().forEach(m -> completions.add(m.getName()));
|
||||||
} else if (a1.equals("schematic") && sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
} else if (a1.equals("schematic") && sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||||
completions.addAll(Arrays.asList("wand", "pos1", "pos2", "save", "paste", "list", "reload"));
|
completions.addAll(SCHEMATIC_SUBCOMMANDS);
|
||||||
|
} else if (a1.equals("create") && sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||||
|
completions.addAll(SCHEMATIC_DIFFICULTIES);
|
||||||
} else if ((a1.equals("forcejoin") || a1.equals("forceleave")) && sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
} else if ((a1.equals("forcejoin") || a1.equals("forceleave")) && sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||||
completions.add("everyone");
|
completions.add("everyone");
|
||||||
if (a1.equals("forcejoin")) completions.add("nearest");
|
if (a1.equals("forcejoin")) completions.add("nearest");
|
||||||
|
|
@ -183,9 +210,45 @@ public class LoParkourCommand implements CommandExecutor, TabCompleter {
|
||||||
return filter(args[1], completions);
|
return filter(args[1], completions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args.length == 3 && sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||||
|
String a1 = args[0].toLowerCase();
|
||||||
|
String a2 = args[1].toLowerCase();
|
||||||
|
if (a1.equals("schematic")) {
|
||||||
|
if (a2.equals("paste")) {
|
||||||
|
addLoadedSchematicIds(completions);
|
||||||
|
} else if (a2.equals("create")) {
|
||||||
|
completions.addAll(SCHEMATIC_DIFFICULTIES);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filter(args[2], completions);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.length == 4 && sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||||
|
String a1 = args[0].toLowerCase();
|
||||||
|
String a2 = args[1].toLowerCase();
|
||||||
|
if (a1.equals("schematic") && a2.equals("create")) {
|
||||||
|
completions.addAll(SCHEMATIC_DIFFICULTIES);
|
||||||
|
}
|
||||||
|
return filter(args[3], completions);
|
||||||
|
}
|
||||||
|
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void addLoadedSchematicIds(@NotNull List<String> completions) {
|
||||||
|
SchematicManager manager = LoParkour.getSchematicManager();
|
||||||
|
if (manager == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (String id : manager.getAll().keySet()) {
|
||||||
|
completions.add(id);
|
||||||
|
String shortId = SchematicManager.configKey(id);
|
||||||
|
if (!shortId.equalsIgnoreCase(id)) {
|
||||||
|
completions.add(shortId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── util ───────────────────────────────────────────────────────────────────
|
// ── util ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private List<String> filter(String input, List<String> options) {
|
private List<String> filter(String input, List<String> options) {
|
||||||
|
|
@ -1,172 +0,0 @@
|
||||||
package dev.loki.loparkour.command;
|
|
||||||
|
|
||||||
import dev.loki.loparkour.LoParkour;
|
|
||||||
import dev.loki.loparkour.schematic.lpschem.LPSchematic;
|
|
||||||
import dev.loki.loparkour.schematic.lpschem.LPSchematicBuilder;
|
|
||||||
import dev.loki.loparkour.util.ColorUtil;
|
|
||||||
import dev.loki.loparkour.util.Locations;
|
|
||||||
import dev.loki.loparkour.util.ParticleUtil;
|
|
||||||
import org.bukkit.Location;
|
|
||||||
import org.bukkit.Particle;
|
|
||||||
import org.bukkit.command.CommandSender;
|
|
||||||
import org.bukkit.entity.Player;
|
|
||||||
import org.bukkit.inventory.ItemStack;
|
|
||||||
import org.bukkit.inventory.meta.ItemMeta;
|
|
||||||
import org.bukkit.Material;
|
|
||||||
import org.bukkit.util.BoundingBox;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles /LoParkour schematic sub-commands (wand, pos1, pos2, save, paste, list, reload).
|
|
||||||
*/
|
|
||||||
public class SchematicCommandHandler {
|
|
||||||
|
|
||||||
public static final Map<Player, Location[]> selections = new HashMap<>();
|
|
||||||
|
|
||||||
private static final ItemStack WAND = buildWand();
|
|
||||||
|
|
||||||
private static ItemStack buildWand() {
|
|
||||||
ItemStack item = new ItemStack(Material.GOLDEN_AXE);
|
|
||||||
ItemMeta meta = item.getItemMeta();
|
|
||||||
if (meta != null) {
|
|
||||||
meta.setDisplayName(ColorUtil.color("<red><bold>LPSchematic Wand"));
|
|
||||||
meta.setLore(List.of(
|
|
||||||
ColorUtil.color("<gray>Left click: first position"),
|
|
||||||
ColorUtil.color("<gray>Right click: second position")
|
|
||||||
));
|
|
||||||
item.setItemMeta(meta);
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ItemStack getWand() {
|
|
||||||
return WAND.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void sendHelp(Player player) {
|
|
||||||
send(player, "");
|
|
||||||
send(player, "<dark_red><bold>Schematics <reset><dark_gray>-----------");
|
|
||||||
send(player, "<red>/LoParkour schematic wand <dark_gray>- <gray>Get the wand");
|
|
||||||
send(player, "<red>/LoParkour schematic pos1 <dark_gray>- <gray>Set position 1");
|
|
||||||
send(player, "<red>/LoParkour schematic pos2 <dark_gray>- <gray>Set position 2");
|
|
||||||
send(player, "<red>/LoParkour schematic save <n> <dark_gray>- <gray>Save selection");
|
|
||||||
send(player, "<red>/LoParkour schematic paste <n> <dark_gray>- <gray>Paste schematic");
|
|
||||||
send(player, "<red>/LoParkour schematic list <dark_gray>- <gray>List loaded schematics");
|
|
||||||
send(player, "<red>/LoParkour schematic reload <dark_gray>- <gray>Reload schematics");
|
|
||||||
send(player, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Called for 2-arg schematic commands (wand/pos1/pos2/list/reload) */
|
|
||||||
public static void handleSubcommand(String sub, CommandSender sender, Player player, PlayerCommandHandler cooldowns) {
|
|
||||||
Location playerLoc = player.getLocation();
|
|
||||||
Location[] sel = selections.get(player);
|
|
||||||
|
|
||||||
switch (sub.toLowerCase()) {
|
|
||||||
case "wand" -> {
|
|
||||||
player.getInventory().addItem(WAND.clone());
|
|
||||||
send(player, "<dark_red><bold>Schematics <reset><gray>Wand given. Left/Right click to set positions.");
|
|
||||||
}
|
|
||||||
case "pos1" -> setPos(player, sel, playerLoc, 0);
|
|
||||||
case "pos2" -> setPos(player, sel, playerLoc, 1);
|
|
||||||
case "list" -> {
|
|
||||||
send(player, "<dark_red><bold>Schematics <reset><gray>Loaded schematics:");
|
|
||||||
LoParkour.getSchematicManager().getAllSchematics().forEach((name, s) ->
|
|
||||||
send(player, "<gray>- <red>" + name + " <dark_gray>(" + s.getMetadata().getDifficulty() + ")"));
|
|
||||||
}
|
|
||||||
case "reload" -> {
|
|
||||||
if (!cooldowns.cooldown(sender, "schematic-reload", 2500)) return;
|
|
||||||
LoParkour.getSchematicManager().reload();
|
|
||||||
send(player, "<dark_red><bold>Schematics <reset><gray>Reloaded.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Called for 3-arg schematic commands (save <n>, paste <n>) */
|
|
||||||
public static void handleSubcommandWithName(String sub, String name, CommandSender sender,
|
|
||||||
Player player, PlayerCommandHandler cooldowns) {
|
|
||||||
switch (sub.toLowerCase()) {
|
|
||||||
case "save" -> handleSave(name, sender, player, cooldowns);
|
|
||||||
case "paste" -> handlePaste(name, sender, player);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── internal ───────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private static void setPos(Player player, Location[] existing, Location loc, int index) {
|
|
||||||
String label = index == 0 ? "1" : "2";
|
|
||||||
send(player, LoParkour.PREFIX + "Position " + label + " set to " + Locations.toString(loc, true));
|
|
||||||
|
|
||||||
Location[] updated = existing != null ? existing.clone() : new Location[]{null, null};
|
|
||||||
updated[index] = loc;
|
|
||||||
selections.put(player, updated);
|
|
||||||
|
|
||||||
if (updated[0] != null && updated[1] != null) {
|
|
||||||
ParticleUtil.box(BoundingBox.of(updated[0], updated[1]), player.getWorld(), Particle.END_ROD, player, 0.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void handleSave(String name, CommandSender sender, Player player, PlayerCommandHandler cooldowns) {
|
|
||||||
if (!cooldowns.cooldown(sender, "schematic-save", 2500)) return;
|
|
||||||
|
|
||||||
// Validate schematic name to prevent path traversal
|
|
||||||
if (!isValidSchematicName(name)) {
|
|
||||||
send(player, "<dark_red><bold>Schematics <reset><red>Invalid name. Use only letters, numbers, hyphens and underscores.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Location[] sel = selections.get(player);
|
|
||||||
if (sel == null || sel[0] == null || sel[1] == null) {
|
|
||||||
send(player, "<dark_red><bold>Schematics <reset><red>Selection incomplete. Set both positions first.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
int w = Math.abs(sel[1].getBlockX() - sel[0].getBlockX()) + 1;
|
|
||||||
int h = Math.abs(sel[1].getBlockY() - sel[0].getBlockY()) + 1;
|
|
||||||
int l = Math.abs(sel[1].getBlockZ() - sel[0].getBlockZ()) + 1;
|
|
||||||
|
|
||||||
LPSchematic schematic = new LPSchematicBuilder(name, player.getName(), 0.5, w, h, l)
|
|
||||||
.fromWorld(player.getWorld(), sel[0], sel[1])
|
|
||||||
.build();
|
|
||||||
|
|
||||||
LoParkour.getSchematicManager().saveSchematic(schematic);
|
|
||||||
send(player, "<dark_red><bold>Schematics <reset><gray>Saved as <red>" + name + "<gray>!");
|
|
||||||
} catch (Exception e) {
|
|
||||||
send(player, "<dark_red><bold>Schematics <reset><red>Failed: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates schematic name to prevent path traversal and filesystem issues.
|
|
||||||
* Only allows alphanumeric characters, hyphens, and underscores.
|
|
||||||
*/
|
|
||||||
private static boolean isValidSchematicName(String name) {
|
|
||||||
if (name == null || name.isEmpty() || name.length() > 64) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return name.matches("^[a-zA-Z0-9_-]+$");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void handlePaste(String name, CommandSender sender, Player player) {
|
|
||||||
// Validate schematic name to prevent path traversal
|
|
||||||
if (!isValidSchematicName(name)) {
|
|
||||||
send(player, "<dark_red><bold>Schematics <reset><red>Invalid name. Use only letters, numbers, hyphens and underscores.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
LPSchematic s = LoParkour.getSchematicManager().getSchematic(name);
|
|
||||||
if (s == null) {
|
|
||||||
send(sender, LoParkour.PREFIX + "Schematic '" + name + "' not found.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
s.paste(player.getLocation(), player.getWorld());
|
|
||||||
send(sender, LoParkour.PREFIX + "Pasted " + name + ".");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void send(CommandSender sender, String msg) {
|
|
||||||
sender.sendMessage(ColorUtil.color(msg));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
package dev.loki.loparkour.command;
|
package dev.loki.loparkour.command.admin;
|
||||||
|
|
||||||
import dev.loki.loparkour.LoParkour;
|
import dev.loki.loparkour.LoParkour;
|
||||||
import dev.loki.loparkour.api.Registry;
|
import dev.loki.loparkour.api.core.Registry;
|
||||||
import dev.loki.loparkour.config.Locales;
|
import dev.loki.loparkour.command.player.PlayerCommandHandler;
|
||||||
import dev.loki.loparkour.menu.ParkourOption;
|
import dev.loki.loparkour.command.schematic.SchematicCommandHandler;
|
||||||
import dev.loki.loparkour.mode.Mode;
|
import dev.loki.loparkour.config.locale.Locales;
|
||||||
import dev.loki.loparkour.mode.Modes;
|
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||||
import dev.loki.loparkour.player.ParkourPlayer;
|
import dev.loki.loparkour.mode.base.Mode;
|
||||||
import dev.loki.loparkour.player.ParkourUser;
|
import dev.loki.loparkour.mode.base.Modes;
|
||||||
|
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||||
|
import dev.loki.loparkour.player.core.ParkourUser;
|
||||||
import dev.loki.loparkour.player.data.InventoryData;
|
import dev.loki.loparkour.player.data.InventoryData;
|
||||||
import dev.loki.loparkour.util.ColorUtil;
|
import dev.loki.loparkour.util.text.ColorUtil;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.OfflinePlayer;
|
import org.bukkit.OfflinePlayer;
|
||||||
|
|
@ -27,8 +29,6 @@ import java.util.UUID;
|
||||||
*/
|
*/
|
||||||
public class AdminCommandHandler {
|
public class AdminCommandHandler {
|
||||||
|
|
||||||
private static final String COOLDOWN_HINT = "<gray>Please wait before using this again.";
|
|
||||||
|
|
||||||
private final PlayerCommandHandler base;
|
private final PlayerCommandHandler base;
|
||||||
|
|
||||||
public AdminCommandHandler(PlayerCommandHandler base) {
|
public AdminCommandHandler(PlayerCommandHandler base) {
|
||||||
|
|
@ -61,18 +61,18 @@ public class AdminCommandHandler {
|
||||||
|
|
||||||
if (target.equalsIgnoreCase("everyone")) {
|
if (target.equalsIgnoreCase("everyone")) {
|
||||||
Bukkit.getOnlinePlayers().forEach(p -> Modes.DEFAULT.create(p));
|
Bukkit.getOnlinePlayers().forEach(p -> Modes.DEFAULT.create(p));
|
||||||
send(sender, LoParkour.PREFIX + "Force joined everyone!");
|
send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.force_join_everyone"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (target.equalsIgnoreCase("nearest")) {
|
if (target.equalsIgnoreCase("nearest")) {
|
||||||
Player closest = findNearest(sender);
|
Player closest = findNearest(sender);
|
||||||
if (closest == null) return;
|
if (closest == null) return;
|
||||||
Modes.DEFAULT.create(closest);
|
Modes.DEFAULT.create(closest);
|
||||||
send(sender, LoParkour.PREFIX + "Force joined " + closest.getName() + "!");
|
send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.force_join_player").formatted(closest.getName()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Player other = Bukkit.getPlayer(target);
|
Player other = Bukkit.getPlayer(target);
|
||||||
if (other == null) { send(sender, LoParkour.PREFIX + "Player not online!"); return; }
|
if (other == null) { send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.player_not_online")); return; }
|
||||||
Modes.DEFAULT.create(other);
|
Modes.DEFAULT.create(other);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,27 +83,27 @@ public class AdminCommandHandler {
|
||||||
|
|
||||||
if (target.equalsIgnoreCase("everyone")) {
|
if (target.equalsIgnoreCase("everyone")) {
|
||||||
ParkourPlayer.getPlayers().forEach(ParkourUser::leave);
|
ParkourPlayer.getPlayers().forEach(ParkourUser::leave);
|
||||||
send(sender, LoParkour.PREFIX + "Force kicked everyone!");
|
send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.force_leave_everyone"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Player other = Bukkit.getPlayer(target);
|
Player other = Bukkit.getPlayer(target);
|
||||||
if (other == null) { send(sender, LoParkour.PREFIX + "Player not online!"); return; }
|
if (other == null) { send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.player_not_online")); return; }
|
||||||
ParkourUser user = ParkourUser.getUser(other);
|
ParkourUser user = ParkourUser.getUser(other);
|
||||||
if (user == null) { send(sender, LoParkour.PREFIX + "Player isn't playing!"); return; }
|
if (user == null) { send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.player_not_playing")); return; }
|
||||||
ParkourUser.leave(user);
|
ParkourUser.leave(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── reset ──────────────────────────────────────────────────────────────────
|
// ── reset ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private void handleReset(String target, CommandSender sender) {
|
private void handleReset(String target, CommandSender sender) {
|
||||||
if (!base.cooldown(sender, "reset", 2500, COOLDOWN_HINT)) return;
|
if (!base.cooldown(sender, "reset", 2500, Locales.getString(sender, "admin.cooldown"))) return;
|
||||||
|
|
||||||
if (target.equalsIgnoreCase("everyone")) {
|
if (target.equalsIgnoreCase("everyone")) {
|
||||||
Registry.getModes().stream()
|
Registry.getModes().stream()
|
||||||
.map(Mode::getLeaderboard)
|
.map(Mode::getLeaderboard)
|
||||||
.filter(lb -> lb != null)
|
.filter(lb -> lb != null)
|
||||||
.forEach(lb -> { lb.resetAll(); lb.write(true); });
|
.forEach(lb -> { lb.resetAll(); lb.write(true); });
|
||||||
send(sender, LoParkour.PREFIX + "Reset all high scores.");
|
send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.reset_all"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,7 +115,7 @@ public class AdminCommandHandler {
|
||||||
.filter(lb -> lb != null)
|
.filter(lb -> lb != null)
|
||||||
.forEach(lb -> { lb.remove(uuid); lb.write(true); });
|
.forEach(lb -> { lb.remove(uuid); lb.write(true); });
|
||||||
|
|
||||||
send(sender, LoParkour.PREFIX + "Reset high score of " + name + ".");
|
send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.reset_player").formatted(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── recoverinventory ───────────────────────────────────────────────────────
|
// ── recoverinventory ───────────────────────────────────────────────────────
|
||||||
|
|
@ -123,13 +123,13 @@ public class AdminCommandHandler {
|
||||||
private void handleRecoverInventory(String target, CommandSender sender) {
|
private void handleRecoverInventory(String target, CommandSender sender) {
|
||||||
if (!base.cooldown(sender, "recoverinventory", 2500, Locales.getString(sender, "admin.cooldown"))) return;
|
if (!base.cooldown(sender, "recoverinventory", 2500, Locales.getString(sender, "admin.cooldown"))) return;
|
||||||
Player other = Bukkit.getPlayer(target);
|
Player other = Bukkit.getPlayer(target);
|
||||||
if (other == null) { send(sender, LoParkour.PREFIX + "Player not online!"); return; }
|
if (other == null) { send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.player_not_online")); return; }
|
||||||
|
|
||||||
new InventoryData(other).load(result -> {
|
new InventoryData(other).load(result -> {
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
send(sender, LoParkour.PREFIX + "Recovered inventory of " + other.getName() + ".");
|
send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.recover_inventory").formatted(other.getName()));
|
||||||
} else {
|
} else {
|
||||||
send(sender, LoParkour.PREFIX + "<red>No saved inventory found for " + other.getName() + ".");
|
send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.recover_inventory_missing").formatted(other.getName()));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
package dev.loki.loparkour.command;
|
package dev.loki.loparkour.command.player;
|
||||||
|
|
||||||
import dev.loki.loparkour.LoParkour;
|
import dev.loki.loparkour.LoParkour;
|
||||||
import dev.loki.loparkour.config.Config;
|
import dev.loki.loparkour.command.schematic.SchematicCommandHandler;
|
||||||
import dev.loki.loparkour.config.Locales;
|
import dev.loki.loparkour.config.core.Config;
|
||||||
import dev.loki.loparkour.menu.Menus;
|
import dev.loki.loparkour.config.locale.Locales;
|
||||||
import dev.loki.loparkour.menu.ParkourOption;
|
import dev.loki.loparkour.menu.core.Menus;
|
||||||
import dev.loki.loparkour.mode.Modes;
|
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||||
import dev.loki.loparkour.player.ParkourUser;
|
import dev.loki.loparkour.mode.base.Modes;
|
||||||
import dev.loki.loparkour.util.ColorUtil;
|
import dev.loki.loparkour.player.core.ParkourUser;
|
||||||
|
import dev.loki.loparkour.util.text.ColorUtil;
|
||||||
import org.bukkit.command.CommandSender;
|
import org.bukkit.command.CommandSender;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
@ -80,34 +81,38 @@ public class PlayerCommandHandler {
|
||||||
private void handleReload(@NotNull CommandSender sender, @Nullable Player player) {
|
private void handleReload(@NotNull CommandSender sender, @Nullable Player player) {
|
||||||
if (!cooldown(sender, "reload", 2500)) return;
|
if (!cooldown(sender, "reload", 2500)) return;
|
||||||
if (!sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
if (!sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||||
send(sender, Locales.getString(player, "other.no_do"));
|
send(sender, Locales.getString(sender, "other.no_do"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Config.reload(false);
|
Config.reload(false);
|
||||||
send(sender, LoParkour.PREFIX + "Reloading config files... (locales are loading asynchronously)");
|
dev.loki.loparkour.adaptive.bootstrap.AdaptiveServices.reload();
|
||||||
|
send(sender, LoParkour.PREFIX + Locales.getString(sender, "commands.reload"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendHelp(@NotNull CommandSender sender) {
|
public void sendHelp(@NotNull CommandSender sender) {
|
||||||
send(sender, "");
|
send(sender, "");
|
||||||
send(sender, "<dark_gray><strikethrough>---------------<reset> " + LoParkour.NAME + " <dark_gray><strikethrough>---------------<reset>");
|
send(sender, Locales.getString(sender, "commands.help.header"));
|
||||||
send(sender, "");
|
send(sender, "");
|
||||||
send(sender, "<gray>/parkour <dark_gray>- Main command");
|
send(sender, Locales.getString(sender, "commands.help.line_parkour"));
|
||||||
if (sender.hasPermission(ParkourOption.JOIN.permission)) {
|
if (sender.hasPermission(ParkourOption.JOIN.permission)) {
|
||||||
send(sender, "<gray>/parkour join [mode/player] <dark_gray>- Join a mode");
|
send(sender, Locales.getString(sender, "commands.help.line_join"));
|
||||||
send(sender, "<gray>/parkour leave <dark_gray>- Leave parkour");
|
send(sender, Locales.getString(sender, "commands.help.line_leave"));
|
||||||
|
}
|
||||||
|
if (sender.hasPermission(ParkourOption.MAIN.permission)) {
|
||||||
|
send(sender, Locales.getString(sender, "commands.help.line_menu"));
|
||||||
|
}
|
||||||
|
if (sender.hasPermission(ParkourOption.PLAY.permission)) {
|
||||||
|
send(sender, Locales.getString(sender, "commands.help.line_play"));
|
||||||
|
}
|
||||||
|
if (sender.hasPermission(ParkourOption.LEADERBOARDS.permission)) {
|
||||||
|
send(sender, Locales.getString(sender, "commands.help.line_leaderboard"));
|
||||||
}
|
}
|
||||||
if (sender.hasPermission(ParkourOption.MAIN.permission))
|
|
||||||
send(sender, "<gray>/parkour menu <dark_gray>- Open the menu");
|
|
||||||
if (sender.hasPermission(ParkourOption.PLAY.permission))
|
|
||||||
send(sender, "<gray>/parkour play <dark_gray>- Mode selection menu");
|
|
||||||
if (sender.hasPermission(ParkourOption.LEADERBOARDS.permission))
|
|
||||||
send(sender, "<gray>/parkour leaderboard [type] <dark_gray>- Leaderboard");
|
|
||||||
if (sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
if (sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||||
send(sender, "<gray>/LoParkour schematic <dark_gray>- Schematic tools");
|
send(sender, Locales.getString(sender, "commands.help.line_schematic"));
|
||||||
send(sender, "<gray>/LoParkour reload <dark_gray>- Reload configs");
|
send(sender, Locales.getString(sender, "commands.help.line_reload"));
|
||||||
send(sender, "<gray>/LoParkour reset <everyone/player> <dark_gray>- Reset scores");
|
send(sender, Locales.getString(sender, "commands.help.line_reset"));
|
||||||
send(sender, "<gray>/LoParkour forcejoin/forceleave <target> <dark_gray>- Force join/leave");
|
send(sender, Locales.getString(sender, "commands.help.line_force"));
|
||||||
send(sender, "<gray>/LoParkour recoverinventory <player> <dark_gray>- Recover inventory");
|
send(sender, Locales.getString(sender, "commands.help.line_recover"));
|
||||||
}
|
}
|
||||||
send(sender, "");
|
send(sender, "");
|
||||||
}
|
}
|
||||||
|
|
@ -117,7 +122,7 @@ public class PlayerCommandHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param whenBlocked message sent (MiniMessage color) if still on cooldown; may be null for silent block
|
* @param whenBlocked message sent ({@code &#RRGGBB} / {@code &} colors) if still on cooldown; may be null for silent block
|
||||||
*/
|
*/
|
||||||
public boolean cooldown(CommandSender sender, String key, long millis, @Nullable String whenBlocked) {
|
public boolean cooldown(CommandSender sender, String key, long millis, @Nullable String whenBlocked) {
|
||||||
String fullKey = sender.getName() + ":" + key;
|
String fullKey = sender.getName() + ":" + key;
|
||||||
|
|
@ -0,0 +1,271 @@
|
||||||
|
package dev.loki.loparkour.command.schematic;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.LoParkour;
|
||||||
|
import dev.loki.loparkour.command.player.PlayerCommandHandler;
|
||||||
|
import dev.loki.loparkour.config.core.Config;
|
||||||
|
import dev.loki.loparkour.config.locale.Locales;
|
||||||
|
import dev.loki.loparkour.config.options.Option;
|
||||||
|
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||||
|
import dev.loki.loparkour.schematic.convert.LpschemConverter;
|
||||||
|
import dev.loki.loparkour.schematic.core.ParkourSchematic;
|
||||||
|
import dev.loki.loparkour.schematic.create.SchematicCreator;
|
||||||
|
import dev.loki.loparkour.util.particle.ParticleUtil;
|
||||||
|
import dev.loki.loparkour.util.text.ColorUtil;
|
||||||
|
import dev.loki.loparkour.util.world.Locations;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.NamespacedKey;
|
||||||
|
import org.bukkit.Particle;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.inventory.meta.ItemMeta;
|
||||||
|
import org.bukkit.persistence.PersistentDataType;
|
||||||
|
import org.bukkit.util.BoundingBox;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin schematic tools: wand, create, convert, list, paste, reload.
|
||||||
|
*/
|
||||||
|
public class SchematicCommandHandler {
|
||||||
|
|
||||||
|
public static final Map<Player, Location[]> selections = new HashMap<>();
|
||||||
|
public static final NamespacedKey WAND_KEY = new NamespacedKey(LoParkour.getPlugin(), "schematic_wand");
|
||||||
|
|
||||||
|
private static ItemStack cachedWand;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public static ItemStack getWand() {
|
||||||
|
return buildWand().clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isWand(@Nullable ItemStack item) {
|
||||||
|
if (item == null || !item.hasItemMeta()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ItemMeta meta = item.getItemMeta();
|
||||||
|
return meta != null
|
||||||
|
&& meta.getPersistentDataContainer().has(WAND_KEY, PersistentDataType.BYTE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public static Location[] getSelection(@NotNull Player player) {
|
||||||
|
Location[] sel = selections.get(player);
|
||||||
|
if (sel == null) {
|
||||||
|
sel = new Location[]{null, null};
|
||||||
|
selections.put(player, sel);
|
||||||
|
}
|
||||||
|
return sel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setPos(@NotNull Player player, @NotNull Location loc, int index) {
|
||||||
|
Location[] updated = getSelection(player).clone();
|
||||||
|
updated[index] = loc.clone();
|
||||||
|
selections.put(player, updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clearWandCache() {
|
||||||
|
cachedWand = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void sendHelp(@NotNull Player player) {
|
||||||
|
send(player, "");
|
||||||
|
send(player, Locales.getString(player, "schematic.help.header"));
|
||||||
|
send(player, "");
|
||||||
|
send(player, Locales.getString(player, "schematic.help.line_wand"));
|
||||||
|
send(player, Locales.getString(player, "schematic.help.line_pos1"));
|
||||||
|
send(player, Locales.getString(player, "schematic.help.line_pos2"));
|
||||||
|
send(player, Locales.getString(player, "schematic.help.line_create"));
|
||||||
|
send(player, Locales.getString(player, "schematic.help.line_create_named"));
|
||||||
|
send(player, Locales.getString(player, "schematic.help.line_convert"));
|
||||||
|
send(player, Locales.getString(player, "schematic.help.line_paste"));
|
||||||
|
send(player, Locales.getString(player, "schematic.help.line_list"));
|
||||||
|
send(player, Locales.getString(player, "schematic.help.line_reload"));
|
||||||
|
send(player, Locales.getString(player, "schematic.help.line_folder"));
|
||||||
|
send(player, Locales.getString(player, "schematic.help.line_docs"));
|
||||||
|
send(player, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void handleSubcommand(String sub, CommandSender sender, Player player, PlayerCommandHandler cooldowns) {
|
||||||
|
Location playerLoc = player.getLocation();
|
||||||
|
Location[] sel = getSelection(player);
|
||||||
|
|
||||||
|
switch (sub.toLowerCase()) {
|
||||||
|
case "wand" -> {
|
||||||
|
player.getInventory().addItem(getWand());
|
||||||
|
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.wand_given"));
|
||||||
|
}
|
||||||
|
case "pos1" -> setPosCommand(player, sel, playerLoc, 0);
|
||||||
|
case "pos2" -> setPosCommand(player, sel, playerLoc, 1);
|
||||||
|
case "list" -> {
|
||||||
|
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.list_header"));
|
||||||
|
LoParkour.getSchematicManager().getAll().forEach((name, s) ->
|
||||||
|
send(player, Locales.getString(player, "schematic.list_entry")
|
||||||
|
.formatted(name, s.getDifficulty(), s.getFormat().name().toLowerCase())));
|
||||||
|
}
|
||||||
|
case "reload" -> {
|
||||||
|
if (!cooldowns.cooldown(sender, "schematic-reload", 2500)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Config.reload(false);
|
||||||
|
LoParkour.getSchematicManager().reload();
|
||||||
|
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.reloaded"));
|
||||||
|
}
|
||||||
|
case "convert" -> handleConvert(sender, player, cooldowns);
|
||||||
|
default -> sendHelp(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void handleSubcommandWithName(String sub, String name, CommandSender sender, Player player) {
|
||||||
|
switch (sub.toLowerCase()) {
|
||||||
|
case "paste" -> handlePaste(name, sender, player);
|
||||||
|
case "create" -> handleCreate(name, null, sender, player);
|
||||||
|
default -> {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@code schematic create <name> <difficulty>} */
|
||||||
|
public static void handleCreateWithDifficulty(
|
||||||
|
String name,
|
||||||
|
String difficultyRaw,
|
||||||
|
CommandSender sender,
|
||||||
|
Player player,
|
||||||
|
PlayerCommandHandler cooldowns
|
||||||
|
) {
|
||||||
|
if (!cooldowns.cooldown(sender, "schematic-create", 2500)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleCreate(name, difficultyRaw, sender, player);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@code create <difficulty>} or {@code schematic create <difficulty>} */
|
||||||
|
public static void handleCreateDifficultyOnly(
|
||||||
|
String difficultyRaw,
|
||||||
|
CommandSender sender,
|
||||||
|
Player player,
|
||||||
|
PlayerCommandHandler cooldowns
|
||||||
|
) {
|
||||||
|
if (!cooldowns.cooldown(sender, "schematic-create", 2500)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleCreate(null, difficultyRaw, sender, player);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void handleCreate(
|
||||||
|
@Nullable String optionalName,
|
||||||
|
@NotNull String difficultyRaw,
|
||||||
|
CommandSender sender,
|
||||||
|
Player player
|
||||||
|
) {
|
||||||
|
Location[] sel = selections.get(player);
|
||||||
|
if (sel == null || sel[0] == null || sel[1] == null) {
|
||||||
|
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.selection_incomplete"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
double difficulty;
|
||||||
|
try {
|
||||||
|
difficulty = SchematicCreator.parseDifficulty(difficultyRaw);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.invalid_difficulty"));
|
||||||
|
return;
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.invalid_difficulty"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
SchematicCreator.CreateResult result = SchematicCreator.create(sel[0], sel[1], optionalName, difficulty);
|
||||||
|
LoParkour.getSchematicManager().reload();
|
||||||
|
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.created")
|
||||||
|
.formatted(result.stem(), result.difficulty(), result.file().getName()));
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.invalid_name"));
|
||||||
|
} catch (Exception e) {
|
||||||
|
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.create_failed").formatted(e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void handleConvert(CommandSender sender, Player player, PlayerCommandHandler cooldowns) {
|
||||||
|
if (!cooldowns.cooldown(sender, "schematic-convert", 5000)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LpschemConverter.ConvertResult result = LpschemConverter.convertAll();
|
||||||
|
if (result.converted() > 0) {
|
||||||
|
LoParkour.getSchematicManager().reload();
|
||||||
|
}
|
||||||
|
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.convert_done")
|
||||||
|
.formatted(result.converted(), result.failed()));
|
||||||
|
for (String line : result.messages()) {
|
||||||
|
if (result.messages().size() <= 12) {
|
||||||
|
send(player, "&#A0A0A0 " + line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result.messages().size() > 12) {
|
||||||
|
send(player, Locales.getString(player, "schematic.convert_truncated").formatted(result.messages().size() - 12));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setPosCommand(Player player, Location[] existing, Location loc, int index) {
|
||||||
|
setPos(player, loc, index);
|
||||||
|
String label = index == 0 ? "1" : "2";
|
||||||
|
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.pos_set")
|
||||||
|
.formatted(label, Locations.toString(loc, true)));
|
||||||
|
|
||||||
|
Location[] updated = getSelection(player);
|
||||||
|
if (updated[0] != null && updated[1] != null) {
|
||||||
|
ParticleUtil.box(BoundingBox.of(updated[0], updated[1]), player.getWorld(), Particle.END_ROD, player, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void handlePaste(String name, CommandSender sender, Player player) {
|
||||||
|
if (!isValidSchematicName(name)) {
|
||||||
|
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.invalid_name"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ParkourSchematic schematic = LoParkour.getSchematicManager().get(name);
|
||||||
|
if (schematic == null) {
|
||||||
|
send(sender, LoParkour.PREFIX + Locales.getString(sender, "schematic.not_found").formatted(name));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
schematic.paste(player.getLocation(), player.getWorld());
|
||||||
|
send(sender, LoParkour.PREFIX + Locales.getString(sender, "schematic.pasted").formatted(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean isValidSchematicName(String name) {
|
||||||
|
if (name == null || name.isEmpty() || name.length() > 64) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return name.matches("^[a-zA-Z0-9_-]+$");
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private static ItemStack buildWand() {
|
||||||
|
if (cachedWand != null) {
|
||||||
|
return cachedWand;
|
||||||
|
}
|
||||||
|
|
||||||
|
String locale = Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG);
|
||||||
|
if (locale == null || locale.isBlank()) {
|
||||||
|
locale = "en";
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemStack item = Locales.getItem(locale, "schematic.wand").build();
|
||||||
|
ItemMeta meta = item.getItemMeta();
|
||||||
|
if (meta != null) {
|
||||||
|
meta.getPersistentDataContainer().set(WAND_KEY, PersistentDataType.BYTE, (byte) 1);
|
||||||
|
item.setItemMeta(meta);
|
||||||
|
}
|
||||||
|
cachedWand = item;
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void send(CommandSender sender, String msg) {
|
||||||
|
sender.sendMessage(ColorUtil.color(msg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.loki.loparkour.config;
|
package dev.loki.loparkour.config.core;
|
||||||
|
|
||||||
import org.bukkit.configuration.file.FileConfiguration;
|
import org.bukkit.configuration.file.FileConfiguration;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.loki.loparkour.config;
|
package dev.loki.loparkour.config.core;
|
||||||
|
|
||||||
import dev.loki.loparkour.LoParkour;
|
import dev.loki.loparkour.LoParkour;
|
||||||
import org.bukkit.configuration.ConfigurationSection;
|
import org.bukkit.configuration.ConfigurationSection;
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package dev.loki.loparkour.config;
|
package dev.loki.loparkour.config.core;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.config.locale.Locales;
|
||||||
|
import dev.loki.loparkour.config.options.Option;
|
||||||
import dev.loki.loparkour.LoParkour;
|
import dev.loki.loparkour.LoParkour;
|
||||||
import org.bukkit.configuration.file.FileConfiguration;
|
import org.bukkit.configuration.file.FileConfiguration;
|
||||||
import org.bukkit.configuration.file.YamlConfiguration;
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
|
|
@ -55,9 +57,9 @@ public class ConfigLoader {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void initializeDependentSystems(boolean firstLoad) {
|
public static void initializeDependentSystems(boolean firstLoad) {
|
||||||
dev.loki.loparkour.reward.Rewards.init();
|
dev.loki.loparkour.reward.core.Rewards.init();
|
||||||
Locales.init();
|
Locales.init();
|
||||||
dev.loki.loparkour.schematic.Schematics.init();
|
dev.loki.loparkour.schematic.registry.Schematics.init();
|
||||||
Option.init(firstLoad);
|
Option.init(firstLoad);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.loki.loparkour.config;
|
package dev.loki.loparkour.config.core;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.loki.loparkour.config;
|
package dev.loki.loparkour.config.locale;
|
||||||
|
|
||||||
import org.bukkit.configuration.file.FileConfiguration;
|
import org.bukkit.configuration.file.FileConfiguration;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.loki.loparkour.config;
|
package dev.loki.loparkour.config.locale;
|
||||||
|
|
||||||
import dev.loki.loparkour.LoParkour;
|
import dev.loki.loparkour.LoParkour;
|
||||||
import org.bukkit.configuration.ConfigurationSection;
|
import org.bukkit.configuration.ConfigurationSection;
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
package dev.loki.loparkour.config;
|
package dev.loki.loparkour.config.locale;
|
||||||
|
|
||||||
import dev.loki.loparkour.LoParkour;
|
import dev.loki.loparkour.LoParkour;
|
||||||
import dev.loki.loparkour.menu.ParkourOption;
|
import dev.loki.loparkour.config.options.Option;
|
||||||
import dev.loki.loparkour.player.ParkourUser;
|
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||||
import dev.loki.loparkour.util.Item;
|
import dev.loki.loparkour.player.core.ParkourUser;
|
||||||
import dev.loki.loparkour.util.Materials;
|
import dev.loki.loparkour.util.text.ColorUtil;
|
||||||
|
import dev.loki.loparkour.util.item.Item;
|
||||||
|
import dev.loki.loparkour.util.item.Materials;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.command.CommandSender;
|
import org.bukkit.command.CommandSender;
|
||||||
import org.bukkit.configuration.file.FileConfiguration;
|
import org.bukkit.configuration.file.FileConfiguration;
|
||||||
|
|
@ -29,6 +31,7 @@ public class Locales {
|
||||||
public static void init() {
|
public static void init() {
|
||||||
Map<String, FileConfiguration> loaded = LocaleLoader.loadAll();
|
Map<String, FileConfiguration> loaded = LocaleLoader.loadAll();
|
||||||
cache.setLocales(loaded);
|
cache.setLocales(loaded);
|
||||||
|
dev.loki.loparkour.command.schematic.SchematicCommandHandler.clearWandCache();
|
||||||
LoParkour.log("Locales loaded successfully (" + loaded.size() + " locales)");
|
LoParkour.log("Locales loaded successfully (" + loaded.size() + " locales)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,6 +64,24 @@ public class Locales {
|
||||||
return cache.cachedValue(locale, c -> c.getString(path), "");
|
return cache.cachedValue(locale, c -> c.getString(path), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a colored message from locale. Safe for console ({@link #getString(CommandSender, String)}).
|
||||||
|
*/
|
||||||
|
public static void send(@NotNull CommandSender sender, @NotNull String path, Object... format) {
|
||||||
|
String message = getString(sender, path);
|
||||||
|
if (format.length > 0) {
|
||||||
|
message = message.formatted(format);
|
||||||
|
}
|
||||||
|
sender.sendMessage(ColorUtil.color(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a colored message using the player's locale.
|
||||||
|
*/
|
||||||
|
public static void send(@NotNull Player player, @NotNull String path, Object... format) {
|
||||||
|
send((CommandSender) player, path, format);
|
||||||
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public static List<String> getStringList(@NotNull String locale, @NotNull String path) {
|
public static List<String> getStringList(@NotNull String locale, @NotNull String path) {
|
||||||
return cache.cachedValue(locale, c -> c.getStringList(path), Collections.emptyList());
|
return cache.cachedValue(locale, c -> c.getStringList(path), Collections.emptyList());
|
||||||
|
|
@ -107,11 +128,9 @@ public class Locales {
|
||||||
String name = applyReplacements(config.getString(namePath, ""), replace);
|
String name = applyReplacements(config.getString(namePath, ""), replace);
|
||||||
String lore = applyReplacements(config.getString(lorePath, ""), replace);
|
String lore = applyReplacements(config.getString(lorePath, ""), replace);
|
||||||
|
|
||||||
// Debug logging
|
if ("STONE".equals(material) && !config.contains(materialPath)) {
|
||||||
if ("STONE".equals(material)) {
|
LoParkour.getPlugin().getLogger().warning(
|
||||||
LoParkour.getPlugin().getLogger().warning("Material defaulted to STONE for path: " + path);
|
"Missing locale key '%s' (material defaults to STONE)".formatted(materialPath));
|
||||||
LoParkour.getPlugin().getLogger().warning(" Tried: " + materialPath + " = " + config.getString(materialPath));
|
|
||||||
LoParkour.getPlugin().getLogger().warning(" Available keys: " + config.getKeys(true));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Material mat = Materials.parse(material);
|
Material mat = Materials.parse(material);
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
package dev.loki.loparkour.config;
|
package dev.loki.loparkour.config.options;
|
||||||
|
|
||||||
import dev.loki.loparkour.api.Registry;
|
import dev.loki.loparkour.api.core.Registry;
|
||||||
|
import dev.loki.loparkour.config.core.Config;
|
||||||
import dev.loki.loparkour.config.options.*;
|
import dev.loki.loparkour.config.options.*;
|
||||||
import dev.loki.loparkour.menu.ParkourOption;
|
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||||
import dev.loki.loparkour.style.RandomStyle;
|
import dev.loki.loparkour.style.core.RandomStyle;
|
||||||
import dev.loki.loparkour.style.Style;
|
import dev.loki.loparkour.style.core.Style;
|
||||||
import dev.loki.loparkour.util.ParticleData;
|
import dev.loki.loparkour.util.particle.ParticleData;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.Particle;
|
import org.bukkit.Particle;
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
package dev.loki.loparkour.config.options;
|
package dev.loki.loparkour.config.options;
|
||||||
|
|
||||||
import dev.loki.loparkour.LoParkour;
|
import dev.loki.loparkour.LoParkour;
|
||||||
import dev.loki.loparkour.config.Config;
|
import dev.loki.loparkour.config.core.Config;
|
||||||
import dev.loki.loparkour.menu.ParkourOption;
|
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package dev.loki.loparkour.config.options;
|
package dev.loki.loparkour.config.options;
|
||||||
|
|
||||||
import dev.loki.loparkour.LoParkour;
|
import dev.loki.loparkour.LoParkour;
|
||||||
import dev.loki.loparkour.config.Config;
|
import dev.loki.loparkour.config.core.Config;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
package dev.loki.loparkour.config.options;
|
package dev.loki.loparkour.config.options;
|
||||||
|
|
||||||
import dev.loki.loparkour.LoParkour;
|
import dev.loki.loparkour.LoParkour;
|
||||||
import dev.loki.loparkour.config.Config;
|
import dev.loki.loparkour.config.core.Config;
|
||||||
import dev.loki.loparkour.config.Option;
|
import dev.loki.loparkour.config.options.Option;
|
||||||
import dev.loki.loparkour.util.ParticleData;
|
import dev.loki.loparkour.util.particle.ParticleData;
|
||||||
import org.bukkit.Particle;
|
import org.bukkit.Particle;
|
||||||
import org.bukkit.Sound;
|
import org.bukkit.Sound;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package dev.loki.loparkour.config.options;
|
package dev.loki.loparkour.config.options;
|
||||||
|
|
||||||
import dev.loki.loparkour.LoParkour;
|
import dev.loki.loparkour.LoParkour;
|
||||||
import dev.loki.loparkour.config.Config;
|
import dev.loki.loparkour.config.core.Config;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SQL database configuration options.
|
* SQL database configuration options.
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
package dev.loki.loparkour.config.options;
|
package dev.loki.loparkour.config.options;
|
||||||
|
|
||||||
import dev.loki.loparkour.LoParkour;
|
import dev.loki.loparkour.LoParkour;
|
||||||
import dev.loki.loparkour.config.ConfigAccessor;
|
import dev.loki.loparkour.config.core.ConfigAccessor;
|
||||||
import dev.loki.loparkour.style.Style;
|
import dev.loki.loparkour.style.core.Style;
|
||||||
import dev.loki.loparkour.util.Materials;
|
import dev.loki.loparkour.util.item.Materials;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.configuration.file.FileConfiguration;
|
import org.bukkit.configuration.file.FileConfiguration;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
package dev.loki.loparkour.generator.core.coordinator;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.generator.jump.calculation.JumpType;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for listening to generator events.
|
||||||
|
* Allows adaptive system to integrate without creating direct dependencies.
|
||||||
|
*
|
||||||
|
* This decouples the generator from the adaptive system:
|
||||||
|
* - Generator fires events through this interface
|
||||||
|
* - Adaptive system implements this interface to collect metrics
|
||||||
|
* - No circular dependencies between generator and adaptive packages
|
||||||
|
*/
|
||||||
|
public interface GeneratorEventListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a block is generated.
|
||||||
|
*
|
||||||
|
* @param player The player for whom the block was generated
|
||||||
|
* @param block The generated block
|
||||||
|
* @param jumpType The type of jump required to reach this block
|
||||||
|
* @param distance The distance from the previous block
|
||||||
|
*/
|
||||||
|
void onBlockGenerated(@NotNull Player player, @NotNull Block block, @NotNull JumpType jumpType, double distance);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a player successfully lands on a block and scores.
|
||||||
|
*
|
||||||
|
* @param player The player who scored
|
||||||
|
* @param block The block the player landed on
|
||||||
|
* @param distance The distance of the jump
|
||||||
|
*/
|
||||||
|
void onPlayerScore(@NotNull Player player, @NotNull Block block, double distance);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a player falls off the parkour.
|
||||||
|
*
|
||||||
|
* @param player The player who fell
|
||||||
|
* @param lastBlock The last block the player was on (may be null)
|
||||||
|
*/
|
||||||
|
void onPlayerFall(@NotNull Player player, Block lastBlock);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a player performs a jump.
|
||||||
|
*
|
||||||
|
* @param player The player who jumped
|
||||||
|
* @param jumpType The type of jump performed
|
||||||
|
*/
|
||||||
|
void onJump(@NotNull Player player, @NotNull JumpType jumpType);
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
package dev.loki.loparkour.generator;
|
package dev.loki.loparkour.generator.core.coordinator;
|
||||||
|
|
||||||
import dev.loki.loparkour.config.Config;
|
import dev.loki.loparkour.adaptive.bootstrap.AdaptiveServices;
|
||||||
|
import dev.loki.loparkour.config.core.Config;
|
||||||
|
import dev.loki.loparkour.generator.core.model.Profile;
|
||||||
|
import dev.loki.loparkour.generator.profile.GenerationProfileLoader;
|
||||||
import org.bukkit.block.data.BlockData;
|
import org.bukkit.block.data.BlockData;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
|
@ -18,12 +21,21 @@ public class GeneratorProfileManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Override profile settings based on generator options.
|
* Rebuilds profile from config, player settings, and adaptive difficulty when enabled.
|
||||||
*/
|
*/
|
||||||
public void overrideProfile() {
|
public void overrideProfile() {
|
||||||
// Generator options are for disabling features, not difficulty
|
Profile rebuilt = GenerationProfileLoader.loadFromConfig();
|
||||||
// Difficulty is controlled by profile settings
|
GenerationProfileLoader.preservePlayerSettings(generator.profile, rebuilt);
|
||||||
// This method can be used to apply custom profile overrides if needed
|
AdaptiveServices.applyAdaptiveProfile(generator, rebuilt);
|
||||||
|
GenerationProfileLoader.copyInto(generator.profile, rebuilt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recalculates chances after a fall or settings change.
|
||||||
|
*/
|
||||||
|
public void recalculateProfile() {
|
||||||
|
overrideProfile();
|
||||||
|
calculateChances();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -42,17 +54,14 @@ public class GeneratorProfileManager {
|
||||||
public double getDifficultyScore() {
|
public double getDifficultyScore() {
|
||||||
double score = 0.0;
|
double score = 0.0;
|
||||||
|
|
||||||
// Distance difficulty
|
|
||||||
for (Map.Entry<Integer, Double> entry : generator.state.distanceChances.entrySet()) {
|
for (Map.Entry<Integer, Double> entry : generator.state.distanceChances.entrySet()) {
|
||||||
score += entry.getKey() * entry.getValue() * 0.3;
|
score += entry.getKey() * entry.getValue() * 0.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Height difficulty
|
|
||||||
for (Map.Entry<Integer, Double> entry : generator.state.heightChances.entrySet()) {
|
for (Map.Entry<Integer, Double> entry : generator.state.heightChances.entrySet()) {
|
||||||
score += Math.abs(entry.getKey()) * entry.getValue() * 0.2;
|
score += Math.abs(entry.getKey()) * entry.getValue() * 0.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special blocks difficulty
|
|
||||||
for (Map.Entry<BlockData, Double> entry : generator.state.specialChances.entrySet()) {
|
for (Map.Entry<BlockData, Double> entry : generator.state.specialChances.entrySet()) {
|
||||||
score += entry.getValue() * 0.5;
|
score += entry.getValue() * 0.5;
|
||||||
}
|
}
|
||||||
|
|
@ -63,47 +72,48 @@ public class GeneratorProfileManager {
|
||||||
private void calculateDistanceChances() {
|
private void calculateDistanceChances() {
|
||||||
generator.state.distanceChances.clear();
|
generator.state.distanceChances.clear();
|
||||||
|
|
||||||
// Get distance range from config
|
int minDistance = profileInt("distance.min", "advanced.distance.min", 1);
|
||||||
int minDistance = Config.GENERATION.getInt("advanced.distance.min", 1);
|
int maxDistance = profileInt("distance.max", "advanced.distance.max", 4);
|
||||||
int maxDistance = Config.GENERATION.getInt("advanced.distance.max", 4);
|
|
||||||
|
|
||||||
for (int distance = minDistance; distance <= maxDistance; distance++) {
|
for (int distance = minDistance; distance <= maxDistance; distance++) {
|
||||||
int chance = Config.GENERATION.getInt("advanced.distance.chances." + distance, 1);
|
int chance = profileInt(
|
||||||
|
"distance.chances." + distance,
|
||||||
|
"advanced.distance.chances." + distance,
|
||||||
|
1);
|
||||||
generator.state.distanceChances.put(distance, (double) chance);
|
generator.state.distanceChances.put(distance, (double) chance);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize chances to sum to 1.0
|
|
||||||
normalizeMap(generator.state.distanceChances);
|
normalizeMap(generator.state.distanceChances);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void calculateHeightChances() {
|
private void calculateHeightChances() {
|
||||||
generator.state.heightChances.clear();
|
generator.state.heightChances.clear();
|
||||||
|
|
||||||
// Get height range from config
|
int minHeight = profileInt("height.min", "advanced.height.min", -1);
|
||||||
int minHeight = Config.GENERATION.getInt("advanced.height.min", -1);
|
int maxHeight = profileInt("height.max", "advanced.height.max", 2);
|
||||||
int maxHeight = Config.GENERATION.getInt("advanced.height.max", 2);
|
|
||||||
|
|
||||||
for (int height = minHeight; height <= maxHeight; height++) {
|
for (int height = minHeight; height <= maxHeight; height++) {
|
||||||
int chance = Config.GENERATION.getInt("advanced.height.chances." + height, 1);
|
int chance = profileInt(
|
||||||
|
"height.chances." + height,
|
||||||
|
"advanced.height.chances." + height,
|
||||||
|
1);
|
||||||
generator.state.heightChances.put(height, (double) chance);
|
generator.state.heightChances.put(height, (double) chance);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize chances to sum to 1.0
|
|
||||||
normalizeMap(generator.state.heightChances);
|
normalizeMap(generator.state.heightChances);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void calculateDefaultChances() {
|
private void calculateDefaultChances() {
|
||||||
generator.state.defaultChances.clear();
|
generator.state.defaultChances.clear();
|
||||||
|
|
||||||
int defaultChance = Config.GENERATION.getInt("advanced.type.default", 80);
|
int defaultChance = profileInt("type.default", "advanced.type.default", 80);
|
||||||
int specialChance = Config.GENERATION.getInt("advanced.type.special", 15);
|
int specialChance = profileInt("type.special", "advanced.type.special", 15);
|
||||||
int schematicChance = Config.GENERATION.getInt("advanced.type.schematic", 5);
|
int schematicChance = profileInt("type.schematic", "advanced.type.schematic", 5);
|
||||||
|
|
||||||
generator.state.defaultChances.put(ParkourGenerator.BlockGenerationType.DEFAULT, (double) defaultChance);
|
generator.state.defaultChances.put(ParkourGenerator.BlockGenerationType.DEFAULT, (double) defaultChance);
|
||||||
generator.state.defaultChances.put(ParkourGenerator.BlockGenerationType.SPECIAL, (double) specialChance);
|
generator.state.defaultChances.put(ParkourGenerator.BlockGenerationType.SPECIAL, (double) specialChance);
|
||||||
generator.state.defaultChances.put(ParkourGenerator.BlockGenerationType.SCHEMATIC, (double) schematicChance);
|
generator.state.defaultChances.put(ParkourGenerator.BlockGenerationType.SCHEMATIC, (double) schematicChance);
|
||||||
|
|
||||||
// Normalize chances to sum to 1.0
|
|
||||||
normalizeMap(generator.state.defaultChances);
|
normalizeMap(generator.state.defaultChances);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,25 +132,28 @@ public class GeneratorProfileManager {
|
||||||
generator.state.specialChances.put(blockData, (double) chance);
|
generator.state.specialChances.put(blockData, (double) chance);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize chances to sum to 1.0
|
|
||||||
normalizeMap(generator.state.specialChances);
|
normalizeMap(generator.state.specialChances);
|
||||||
} catch (Exception e) {
|
} catch (Exception ignored) {
|
||||||
// If special blocks config is missing, just skip
|
// special blocks config optional
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private int profileInt(@NotNull String profileKey, @NotNull String configKey, int defaultValue) {
|
||||||
* Normalize a map so all values sum to 1.0
|
if (generator.profile.settings.containsKey(profileKey)) {
|
||||||
* If sum is 0, distributes values equally.
|
return generator.profile.get(profileKey).asInt();
|
||||||
*/
|
}
|
||||||
|
return Config.GENERATION.getInt(configKey, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
private <K> void normalizeMap(Map<K, Double> map) {
|
private <K> void normalizeMap(Map<K, Double> map) {
|
||||||
if (map.isEmpty()) return;
|
if (map.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
double sum = map.values().stream().mapToDouble(Double::doubleValue).sum();
|
double sum = map.values().stream().mapToDouble(Double::doubleValue).sum();
|
||||||
if (sum > 0) {
|
if (sum > 0) {
|
||||||
map.replaceAll((k, v) -> v / sum);
|
map.replaceAll((k, v) -> v / sum);
|
||||||
} else {
|
} else {
|
||||||
// If all values are 0, distribute equally
|
|
||||||
double equalValue = 1.0 / map.size();
|
double equalValue = 1.0 / map.size();
|
||||||
map.replaceAll((k, v) -> equalValue);
|
map.replaceAll((k, v) -> equalValue);
|
||||||
}
|
}
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
package dev.loki.loparkour.generator;
|
package dev.loki.loparkour.generator.core.coordinator;
|
||||||
|
|
||||||
import dev.loki.loparkour.config.Locales;
|
import dev.loki.loparkour.config.locale.Locales;
|
||||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
import dev.loki.loparkour.leaderboard.core.Leaderboard;
|
||||||
import dev.loki.loparkour.leaderboard.Score;
|
import dev.loki.loparkour.leaderboard.model.Score;
|
||||||
import dev.loki.loparkour.mode.Mode;
|
import dev.loki.loparkour.mode.base.Mode;
|
||||||
import dev.loki.loparkour.player.ParkourPlayer;
|
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
|
|
@ -1,19 +1,23 @@
|
||||||
package dev.loki.loparkour.generator;
|
package dev.loki.loparkour.generator.core.coordinator;
|
||||||
|
|
||||||
import dev.loki.loparkour.generator.jump.BlockPlacer;
|
import dev.loki.loparkour.generator.core.model.GeneratorOption;
|
||||||
import dev.loki.loparkour.generator.lifecycle.GeneratorLifecycle;
|
import dev.loki.loparkour.generator.core.model.GeneratorState;
|
||||||
import dev.loki.loparkour.menu.Menus;
|
import dev.loki.loparkour.generator.core.model.Island;
|
||||||
import dev.loki.loparkour.mode.Mode;
|
import dev.loki.loparkour.generator.core.model.Profile;
|
||||||
import dev.loki.loparkour.mode.Modes;
|
import dev.loki.loparkour.generator.jump.placement.BlockPlacer;
|
||||||
import dev.loki.loparkour.player.ParkourPlayer;
|
import dev.loki.loparkour.generator.lifecycle.loop.GeneratorLifecycle;
|
||||||
import dev.loki.loparkour.player.ParkourSpectator;
|
import dev.loki.loparkour.menu.core.Menus;
|
||||||
import dev.loki.loparkour.schematic.lpschem.LPSchematic;
|
import dev.loki.loparkour.mode.base.Mode;
|
||||||
import dev.loki.loparkour.session.Session;
|
import dev.loki.loparkour.mode.base.Modes;
|
||||||
|
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||||
|
import dev.loki.loparkour.player.spectator.ParkourSpectator;
|
||||||
|
import dev.loki.loparkour.session.core.Session;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.block.Block;
|
import org.bukkit.block.Block;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -33,16 +37,20 @@ public class ParkourGenerator {
|
||||||
public final GeneratorProfileManager profileManager;
|
public final GeneratorProfileManager profileManager;
|
||||||
private final GeneratorStatistics statistics;
|
private final GeneratorStatistics statistics;
|
||||||
|
|
||||||
public ParkourGenerator(@NotNull Session session, @Nullable LPSchematic schematic, GeneratorOption... generatorOptions) {
|
// Event listeners for adaptive system integration
|
||||||
|
private final List<GeneratorEventListener> eventListeners;
|
||||||
|
|
||||||
|
public ParkourGenerator(@NotNull Session session, GeneratorOption... generatorOptions) {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
this.generatorOptions = generatorOptions;
|
this.generatorOptions = generatorOptions;
|
||||||
this.island = new Island(session, schematic);
|
this.island = new Island(session);
|
||||||
this.profile = new Profile();
|
this.profile = new Profile();
|
||||||
this.state = new GeneratorState();
|
this.state = new GeneratorState();
|
||||||
|
this.eventListeners = new ArrayList<>();
|
||||||
|
|
||||||
// Initialize zone from session
|
// Initialize zone from session
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
Location[] selection = dev.loki.loparkour.world.Divider.toSelection(session);
|
Location[] selection = dev.loki.loparkour.world.core.Divider.toSelection(session);
|
||||||
if (selection == null || selection.length < 2) {
|
if (selection == null || selection.length < 2) {
|
||||||
throw new IllegalArgumentException("Invalid session zone: selection must contain at least 2 locations");
|
throw new IllegalArgumentException("Invalid session zone: selection must contain at least 2 locations");
|
||||||
}
|
}
|
||||||
|
|
@ -60,9 +68,6 @@ public class ParkourGenerator {
|
||||||
profileManager.calculateChances();
|
profileManager.calculateChances();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ParkourGenerator(@NotNull Session session, GeneratorOption... generatorOptions) {
|
|
||||||
this(session, null, generatorOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Core generation methods
|
// Core generation methods
|
||||||
public void generate() { placer.generate(); }
|
public void generate() { placer.generate(); }
|
||||||
|
|
@ -126,6 +131,39 @@ public class ParkourGenerator {
|
||||||
return state.history.get(state.history.size() - 1);
|
return state.history.get(state.history.size() - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Event listener management
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register an event listener for generator events.
|
||||||
|
* Used by adaptive system to collect metrics without creating direct dependencies.
|
||||||
|
*
|
||||||
|
* @param listener The listener to register
|
||||||
|
*/
|
||||||
|
public void registerEventListener(@NotNull GeneratorEventListener listener) {
|
||||||
|
if (!eventListeners.contains(listener)) {
|
||||||
|
eventListeners.add(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister an event listener.
|
||||||
|
*
|
||||||
|
* @param listener The listener to unregister
|
||||||
|
*/
|
||||||
|
public void unregisterEventListener(@NotNull GeneratorEventListener listener) {
|
||||||
|
eventListeners.remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all registered event listeners.
|
||||||
|
*
|
||||||
|
* @return Immutable list of event listeners
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
public List<GeneratorEventListener> getEventListeners() {
|
||||||
|
return new ArrayList<>(eventListeners);
|
||||||
|
}
|
||||||
|
|
||||||
public enum BlockGenerationType {
|
public enum BlockGenerationType {
|
||||||
DEFAULT, SPECIAL, SCHEMATIC
|
DEFAULT, SPECIAL, SCHEMATIC
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.loki.loparkour.generator;
|
package dev.loki.loparkour.generator.core.model;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for {@link ParkourGenerator}s.
|
* Options for {@link ParkourGenerator}s.
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package dev.loki.loparkour.generator;
|
package dev.loki.loparkour.generator.core.model;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.block.Block;
|
import org.bukkit.block.Block;
|
||||||
import org.bukkit.block.data.BlockData;
|
import org.bukkit.block.data.BlockData;
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
package dev.loki.loparkour.generator;
|
package dev.loki.loparkour.generator.core.model;
|
||||||
|
|
||||||
import dev.loki.loparkour.config.Config;
|
import dev.loki.loparkour.config.core.Config;
|
||||||
import dev.loki.loparkour.config.Option;
|
import dev.loki.loparkour.config.options.Option;
|
||||||
import dev.loki.loparkour.session.Session;
|
import dev.loki.loparkour.session.core.Session;
|
||||||
import dev.loki.loparkour.util.Materials;
|
import dev.loki.loparkour.util.item.Materials;
|
||||||
|
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
|
|
@ -12,8 +12,6 @@ import org.bukkit.block.Block;
|
||||||
import org.bukkit.block.BlockFace;
|
import org.bukkit.block.BlockFace;
|
||||||
import org.bukkit.util.Vector;
|
import org.bukkit.util.Vector;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|
@ -26,16 +24,11 @@ public final class Island {
|
||||||
private static final int PLATFORM_RADIUS = 4;
|
private static final int PLATFORM_RADIUS = 4;
|
||||||
|
|
||||||
public final Session session;
|
public final Session session;
|
||||||
/**
|
|
||||||
* Unused for spawn; kept for API / {@link dev.loki.loparkour.generator.jump.BlockPlacer} schematic branch.
|
|
||||||
*/
|
|
||||||
public final dev.loki.loparkour.schematic.lpschem.LPSchematic schematic;
|
|
||||||
|
|
||||||
public List<Block> blocks;
|
public List<Block> blocks;
|
||||||
|
|
||||||
public Island(@NotNull Session session, @Nullable dev.loki.loparkour.schematic.lpschem.LPSchematic schematic) {
|
public Island(@NotNull Session session) {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
this.schematic = schematic;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void build(Location location) {
|
public void build(Location location) {
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.loki.loparkour.generator;
|
package dev.loki.loparkour.generator.core.model;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
package dev.loki.loparkour.generator.effect;
|
package dev.loki.loparkour.generator.effect;
|
||||||
|
|
||||||
import dev.loki.loparkour.config.Option;
|
import dev.loki.loparkour.config.options.Option;
|
||||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||||
import dev.loki.loparkour.player.ParkourPlayer;
|
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||||
import dev.loki.loparkour.util.ParticleUtil;
|
import dev.loki.loparkour.util.particle.ParticleUtil;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.block.Block;
|
import org.bukkit.block.Block;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
package dev.loki.loparkour.generator.jump;
|
package dev.loki.loparkour.generator.jump.calculation;
|
||||||
|
|
||||||
import dev.loki.loparkour.config.Option;
|
import dev.loki.loparkour.config.options.Option;
|
||||||
import dev.loki.loparkour.generator.GeneratorOption;
|
import dev.loki.loparkour.generator.core.model.GeneratorOption;
|
||||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||||
|
import dev.loki.loparkour.generator.jump.placement.BlockSelector;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.block.Block;
|
import org.bukkit.block.Block;
|
||||||
import org.bukkit.block.data.BlockData;
|
import org.bukkit.block.data.BlockData;
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.loki.loparkour.generator.jump;
|
package dev.loki.loparkour.generator.jump.calculation;
|
||||||
|
|
||||||
import org.bukkit.util.BoundingBox;
|
import org.bukkit.util.BoundingBox;
|
||||||
import org.bukkit.util.Vector;
|
import org.bukkit.util.Vector;
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package dev.loki.loparkour.generator.jump;
|
package dev.loki.loparkour.generator.jump.calculation;
|
||||||
|
|
||||||
import dev.loki.loparkour.util.Probs;
|
import dev.loki.loparkour.util.misc.Probs;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.loki.loparkour.generator.jump;
|
package dev.loki.loparkour.generator.jump.calculation;
|
||||||
|
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.loki.loparkour.generator.jump;
|
package dev.loki.loparkour.generator.jump.calculation;
|
||||||
|
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.util.Vector;
|
import org.bukkit.util.Vector;
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
package dev.loki.loparkour.generator.jump;
|
package dev.loki.loparkour.generator.jump.placement;
|
||||||
|
|
||||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
import dev.loki.loparkour.LoParkour;
|
||||||
import dev.loki.loparkour.player.ParkourPlayer;
|
import dev.loki.loparkour.api.event.ParkourSchematicGenerateEvent;
|
||||||
import dev.loki.loparkour.schematic.lpschem.LPSchematic;
|
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||||
import dev.loki.loparkour.util.Probs;
|
import dev.loki.loparkour.generator.jump.calculation.JumpCalculator;
|
||||||
|
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||||
|
import dev.loki.loparkour.schematic.core.ParkourSchematic;
|
||||||
|
import dev.loki.loparkour.schematic.core.SchematicManager;
|
||||||
|
import dev.loki.loparkour.util.misc.Probs;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.Particle;
|
import org.bukkit.Particle;
|
||||||
import org.bukkit.Sound;
|
import org.bukkit.Sound;
|
||||||
|
|
@ -154,31 +158,38 @@ public class BlockPlacer {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean tryGenerateSchematic() {
|
private boolean tryGenerateSchematic() {
|
||||||
// Simplified schematic generation
|
SchematicManager manager = LoParkour.getSchematicManager();
|
||||||
LPSchematic schematic = generator.island.schematic; // Получаем схематик из island
|
if (manager == null) {
|
||||||
if (schematic == null) return false;
|
|
||||||
|
|
||||||
List<Block> blocks = selectBlocks();
|
|
||||||
if (blocks.isEmpty()) return false;
|
|
||||||
|
|
||||||
Block targetBlock = blocks.get(0);
|
|
||||||
Location pasteLocation = targetBlock.getLocation();
|
|
||||||
|
|
||||||
// Paste schematic (simplified)
|
|
||||||
List<Block> schematicBlocks = pasteSchematic(schematic, pasteLocation);
|
|
||||||
|
|
||||||
if (!schematicBlocks.isEmpty()) {
|
|
||||||
generator.state.history.addAll(schematicBlocks);
|
|
||||||
generator.state.schematicBlocks = schematicBlocks;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
double maxDifficulty = generator.getPlayers().stream()
|
||||||
private List<Block> pasteSchematic(@NotNull LPSchematic schematic, @NotNull Location location) {
|
.mapToDouble(p -> p.schematicDifficulty)
|
||||||
// Use the schematic's built-in paste method
|
.min()
|
||||||
return schematic.paste(location, location.getWorld());
|
.orElse(1.0);
|
||||||
|
|
||||||
|
ParkourSchematic schematic = manager.pick(maxDifficulty);
|
||||||
|
if (schematic == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Block> blocks = selectBlocks();
|
||||||
|
if (blocks.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Location pasteLocation = blocks.get(0).getLocation();
|
||||||
|
List<Block> schematicBlocks = schematic.paste(pasteLocation, pasteLocation.getWorld());
|
||||||
|
if (schematicBlocks.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
generator.state.history.addAll(schematicBlocks);
|
||||||
|
generator.state.schematicBlocks = schematicBlocks;
|
||||||
|
|
||||||
|
for (ParkourPlayer player : generator.getPlayers()) {
|
||||||
|
new ParkourSchematicGenerateEvent(schematic, generator, player).call();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
package dev.loki.loparkour.generator.jump;
|
package dev.loki.loparkour.generator.jump.placement;
|
||||||
|
|
||||||
import dev.loki.loparkour.api.Registry;
|
import dev.loki.loparkour.api.core.Registry;
|
||||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||||
import dev.loki.loparkour.style.Style;
|
import dev.loki.loparkour.generator.jump.calculation.JumpType;
|
||||||
|
import dev.loki.loparkour.style.core.Style;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.block.data.BlockData;
|
import org.bukkit.block.data.BlockData;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package dev.loki.loparkour.generator.lifecycle;
|
package dev.loki.loparkour.generator.lifecycle.loop;
|
||||||
|
|
||||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||||
|
import dev.loki.loparkour.generator.lifecycle.player.GeneratorCleanup;
|
||||||
|
import dev.loki.loparkour.generator.lifecycle.player.PlayerInteractionHandler;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
package dev.loki.loparkour.generator.lifecycle;
|
package dev.loki.loparkour.generator.lifecycle.loop;
|
||||||
|
|
||||||
import dev.loki.loparkour.api.event.ParkourFallEvent;
|
import dev.loki.loparkour.api.event.ParkourFallEvent;
|
||||||
import dev.loki.loparkour.api.event.ParkourScoreEvent;
|
import dev.loki.loparkour.api.event.ParkourScoreEvent;
|
||||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||||
import dev.loki.loparkour.mode.Mode;
|
import dev.loki.loparkour.mode.base.Mode;
|
||||||
import dev.loki.loparkour.player.ParkourPlayer;
|
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||||
import dev.loki.loparkour.reward.Reward;
|
import dev.loki.loparkour.reward.core.Reward;
|
||||||
import dev.loki.loparkour.reward.Rewards;
|
import dev.loki.loparkour.reward.core.Rewards;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
package dev.loki.loparkour.generator.lifecycle;
|
package dev.loki.loparkour.generator.lifecycle.loop;
|
||||||
|
|
||||||
import dev.loki.loparkour.config.Option;
|
import dev.loki.loparkour.config.options.Option;
|
||||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||||
|
import dev.loki.loparkour.generator.lifecycle.player.GeneratorCleanup;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package dev.loki.loparkour.generator.lifecycle;
|
package dev.loki.loparkour.generator.lifecycle.loop;
|
||||||
|
|
||||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||||
import dev.loki.loparkour.player.ParkourPlayer;
|
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
package dev.loki.loparkour.generator.lifecycle;
|
package dev.loki.loparkour.generator.lifecycle.player;
|
||||||
|
|
||||||
import dev.loki.loparkour.config.Option;
|
import dev.loki.loparkour.config.options.Option;
|
||||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||||
import dev.loki.loparkour.player.ParkourPlayer;
|
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.block.Block;
|
import org.bukkit.block.Block;
|
||||||
|
|
@ -115,7 +115,7 @@ public class GeneratorCleanup {
|
||||||
Location blockSpawn = spawn.clone();
|
Location blockSpawn = spawn.clone();
|
||||||
|
|
||||||
// Move block forward based on heading (6 blocks forward, same height)
|
// Move block forward based on heading (6 blocks forward, same height)
|
||||||
switch (dev.loki.loparkour.config.Option.HEADING) {
|
switch (dev.loki.loparkour.config.options.Option.HEADING) {
|
||||||
case NORTH -> blockSpawn.add(0, 0, -6);
|
case NORTH -> blockSpawn.add(0, 0, -6);
|
||||||
case SOUTH -> blockSpawn.add(0, 0, 6);
|
case SOUTH -> blockSpawn.add(0, 0, 6);
|
||||||
case WEST -> blockSpawn.add(-6, 0, 0);
|
case WEST -> blockSpawn.add(-6, 0, 0);
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
package dev.loki.loparkour.generator.lifecycle;
|
package dev.loki.loparkour.generator.lifecycle.player;
|
||||||
|
|
||||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
import dev.loki.loparkour.adaptive.bootstrap.AdaptiveServices;
|
||||||
import dev.loki.loparkour.player.ParkourPlayer;
|
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||||
import dev.loki.loparkour.schematic.lpschem.LPSchematic;
|
import dev.loki.loparkour.generator.core.coordinator.GeneratorEventListener;
|
||||||
|
import dev.loki.loparkour.generator.lifecycle.loop.LifecycleEventManager;
|
||||||
|
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.block.Block;
|
import org.bukkit.block.Block;
|
||||||
|
|
@ -48,6 +50,17 @@ public class PlayerInteractionHandler {
|
||||||
generator.state.score++;
|
generator.state.score++;
|
||||||
eventManager.handleScore();
|
eventManager.handleScore();
|
||||||
|
|
||||||
|
// Fire event listeners for adaptive system
|
||||||
|
Block latestBlock = generator.getLatest();
|
||||||
|
if (latestBlock != null) {
|
||||||
|
for (ParkourPlayer player : generator.getPlayers()) {
|
||||||
|
double distance = calculateDistance(player, latestBlock);
|
||||||
|
for (GeneratorEventListener listener : generator.getEventListeners()) {
|
||||||
|
listener.onPlayerScore(player.player, latestBlock, distance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Generate more blocks after scoring
|
// Generate more blocks after scoring
|
||||||
generator.generate(1);
|
generator.generate(1);
|
||||||
}
|
}
|
||||||
|
|
@ -56,7 +69,20 @@ public class PlayerInteractionHandler {
|
||||||
* Handle player falling.
|
* Handle player falling.
|
||||||
*/
|
*/
|
||||||
public void handleFall() {
|
public void handleFall() {
|
||||||
|
// Fire event listeners for adaptive system
|
||||||
|
Block latestBlock = generator.getLatest();
|
||||||
|
for (ParkourPlayer player : generator.getPlayers()) {
|
||||||
|
for (GeneratorEventListener listener : generator.getEventListeners()) {
|
||||||
|
listener.onPlayerFall(player.player, latestBlock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
eventManager.handleFall();
|
eventManager.handleFall();
|
||||||
|
|
||||||
|
for (ParkourPlayer player : generator.getPlayers()) {
|
||||||
|
AdaptiveServices.persistRatingAfterFall(player);
|
||||||
|
}
|
||||||
|
AdaptiveServices.refreshGeneratorDifficulty(generator);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleBlockInteraction(@NotNull ParkourPlayer player, @NotNull Block block) {
|
private void handleBlockInteraction(@NotNull ParkourPlayer player, @NotNull Block block) {
|
||||||
|
|
@ -84,12 +110,8 @@ public class PlayerInteractionHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isSchematicEndBlock(@NotNull Block block) {
|
private boolean isSchematicEndBlock(@NotNull Block block) {
|
||||||
LPSchematic schematic = generator.island.schematic;
|
return generator.state.schematicBlocks != null
|
||||||
if (schematic == null) return false;
|
&& generator.state.schematicBlocks.contains(block);
|
||||||
|
|
||||||
// Simplified check - would need full schematic logic
|
|
||||||
return generator.state.schematicBlocks != null &&
|
|
||||||
generator.state.schematicBlocks.contains(block);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Block directly under feet; {@code null} in air — scoring runs when landed. */
|
/** Block directly under feet; {@code null} in air — scoring runs when landed. */
|
||||||
|
|
@ -100,4 +122,13 @@ public class PlayerInteractionHandler {
|
||||||
|
|
||||||
return below.getType() != Material.AIR ? below : null;
|
return below.getType() != Material.AIR ? below : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate distance between player and block.
|
||||||
|
*/
|
||||||
|
private double calculateDistance(@NotNull ParkourPlayer player, @NotNull Block block) {
|
||||||
|
Location playerLoc = player.player.getLocation();
|
||||||
|
Location blockLoc = block.getLocation().add(0.5, 0, 0.5);
|
||||||
|
return playerLoc.distance(blockLoc);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
package dev.loki.loparkour.generator.profile;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.config.core.Config;
|
||||||
|
import dev.loki.loparkour.generator.core.model.Profile;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads default generation weights from generation.yml into a {@link Profile}.
|
||||||
|
*/
|
||||||
|
public final class GenerationProfileLoader {
|
||||||
|
|
||||||
|
private static final Set<String> PLAYER_KEYS = Set.of(
|
||||||
|
"schematicDifficulty", "blockLead", "particles", "sound",
|
||||||
|
"useSpecialBlocks", "showFallMessage", "showScoreboard",
|
||||||
|
"selectedTime", "style"
|
||||||
|
);
|
||||||
|
|
||||||
|
private GenerationProfileLoader() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public static Profile loadFromConfig() {
|
||||||
|
Profile profile = new Profile();
|
||||||
|
|
||||||
|
int minDistance = Config.GENERATION.getInt("advanced.distance.min", 1);
|
||||||
|
int maxDistance = Config.GENERATION.getInt("advanced.distance.max", 4);
|
||||||
|
profile.set("distance.min", String.valueOf(minDistance));
|
||||||
|
profile.set("distance.max", String.valueOf(maxDistance));
|
||||||
|
for (int distance = minDistance; distance <= maxDistance; distance++) {
|
||||||
|
int chance = Config.GENERATION.getInt("advanced.distance.chances." + distance, 1);
|
||||||
|
profile.set("distance.chances." + distance, String.valueOf(chance));
|
||||||
|
}
|
||||||
|
|
||||||
|
int minHeight = Config.GENERATION.getInt("advanced.height.min", -1);
|
||||||
|
int maxHeight = Config.GENERATION.getInt("advanced.height.max", 2);
|
||||||
|
profile.set("height.min", String.valueOf(minHeight));
|
||||||
|
profile.set("height.max", String.valueOf(maxHeight));
|
||||||
|
for (int height = minHeight; height <= maxHeight; height++) {
|
||||||
|
int chance = Config.GENERATION.getInt("advanced.height.chances." + height, 1);
|
||||||
|
profile.set("height.chances." + height, String.valueOf(chance));
|
||||||
|
}
|
||||||
|
|
||||||
|
profile.set("type.default", String.valueOf(Config.GENERATION.getInt("advanced.type.default", 80)));
|
||||||
|
profile.set("type.special", String.valueOf(Config.GENERATION.getInt("advanced.type.special", 15)));
|
||||||
|
profile.set("type.schematic", String.valueOf(Config.GENERATION.getInt("advanced.type.schematic", 5)));
|
||||||
|
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void preservePlayerSettings(@NotNull Profile from, @NotNull Profile to) {
|
||||||
|
for (String key : PLAYER_KEYS) {
|
||||||
|
if (from.settings.containsKey(key)) {
|
||||||
|
to.set(key, from.get(key).value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Replaces all settings on {@code target} with a snapshot from {@code source}. */
|
||||||
|
public static void copyInto(@NotNull Profile target, @NotNull Profile source) {
|
||||||
|
target.settings.clear();
|
||||||
|
for (Map.Entry<String, Profile.ProfileValue> entry : source.settings.entrySet()) {
|
||||||
|
target.set(entry.getKey(), entry.getValue().value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
package dev.loki.loparkour.ghost;
|
package dev.loki.loparkour.ghost.core;
|
||||||
|
import dev.loki.loparkour.ghost.core.GhostPlayer;
|
||||||
|
import dev.loki.loparkour.ghost.model.GhostData;
|
||||||
|
|
||||||
import dev.loki.loparkour.LoParkour;
|
import dev.loki.loparkour.LoParkour;
|
||||||
import dev.loki.loparkour.config.Config;
|
import dev.loki.loparkour.config.core.Config;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
package dev.loki.loparkour.ghost;
|
package dev.loki.loparkour.ghost.core;
|
||||||
|
import dev.loki.loparkour.ghost.model.GhostFrame;
|
||||||
|
import dev.loki.loparkour.ghost.model.GhostData;
|
||||||
|
|
||||||
import dev.lolib.scheduler.Scheduler;
|
import dev.lolib.scheduler.Scheduler;
|
||||||
import dev.lolib.scheduler.ScheduledTask;
|
import dev.lolib.scheduler.ScheduledTask;
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
package dev.loki.loparkour.ghost;
|
package dev.loki.loparkour.ghost.core;
|
||||||
|
import dev.loki.loparkour.ghost.model.GhostFrame;
|
||||||
|
import dev.loki.loparkour.ghost.model.GhostData;
|
||||||
|
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.loki.loparkour.ghost;
|
package dev.loki.loparkour.ghost.model;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.loki.loparkour.ghost;
|
package dev.loki.loparkour.ghost.model;
|
||||||
|
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.loki.loparkour.hook;
|
package dev.loki.loparkour.hook.floodgate;
|
||||||
|
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.geysermc.floodgate.api.FloodgateApi;
|
import org.geysermc.floodgate.api.FloodgateApi;
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
package dev.loki.loparkour.hook;
|
package dev.loki.loparkour.hook.holo;
|
||||||
|
|
||||||
import dev.loki.loparkour.LoParkour;
|
import dev.loki.loparkour.LoParkour;
|
||||||
import dev.loki.loparkour.api.Registry;
|
import dev.loki.loparkour.api.core.Registry;
|
||||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
import dev.loki.loparkour.leaderboard.core.Leaderboard;
|
||||||
import dev.loki.loparkour.leaderboard.Score;
|
import dev.loki.loparkour.leaderboard.model.Score;
|
||||||
import dev.loki.loparkour.mode.Mode;
|
import dev.loki.loparkour.mode.base.Mode;
|
||||||
import me.filoghost.holographicdisplays.api.HolographicDisplaysAPI;
|
import me.filoghost.holographicdisplays.api.HolographicDisplaysAPI;
|
||||||
|
|
||||||
public class HoloHook {
|
public class HoloHook {
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
package dev.loki.loparkour.hook;
|
package dev.loki.loparkour.hook.papi;
|
||||||
|
|
||||||
import dev.loki.loparkour.LoParkour;
|
import dev.loki.loparkour.LoParkour;
|
||||||
import dev.loki.loparkour.api.Registry;
|
import dev.loki.loparkour.api.core.Registry;
|
||||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
import dev.loki.loparkour.leaderboard.core.Leaderboard;
|
||||||
import dev.loki.loparkour.leaderboard.Score;
|
import dev.loki.loparkour.leaderboard.model.Score;
|
||||||
import dev.loki.loparkour.mode.Mode;
|
import dev.loki.loparkour.mode.base.Mode;
|
||||||
import dev.loki.loparkour.mode.Modes;
|
import dev.loki.loparkour.mode.base.Modes;
|
||||||
import dev.loki.loparkour.player.ParkourPlayer;
|
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||||
import dev.loki.loparkour.player.ParkourSpectator;
|
import dev.loki.loparkour.player.spectator.ParkourSpectator;
|
||||||
import dev.loki.loparkour.player.ParkourUser;
|
import dev.loki.loparkour.player.core.ParkourUser;
|
||||||
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
|
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.loki.loparkour.hook;
|
package dev.loki.loparkour.hook.vault;
|
||||||
|
|
||||||
import dev.loki.loparkour.LoParkour;
|
import dev.loki.loparkour.LoParkour;
|
||||||
import net.milkbowl.vault.economy.Economy;
|
import net.milkbowl.vault.economy.Economy;
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
package dev.loki.loparkour.leaderboard;
|
package dev.loki.loparkour.leaderboard.core;
|
||||||
|
import dev.loki.loparkour.leaderboard.persistence.LeaderboardStorage;
|
||||||
|
import dev.loki.loparkour.leaderboard.model.Score;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
package dev.loki.loparkour.leaderboard;
|
package dev.loki.loparkour.leaderboard.core;
|
||||||
|
import dev.loki.loparkour.leaderboard.model.Score;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.loki.loparkour.leaderboard;
|
package dev.loki.loparkour.leaderboard.model;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a record, used to keep track of the score a player may achieve.
|
* Represents a record, used to keep track of the score a player may achieve.
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
package dev.loki.loparkour.leaderboard;
|
package dev.loki.loparkour.leaderboard.persistence;
|
||||||
|
import dev.loki.loparkour.leaderboard.model.Score;
|
||||||
|
import dev.loki.loparkour.leaderboard.core.LeaderboardSorter;
|
||||||
|
import dev.loki.loparkour.leaderboard.core.Leaderboard;
|
||||||
|
|
||||||
import dev.loki.loparkour.LoParkour;
|
import dev.loki.loparkour.LoParkour;
|
||||||
import dev.loki.loparkour.config.Config;
|
import dev.loki.loparkour.config.core.Config;
|
||||||
import dev.loki.loparkour.storage.Storage;
|
import dev.loki.loparkour.storage.Storage;
|
||||||
import dev.lolib.scheduler.Scheduler;
|
import dev.lolib.scheduler.Scheduler;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
package dev.loki.loparkour.listener;
|
|
||||||
|
|
||||||
import dev.loki.loparkour.LoParkour;
|
|
||||||
import dev.loki.loparkour.command.SchematicCommandHandler;
|
|
||||||
import dev.loki.loparkour.util.ColorUtil;
|
|
||||||
import dev.loki.loparkour.util.Locations;
|
|
||||||
import dev.loki.loparkour.util.ParticleUtil;
|
|
||||||
import org.bukkit.Location;
|
|
||||||
import org.bukkit.Particle;
|
|
||||||
import org.bukkit.command.CommandSender;
|
|
||||||
import org.bukkit.entity.Player;
|
|
||||||
import org.bukkit.event.EventHandler;
|
|
||||||
import org.bukkit.event.Listener;
|
|
||||||
import org.bukkit.event.block.Action;
|
|
||||||
import org.bukkit.event.player.PlayerInteractEvent;
|
|
||||||
import org.bukkit.inventory.EquipmentSlot;
|
|
||||||
import org.bukkit.inventory.ItemStack;
|
|
||||||
import org.bukkit.util.BoundingBox;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles LPSchematic wand clicks (left/right click on blocks).
|
|
||||||
*/
|
|
||||||
public class SchematicWandListener implements Listener {
|
|
||||||
|
|
||||||
@EventHandler
|
|
||||||
public void onWandInteract(PlayerInteractEvent event) {
|
|
||||||
Player player = event.getPlayer();
|
|
||||||
ItemStack item = player.getInventory().getItemInMainHand();
|
|
||||||
|
|
||||||
if (!player.hasPermission("LoParkour.admin")) return;
|
|
||||||
if (item.getItemMeta() == null || !item.getItemMeta().getDisplayName().contains("LPSchematic Wand")) return;
|
|
||||||
if (event.getClickedBlock() == null || event.getHand() != EquipmentSlot.HAND) return;
|
|
||||||
|
|
||||||
Location loc = event.getClickedBlock().getLocation();
|
|
||||||
Location[] sel = SchematicCommandHandler.selections.get(player);
|
|
||||||
event.setCancelled(true);
|
|
||||||
|
|
||||||
if (event.getAction() == Action.LEFT_CLICK_BLOCK) {
|
|
||||||
Location[] updated = new Location[]{loc, sel != null ? sel[1] : null};
|
|
||||||
SchematicCommandHandler.selections.put(player, updated);
|
|
||||||
send(player, LoParkour.PREFIX + "Position 1 set to " + Locations.toString(loc, true));
|
|
||||||
if (updated[1] != null)
|
|
||||||
ParticleUtil.box(BoundingBox.of(loc, updated[1]), player.getWorld(), Particle.END_ROD, player, 0.2);
|
|
||||||
|
|
||||||
} else if (event.getAction() == Action.RIGHT_CLICK_BLOCK) {
|
|
||||||
Location[] updated = new Location[]{sel != null ? sel[0] : null, loc};
|
|
||||||
SchematicCommandHandler.selections.put(player, updated);
|
|
||||||
send(player, LoParkour.PREFIX + "Position 2 set to " + Locations.toString(loc, true));
|
|
||||||
if (updated[0] != null)
|
|
||||||
ParticleUtil.box(BoundingBox.of(updated[0], loc), player.getWorld(), Particle.END_ROD, player, 0.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void send(CommandSender sender, String msg) {
|
|
||||||
sender.sendMessage(ColorUtil.color(msg));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
package dev.loki.loparkour.listener;
|
package dev.loki.loparkour.listener.gameplay;
|
||||||
|
|
||||||
import dev.loki.loparkour.config.Config;
|
import dev.loki.loparkour.config.core.Config;
|
||||||
import dev.loki.loparkour.config.Locales;
|
import dev.loki.loparkour.config.locale.Locales;
|
||||||
import dev.loki.loparkour.config.Option;
|
import dev.loki.loparkour.config.options.Option;
|
||||||
import dev.loki.loparkour.menu.Menus;
|
import dev.loki.loparkour.menu.core.Menus;
|
||||||
import dev.loki.loparkour.menu.ParkourOption;
|
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||||
import dev.loki.loparkour.player.ParkourPlayer;
|
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||||
import dev.loki.loparkour.player.ParkourUser;
|
import dev.loki.loparkour.player.core.ParkourUser;
|
||||||
import dev.loki.loparkour.session.Session;
|
import dev.loki.loparkour.session.core.Session;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.event.Cancellable;
|
import org.bukkit.event.Cancellable;
|
||||||
import org.bukkit.event.EventHandler;
|
import org.bukkit.event.EventHandler;
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
package dev.loki.loparkour.listener;
|
package dev.loki.loparkour.listener.player;
|
||||||
|
|
||||||
import dev.loki.loparkour.config.Config;
|
import dev.loki.loparkour.config.core.Config;
|
||||||
import dev.loki.loparkour.menu.ParkourOption;
|
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||||
import dev.loki.loparkour.mode.Modes;
|
import dev.loki.loparkour.mode.base.Modes;
|
||||||
import dev.loki.loparkour.player.ParkourUser;
|
import dev.loki.loparkour.player.core.ParkourUser;
|
||||||
import dev.loki.loparkour.world.World;
|
import dev.loki.loparkour.world.core.World;
|
||||||
import io.papermc.lib.PaperLib;
|
import io.papermc.lib.PaperLib;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
package dev.loki.loparkour.listener.schematic;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.command.schematic.SchematicCommandHandler;
|
||||||
|
import dev.loki.loparkour.config.locale.Locales;
|
||||||
|
import dev.loki.loparkour.util.particle.ParticleUtil;
|
||||||
|
import dev.loki.loparkour.util.text.ColorUtil;
|
||||||
|
import dev.loki.loparkour.util.world.Locations;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Particle;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.block.Action;
|
||||||
|
import org.bukkit.event.player.PlayerInteractEvent;
|
||||||
|
import org.bukkit.inventory.EquipmentSlot;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wand left/right click sets schematic selection corners.
|
||||||
|
*/
|
||||||
|
public class SchematicWandListener implements Listener {
|
||||||
|
|
||||||
|
@EventHandler(ignoreCancelled = true)
|
||||||
|
public void onInteract(@NotNull PlayerInteractEvent event) {
|
||||||
|
if (event.getHand() != EquipmentSlot.HAND) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!SchematicCommandHandler.isWand(event.getItem())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Action action = event.getAction();
|
||||||
|
if (action != Action.LEFT_CLICK_BLOCK && action != Action.RIGHT_CLICK_BLOCK) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.getClickedBlock() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.setCancelled(true);
|
||||||
|
var player = event.getPlayer();
|
||||||
|
Location loc = event.getClickedBlock().getLocation();
|
||||||
|
int index = action == Action.LEFT_CLICK_BLOCK ? 0 : 1;
|
||||||
|
|
||||||
|
SchematicCommandHandler.setPos(player, loc, index);
|
||||||
|
String label = index == 0 ? "1" : "2";
|
||||||
|
player.sendMessage(ColorUtil.color(
|
||||||
|
Locales.getString(player, "schematic.pos_set").formatted(label, Locations.toString(loc, true))));
|
||||||
|
|
||||||
|
Location[] sel = SchematicCommandHandler.getSelection(player);
|
||||||
|
if (sel[0] != null && sel[1] != null) {
|
||||||
|
ParticleUtil.box(
|
||||||
|
org.bukkit.util.BoundingBox.of(sel[0], sel[1]),
|
||||||
|
player.getWorld(),
|
||||||
|
Particle.END_ROD,
|
||||||
|
player,
|
||||||
|
0.2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
package dev.loki.loparkour.menu.community;
|
package dev.loki.loparkour.menu.community;
|
||||||
|
|
||||||
import dev.loki.loparkour.config.Locales;
|
import dev.loki.loparkour.config.locale.Locales;
|
||||||
import dev.loki.loparkour.menu.LPMenu;
|
import dev.loki.loparkour.menu.core.LPMenu;
|
||||||
import dev.loki.loparkour.menu.Menus;
|
import dev.loki.loparkour.menu.core.Menus;
|
||||||
import dev.loki.loparkour.menu.ParkourOption;
|
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
package dev.loki.loparkour.menu.community;
|
package dev.loki.loparkour.menu.community;
|
||||||
|
|
||||||
import dev.loki.loparkour.api.Registry;
|
import dev.loki.loparkour.api.core.Registry;
|
||||||
import dev.loki.loparkour.config.Locales;
|
import dev.loki.loparkour.config.locale.Locales;
|
||||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
import dev.loki.loparkour.leaderboard.core.Leaderboard;
|
||||||
import dev.loki.loparkour.menu.LPMenu;
|
import dev.loki.loparkour.menu.core.LPMenu;
|
||||||
import dev.loki.loparkour.menu.Menus;
|
import dev.loki.loparkour.menu.core.Menus;
|
||||||
import dev.loki.loparkour.menu.ParkourOption;
|
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||||
import dev.loki.loparkour.mode.Mode;
|
import dev.loki.loparkour.mode.base.Mode;
|
||||||
import dev.lolib.gui.InventoryGUI;
|
import dev.lolib.gui.InventoryGUI;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
|
@ -64,12 +64,12 @@ public class LeaderboardsMenu extends LPMenu {
|
||||||
// Add navigation buttons
|
// Add navigation buttons
|
||||||
if (currentPage > 0) {
|
if (currentPage > 0) {
|
||||||
// Previous page button
|
// Previous page button
|
||||||
gui = gui.setItem(0, backItem(), e -> open(player, currentPage - 1));
|
gui = gui.setItem(0, backItem(player), e -> open(player, currentPage - 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentPage < totalPages - 1) {
|
if (currentPage < totalPages - 1) {
|
||||||
// Next page button
|
// Next page button
|
||||||
gui = gui.setItem(8, nextItem(), e -> open(player, currentPage + 1));
|
gui = gui.setItem(8, nextItem(player), e -> open(player, currentPage + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
gui.open(player);
|
gui.open(player);
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
package dev.loki.loparkour.menu.community;
|
package dev.loki.loparkour.menu.community;
|
||||||
|
|
||||||
import dev.loki.loparkour.config.Locales;
|
import dev.loki.loparkour.config.locale.Locales;
|
||||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
import dev.loki.loparkour.leaderboard.core.Leaderboard;
|
||||||
import dev.loki.loparkour.leaderboard.Score;
|
import dev.loki.loparkour.leaderboard.model.Score;
|
||||||
import dev.loki.loparkour.menu.LPMenu;
|
import dev.loki.loparkour.menu.core.LPMenu;
|
||||||
import dev.loki.loparkour.menu.ParkourOption;
|
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||||
import dev.loki.loparkour.mode.Mode;
|
import dev.loki.loparkour.mode.base.Mode;
|
||||||
import dev.loki.loparkour.player.ParkourUser;
|
import dev.loki.loparkour.player.core.ParkourUser;
|
||||||
import dev.lolib.gui.InventoryGUI;
|
import dev.lolib.gui.InventoryGUI;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.OfflinePlayer;
|
import org.bukkit.OfflinePlayer;
|
||||||
|
|
@ -89,12 +89,12 @@ public class SingleLeaderboardMenu extends LPMenu {
|
||||||
// Add navigation buttons
|
// Add navigation buttons
|
||||||
if (currentPage > 0) {
|
if (currentPage > 0) {
|
||||||
// Previous page button
|
// Previous page button
|
||||||
gui = gui.setItem(0, backItem(), e -> open(player, mode, sort, currentPage - 1));
|
gui = gui.setItem(0, backItem(player), e -> open(player, mode, sort, currentPage - 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentPage < totalPages - 1) {
|
if (currentPage < totalPages - 1) {
|
||||||
// Next page button
|
// Next page button
|
||||||
gui = gui.setItem(8, nextItem(), e -> open(player, mode, sort, currentPage + 1));
|
gui = gui.setItem(8, nextItem(player), e -> open(player, mode, sort, currentPage + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
gui.open(player);
|
gui.open(player);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package dev.loki.loparkour.menu;
|
package dev.loki.loparkour.menu.core;
|
||||||
|
|
||||||
import dev.loki.loparkour.player.ParkourUser;
|
import dev.loki.loparkour.player.core.ParkourUser;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
@ -29,7 +29,7 @@ public abstract class DynamicMenu {
|
||||||
*/
|
*/
|
||||||
// Stub: migrate to LoLib GUI when dynamic rows are wired up.
|
// Stub: migrate to LoLib GUI when dynamic rows are wired up.
|
||||||
/*
|
/*
|
||||||
public void registerMainItem(int row, int id, BiFunction<@NotNull Player, @Nullable ParkourUser, dev.loki.loparkour.util.MenuItem> item, Predicate<Player> shouldDisplay) {
|
public void registerMainItem(int row, int id, BiFunction<@NotNull Player, @Nullable ParkourUser, dev.loki.loparkour.util.gui.MenuItem> item, Predicate<Player> shouldDisplay) {
|
||||||
if (id < 0 || row < 0 || row > 4) {
|
if (id < 0 || row < 0 || row > 4) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -75,6 +75,6 @@ public abstract class DynamicMenu {
|
||||||
/**
|
/**
|
||||||
* Data class for registered items
|
* Data class for registered items
|
||||||
*/
|
*/
|
||||||
protected record ItemContainer(int id, BiFunction<@NotNull Player, @Nullable ParkourUser, dev.loki.loparkour.util.MenuItem> item,
|
protected record ItemContainer(int id, BiFunction<@NotNull Player, @Nullable ParkourUser, dev.loki.loparkour.util.gui.MenuItem> item,
|
||||||
Predicate<Player> predicate) {}
|
Predicate<Player> predicate) {}
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
package dev.loki.loparkour.menu;
|
package dev.loki.loparkour.menu.core;
|
||||||
|
|
||||||
import dev.loki.loparkour.config.Locales;
|
import dev.loki.loparkour.config.locale.Locales;
|
||||||
import dev.loki.loparkour.config.Option;
|
import dev.loki.loparkour.config.options.Option;
|
||||||
import dev.loki.loparkour.util.ColorUtil;
|
import dev.loki.loparkour.util.text.ColorUtil;
|
||||||
import dev.lolib.gui.InventoryGUI;
|
import dev.lolib.gui.InventoryGUI;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
@ -25,7 +25,7 @@ public abstract class LPMenu {
|
||||||
public abstract void open(@NotNull Player player);
|
public abstract void open(@NotNull Player player);
|
||||||
|
|
||||||
protected String locale(@NotNull Player player) {
|
protected String locale(@NotNull Player player) {
|
||||||
var user = dev.loki.loparkour.player.ParkourUser.getUser(player);
|
var user = dev.loki.loparkour.player.core.ParkourUser.getUser(player);
|
||||||
String loc = user != null ? user.locale : Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG);
|
String loc = user != null ? user.locale : Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG);
|
||||||
return (loc != null && !loc.isBlank()) ? loc : "en";
|
return (loc != null && !loc.isBlank()) ? loc : "en";
|
||||||
}
|
}
|
||||||
|
|
@ -40,24 +40,12 @@ public abstract class LPMenu {
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected ItemStack backItem() {
|
protected ItemStack backItem(@NotNull Player player) {
|
||||||
ItemStack item = new ItemStack(Material.RED_DYE);
|
return localeItem(player, "other.back");
|
||||||
ItemMeta meta = item.getItemMeta();
|
|
||||||
if (meta != null) {
|
|
||||||
meta.setDisplayName(ColorUtil.color("<#DE1F1F><bold>Назад"));
|
|
||||||
item.setItemMeta(meta);
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected ItemStack nextItem() {
|
protected ItemStack nextItem(@NotNull Player player) {
|
||||||
ItemStack item = new ItemStack(Material.LIME_DYE);
|
return localeItem(player, "other.next");
|
||||||
ItemMeta meta = item.getItemMeta();
|
|
||||||
if (meta != null) {
|
|
||||||
meta.setDisplayName(ColorUtil.color("<#0DCB07><bold>Далее"));
|
|
||||||
item.setItemMeta(meta);
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected ItemStack closeButton(@NotNull Player player) {
|
protected ItemStack closeButton(@NotNull Player player) {
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
package dev.loki.loparkour.menu;
|
package dev.loki.loparkour.menu.core;
|
||||||
|
|
||||||
import dev.loki.loparkour.config.Config;
|
import dev.loki.loparkour.config.core.Config;
|
||||||
import dev.loki.loparkour.config.Locales;
|
import dev.loki.loparkour.config.locale.Locales;
|
||||||
import dev.loki.loparkour.player.ParkourUser;
|
import dev.loki.loparkour.player.core.ParkourUser;
|
||||||
import dev.lolib.gui.InventoryGUI;
|
import dev.lolib.gui.InventoryGUI;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
|
@ -59,7 +59,7 @@ public class MainMenu extends LPMenu {
|
||||||
|
|
||||||
int count = visibleItems.size();
|
int count = visibleItems.size();
|
||||||
if (count == 0) {
|
if (count == 0) {
|
||||||
player.sendMessage(Locales.getString(player, "other.no_menu_items"));
|
Locales.send(player, "other.no_menu_items");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.loki.loparkour.menu;
|
package dev.loki.loparkour.menu.core;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Placeholder during menu migration; real flows use {@link Menus} / LoLib GUI.
|
* Placeholder during menu migration; real flows use {@link Menus} / LoLib GUI.
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.loki.loparkour.menu;
|
package dev.loki.loparkour.menu.core;
|
||||||
|
|
||||||
import dev.loki.loparkour.menu.community.CommunityMenu;
|
import dev.loki.loparkour.menu.community.CommunityMenu;
|
||||||
import dev.loki.loparkour.menu.community.LeaderboardsMenu;
|
import dev.loki.loparkour.menu.community.LeaderboardsMenu;
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package dev.loki.loparkour.menu;
|
package dev.loki.loparkour.menu.core;
|
||||||
|
|
||||||
import dev.loki.loparkour.config.Config;
|
import dev.loki.loparkour.config.core.Config;
|
||||||
import dev.loki.loparkour.config.Option;
|
import dev.loki.loparkour.config.options.Option;
|
||||||
import org.bukkit.permissions.Permissible;
|
import org.bukkit.permissions.Permissible;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
package dev.loki.loparkour.menu.lobby;
|
package dev.loki.loparkour.menu.lobby;
|
||||||
|
|
||||||
import dev.loki.loparkour.config.Locales;
|
import dev.loki.loparkour.config.locale.Locales;
|
||||||
import dev.loki.loparkour.menu.LPMenu;
|
import dev.loki.loparkour.menu.core.LPMenu;
|
||||||
import dev.loki.loparkour.menu.Menus;
|
import dev.loki.loparkour.menu.core.Menus;
|
||||||
import dev.loki.loparkour.menu.ParkourOption;
|
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||||
import dev.loki.loparkour.player.ParkourUser;
|
import dev.loki.loparkour.player.core.ParkourUser;
|
||||||
import dev.loki.loparkour.session.Session;
|
import dev.loki.loparkour.session.core.Session;
|
||||||
import dev.loki.loparkour.util.ColorUtil;
|
import dev.loki.loparkour.util.text.ColorUtil;
|
||||||
import dev.lolib.gui.InventoryGUI;
|
import dev.lolib.gui.InventoryGUI;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
package dev.loki.loparkour.menu.lobby;
|
package dev.loki.loparkour.menu.lobby;
|
||||||
|
|
||||||
import dev.loki.loparkour.config.Locales;
|
import dev.loki.loparkour.config.locale.Locales;
|
||||||
import dev.loki.loparkour.menu.LPMenu;
|
import dev.loki.loparkour.menu.core.LPMenu;
|
||||||
import dev.loki.loparkour.menu.Menus;
|
import dev.loki.loparkour.menu.core.Menus;
|
||||||
import dev.loki.loparkour.mode.Modes;
|
import dev.loki.loparkour.mode.base.Modes;
|
||||||
import dev.loki.loparkour.player.ParkourPlayer;
|
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||||
import dev.loki.loparkour.player.ParkourUser;
|
import dev.loki.loparkour.player.core.ParkourUser;
|
||||||
import dev.loki.loparkour.util.ColorUtil;
|
import dev.loki.loparkour.util.text.ColorUtil;
|
||||||
import dev.lolib.gui.InventoryGUI;
|
import dev.lolib.gui.InventoryGUI;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
@ -51,10 +51,18 @@ public class PlayerManagementMenu extends LPMenu {
|
||||||
boolean muted = session.isMuted(other);
|
boolean muted = session.isMuted(other);
|
||||||
ItemMeta displayMeta = skull.getItemMeta();
|
ItemMeta displayMeta = skull.getItemMeta();
|
||||||
if (displayMeta != null) {
|
if (displayMeta != null) {
|
||||||
displayMeta.setDisplayName(ColorUtil.color("<yellow>" + other.getName() + (muted ? " §c[muted]" : "")));
|
String muteSuffix = muted
|
||||||
|
? Locales.getString(locale, "lobby.player_management.head.muted_suffix")
|
||||||
|
: "";
|
||||||
|
displayMeta.setDisplayName(ColorUtil.color(
|
||||||
|
Locales.getString(locale, "lobby.player_management.head.name")
|
||||||
|
.formatted(other.getName(), muteSuffix)));
|
||||||
|
String muteLoreKey = muted
|
||||||
|
? "lobby.player_management.head.lore_unmute"
|
||||||
|
: "lobby.player_management.head.lore_mute";
|
||||||
displayMeta.setLore(List.of(
|
displayMeta.setLore(List.of(
|
||||||
ColorUtil.color("§7Left-click §fto kick"),
|
ColorUtil.color(Locales.getString(locale, "lobby.player_management.head.lore_kick")),
|
||||||
ColorUtil.color("§7Right-click §f" + (muted ? "to unmute" : "to mute"))
|
ColorUtil.color(Locales.getString(locale, muteLoreKey))
|
||||||
));
|
));
|
||||||
skull.setItemMeta(displayMeta);
|
skull.setItemMeta(displayMeta);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
package dev.loki.loparkour.menu.play;
|
package dev.loki.loparkour.menu.play;
|
||||||
|
|
||||||
import dev.loki.loparkour.config.Locales;
|
import dev.loki.loparkour.config.locale.Locales;
|
||||||
import dev.loki.loparkour.menu.LPMenu;
|
import dev.loki.loparkour.menu.core.LPMenu;
|
||||||
import dev.loki.loparkour.menu.Menus;
|
import dev.loki.loparkour.menu.core.Menus;
|
||||||
import dev.loki.loparkour.menu.ParkourOption;
|
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
package dev.loki.loparkour.menu.play;
|
package dev.loki.loparkour.menu.play;
|
||||||
|
|
||||||
import dev.loki.loparkour.api.Registry;
|
import dev.loki.loparkour.api.core.Registry;
|
||||||
import dev.loki.loparkour.config.Config;
|
import dev.loki.loparkour.config.core.Config;
|
||||||
import dev.loki.loparkour.config.Locales;
|
import dev.loki.loparkour.config.locale.Locales;
|
||||||
import dev.loki.loparkour.menu.LPMenu;
|
import dev.loki.loparkour.menu.core.LPMenu;
|
||||||
import dev.loki.loparkour.mode.Mode;
|
import dev.loki.loparkour.mode.base.Mode;
|
||||||
import dev.loki.loparkour.player.ParkourUser;
|
import dev.loki.loparkour.player.core.ParkourUser;
|
||||||
import dev.lolib.gui.InventoryGUI;
|
import dev.lolib.gui.InventoryGUI;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
|
@ -81,12 +81,12 @@ public class SingleMenu extends LPMenu {
|
||||||
// Add navigation buttons
|
// Add navigation buttons
|
||||||
if (currentPage > 0) {
|
if (currentPage > 0) {
|
||||||
// Previous page button
|
// Previous page button
|
||||||
gui = gui.setItem(0, backItem(), e -> open(player, currentPage - 1));
|
gui = gui.setItem(0, backItem(player), e -> open(player, currentPage - 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentPage < totalPages - 1) {
|
if (currentPage < totalPages - 1) {
|
||||||
// Next page button
|
// Next page button
|
||||||
gui = gui.setItem(8, nextItem(), e -> open(player, currentPage + 1));
|
gui = gui.setItem(8, nextItem(player), e -> open(player, currentPage + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
gui.open(player);
|
gui.open(player);
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue