v1.2.4: Remove roguelike perks, update bStats to 3.2.1, add new game modes and lpschem system
- Removed roguelike perk system (double jump, magnet, shield, second chance) - Updated bStats dependency to 3.2.1 with proper relocation - Changed bStats plugin ID to 29754 - Added new game modes: Speedrun, Gravity Shift, Hardcore - Implemented .lpschem format with GZIP + JSON compression - Added JumpValidator for physics-based jump validation - Added JumpType system (neo-jump, head-hitter, fence, trapdoor, ladder) - Implemented Ghost system for recording/playback top players - Added loparkour-vilib as local dependency - Updated config with new mode settings and removed roguelike section - Cleaned up TODO.md roadmap
This commit is contained in:
parent
aecb03ccf8
commit
8aeb547897
68 changed files with 6186 additions and 163 deletions
38
CHANGES.md
38
CHANGES.md
|
|
@ -1,38 +0,0 @@
|
|||
# Changelog
|
||||
|
||||
## Version 6.0.0 (2026-02-24)
|
||||
|
||||
### 🔄 Major Changes
|
||||
- **Полная переработка проекта**
|
||||
- Название: LoParkour
|
||||
- Package: `dev.loki.loparkour`
|
||||
- Главный класс: `LoParkour`
|
||||
- Автор: loki
|
||||
|
||||
### 🏗️ Build System
|
||||
- **Миграция на Gradle**
|
||||
- Удалена Maven сборка (pom.xml)
|
||||
- Добавлен Gradle с Wrapper
|
||||
- Оптимизирована конфигурация зависимостей
|
||||
- Улучшена скорость сборки
|
||||
|
||||
### 📦 Technical
|
||||
- Обновлена версия PlaceholderAPI: 2.11.2 → 2.11.6
|
||||
- Исключена проблемная зависимость ConfigUpdater
|
||||
- Настроен Shadow plugin для shading
|
||||
- Добавлен .gitignore для Gradle
|
||||
|
||||
### 🎯 Commands
|
||||
- Новая команда: `/loparkour` (aliases: `/parkour`, `/lp`)
|
||||
- Старая команда `/witp` удалена
|
||||
|
||||
### 📝 Documentation
|
||||
- Создан TODO.md с детальным планом развития
|
||||
- Обновлен README.md
|
||||
- Добавлены инструкции по сборке
|
||||
|
||||
---
|
||||
|
||||
## Version 1.0.0 and earlier
|
||||
|
||||
Initial development versions.
|
||||
12
build.gradle
12
build.gradle
|
|
@ -4,7 +4,7 @@ plugins {
|
|||
}
|
||||
|
||||
group = 'dev.loki'
|
||||
version = '1.1.0'
|
||||
version = '1.2.4'
|
||||
description = 'LoParkour - Advanced parkour plugin for Minecraft'
|
||||
|
||||
java {
|
||||
|
|
@ -72,11 +72,13 @@ dependencies {
|
|||
// Spigot API
|
||||
compileOnly 'org.spigotmc:spigot-api:1.20.4-R0.1-SNAPSHOT'
|
||||
|
||||
// Local vilib fork
|
||||
implementation project(':loparkour-vilib')
|
||||
|
||||
// Shaded dependencies
|
||||
implementation('com.github.Efnilite:vilib:dd786231c3') {
|
||||
exclude group: 'com.tchristofferson', module: 'ConfigUpdater'
|
||||
}
|
||||
implementation 'io.papermc:paperlib:1.0.7'
|
||||
implementation 'com.google.code.gson:gson:2.10.1'
|
||||
implementation 'org.bstats:bstats-bukkit:3.2.1'
|
||||
|
||||
// Provided dependencies (plugins)
|
||||
compileOnly 'me.clip:placeholderapi:2.11.6'
|
||||
|
|
@ -99,6 +101,8 @@ shadowJar {
|
|||
// Relocate shaded dependencies
|
||||
relocate 'dev.efnilite.vilib', 'dev.loki.loparkour.lib.vilib'
|
||||
relocate 'io.papermc.lib', 'dev.loki.loparkour.lib.paperlib'
|
||||
relocate 'com.google.gson', 'dev.loki.loparkour.lib.gson'
|
||||
relocate 'org.bstats', 'dev.loki.loparkour.lib.bstats'
|
||||
|
||||
// Minimize jar size
|
||||
minimize()
|
||||
|
|
|
|||
74
docs/CHANGES.md
Normal file
74
docs/CHANGES.md
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# Changelog - LoParkour
|
||||
|
||||
## Version 1.1.0 (Current)
|
||||
|
||||
### Added
|
||||
- **Jump Validation System**
|
||||
- Mathematical validation using Pythagorean theorem
|
||||
- Maximum jump distance: 4.5 blocks
|
||||
- Configurable safe ranges for X, Y, Z axes
|
||||
- `JumpValidator` class for precise jump checking
|
||||
|
||||
- **Jump Types System**
|
||||
- NEO_JUMP - jumps around fences/walls
|
||||
- HEAD_HITTER - jumps with blocks overhead
|
||||
- FENCE_JUMP - jumps onto fences
|
||||
- TRAPDOOR_JUMP - jumps onto trapdoors
|
||||
- LADDER_JUMP - jumps onto ladders
|
||||
- Each type has difficulty multiplier
|
||||
- Configurable in config.yml
|
||||
|
||||
- **Memory Optimization**
|
||||
- Changed history storage from ArrayList to LinkedList
|
||||
- Automatic block cleanup at 20+ blocks distance
|
||||
- Periodic cleanup task every 100 ticks
|
||||
- Configurable cleanup settings
|
||||
|
||||
- **New Game Modes**
|
||||
- Speedrun Mode - blocks disappear after 1.5 seconds
|
||||
- Gravity Shift Mode - random potion effects every 10 jumps
|
||||
- Hardcore Mode - full reset on fall with optional reward loss
|
||||
|
||||
- **Ghost Mode System**
|
||||
- Records top-3 player movements per mode
|
||||
- Compression (only movements > 0.1 blocks)
|
||||
- Playback using ArmorStand entities
|
||||
- Binary format for efficient storage
|
||||
- Configurable ghost settings
|
||||
|
||||
- **Roguelike Perk System**
|
||||
- Double Jump perk (1 use)
|
||||
- Magnet perk (10 jumps, 20% distance reduction)
|
||||
- Second Chance perk (1 use, teleport back 3 blocks)
|
||||
- Shield perk (5 jumps, fall protection)
|
||||
- Rarity system: Common (60%), Rare (30%), Epic (10%)
|
||||
- Bonus blocks every 20 jumps
|
||||
|
||||
- **New .lpschem Schematic Format**
|
||||
- JSON + GZIP compression
|
||||
- Palette encoding for optimization
|
||||
- BlockData support (not just Material)
|
||||
- Auto-detect LIME_WOOL (start) and RED_WOOL (end)
|
||||
- Support for BlockDisplay entities
|
||||
- Support for particle effects
|
||||
- Ghost path data storage
|
||||
- Converter from old vilib format
|
||||
- New commands: save, paste, list, reload, convert
|
||||
|
||||
### Changed
|
||||
- Migrated build system from Maven to Gradle
|
||||
- Package renamed to dev.loki.loparkour
|
||||
- Improved schematic command structure
|
||||
- Updated all mode registrations
|
||||
|
||||
### Technical
|
||||
- Java 17
|
||||
- Spigot API 1.20.4
|
||||
- Gradle 8.1.1
|
||||
- Added Gson dependency for JSON serialization
|
||||
|
||||
---
|
||||
|
||||
## Version 1.0.0 (Legacy)
|
||||
|
||||
Initial release with basic parkour functionality.
|
||||
177
docs/LPSCHEM_USAGE.md
Normal file
177
docs/LPSCHEM_USAGE.md
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
# LPSchematic - Примеры использования
|
||||
|
||||
## Создание схематики из мира
|
||||
|
||||
```java
|
||||
// Выделить область в игре (WorldEdit style)
|
||||
Location pos1 = player.getLocation();
|
||||
Location pos2 = player.getLocation().add(10, 5, 10);
|
||||
|
||||
// Создать схематику
|
||||
LPSchematic schematic = new LPSchematicBuilder("easy-jump-1", "Loki", 0.25, 10, 5, 10)
|
||||
.fromWorld(world, pos1, pos2)
|
||||
.addTag("easy")
|
||||
.addTag("beginner")
|
||||
.build();
|
||||
|
||||
// Сохранить
|
||||
LPSchematicManager manager = new LPSchematicManager();
|
||||
manager.saveSchematic(schematic);
|
||||
```
|
||||
|
||||
## Создание схематики вручную
|
||||
|
||||
```java
|
||||
LPSchematic schematic = new LPSchematicBuilder("custom-jump", "Loki", 0.5, 5, 3, 5)
|
||||
.setBlock(0, 0, 0, Material.STONE)
|
||||
.setBlock(1, 0, 1, Material.STONE)
|
||||
.setBlock(2, 1, 2, Material.STONE)
|
||||
.setBlock(3, 0, 3, Material.STONE)
|
||||
.setBlock(4, 0, 4, Material.STONE)
|
||||
.setStart(0, 0, 0)
|
||||
.setEnd(4, 0, 4)
|
||||
.addTag("medium")
|
||||
.build();
|
||||
|
||||
manager.saveSchematic(schematic);
|
||||
```
|
||||
|
||||
## Загрузка и вставка
|
||||
|
||||
```java
|
||||
LPSchematicManager manager = new LPSchematicManager();
|
||||
manager.loadAll();
|
||||
|
||||
LPSchematic schematic = manager.getSchematic("easy-jump-1");
|
||||
if (schematic != null) {
|
||||
Location origin = player.getLocation();
|
||||
List<Block> blocks = schematic.paste(origin, world);
|
||||
|
||||
player.sendMessage("Pasted " + blocks.size() + " blocks");
|
||||
}
|
||||
```
|
||||
|
||||
## Добавление визуальных эффектов
|
||||
|
||||
```java
|
||||
LPSchematic schematic = manager.getSchematic("neon-city");
|
||||
|
||||
SchematicVisuals visuals = new SchematicVisuals();
|
||||
|
||||
// Добавить вращающийся золотой блок
|
||||
SchematicVisuals.BlockDisplay display = new SchematicVisuals.BlockDisplay(
|
||||
"minecraft:gold_block",
|
||||
new float[]{5.5f, 2.0f, 5.5f},
|
||||
new float[]{0.8f, 0.8f, 0.8f}
|
||||
);
|
||||
display.animation = new SchematicVisuals.Animation("rotate", "y", 1.5f);
|
||||
visuals.addDisplay(display);
|
||||
|
||||
// Добавить частицы
|
||||
SchematicVisuals.ParticleEffect particle = new SchematicVisuals.ParticleEffect(
|
||||
"end_rod",
|
||||
new float[]{5.0f, 3.0f, 5.0f},
|
||||
5,
|
||||
0.2f
|
||||
);
|
||||
visuals.addParticle(particle);
|
||||
|
||||
schematic.setVisuals(visuals);
|
||||
manager.saveSchematic(schematic);
|
||||
```
|
||||
|
||||
## Конвертация из старого формата
|
||||
|
||||
```java
|
||||
// TODO: Создать конвертер
|
||||
public class SchematicConverter {
|
||||
public static LPSchematic fromVilib(dev.efnilite.vilib.schematic.Schematic oldSchematic) {
|
||||
// Извлечь данные из старой схематики
|
||||
// Создать новую LPSchematic
|
||||
// Вернуть результат
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Команда для создания схематики
|
||||
|
||||
```java
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
if (!(sender instanceof Player)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Player player = (Player) sender;
|
||||
|
||||
// /lpschem create <name> <difficulty>
|
||||
if (args[0].equals("create")) {
|
||||
String name = args[1];
|
||||
double difficulty = Double.parseDouble(args[2]);
|
||||
|
||||
// Получить выделенную область (из WorldEdit или своей системы)
|
||||
Location pos1 = getPos1(player);
|
||||
Location pos2 = getPos2(player);
|
||||
|
||||
int width = Math.abs(pos2.getBlockX() - pos1.getBlockX()) + 1;
|
||||
int height = Math.abs(pos2.getBlockY() - pos1.getBlockY()) + 1;
|
||||
int length = Math.abs(pos2.getBlockZ() - pos1.getBlockZ()) + 1;
|
||||
|
||||
LPSchematic schematic = new LPSchematicBuilder(name, player.getName(), difficulty, width, height, length)
|
||||
.fromWorld(player.getWorld(), pos1, pos2)
|
||||
.build();
|
||||
|
||||
try {
|
||||
manager.saveSchematic(schematic);
|
||||
player.sendMessage("§aSchematic saved: " + name);
|
||||
} catch (IOException e) {
|
||||
player.sendMessage("§cFailed to save schematic");
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
## Формат файла (.lpschem)
|
||||
|
||||
Файл сжат GZIP и содержит JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": 2,
|
||||
"metadata": {
|
||||
"name": "easy-jump-1",
|
||||
"author": "Loki",
|
||||
"difficulty": 0.25,
|
||||
"tags": ["easy", "beginner"]
|
||||
},
|
||||
"dimensions": {
|
||||
"width": 5,
|
||||
"height": 3,
|
||||
"length": 5
|
||||
},
|
||||
"palette": [
|
||||
"minecraft:air",
|
||||
"minecraft:stone",
|
||||
"minecraft:oak_planks"
|
||||
],
|
||||
"blocks": [0, 1, 1, 2, 0, 1, ...],
|
||||
"markers": {
|
||||
"start": {"x": 0, "y": 0, "z": 0},
|
||||
"end": {"x": 4, "y": 0, "z": 4},
|
||||
"checkpoints": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Преимущества нового формата
|
||||
|
||||
1. **Человекочитаемый** - можно редактировать в текстовом редакторе
|
||||
2. **Компактный** - GZIP сжатие + palette encoding
|
||||
3. **Расширяемый** - легко добавить новые поля
|
||||
4. **Совместимый** - можно конвертировать в/из других форматов
|
||||
5. **Современный** - поддержка BlockDisplay и визуальных эффектов
|
||||
220
docs/sd.md
Normal file
220
docs/sd.md
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
# Спецификация формата LoParkour Schematic (.lpschem)
|
||||
|
||||
## 1. Общее описание
|
||||
|
||||
**LoParkour Schematic** — это легковесный, расширяемый формат на базе JSON, предназначенный для хранения паркур-уровней в Minecraft. Формат оптимизирован для быстрой загрузки, сетевой передачи (для онлайн-редакторов) и поддержки современных визуальных элементов (Block Displays).
|
||||
|
||||
### Основные характеристики
|
||||
- **Расширение:** `.lpschem`
|
||||
- **Кодировка:** UTF-8
|
||||
- **Сжатие:** GZIP (стандарт Java `GZIPOutputStream`)
|
||||
- **Версия формата:** 2.0
|
||||
|
||||
---
|
||||
|
||||
## 2. Структура файла
|
||||
|
||||
### 2.1. Основные секции
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": 2,
|
||||
"metadata": { ... },
|
||||
"dimensions": { ... },
|
||||
"palette": [ ... ],
|
||||
"blocks": [ ... ],
|
||||
"markers": { ... },
|
||||
"visuals": { ... },
|
||||
"logic": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2. Пример полного файла
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": 2,
|
||||
"metadata": {
|
||||
"name": "Neon City Rush",
|
||||
"author": "Loki",
|
||||
"difficulty": 0.65,
|
||||
"tags": ["urban", "hard", "neon"]
|
||||
},
|
||||
"dimensions": {
|
||||
"width": 10,
|
||||
"height": 8,
|
||||
"length": 10
|
||||
},
|
||||
"palette": [
|
||||
"minecraft:air",
|
||||
"minecraft:black_concrete",
|
||||
"minecraft:cyan_stained_glass",
|
||||
"minecraft:oak_stairs[facing=north,half=bottom]"
|
||||
],
|
||||
"blocks": [0, 1, 1, 2, 0, 3],
|
||||
"markers": {
|
||||
"start": {"x": 1, "y": 1, "z": 1},
|
||||
"end": {"x": 9, "y": 7, "z": 9},
|
||||
"checkpoints": [{"x": 5, "y": 4, "z": 5}]
|
||||
},
|
||||
"visuals": {
|
||||
"displays": [
|
||||
{
|
||||
"type": "block",
|
||||
"block": "minecraft:gold_block",
|
||||
"pos": [5.5, 2.0, 5.5],
|
||||
"scale": [0.8, 0.8, 0.8],
|
||||
"animation": {"type": "rotate", "axis": "y", "speed": 1.5}
|
||||
}
|
||||
],
|
||||
"particles": [
|
||||
{"type": "end_rod", "pos": [5.0, 3.0, 5.0], "amount": 5, "spread": 0.2}
|
||||
]
|
||||
},
|
||||
"logic": {
|
||||
"ghost_path": [[1,1,1], [2,1,3], [4,2,5]]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Детальное описание полей
|
||||
|
||||
### 3.1. metadata (обязательное)
|
||||
Метаданные схематики.
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|------|-----|----------|
|
||||
| `name` | string | Название уровня |
|
||||
| `author` | string | Автор |
|
||||
| `difficulty` | float | Сложность (0.0 - 1.0) |
|
||||
| `tags` | string[] | Метки для поиска |
|
||||
|
||||
### 3.2. dimensions (обязательное)
|
||||
Размеры уровня.
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|------|-----|----------|
|
||||
| `width` | int | Ширина по оси X |
|
||||
| `height` | int | Высота по оси Y |
|
||||
| `length` | int | Длина по оси Z |
|
||||
|
||||
### 3.3. palette (обязательное)
|
||||
Уникальный список используемых блоков. Индекс `0` всегда зарезервирован за `minecraft:air`.
|
||||
|
||||
### 3.4. blocks (обязательное)
|
||||
Одномерный массив индексов блоков. Размер: `width × height × length`.
|
||||
|
||||
Формула индекса:
|
||||
`index = x + (z × width) + (y × width × length)`
|
||||
|
||||
### 3.5. markers (обязательное)
|
||||
Ключевые точки уровня.
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|------|-----|----------|
|
||||
| `start` | object | Координаты старта |
|
||||
| `end` | object | Координаты финиша |
|
||||
| `checkpoints` | object[] | Массив чекпоинтов |
|
||||
|
||||
Каждая точка содержит поля `x`, `y`, `z`.
|
||||
|
||||
### 3.6. visuals (опциональное)
|
||||
Визуальные эффекты.
|
||||
|
||||
#### 3.6.1. displays (BlockDisplay)
|
||||
Сущности для декораций в воздухе.
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|------|-----|----------|
|
||||
| `type` | string | Тип дисплея (`block`) |
|
||||
| `block` | string | Блок для отображения |
|
||||
| `pos` | float[3] | Позиция (x, y, z) |
|
||||
| `scale` | float[3] | Масштаб (x, y, z) |
|
||||
| `animation` | object | Анимация (опционально) |
|
||||
|
||||
Анимация:
|
||||
| Поле | Тип | Описание |
|
||||
|------|-----|----------|
|
||||
| `type` | string | Тип (`rotate`) |
|
||||
| `axis` | string | Ось вращения (`x`, `y`, `z`) |
|
||||
| `speed` | float | Скорость вращения |
|
||||
|
||||
#### 3.6.2. particles (опциональное)
|
||||
Точки спавна частиц.
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|------|-----|----------|
|
||||
| `type` | string | Тип частиц |
|
||||
| `pos` | float[3] | Позиция |
|
||||
| `amount` | int | Количество частиц |
|
||||
| `spread` | float | Рассеивание |
|
||||
|
||||
### 3.7. logic (опциональное)
|
||||
Логические данные уровня.
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|------|-----|----------|
|
||||
| `ghost_path` | int[][] | Путь призрака (массив координат) |
|
||||
|
||||
---
|
||||
|
||||
## 4. Алгоритмы и оптимизация
|
||||
|
||||
### 4.1. Palette Encoding
|
||||
Вместо хранения полных строк `BlockData` для каждого блока используется уникальный список в `palette`. В `blocks` хранятся только числовые индексы.
|
||||
|
||||
**Результат:** Сокращение размера файла в 5-10 раз для повторяющихся структур.
|
||||
|
||||
### 4.2. 1D Array Flattening
|
||||
Одномерный массив обеспечивает быстрый доступ к блокам без вложенных объектов.
|
||||
|
||||
### 4.3. Sparse Storage
|
||||
Индекс `0` всегда зарезервирован за `minecraft:air`. При загрузке блоки воздуха можно игнорировать, что ускоряет вставку.
|
||||
|
||||
---
|
||||
|
||||
## 5. Визуальные эффекты (Killer Features)
|
||||
|
||||
### 5.1. Block Displays
|
||||
- Висят в воздухе (не по сетке блоков)
|
||||
- Любого размера (scale)
|
||||
- Плавное вращение/движение без модов
|
||||
|
||||
### 5.2. Частицы
|
||||
Один асинхронный поток для обработки всех эффектов активных уровней.
|
||||
|
||||
---
|
||||
|
||||
## 6. Онлайн-редактор (Web Concept)
|
||||
|
||||
### Технологии
|
||||
- **Frontend:** Three.js или React-Three-Fiber
|
||||
- **Asset Loading:** Текстуры блоков из Minecraft Assets
|
||||
- **Интерфейс:** Рисование блоков, расстановка чекпоинтов, предпросмотр анимаций
|
||||
|
||||
---
|
||||
|
||||
## 7. Техника безопасности
|
||||
|
||||
| Проблема | Решение |
|
||||
|----------|---------|
|
||||
| Несовместимость версий | Проверять `format_version`, уведомлять об обновлении |
|
||||
| Missing Blocks | Заменять на `MAGENTA_GLAZED_TERRACOTTA` |
|
||||
| Entity Leak | Хранить UUID дисплеев, удалять при выгрузке |
|
||||
| Lag Spikes | Лимитировать `visuals` (макс. 50 дисплеев на уровень) |
|
||||
|
||||
---
|
||||
|
||||
## 8. План реализации (Roadmap)
|
||||
|
||||
1. [ ] Создать класс `LPSchematic` с поддержкой `GSON`
|
||||
2. [ ] Написать `PaletteManager` для автоматической сборки уникальных блоков
|
||||
3. [ ] Реализовать вставку блоков через `EditSession` или Bukkit API
|
||||
4. [ ] Добавить поддержку `BlockDisplay` через пакеты (1.19.4+)
|
||||
5. [ ] Написать конвертер из старого `.dat` / Java Serialization формата
|
||||
|
||||
---
|
||||
|
||||
*Документация подготовлена для проекта LoParkour. 2026 г.*
|
||||
29
loparkour-vilib/build.gradle
Normal file
29
loparkour-vilib/build.gradle
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
plugins {
|
||||
id 'java-library'
|
||||
}
|
||||
|
||||
group = 'dev.loki.loparkour'
|
||||
version = '2.0.1-loparkour'
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url = 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' }
|
||||
maven { url = 'https://jitpack.io' }
|
||||
maven { url = 'https://oss.sonatype.org/content/groups/public' }
|
||||
maven { url = 'https://repo.codemc.io/repository/maven-snapshots/' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly 'org.spigotmc:spigot-api:1.20.4-R0.1-SNAPSHOT'
|
||||
compileOnly 'org.jetbrains:annotations:24.1.0'
|
||||
|
||||
api 'fr.mrmicky:fastboard:2.1.2'
|
||||
api 'org.bstats:bstats-bukkit:3.2.1'
|
||||
api 'net.kyori:adventure-text-minimessage:4.16.0'
|
||||
api 'net.kyori:adventure-text-serializer-legacy:4.16.0'
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
package dev.efnilite.vilib;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import dev.efnilite.vilib.command.ViCommand;
|
||||
import dev.efnilite.vilib.util.Version;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
/**
|
||||
* Main class which plugins may inherit to reduce the amount of setup required.
|
||||
* Classes inheriting this may want to create static methods to inherit the logging and ViPlugin vars.
|
||||
* Made by Efnilite, 2021-2023
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public abstract class ViPlugin extends JavaPlugin {
|
||||
|
||||
protected static Gson gson;
|
||||
protected static Version version;
|
||||
|
||||
/**
|
||||
* @return a Gson instance which has already been set up.
|
||||
*/
|
||||
public static Gson getGson() {
|
||||
return gson;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The version.
|
||||
*/
|
||||
public static Version getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
version = Version.getVersion();
|
||||
|
||||
gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().disableHtmlEscaping().create();
|
||||
|
||||
enable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
disable();
|
||||
|
||||
HandlerList.unregisterAll(this);
|
||||
Bukkit.getScheduler().cancelTasks(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* What happens on enable of the plugin inheriting this library.
|
||||
*/
|
||||
public abstract void enable();
|
||||
|
||||
/**
|
||||
* What happens on disable of the plugin inheriting this library.
|
||||
* Disabling will automatically cancel all active tasks and unregister all EventWatchers.
|
||||
*/
|
||||
public abstract void disable();
|
||||
|
||||
/**
|
||||
* Register a command to this plugin.
|
||||
*
|
||||
* @param name The name of the command in plugin.yml
|
||||
* @param command The command class
|
||||
*/
|
||||
public void registerCommand(String name, ViCommand command) {
|
||||
var cmd = getCommand(name);
|
||||
|
||||
if (cmd == null) return;
|
||||
|
||||
cmd.setExecutor(command);
|
||||
cmd.setTabCompleter(command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a Listener, with this plugin as its owner.
|
||||
*
|
||||
* @param listener The listener to register.
|
||||
* @see dev.efnilite.vilib.event.EventWatcher
|
||||
*/
|
||||
public void registerListener(Listener listener) {
|
||||
getServer().getPluginManager().registerEvents(listener, this);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
package dev.efnilite.vilib.command;
|
||||
|
||||
import org.bukkit.command.*;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Class to wrap commands, which makes it a lot easier to produce them.
|
||||
*
|
||||
* @author Efnilite
|
||||
*/
|
||||
public abstract class ViCommand implements CommandExecutor, TabCompleter {
|
||||
|
||||
/**
|
||||
* UUID-based cooldown system
|
||||
*/
|
||||
private final Map<UUID, List<CommandCooldown>> cooldowns = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Execute a command
|
||||
*/
|
||||
public abstract boolean execute(CommandSender sender, String[] args);
|
||||
|
||||
/**
|
||||
* Get what should be suggested
|
||||
*/
|
||||
public abstract List<String> tabComplete(CommandSender sender, String[] args);
|
||||
|
||||
/**
|
||||
* Checks the cooldown
|
||||
*
|
||||
* @param sender The CommandSender which may have a cooldown
|
||||
* @param arg The argument to which this cooldown applies
|
||||
* @param cooldownMs The cooldown in ms
|
||||
* @return false if the cooldown is not over yet, true if it has been.
|
||||
*/
|
||||
protected boolean cooldown(CommandSender sender, String arg, long cooldownMs) {
|
||||
if (sender instanceof ConsoleCommandSender) { // ignore console (has no UUID)
|
||||
return true;
|
||||
}
|
||||
|
||||
Player player = (Player) sender;
|
||||
UUID uuid = player.getUniqueId();
|
||||
|
||||
CommandCooldown cooldown; // the current cooldown
|
||||
List<CommandCooldown> playerCooldowns = cooldowns.get(uuid);
|
||||
|
||||
if (playerCooldowns == null) {
|
||||
playerCooldowns = new ArrayList<>();
|
||||
playerCooldowns.add(new CommandCooldown(System.currentTimeMillis(), arg));
|
||||
cooldowns.put(uuid, playerCooldowns);
|
||||
return true;
|
||||
}
|
||||
|
||||
// get the appropriate commandcooldown class
|
||||
cooldown = playerCooldowns.stream()
|
||||
.filter(plCooldown -> plCooldown.arg().equals(arg))
|
||||
.findFirst().orElse(null);
|
||||
|
||||
if (cooldown == null) { // cooldown doesnt exist yet
|
||||
playerCooldowns.add(new CommandCooldown(System.currentTimeMillis(), arg));
|
||||
cooldowns.put(uuid, playerCooldowns);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (System.currentTimeMillis() - cooldown.lastExecuted() <= cooldownMs) {
|
||||
return false;
|
||||
}
|
||||
|
||||
playerCooldowns.remove(cooldown); // update cooldown
|
||||
playerCooldowns.add(new CommandCooldown(System.currentTimeMillis(), arg));
|
||||
cooldowns.put(uuid, playerCooldowns);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets completions in relation to what the user has already typed
|
||||
*
|
||||
* @param typed What the player has typed so far
|
||||
* @param possible The possible completions
|
||||
* @return the updated possible completions
|
||||
*/
|
||||
protected List<String> completions(String typed, List<String> possible) {
|
||||
return possible.stream().filter(option -> option.toLowerCase().contains(typed)).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets completions in relation to what the user has already typed
|
||||
*
|
||||
* @param typed What the player has typed so far
|
||||
* @param possible The possible completions
|
||||
* @return the updated possible completions
|
||||
*/
|
||||
protected List<String> completions(String typed, String... possible) {
|
||||
return Arrays.stream(possible).filter(option -> option.toLowerCase().contains(typed)).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
|
||||
return execute(sender, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
|
||||
return tabComplete(sender, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param arg The argument
|
||||
* @param lastExecuted When the command with arg was last executed
|
||||
*/
|
||||
private record CommandCooldown(long lastExecuted, String arg) {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package dev.efnilite.vilib.configupdater;
|
||||
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
public class ConfigUpdater {
|
||||
|
||||
public static void update(Plugin plugin, String resourceName, File toUpdate, List<String> ignoredSections) throws Exception {
|
||||
// Stub implementation - config updating disabled
|
||||
// Original library not available, manual config updates required
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package dev.efnilite.vilib.event;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
/**
|
||||
* Wrapper for Listener, adding useful methods.
|
||||
*
|
||||
* @author Efnilite
|
||||
*/
|
||||
public interface EventWatcher extends Listener {
|
||||
|
||||
/**
|
||||
* Unregisters every listener in this class
|
||||
*/
|
||||
default void unregisterAll() {
|
||||
HandlerList.unregisterAll(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers this listener
|
||||
*/
|
||||
default void register(Plugin plugin) {
|
||||
Bukkit.getPluginManager().registerEvents(this, plugin);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package dev.efnilite.vilib.event;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* A class for removing unnecessary methods that are the same across all Event classes,
|
||||
* making the actual Event classes cleaner.
|
||||
*
|
||||
* @author Efnilite
|
||||
*/
|
||||
public abstract class EventWrapper extends Event {
|
||||
|
||||
private static final HandlerList handlerList = new HandlerList();
|
||||
protected boolean cancelled;
|
||||
|
||||
public boolean call() {
|
||||
Bukkit.getPluginManager().callEvent(this);
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return handlerList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull HandlerList getHandlers() {
|
||||
return handlerList;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package dev.efnilite.vilib.fastboard;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public class FastBoard extends fr.mrmicky.fastboard.FastBoard {
|
||||
|
||||
public FastBoard(Player player) {
|
||||
super(player);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,337 @@
|
|||
package dev.efnilite.vilib.inventory;
|
||||
|
||||
import dev.efnilite.vilib.event.EventWatcher;
|
||||
import dev.efnilite.vilib.inventory.item.Item;
|
||||
import dev.efnilite.vilib.inventory.item.MenuItem;
|
||||
import dev.efnilite.vilib.util.Numbers;
|
||||
import dev.efnilite.vilib.util.Strings;
|
||||
import dev.efnilite.vilib.util.Task;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.event.inventory.InventoryCloseEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
/**
|
||||
* Class for Menu handling
|
||||
*
|
||||
* @author Efnilite
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class Menu implements EventWatcher {
|
||||
|
||||
private final static Map<UUID, UUID> openMenus = new HashMap<>();
|
||||
private final static List<Menu> disabledMenus = new ArrayList<>();
|
||||
|
||||
private static Plugin plugin;
|
||||
|
||||
public static void init(Plugin pl) {
|
||||
Task.create(pl).repeat(5 * 20).execute(() -> {
|
||||
if (openMenus.keySet().isEmpty()) {
|
||||
for (Menu menu : disabledMenus) {
|
||||
menu.unregisterAll();
|
||||
}
|
||||
disabledMenus.clear();
|
||||
}
|
||||
}).run();
|
||||
|
||||
plugin = pl;
|
||||
}
|
||||
|
||||
protected final int rows;
|
||||
protected final String title;
|
||||
protected final Map<Integer, MenuItem> items = new HashMap<>();
|
||||
protected final List<Integer> evenlyDistributedRows = new ArrayList<>();
|
||||
protected boolean deactivated = false;
|
||||
protected UUID inventoryId;
|
||||
protected Player player;
|
||||
protected Material filler = null;
|
||||
|
||||
public Menu(int rows, String name) {
|
||||
if (rows < 1 || rows > 6) {
|
||||
throw new IllegalArgumentException("Rows must be above 1 and below 6");
|
||||
}
|
||||
this.rows = rows;
|
||||
this.title = Strings.colour(name);
|
||||
this.inventoryId = UUID.randomUUID();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of slot numbers that evenly distribute the amount of items in a row.
|
||||
*
|
||||
* @param amountInRow The amount of items in the row.
|
||||
* @return A list of slot numbers that ensure an equal distribution.
|
||||
*/
|
||||
public static List<Integer> getEvenlyDistributedSlots(int amountInRow) {
|
||||
return switch (amountInRow) {
|
||||
case 0 -> Collections.emptyList();
|
||||
case 1 -> Collections.singletonList(4);
|
||||
case 2 -> List.of(3, 5);
|
||||
case 3 -> List.of(3, 4, 5);
|
||||
case 4 -> List.of(2, 3, 5, 6);
|
||||
case 5 -> List.of(2, 3, 4, 5, 6);
|
||||
case 6 -> List.of(1, 2, 3, 5, 6, 7);
|
||||
case 7 -> List.of(1, 2, 3, 4, 5, 6, 7);
|
||||
case 8 -> List.of(0, 1, 2, 3, 5, 6, 7, 8);
|
||||
default -> List.of(0, 1, 2, 3, 4, 5, 6, 7, 8);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an item to a slot
|
||||
*
|
||||
* @param slot The slot
|
||||
* @param item The item
|
||||
* @return the instance of this class
|
||||
*/
|
||||
public Menu item(int slot, MenuItem item) {
|
||||
if (slot > rows * 9 || slot < 0) {
|
||||
throw new IllegalArgumentException("Slot %d is not in inventory".formatted(slot));
|
||||
}
|
||||
|
||||
items.put(slot, item);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a specific set of rows to be distributed evenly. The items will be distributed.
|
||||
* Starts from 0 and goes up to 5.
|
||||
*
|
||||
* @param rows The rows
|
||||
* @return the instance of this class
|
||||
*/
|
||||
public Menu distributeRowEvenly(int... rows) {
|
||||
for (int row : rows) {
|
||||
if (row < 0 || row > 5) {
|
||||
throw new IllegalArgumentException("Rows must be above 1 and below 6");
|
||||
}
|
||||
evenlyDistributedRows.add(row);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will distribute all rows evenly.
|
||||
*
|
||||
* @return the instance of this class
|
||||
* @see #distributeRowEvenly(int...)
|
||||
*/
|
||||
public Menu distributeRowsEvenly() {
|
||||
evenlyDistributedRows.addAll(Numbers.getFromZero(rows));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills the background with a specific item
|
||||
*
|
||||
* @param filler The background filler
|
||||
* @return the instance of this class
|
||||
*/
|
||||
public Menu fillBackground(@NotNull Material filler) {
|
||||
this.filler = filler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a specific item
|
||||
*
|
||||
* @param slots The slots which are to be updated
|
||||
*/
|
||||
public void updateItem(int... slots) {
|
||||
Inventory inventory = player.getOpenInventory().getTopInventory();
|
||||
|
||||
if (inventory.getSize() % 9 != 0) {
|
||||
throw new IllegalArgumentException("Invalid inventory type");
|
||||
}
|
||||
|
||||
for (int slot : slots) {
|
||||
inventory.setItem(slot, items.get(slot).build());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates all items in the inventory
|
||||
*/
|
||||
public void update() {
|
||||
Inventory inventory = player.getOpenInventory().getTopInventory();
|
||||
|
||||
if (inventory.getSize() % 9 != 0) {
|
||||
throw new IllegalArgumentException("Invalid inventory type");
|
||||
}
|
||||
|
||||
inventory.clear();
|
||||
items.forEach((slot, item) -> inventory.setItem(slot, item.build()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates all items by calling {@link #update()} in the inventory periodically.
|
||||
*
|
||||
* @param tickInterval The amount of ticks to wait between calling {@link #update()}
|
||||
*/
|
||||
public void update(int tickInterval) {
|
||||
if (tickInterval <= 0) {
|
||||
throw new IllegalArgumentException("Tick interval must be above 0");
|
||||
}
|
||||
|
||||
Task.create(plugin)
|
||||
.repeat(tickInterval)
|
||||
.execute(new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (deactivated) {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
update();
|
||||
}
|
||||
})
|
||||
.run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the menu for the player. This distributes items on the same row automatically if these rows are assigned to automatically distribute.
|
||||
*
|
||||
* @param player The player to open it to
|
||||
*/
|
||||
public void open(Player player) {
|
||||
this.player = player;
|
||||
Inventory inventory = Bukkit.createInventory(null, rows * 9, title);
|
||||
|
||||
// Evenly distributed rows
|
||||
for (int row : evenlyDistributedRows) {
|
||||
int min = row * 9; // 0 * 9 = 0
|
||||
int max = min + 8; // 0 + 8 = slot 8
|
||||
Map<Integer, MenuItem> itemsInRow = new HashMap<>();
|
||||
|
||||
for (int slot : items.keySet()) { // get all items in the specified row
|
||||
if (slot >= min && slot <= max) {
|
||||
itemsInRow.put(slot, items.get(slot));
|
||||
}
|
||||
}
|
||||
|
||||
if (itemsInRow.keySet().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<Integer> sortedSlots = itemsInRow.keySet().stream().sorted().toList(); // sort all slots
|
||||
List<Integer> slots = getEvenlyDistributedSlots(sortedSlots.size()); // evenly distribute items
|
||||
List<Integer> olds = new ArrayList<>();
|
||||
List<Integer> news = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < slots.size(); i++) {
|
||||
int newSlot = slots.get(i) + (9 * row); // gets the new slot
|
||||
int oldSlot = sortedSlots.get(i); // the previous slot
|
||||
MenuItem item = itemsInRow.get(oldSlot); // the item in the previous slot
|
||||
|
||||
news.add(newSlot);
|
||||
olds.add(oldSlot);
|
||||
items.put(newSlot, item); // put item in new slot
|
||||
}
|
||||
|
||||
for (int oldSlot : olds) {
|
||||
if (news.contains(oldSlot)) {
|
||||
continue;
|
||||
}
|
||||
items.remove(oldSlot); // remove items from previous slot without deleting ones that are to-be moved
|
||||
}
|
||||
}
|
||||
|
||||
// Filler
|
||||
if (filler != null) {
|
||||
Item fillerItem = new Item(filler, "<red> "); // fill the background with the same material
|
||||
|
||||
// ignore already-set items
|
||||
IntStream.range(0, rows * 9)
|
||||
.filter(slot -> items.get(slot) == null)
|
||||
.forEach(slot -> items.put(slot, fillerItem));
|
||||
}
|
||||
|
||||
|
||||
player.openInventory(inventory);
|
||||
openMenus.put(player.getUniqueId(), inventoryId);
|
||||
|
||||
items.forEach((slot, item) -> inventory.setItem(slot, item.build()));
|
||||
|
||||
register(plugin);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void click(@NotNull InventoryClickEvent event) {
|
||||
if (deactivated || event.getClickedInventory() != event.getView().getTopInventory()) {
|
||||
return;
|
||||
}
|
||||
|
||||
UUID id = openMenus.get(event.getWhoClicked().getUniqueId());
|
||||
if (id != inventoryId) {
|
||||
return;
|
||||
}
|
||||
|
||||
MenuItem clickedItem = items.get(event.getSlot());
|
||||
if (clickedItem == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.setCancelled(!clickedItem.isMovable());
|
||||
|
||||
clickedItem.handleClick(this, event, event.getClick());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void close(InventoryCloseEvent event) {
|
||||
if (deactivated) {
|
||||
return;
|
||||
}
|
||||
|
||||
UUID viewerId = event.getPlayer().getUniqueId();
|
||||
UUID id = openMenus.get(viewerId);
|
||||
if (id != inventoryId) {
|
||||
return;
|
||||
}
|
||||
|
||||
deactivated = true;
|
||||
disabledMenus.add(this);
|
||||
openMenus.remove(viewerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the item in the respective slot.
|
||||
*
|
||||
* @param slot The slot
|
||||
* @return the item in this slot. This may be null.
|
||||
*/
|
||||
public @Nullable MenuItem getItem(int slot) {
|
||||
return items.get(slot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the slots and their respective items
|
||||
*
|
||||
* @return a Map with the slots and items.
|
||||
*/
|
||||
public Map<Integer, MenuItem> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the player
|
||||
*
|
||||
* @return the player
|
||||
*/
|
||||
public Player getPlayer() {
|
||||
return player;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package dev.efnilite.vilib.inventory;
|
||||
|
||||
import dev.efnilite.vilib.inventory.item.MenuItem;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
|
||||
/**
|
||||
* Container class for clicking on menus
|
||||
*/
|
||||
public record MenuClickEvent(int slot, Menu menu, MenuItem item, InventoryClickEvent event) {
|
||||
|
||||
public Player getPlayer() {
|
||||
return (Player) event.getWhoClicked();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
package dev.efnilite.vilib.inventory;
|
||||
|
||||
import dev.efnilite.vilib.inventory.item.Item;
|
||||
import dev.efnilite.vilib.inventory.item.MenuItem;
|
||||
import dev.efnilite.vilib.util.Numbers;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A menu which contains multiple pages
|
||||
*
|
||||
* @author Efnilite
|
||||
*/
|
||||
public class PagedMenu extends Menu {
|
||||
|
||||
private int total;
|
||||
private int current;
|
||||
private int nextPageSlot;
|
||||
private MenuItem nextPageItem;
|
||||
private int prevPageSlot;
|
||||
private MenuItem prevPageItem;
|
||||
private final List<Integer> displaySlots = new ArrayList<>();
|
||||
private final List<MenuItem> totalToDisplay = new ArrayList<>();
|
||||
private final Map<Integer, List<MenuItem>> assigned = new HashMap<>();
|
||||
|
||||
public PagedMenu(int rows, String name) {
|
||||
super(rows, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Excludes a specific set of rows from being used to display items
|
||||
*
|
||||
* @param rows The rows which will be excluded. Starts from 0.
|
||||
* @return the instance of this
|
||||
*/
|
||||
public PagedMenu displayRows(int... rows) {
|
||||
int begin = Integer.MAX_VALUE;
|
||||
int end = Integer.MIN_VALUE;
|
||||
|
||||
for (int row : rows) {
|
||||
if (row < 0 || row > 5) {
|
||||
throw new IllegalArgumentException("Row must be above 0 and below 6!");
|
||||
}
|
||||
|
||||
int min = row * 9;
|
||||
int max = min + 8;
|
||||
|
||||
begin = Numbers.min(begin, min);
|
||||
end = Numbers.max(end, max);
|
||||
}
|
||||
displaySlots.addAll(Numbers.getFromTo(begin, end));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void open(Player player) {
|
||||
assignPages();
|
||||
|
||||
page(0);
|
||||
super.open(player);
|
||||
}
|
||||
|
||||
public void page(int delta) {
|
||||
int newPage = current + delta;
|
||||
if (newPage < 0 || newPage > total) {
|
||||
return;
|
||||
}
|
||||
if (!assigned.containsKey(newPage) || assigned.get(newPage) == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<MenuItem> values = new ArrayList<>(assigned.get(newPage));
|
||||
Item missingFiller = new Item(filler, "<red> ");
|
||||
|
||||
items.remove(prevPageSlot);
|
||||
items.remove(nextPageSlot);
|
||||
|
||||
if (newPage > 0) {
|
||||
items.put(prevPageSlot, prevPageItem);
|
||||
} else if (filler != null) {
|
||||
items.put(prevPageSlot, missingFiller);
|
||||
}
|
||||
if (newPage < total - 1) {
|
||||
items.put(nextPageSlot, nextPageItem);
|
||||
} else if (filler != null) {
|
||||
items.put(nextPageSlot, missingFiller);
|
||||
}
|
||||
|
||||
for (int slot : displaySlots) {
|
||||
items.remove(slot);
|
||||
|
||||
if (!values.isEmpty()) {
|
||||
items.put(slot, values.get(0));
|
||||
values.remove(0);
|
||||
} else if (filler != null) {
|
||||
items.put(slot, missingFiller);
|
||||
}
|
||||
}
|
||||
if (delta != 0) {
|
||||
update();
|
||||
}
|
||||
current = newPage;
|
||||
}
|
||||
|
||||
private void assignPages() {
|
||||
List<MenuItem> total = new ArrayList<>(totalToDisplay);
|
||||
List<MenuItem> thisPage = new ArrayList<>();
|
||||
|
||||
int page = 0;
|
||||
while (!total.isEmpty()) {
|
||||
for (int slot = 0; slot < displaySlots.size(); slot++) {
|
||||
if (total.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
thisPage.add(total.get(0));
|
||||
total.remove(0);
|
||||
}
|
||||
|
||||
this.assigned.put(page, new ArrayList<>(thisPage));
|
||||
thisPage.clear();
|
||||
page++;
|
||||
}
|
||||
|
||||
this.current = 0;
|
||||
this.total = assigned.keySet().size();
|
||||
}
|
||||
|
||||
public PagedMenu addToDisplay(List<MenuItem> items) {
|
||||
totalToDisplay.addAll(items);
|
||||
return this;
|
||||
}
|
||||
|
||||
public void setToDisplay(List<MenuItem> items) {
|
||||
totalToDisplay.clear();
|
||||
totalToDisplay.addAll(items);
|
||||
}
|
||||
|
||||
public PagedMenu nextPage(int slot, MenuItem item) {
|
||||
this.nextPageSlot = slot;
|
||||
this.nextPageItem = item;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PagedMenu prevPage(int slot, MenuItem item) {
|
||||
this.prevPageSlot = slot;
|
||||
this.prevPageItem = item;
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<MenuItem> getTotalToDisplay() {
|
||||
return totalToDisplay;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
package dev.efnilite.vilib.inventory.item;
|
||||
|
||||
import com.google.common.annotations.Beta;
|
||||
import dev.efnilite.vilib.inventory.Menu;
|
||||
import dev.efnilite.vilib.inventory.MenuClickEvent;
|
||||
import dev.efnilite.vilib.util.Colls;
|
||||
import dev.efnilite.vilib.util.Task;
|
||||
import org.bukkit.event.inventory.ClickType;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.inventory.InventoryView;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Class for an item which automatically slides.
|
||||
*
|
||||
* @author Efnilite
|
||||
*/
|
||||
@Beta
|
||||
public class AutoSliderItem extends MenuItem {
|
||||
|
||||
/**
|
||||
* The initial value which will be displayed
|
||||
*/
|
||||
private int current;
|
||||
private int cooldown;
|
||||
private final int slot;
|
||||
private final Plugin plugin;
|
||||
private final Menu menu;
|
||||
private final Map<Integer, Item> items = new HashMap<>();
|
||||
private final Map<Integer, Consumer<MenuClickEvent>> clickFunctions = new HashMap<>();
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*
|
||||
* @param menu The menu that this item will be displayed in.
|
||||
*/
|
||||
public AutoSliderItem(int slot, Menu menu, Plugin plugin) {
|
||||
this.slot = slot;
|
||||
this.menu = menu;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the initial viewing index.
|
||||
* If you set up 2 items, with index 0 and 1, you can specify which will be viewed first.
|
||||
*
|
||||
* @param initial The initial viewing index
|
||||
* @return the instance of this class
|
||||
*/
|
||||
public AutoSliderItem initial(int initial) {
|
||||
this.current = initial;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the cooldown between sliding to the next item.
|
||||
*
|
||||
* @param ticks The amount of ticks until the transition occurs.
|
||||
* @return the instance of this class
|
||||
*/
|
||||
public AutoSliderItem cooldown(int ticks) {
|
||||
this.cooldown = ticks;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an item to the possible options. This uses a Function.
|
||||
* The Function will determine whether the item will update in the inventory.
|
||||
* If this returns false, it will not update the item in the menu, but it will execute the code.
|
||||
*
|
||||
* @param value The value assigned to this item
|
||||
* @param item The item
|
||||
* @param onClick What happens on switch to this item. Returns true if it should update, false if not.
|
||||
* @return the instance of this class
|
||||
*/
|
||||
public AutoSliderItem add(int value, Item item, Consumer<MenuClickEvent> onClick) {
|
||||
items.put(value, item);
|
||||
clickFunctions.put(value, onClick);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleClick(Menu menu, InventoryClickEvent event, ClickType clickType) {
|
||||
Consumer<MenuClickEvent> function = clickFunctions.get(current);
|
||||
if (function == null) {
|
||||
return;
|
||||
}
|
||||
function.accept(new MenuClickEvent(event.getSlot(), menu, this, event));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack build() {
|
||||
if (items.keySet().isEmpty()) {
|
||||
throw new IllegalArgumentException("Items size is <0 or 0!");
|
||||
}
|
||||
|
||||
Item init = items.get(current);
|
||||
if (init == null) {
|
||||
init = items.get(Colls.random(new ArrayList<>(items.keySet())));
|
||||
}
|
||||
|
||||
if (items.size() > 1) { // loop through if there is more than 1 player
|
||||
BukkitRunnable runnable = new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
InventoryView view = menu.getPlayer().getOpenInventory();
|
||||
if (view.getTitle().equals(menu.getTitle())) {
|
||||
view.getTopInventory().setItem(slot, getNextItem().build());
|
||||
} else {
|
||||
cancel(); // prevent going on forever
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Task task = Task.create(plugin).delay(cooldown).repeat(cooldown).execute(runnable);
|
||||
task.run();
|
||||
}
|
||||
|
||||
return init.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next item. Increases the counter.
|
||||
*
|
||||
* @return the next item
|
||||
*/
|
||||
public Item getNextItem() {
|
||||
current++;
|
||||
if (current >= items.size() - 1) {
|
||||
current = 0;
|
||||
}
|
||||
|
||||
return items.get(current);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMovable() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,403 @@
|
|||
package dev.efnilite.vilib.inventory.item;
|
||||
|
||||
import com.google.common.collect.HashMultimap;
|
||||
import com.google.common.collect.Multimap;
|
||||
import dev.efnilite.vilib.util.Strings;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.attribute.Attribute;
|
||||
import org.bukkit.attribute.AttributeModifier;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.inventory.EquipmentSlot;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.Damageable;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* A class for creating items.
|
||||
*
|
||||
* @author Efnilite
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class Item extends MenuItem {
|
||||
|
||||
private int amount;
|
||||
private int modelId = -1;
|
||||
private int durability;
|
||||
private boolean glowing = false;
|
||||
private boolean unbreakable = false;
|
||||
private String name;
|
||||
private ItemMeta meta;
|
||||
private Material material;
|
||||
private List<String> lore = new ArrayList<>();
|
||||
private Multimap<Attribute, AttributeModifier> attributes = HashMultimap.create();
|
||||
private Map<Enchantment, Integer> enchantments = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Creates a new instance
|
||||
*
|
||||
* @param material The material
|
||||
* @param name The name of the item
|
||||
*/
|
||||
public Item(Material material, String name) {
|
||||
this(material, 1, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance
|
||||
*
|
||||
* @param material The material
|
||||
* @param amount The amount of the item
|
||||
* @param name The name of the item
|
||||
*/
|
||||
public Item(Material material, int amount, String name) {
|
||||
this.amount = amount;
|
||||
|
||||
if (material != null) {
|
||||
this.durability = material.getMaxDurability();
|
||||
} else {
|
||||
material = Material.GRASS_BLOCK;
|
||||
}
|
||||
|
||||
this.name = name;
|
||||
this.material = material;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack build() {
|
||||
ItemStack item = new ItemStack(material, amount);
|
||||
|
||||
if (meta == null) {
|
||||
meta = Bukkit.getItemFactory().getItemMeta(item.getType());
|
||||
}
|
||||
|
||||
// if this item's meta cant be modified, return itemstack instance
|
||||
if (meta == null) {
|
||||
return item;
|
||||
}
|
||||
|
||||
if (glowing) {
|
||||
meta.addEnchant(Enchantment.KNOCKBACK, 1, false);
|
||||
meta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
|
||||
} else {
|
||||
for (Enchantment enchantment : enchantments.keySet()) {
|
||||
meta.addEnchant(enchantment, enchantments.get(enchantment), true);
|
||||
}
|
||||
}
|
||||
|
||||
meta.setDisplayName(Strings.colour(name));
|
||||
meta.setLore(Strings.colour(lore));
|
||||
meta.setCustomModelData(modelId);
|
||||
|
||||
attributes.forEach(meta::addAttributeModifier);
|
||||
|
||||
((Damageable) meta).setDamage(Math.abs(durability - material.getMaxDurability()));
|
||||
meta.setUnbreakable(unbreakable);
|
||||
|
||||
item.setItemMeta(meta);
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the meta for this item and passes it in f.
|
||||
* The result of f is set as the meta for this item.
|
||||
* The updated item is returned.
|
||||
*
|
||||
* @param f The function where the beginning item meta is provided and the final meta returned.
|
||||
* @return This item with the returned item meta.
|
||||
*/
|
||||
public ItemStack build(Function<ItemMeta, ItemMeta> f) {
|
||||
ItemStack item = build();
|
||||
item.setItemMeta(f.apply(item.getItemMeta()));
|
||||
return item;
|
||||
}
|
||||
|
||||
@SuppressWarnings("all")
|
||||
@Override
|
||||
public Item clone() {
|
||||
Item item = new Item(material, amount, name);
|
||||
|
||||
item.glowing = glowing;
|
||||
item.durability = durability;
|
||||
item.unbreakable = unbreakable;
|
||||
item.meta = meta;
|
||||
item.lore = lore;
|
||||
item.attributes = attributes;
|
||||
item.enchantments = enchantments;
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMovable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set unbreakable
|
||||
*
|
||||
* @return the instance of this class
|
||||
*/
|
||||
public Item unbreakable() {
|
||||
this.unbreakable = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set glowing
|
||||
*
|
||||
* @return the instance of this class
|
||||
*/
|
||||
public Item glowing() {
|
||||
this.glowing = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set glowing
|
||||
*
|
||||
* @return the instance of this class
|
||||
*/
|
||||
public Item glowing(boolean predicate) {
|
||||
if (predicate) {
|
||||
this.glowing = true;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name
|
||||
*
|
||||
* @param name The name
|
||||
* @return the instance of this class
|
||||
*/
|
||||
public Item name(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the ItemMeta
|
||||
*
|
||||
* @param meta The meta
|
||||
* @return the instance of this class
|
||||
*/
|
||||
public Item meta(ItemMeta meta) {
|
||||
this.meta = meta;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the durability of the item
|
||||
*
|
||||
* @return the instance of this class
|
||||
*/
|
||||
public Item durability(int durability) {
|
||||
this.durability = durability;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the item amount
|
||||
*
|
||||
* @param amount The item amount
|
||||
* @return the instance of this class
|
||||
*/
|
||||
public Item amount(int amount) {
|
||||
this.amount = amount;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the type
|
||||
*
|
||||
* @param material The type
|
||||
* @return the instance of this class
|
||||
*/
|
||||
public Item material(Material material) {
|
||||
this.material = material;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the lore
|
||||
*
|
||||
* @param lore The lore
|
||||
* @return the instance of this class
|
||||
*/
|
||||
public Item lore(@Nullable List<String> lore) {
|
||||
if (lore != null) {
|
||||
this.lore = lore;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the lore
|
||||
*
|
||||
* @param lore The lore
|
||||
* @return the instance of this class
|
||||
*/
|
||||
public Item lore(String... lore) {
|
||||
return lore(List.of(lore));
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Sets the lore supplier.
|
||||
// *
|
||||
// * @param supplier The lore supplier.
|
||||
// * @return the instance of this class
|
||||
// */
|
||||
// public Item lore(@Nullable Supplier<List<String>> supplier) {
|
||||
// if (supplier != null) {
|
||||
// this.lore = lore;
|
||||
// }
|
||||
//
|
||||
// return this;
|
||||
// }
|
||||
|
||||
/**
|
||||
* Enchants this item with a specific enchantment and a provided level.
|
||||
* This ignores enchantment level limit restrictions.
|
||||
*
|
||||
* @param enchantment The enchantment instance.
|
||||
* @param level The level.
|
||||
* @return the instance of this class
|
||||
*/
|
||||
public Item enchant(@NotNull Enchantment enchantment, int level) {
|
||||
enchantments.put(enchantment, level);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the provided attribute to this item, with the specified value and the operation.
|
||||
* Example:
|
||||
* <br>
|
||||
* <code>
|
||||
* item.attribute(Attribute.GENERIC_ATTACK_SPEED, -10, AttributeModifier.Operation.ADD_NUMBER)
|
||||
* </code>
|
||||
*
|
||||
* @param attribute The attribute
|
||||
* @param value The value
|
||||
* @param operation The operation
|
||||
* @return the instance of this class
|
||||
* @see AttributeModifier.Operation
|
||||
*/
|
||||
public Item attribute(@NotNull Attribute attribute, double value, AttributeModifier.Operation operation) {
|
||||
attributes.put(attribute, new AttributeModifier(attribute.getKey().getKey(), value, operation));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the provided attribute to this item, with the specified value and the operation, only applying to a specific slot.
|
||||
*
|
||||
* @param attribute The attribute
|
||||
* @param value The value
|
||||
* @param operation The operation
|
||||
* @param slot The slot
|
||||
* @return the instance of this class
|
||||
* @see AttributeModifier.Operation
|
||||
*/
|
||||
public Item attribute(@NotNull Attribute attribute, double value, @NotNull AttributeModifier.Operation operation, @NotNull EquipmentSlot slot) {
|
||||
attributes.put(attribute, new AttributeModifier(UUID.randomUUID(), attribute.getKey().getKey(), value, operation, slot));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the provided {@link AttributeModifier} to the item with {@link Attribute} used as identification.
|
||||
*
|
||||
* @param attribute The attribute.
|
||||
* @param modifier The modifier.
|
||||
* @return the instance of this class
|
||||
*/
|
||||
public Item attribute(@NotNull Attribute attribute, @NotNull AttributeModifier modifier) {
|
||||
attributes.put(attribute, modifier);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets this item's model id
|
||||
*
|
||||
* @param modelId The model id
|
||||
* @return the instance of this class
|
||||
*/
|
||||
public Item modelId(int modelId) {
|
||||
this.modelId = modelId;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the lore line by line.
|
||||
* Useful for updating items.
|
||||
*
|
||||
* @param function The function. The line of lore is given, and it must return the modified version of that lore line
|
||||
* @return the instance of this class
|
||||
*/
|
||||
public Item modifyLore(Function<String, String> function) {
|
||||
this.lore = lore.stream()
|
||||
.map(function)
|
||||
.toList();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the name of an item.
|
||||
* Useful for updating items.
|
||||
*
|
||||
* @param function The function. The title is given, and it must return an altered version of this title.
|
||||
* @return the instance of this class
|
||||
*/
|
||||
public Item modifyName(Function<String, String> function) {
|
||||
name = function.apply(name);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the amount
|
||||
*
|
||||
* @return the amount
|
||||
*/
|
||||
public int getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the lore
|
||||
*
|
||||
* @return the lore
|
||||
*/
|
||||
public List<String> getLore() {
|
||||
return lore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the item type
|
||||
*
|
||||
* @return the type
|
||||
*/
|
||||
public Material getMaterial() {
|
||||
return material;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name
|
||||
*
|
||||
* @return the name
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package dev.efnilite.vilib.inventory.item;
|
||||
|
||||
import dev.efnilite.vilib.inventory.Menu;
|
||||
import dev.efnilite.vilib.inventory.MenuClickEvent;
|
||||
import org.bukkit.event.inventory.ClickType;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Super class for every type of item which will be displayed in the menu
|
||||
*
|
||||
* @author Efnilite
|
||||
*/
|
||||
public abstract class MenuItem {
|
||||
|
||||
/**
|
||||
* Prevent shift clicking from causing problems on inventory close
|
||||
*/
|
||||
private static final ClickType[] DEFAULT_TYPES = new ClickType[]{ClickType.LEFT, ClickType.RIGHT, ClickType.MIDDLE};
|
||||
|
||||
protected Map<ClickType, Consumer<MenuClickEvent>> clickFunctions = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Set the function on click
|
||||
*
|
||||
* @param consumer Useful values which are gathered in the click e vent
|
||||
* @return the instance of this class
|
||||
*/
|
||||
public MenuItem click(Consumer<MenuClickEvent> consumer, ClickType... clickType) {
|
||||
if (clickType.length == 0) {
|
||||
for (ClickType type : DEFAULT_TYPES) {
|
||||
clickFunctions.put(type, consumer);
|
||||
}
|
||||
} else {
|
||||
for (ClickType type : clickType) {
|
||||
clickFunctions.put(type, consumer);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public void handleClick(Menu menu, InventoryClickEvent event, ClickType clickType) {
|
||||
Consumer<MenuClickEvent> consumer = clickFunctions.get(clickType);
|
||||
if (consumer == null) {
|
||||
return;
|
||||
}
|
||||
consumer.accept(new MenuClickEvent(event.getSlot(), menu, this, event));
|
||||
}
|
||||
|
||||
/**
|
||||
* Finishes everything and gives the ItemStack result.
|
||||
*
|
||||
* @return the result
|
||||
*/
|
||||
public abstract ItemStack build();
|
||||
|
||||
/**
|
||||
* Whether this item can be moved by the player
|
||||
*
|
||||
* @return true -> it can, false -> it cant
|
||||
*/
|
||||
public abstract boolean isMovable();
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package dev.efnilite.vilib.inventory.item;
|
||||
|
||||
import org.bukkit.Material;
|
||||
|
||||
/**
|
||||
* An item which may be moved by the player
|
||||
*
|
||||
* @author Efnilite
|
||||
*/
|
||||
public class MovableItem extends Item {
|
||||
|
||||
public MovableItem(Material material, String name) {
|
||||
super(material, name);
|
||||
}
|
||||
|
||||
public MovableItem(Material material, int amount, String name) {
|
||||
super(material, amount, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMovable() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
package dev.efnilite.vilib.inventory.item;
|
||||
|
||||
import dev.efnilite.vilib.inventory.Menu;
|
||||
import dev.efnilite.vilib.inventory.MenuClickEvent;
|
||||
import dev.efnilite.vilib.util.Colls;
|
||||
import org.bukkit.event.inventory.ClickType;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* Class for an item which uses the difference between left/right click to assign values.
|
||||
*
|
||||
* @author Efnilite
|
||||
*/
|
||||
public class SliderItem extends MenuItem {
|
||||
|
||||
/**
|
||||
* The initial value which will be displayed
|
||||
*/
|
||||
private int current;
|
||||
private final Map<Integer, Item> items = new HashMap<>();
|
||||
private final Map<Integer, Predicate<MenuClickEvent>> switchFunctions = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Sets the initial viewing index.
|
||||
* If you set up 2 items, with index 0 and 1, you can specify which will be viewed first.
|
||||
*
|
||||
* @param initial The initial viewing index
|
||||
* @return the instance of this class
|
||||
*/
|
||||
public SliderItem initial(int initial) {
|
||||
this.current = initial;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an item to the possible options. This uses a Function.
|
||||
* The Function will determine whether the item will update in the inventory.
|
||||
* If this returns false, it will not update the item in the menu, but it will execute the code.
|
||||
*
|
||||
* @param value The value assigned to this item
|
||||
* @param item The item
|
||||
* @param onSwitchTo What happens on switch to this item. Returns true if it should update, false if not.
|
||||
* @return the instance of this class
|
||||
*/
|
||||
public SliderItem add(int value, Item item, Predicate<MenuClickEvent> onSwitchTo) {
|
||||
items.put(value, item);
|
||||
switchFunctions.put(value, onSwitchTo);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleClick(Menu menu, InventoryClickEvent event, ClickType clickType) {
|
||||
int currentTo = current;
|
||||
switch (clickType) {
|
||||
case LEFT:
|
||||
currentTo++;
|
||||
if (currentTo >= items.size()) {
|
||||
currentTo = 0;
|
||||
}
|
||||
break;
|
||||
case SHIFT_LEFT:
|
||||
currentTo = items.size() - 1;
|
||||
break;
|
||||
case RIGHT:
|
||||
currentTo--;
|
||||
if (currentTo == -1) {
|
||||
currentTo = items.size() - 1;
|
||||
}
|
||||
break;
|
||||
case SHIFT_RIGHT:
|
||||
currentTo = 0;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
Predicate<MenuClickEvent> function = switchFunctions.get(currentTo);
|
||||
if (function == null) {
|
||||
return;
|
||||
}
|
||||
boolean update = function.test(new MenuClickEvent(event.getSlot(), menu, this, event));
|
||||
|
||||
if (update) {
|
||||
event.getInventory().setItem(event.getSlot(), items.get(currentTo).build());
|
||||
|
||||
current = currentTo;
|
||||
|
||||
menu.updateItem(event.getSlot());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack build() {
|
||||
if (items.keySet().size() == 0) {
|
||||
throw new IllegalArgumentException("Items size is 0!");
|
||||
}
|
||||
|
||||
Item init = items.get(current);
|
||||
if (init == null) {
|
||||
init = items.get(Colls.random(new ArrayList<>(items.keySet())));
|
||||
}
|
||||
|
||||
return init.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMovable() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package dev.efnilite.vilib.inventory.item;
|
||||
|
||||
import dev.efnilite.vilib.inventory.Menu;
|
||||
import dev.efnilite.vilib.inventory.MenuClickEvent;
|
||||
import dev.efnilite.vilib.util.Task;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.inventory.ClickType;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.inventory.InventoryView;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
|
||||
/**
|
||||
* A MenuItem which only stays for a certain amount of ticks. This item is meant as a confirm option or to display items for a small amount of time.
|
||||
* On click of this item it will automatically go back to the previous item.
|
||||
*/
|
||||
public class TimedItem extends MenuItem {
|
||||
|
||||
private int timeStay;
|
||||
private Task task;
|
||||
private MenuItem revertTo;
|
||||
private final Plugin plugin;
|
||||
private final Player player;
|
||||
private final MenuItem item;
|
||||
private final MenuClickEvent event;
|
||||
|
||||
public TimedItem(MenuItem item, MenuClickEvent event, Plugin plugin) {
|
||||
this.item = item;
|
||||
this.revertTo = event.menu().getItem(event.slot());
|
||||
if (revertTo == null) {
|
||||
revertTo = new Item(Material.AIR, "<red> ");
|
||||
}
|
||||
this.player = event.getPlayer();
|
||||
this.event = event;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the amount of ticks this item will stay in view for.
|
||||
*
|
||||
* @param ticks The amount of ticks
|
||||
* @return the instance of the class
|
||||
*/
|
||||
public TimedItem stay(int ticks) {
|
||||
this.timeStay = ticks;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack build() {
|
||||
BukkitRunnable runnable = new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Menu menu = event.menu();
|
||||
InventoryView view = player.getOpenInventory();
|
||||
if (view.getTitle().equals(menu.getTitle())) {
|
||||
menu.item(event.slot(), revertTo);
|
||||
menu.updateItem(event.slot());
|
||||
} else {
|
||||
cancel(); // prevent going on forever
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
task = Task.create(plugin).delay(timeStay).execute(runnable);
|
||||
task.run();
|
||||
|
||||
return item.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleClick(Menu menu, InventoryClickEvent event, ClickType clickType) {
|
||||
item.handleClick(menu, event, clickType);
|
||||
|
||||
task.cancelAndRunImmediately();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMovable() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
package dev.efnilite.vilib.particle;
|
||||
|
||||
import org.bukkit.Particle;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Wrapper for data used in {@link Particles}
|
||||
*
|
||||
* @author Efnilite
|
||||
*/
|
||||
public class ParticleData<T> {
|
||||
|
||||
private int size;
|
||||
private double speed;
|
||||
private double offsetX;
|
||||
private double offsetY;
|
||||
private double offsetZ;
|
||||
private Particle type;
|
||||
@Nullable
|
||||
private T data;
|
||||
|
||||
public ParticleData(Particle type, @Nullable T data, int size) {
|
||||
this(type, data, size, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
public ParticleData(Particle type, @Nullable T data, int size, double speed) {
|
||||
this(type, data, size, speed, 0, 0, 0);
|
||||
}
|
||||
|
||||
public ParticleData(Particle type, @Nullable T data, int size, double offsetX, double offsetY, double offsetZ) {
|
||||
this(type, data, size, 0, offsetX, offsetY, offsetZ);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main constructor for Particle Data.
|
||||
*
|
||||
* @param type The type of particle
|
||||
* @param data The possible data associated with this particle (colour, etc.)
|
||||
* @param size The size of the particle
|
||||
* @param speed The speed of the particle
|
||||
* @param offsetX The max x offset in moving
|
||||
* @param offsetY The max y offset in moving
|
||||
* @param offsetZ The max z offset in moving
|
||||
*/
|
||||
public ParticleData(Particle type, @Nullable T data, int size, double speed, double offsetX, double offsetY, double offsetZ) {
|
||||
this.data = data;
|
||||
this.type = type;
|
||||
this.size = size;
|
||||
this.speed = speed;
|
||||
this.offsetX = offsetX;
|
||||
this.offsetY = offsetY;
|
||||
this.offsetZ = offsetZ;
|
||||
}
|
||||
|
||||
public ParticleData<T> size(int size) {
|
||||
this.size = size;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ParticleData<T> speed(double speed) {
|
||||
this.speed = speed;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ParticleData<T> offsetX(double offsetX) {
|
||||
this.offsetX = offsetX;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ParticleData<T> offsetY(double offsetY) {
|
||||
this.offsetY = offsetY;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ParticleData<T> offsetZ(double offsetZ) {
|
||||
this.offsetZ = offsetZ;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ParticleData<T> data(@Nullable T data) {
|
||||
this.data = data;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ParticleData<T> type(Particle type) {
|
||||
this.type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public double getSpeed() {
|
||||
return speed;
|
||||
}
|
||||
|
||||
public double getOffsetX() {
|
||||
return offsetX;
|
||||
}
|
||||
|
||||
public double getOffsetY() {
|
||||
return offsetY;
|
||||
}
|
||||
|
||||
public double getOffsetZ() {
|
||||
return offsetZ;
|
||||
}
|
||||
|
||||
public @Nullable T getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public Particle getType() {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,230 @@
|
|||
package dev.efnilite.vilib.particle;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.util.BoundingBox;
|
||||
import org.bukkit.util.Vector;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Does particle stuff
|
||||
*
|
||||
* @author Efnilite
|
||||
*/
|
||||
public class Particles {
|
||||
|
||||
/**
|
||||
* Draws particles.
|
||||
* Uses default Bukkit World methods to spawn particles using {@link ParticleData}
|
||||
*
|
||||
* @param at The location of the particles
|
||||
* @param data The particle data
|
||||
*/
|
||||
public static <T> void draw(Location at, @NotNull ParticleData<T> data) {
|
||||
World world = at.getWorld();
|
||||
if (world == null) {
|
||||
throw new NullPointerException("World is null (Particles#draw)");
|
||||
}
|
||||
|
||||
world.spawnParticle(data.getType(), at, data.getSize(), data.getOffsetX(), data.getOffsetY(), data.getOffsetZ(), data.getSpeed(), data.getData());
|
||||
}
|
||||
|
||||
public static <T> void draw(Location at, @NotNull ParticleData<T> data, Player player) {
|
||||
World world = at.getWorld();
|
||||
if (world == null) {
|
||||
throw new NullPointerException("World is null (Particles#draw)");
|
||||
}
|
||||
|
||||
player.spawnParticle(data.getType(), at, data.getSize(), data.getOffsetX(), data.getOffsetY(), data.getOffsetZ(), data.getSpeed(), data.getData());
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a particle line between 2 points
|
||||
* <p>
|
||||
* First it finds the max and min locations, then it calculates the amount of particles based on param distanceBetween
|
||||
* After that it gets the 3D distances (x, y and z) between the max and min points (delta X / amount of particles).
|
||||
* <p>
|
||||
* Then it loops the amount of times particles will spawn and in every loop removes delta X,
|
||||
* delta Y and delta Z from the max location.
|
||||
* </p><p>
|
||||
* Example: max is (10, 10, 10), min is (0, 0, 0), distance between particles = 1 block.
|
||||
* Gets distance between max and min (~ 14) and divides that by distance between (14 / 1 = 14 particles).
|
||||
* Then the difference between max and min gets calculated (10 - 0 = 10) and that gets divided by amount (10 / 14 = ~ 0.7).
|
||||
* This means that on the x, y and z axis every time the loop fires 0.7 blocks get removed. Looping 14 times and removing
|
||||
* 0.7 every time, means it goes from (10, 10, 10) to (0, 0, 0) and gets all the locations inbetween those 2 locations, where
|
||||
* particles need to be spawned.
|
||||
* </p>
|
||||
*
|
||||
* @param one The location from where the tower shoots (always shoot variable)
|
||||
* @param two The location of the entity
|
||||
* @param data The particle data
|
||||
* @param distanceBetween The distance between particles in blocks
|
||||
*/
|
||||
public static <T> void line(Location one, Location two, ParticleData<T> data, double distanceBetween) {
|
||||
World world = one.getWorld();
|
||||
if (world == null) {
|
||||
throw new NullPointerException("World is null (Particles#draw)");
|
||||
}
|
||||
double dist = one.distance(two);
|
||||
Vector p1 = one.toVector();
|
||||
Vector p2 = two.toVector();
|
||||
Vector vec = p2.clone().subtract(p1).normalize().multiply(distanceBetween);
|
||||
world.spawnParticle(data.getType(), p1.getX(), p1.getY(), p1.getZ(), data.getSize(), data.getOffsetX(), data.getOffsetY(), data.getOffsetZ(), data.getSpeed(), data.getData());
|
||||
world.spawnParticle(data.getType(), p2.getX(), p2.getY(), p2.getZ(), data.getSize(), data.getOffsetX(), data.getOffsetY(), data.getOffsetZ(), data.getSpeed(), data.getData());
|
||||
double length = 0;
|
||||
for (; length < dist; p1.add(vec)) {
|
||||
world.spawnParticle(data.getType(), p1.getX(), p1.getY(), p1.getZ(), data.getSize(), data.getOffsetX(), data.getOffsetY(), data.getOffsetZ(), data.getSpeed(), data.getData());
|
||||
length += distanceBetween;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a box of particles
|
||||
* Calculates the min point and then adds all the dimensions to each other point to get the locations of all the points
|
||||
*
|
||||
* @param box The box which the particles will go around
|
||||
* @param world The world
|
||||
* @param data The particle data
|
||||
* @param distanceBetween The distance between particles
|
||||
*/
|
||||
public static <T> void box(BoundingBox box, @NotNull World world, ParticleData<T> data, Player player, double distanceBetween) {
|
||||
Location point1 = box.getMin().toLocation(world);
|
||||
Location point2, point3, point4, point5, point6, point7, point8;
|
||||
if (box.getWidthX() == 1 && box.getWidthZ() == 1) {
|
||||
point2 = point1.clone().add(box.getWidthX(), 0, 0);
|
||||
point3 = point2.clone().add(0, 0, box.getWidthZ());
|
||||
point4 = point1.clone().add(0, 0, box.getWidthZ());
|
||||
point5 = point1.clone().add(0, box.getHeight(), 0);
|
||||
point6 = point2.clone().add(0, box.getHeight(), 0);
|
||||
point7 = point3.clone().add(0, box.getHeight(), 0);
|
||||
point8 = point4.clone().add(0, box.getHeight(), 0);
|
||||
} else {
|
||||
point2 = point1.clone().add(box.getWidthX() + 1, 0, 0);
|
||||
point3 = point2.clone().add(0, 0, box.getWidthZ() + 1);
|
||||
point4 = point1.clone().add(0, 0, box.getWidthZ() + 1);
|
||||
point5 = point1.clone().add(0, box.getHeight() + 1, 0);
|
||||
point6 = point2.clone().add(0, box.getHeight() + 1, 0);
|
||||
point7 = point3.clone().add(0, box.getHeight() + 1, 0);
|
||||
point8 = point4.clone().add(0, box.getHeight() + 1, 0);
|
||||
}
|
||||
|
||||
line(point1, point2, data, player, distanceBetween);
|
||||
line(point2, point3, data, player, distanceBetween);
|
||||
line(point3, point4, data, player, distanceBetween);
|
||||
line(point4, point1, data, player, distanceBetween);
|
||||
|
||||
line(point5, point6, data, player, distanceBetween);
|
||||
line(point6, point7, data, player, distanceBetween);
|
||||
line(point7, point8, data, player, distanceBetween);
|
||||
line(point5, point8, data, player, distanceBetween);
|
||||
|
||||
line(point1, point5, data, player, distanceBetween);
|
||||
line(point2, point6, data, player, distanceBetween);
|
||||
line(point3, point7, data, player, distanceBetween);
|
||||
line(point4, point8, data, player, distanceBetween);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a box of particles.
|
||||
* Calculates the min point and then adds all the dimensions to each other point to get the locations of all the points
|
||||
*
|
||||
* @param box The box which the particles will go around
|
||||
* @param world The world
|
||||
* @param data The particle data
|
||||
* @param distanceBetween The distance between particles
|
||||
*/
|
||||
public static <T> void box(BoundingBox box, @NotNull World world, ParticleData<T> data, double distanceBetween) {
|
||||
Location point1 = box.getMin().toLocation(world);
|
||||
Location point2, point3, point4, point5, point6, point7, point8;
|
||||
|
||||
if (box.getWidthX() == 1.0 && box.getWidthZ() == 1.0) {
|
||||
point2 = point1.clone().add(box.getWidthX(), 0.0, 0.0);
|
||||
point3 = point2.clone().add(0.0, 0.0, box.getWidthZ());
|
||||
point4 = point1.clone().add(0.0, 0.0, box.getWidthZ());
|
||||
point5 = point1.clone().add(0.0, box.getHeight(), 0.0);
|
||||
point6 = point2.clone().add(0.0, box.getHeight(), 0.0);
|
||||
point7 = point3.clone().add(0.0, box.getHeight(), 0.0);
|
||||
point8 = point4.clone().add(0.0, box.getHeight(), 0.0);
|
||||
} else {
|
||||
point2 = point1.clone().add(box.getWidthX() + 1.0, 0.0, 0.0);
|
||||
point3 = point2.clone().add(0.0, 0.0, box.getWidthZ() + 1.0);
|
||||
point4 = point1.clone().add(0.0, 0.0, box.getWidthZ() + 1.0);
|
||||
point5 = point1.clone().add(0.0, box.getHeight() + 1.0, 0.0);
|
||||
point6 = point2.clone().add(0.0, box.getHeight() + 1.0, 0.0);
|
||||
point7 = point3.clone().add(0.0, box.getHeight() + 1.0, 0.0);
|
||||
point8 = point4.clone().add(0.0, box.getHeight() + 1.0, 0.0);
|
||||
}
|
||||
|
||||
line(point1, point2, data, distanceBetween);
|
||||
line(point2, point3, data, distanceBetween);
|
||||
line(point3, point4, data, distanceBetween);
|
||||
line(point4, point1, data, distanceBetween);
|
||||
line(point5, point6, data, distanceBetween);
|
||||
line(point6, point7, data, distanceBetween);
|
||||
line(point7, point8, data, distanceBetween);
|
||||
line(point5, point8, data, distanceBetween);
|
||||
line(point1, point5, data, distanceBetween);
|
||||
line(point2, point6, data, distanceBetween);
|
||||
line(point3, point7, data, distanceBetween);
|
||||
line(point4, point8, data, distanceBetween);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link #line(Location, Location, ParticleData, double)} but for players
|
||||
*/
|
||||
public static <T> void line(Location one, Location two, ParticleData<T> data, Player player, double distanceBetween) {
|
||||
World world = one.getWorld();
|
||||
if (world == null) {
|
||||
throw new NullPointerException("World is null (Particles#draw)");
|
||||
}
|
||||
double dist = one.distance(two);
|
||||
Vector p1 = one.toVector();
|
||||
Vector p2 = two.toVector();
|
||||
Vector vec = p2.clone().subtract(p1).normalize().multiply(distanceBetween);
|
||||
player.spawnParticle(data.getType(), p1.getX(), p1.getY(), p1.getZ(), data.getSize(), data.getOffsetX(), data.getOffsetY(), data.getOffsetZ(), data.getSpeed(), data.getData());
|
||||
player.spawnParticle(data.getType(), p2.getX(), p2.getY(), p2.getZ(), data.getSize(), data.getOffsetX(), data.getOffsetY(), data.getOffsetZ(), data.getSpeed(), data.getData());
|
||||
double length = 0;
|
||||
for (; length < dist; p1.add(vec)) {
|
||||
player.spawnParticle(data.getType(), p1.getX(), p1.getY(), p1.getZ(), data.getSize(), data.getOffsetX(), data.getOffsetY(), data.getOffsetZ(), data.getSpeed(), data.getData());
|
||||
length += distanceBetween;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a circle
|
||||
*
|
||||
* @param location The center
|
||||
* @param data The particle data
|
||||
* @param radius The radius of the circle
|
||||
* @param amount The amount of particles
|
||||
*/
|
||||
public static <T> void circle(Location location, ParticleData<T> data, double radius, double amount) {
|
||||
World world = location.getWorld();
|
||||
if (world == null) {
|
||||
throw new NullPointerException("World is null");
|
||||
}
|
||||
|
||||
double increment = (2 * Math.PI) / amount; // calc degree per amount, 2 x pi x r = circumference
|
||||
double y = location.getY();
|
||||
|
||||
for (int i = 0; i < amount; i++) {
|
||||
double angle = i * increment;
|
||||
double x = location.getX() + (radius * Math.cos(angle));
|
||||
double z = location.getZ() + (radius * Math.sin(angle));
|
||||
world.spawnParticle(data.getType(), x, y, z, data.getSize(), data.getOffsetX(), data.getOffsetY(), data.getOffsetZ(), data.getSpeed(), data.getData());
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> void circle(Location location, ParticleData<T> data, @NotNull Player player, double radius, double amount) {
|
||||
double increment = (2 * Math.PI) / amount; // calc degree per amount, 2 x pi x r = circumference
|
||||
double y = location.getY();
|
||||
|
||||
for (int i = 0; i < amount; i++) {
|
||||
double angle = i * increment;
|
||||
double x = location.getX() + (radius * Math.cos(angle));
|
||||
double z = location.getZ() + (radius * Math.sin(angle));
|
||||
player.spawnParticle(data.getType(), x, y, z, data.getSize(), data.getOffsetX(), data.getOffsetY(), data.getOffsetZ(), data.getSpeed(), data.getData());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
package dev.efnilite.vilib.schematic;
|
||||
|
||||
import dev.efnilite.vilib.schematic.io.SchematicPaster;
|
||||
import dev.efnilite.vilib.schematic.io.SchematicReader;
|
||||
import dev.efnilite.vilib.schematic.io.SchematicWriter;
|
||||
import dev.efnilite.vilib.util.Task;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.util.Vector;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static java.lang.Math.max;
|
||||
import static java.lang.Math.min;
|
||||
|
||||
/**
|
||||
* Main schematic handling class.
|
||||
*
|
||||
* @author Efnilite
|
||||
* @since 5.0.0
|
||||
*/
|
||||
public class Schematic {
|
||||
|
||||
/**
|
||||
* The version of this library instance.
|
||||
*/
|
||||
public static final int VERSION = 1;
|
||||
private final File file;
|
||||
private final Map<Vector, BlockData> vectorBlockMap;
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param file The file.
|
||||
*/
|
||||
public Schematic(@NotNull File file, @NotNull Plugin plugin) throws IOException, ClassNotFoundException {
|
||||
this.file = file;
|
||||
this.vectorBlockMap = new SchematicReader().read(file, plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pastes a schematic.
|
||||
*
|
||||
* @param location The smallest location.
|
||||
*/
|
||||
public List<Block> paste(Location location) {
|
||||
return new SchematicPaster().paste(location, vectorBlockMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pastes a schematic at angles rotation.
|
||||
*
|
||||
* @param location The smallest location.
|
||||
* @param rotation The rotation where y = yaw in rad.
|
||||
*/
|
||||
public List<Block> paste(Location location, double rotation) {
|
||||
return new SchematicPaster().paste(location, rotation, vectorBlockMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The dimensions of this schematic.
|
||||
*/
|
||||
public Vector getDimensions() {
|
||||
Set<Vector> offsets = vectorBlockMap.keySet();
|
||||
|
||||
Vector min = offsets.stream().reduce((a, b) -> new Vector(min(a.getX(), b.getX()), min(a.getY(), b.getY()), min(a.getZ(), b.getZ()))).orElseThrow();
|
||||
Vector max = offsets.stream().reduce((a, b) -> new Vector(max(a.getX(), b.getX()), max(a.getY(), b.getY()), max(a.getZ(), b.getZ()))).orElseThrow();
|
||||
|
||||
return max.subtract(min);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True when this schematic contains unknown {@link BlockData}, false if it does.
|
||||
*/
|
||||
public boolean hasUnknownMaterials() {
|
||||
return vectorBlockMap.containsValue(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The map of vectors mapped to each {@link BlockData}.
|
||||
*/
|
||||
public Map<Vector, BlockData> getVectorBlockMap() {
|
||||
return vectorBlockMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The file.
|
||||
*/
|
||||
public File getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a schematic asynchronously.
|
||||
*
|
||||
* @param file The file.
|
||||
* @return A new {@link Schematic} instance.
|
||||
*/
|
||||
public static Schematic load(File file, Plugin plugin) throws IOException, ClassNotFoundException {
|
||||
return new Schematic(file, plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a schematic asynchronously.
|
||||
*
|
||||
* @param file The file.
|
||||
* @return A new {@link Schematic} instance.
|
||||
*/
|
||||
public static Schematic load(String file, Plugin plugin) throws IOException, ClassNotFoundException {
|
||||
return new Schematic(new File(file), plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the selection between the two locations asynchronously to file.
|
||||
*
|
||||
* @param file The file.
|
||||
* @param pos1 The first position.
|
||||
* @param pos2 The second position.
|
||||
*/
|
||||
public static void save(String file, Location pos1, Location pos2, Plugin plugin) {
|
||||
save(new File(file), pos1, pos2, plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the selection between the two locations asynchronously to file.
|
||||
*
|
||||
* @param file The file.
|
||||
* @param pos1 The first position.
|
||||
* @param pos2 The second position.
|
||||
*/
|
||||
public static void save(File file, Location pos1, Location pos2, Plugin plugin) {
|
||||
Task.create(plugin).async().execute(() -> new SchematicWriter().save(file, pos1, pos2, plugin)).run();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package dev.efnilite.vilib.schematic;
|
||||
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class Schematics {
|
||||
|
||||
private static final Map<Plugin, Map<String, Schematic>> cache = new HashMap<>();
|
||||
|
||||
public static void addFromFiles(@NotNull Plugin plugin, @NotNull File... files) throws IOException, ClassNotFoundException {
|
||||
Map<String, Schematic> current = cache.getOrDefault(plugin, new HashMap<>());
|
||||
|
||||
for (File file : files) {
|
||||
current.put(file.getName(), Schematic.load(file, plugin));
|
||||
}
|
||||
|
||||
cache.put(plugin, current);
|
||||
|
||||
plugin.getLogger().info("Loaded all schematics!");
|
||||
}
|
||||
|
||||
public static Schematic getSchematic(@NotNull Plugin plugin, @NotNull String schematicName) {
|
||||
return cache.get(plugin).get(schematicName);
|
||||
}
|
||||
|
||||
public static Set<String> getSchematicNames(@NotNull Plugin plugin) {
|
||||
return cache.get(plugin).keySet();
|
||||
}
|
||||
|
||||
public static Collection<Schematic> getSchematics(@NotNull Plugin plugin) {
|
||||
return cache.get(plugin).values();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
package dev.efnilite.vilib.schematic.io;
|
||||
|
||||
import dev.efnilite.vilib.util.Colls;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.block.data.Directional;
|
||||
import org.bukkit.block.data.MultipleFacing;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Schematic pasting handler.
|
||||
*
|
||||
* @author Efnilite
|
||||
* @since 5.0.0
|
||||
*/
|
||||
public class SchematicPaster {
|
||||
|
||||
/**
|
||||
* Pastes a schematic with the provided map of offsets and BlockData.
|
||||
*
|
||||
* @param location The smallest location.
|
||||
* @param vectorDataMap The map.
|
||||
* @return The affected blocks.
|
||||
*/
|
||||
public List<Block> paste(Location location, Map<Vector, BlockData> vectorDataMap) {
|
||||
return paste(() -> Colls.thread(vectorDataMap)
|
||||
.mapk((k, v) -> location.clone().add(k).getBlock())
|
||||
.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Pastes a schematic at angle rotation with the provided map of offsets and BlockData.
|
||||
* Rotates {@link Directional} and {@link MultipleFacing} blocks.
|
||||
*
|
||||
* @param location The smallest location.
|
||||
* @param rotation The rotation where y is the yaw in rad.
|
||||
* @param vectorDataMap The map.
|
||||
* @return The affected blocks.
|
||||
*/
|
||||
public List<Block> paste(Location location, double rotation, Map<Vector, BlockData> vectorDataMap) {
|
||||
return paste(() -> Colls.thread(vectorDataMap)
|
||||
.mapkv((vector) -> location.clone().add(round(vector.rotateAroundY(rotation))).getBlock(),
|
||||
(data) -> {
|
||||
if (data instanceof Directional directional) {
|
||||
directional.setFacing(getClosest(directional.getFacing().getDirection(), rotation, directional.getFaces()));
|
||||
return directional;
|
||||
}
|
||||
|
||||
// todo add
|
||||
// - Orientatable
|
||||
// why are there so many?
|
||||
|
||||
return data;
|
||||
})
|
||||
.get());
|
||||
}
|
||||
|
||||
private BlockFace getClosest(Vector direction, double rotation, Set<BlockFace> allowedFaces) {
|
||||
Vector rotated = round(direction.clone().rotateAroundY(rotation));
|
||||
|
||||
return allowedFaces.stream()
|
||||
.min(Comparator.comparingDouble(f -> f.getDirection().angle(rotated)))
|
||||
.orElseThrow();
|
||||
}
|
||||
|
||||
// rounds a vector to avoid small discrepancies like having an offset of E-16
|
||||
private Vector round(Vector vector) {
|
||||
double x = vector.getX();
|
||||
double y = vector.getY();
|
||||
double z = vector.getZ();
|
||||
double epsilon = Vector.getEpsilon();
|
||||
|
||||
return new Vector(Math.abs(x) >= epsilon ? x : 0,
|
||||
Math.abs(y) >= epsilon ? y : 0,
|
||||
Math.abs(z) >= epsilon ? z : 0);
|
||||
}
|
||||
|
||||
private List<Block> paste(Supplier<Map<Block, BlockData>> blocksGetter) {
|
||||
try {
|
||||
Map<Block, BlockData> blocks = CompletableFuture.supplyAsync(blocksGetter).get();
|
||||
|
||||
blocks.forEach((block, data) -> {
|
||||
if (block == null || data == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
block.setBlockData(data, false);
|
||||
});
|
||||
|
||||
return new ArrayList<>(blocks.keySet());
|
||||
} catch (InterruptedException | ExecutionException ex) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package dev.efnilite.vilib.schematic.io;
|
||||
|
||||
import dev.efnilite.vilib.util.Colls;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Schematic reading handler.
|
||||
*
|
||||
* @author Efnilite
|
||||
* @since 5.0.0
|
||||
*/
|
||||
public class SchematicReader {
|
||||
|
||||
/**
|
||||
* @param file The file.
|
||||
* @return A new {@link dev.efnilite.vilib.schematic.Schematic} instance based on the read blocks.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<Vector, BlockData> read(File file, Plugin plugin) throws IOException, ClassNotFoundException {
|
||||
try (ObjectInputStream stream = new ObjectInputStream(new BufferedInputStream(new FileInputStream(file)))) {
|
||||
var version = (int) stream.readObject();
|
||||
var palette = (Map<String, Integer>) stream.readObject();
|
||||
var offsets = (Map<String, Integer>) stream.readObject();
|
||||
|
||||
Map<Integer, BlockData> paletteRef = Colls.thread(palette).inverse().mapv((k, ov) -> {
|
||||
try {
|
||||
return Bukkit.createBlockData(ov);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
try {
|
||||
// < 1.19 versions for spawn schematic
|
||||
if (ov.contains("leaves")) {
|
||||
return Bukkit.createBlockData(ov.replaceAll(",?waterlogged=(false|true)", ""));
|
||||
}
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
|
||||
}
|
||||
|
||||
plugin.getLogger().warning("Unknown block data %s in %s".formatted(ov, file.getName()));
|
||||
|
||||
return null;
|
||||
}
|
||||
}).get();
|
||||
|
||||
// create final map by parse Map<String, Object> -> Vector and applying possible State
|
||||
return Colls.thread(offsets).mapkv(this::fromString, paletteRef::get).get();
|
||||
}
|
||||
}
|
||||
|
||||
private Vector fromString(String string) {
|
||||
String[] parts = string.split(",");
|
||||
return new Vector(Double.parseDouble(parts[0]), Double.parseDouble(parts[1]), Double.parseDouble(parts[2]));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
package dev.efnilite.vilib.schematic.io;
|
||||
|
||||
import dev.efnilite.vilib.schematic.Schematic;
|
||||
import dev.efnilite.vilib.util.Locations;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Schematic writing handler.
|
||||
*
|
||||
* @author Efnilite
|
||||
* @since 5.0.0
|
||||
*/
|
||||
public class SchematicWriter {
|
||||
|
||||
/**
|
||||
* Saves blocks to the specified file.
|
||||
*
|
||||
* @param file The file.
|
||||
* @param pos1 The first position.
|
||||
* @param pos2 The second position.
|
||||
*/
|
||||
public void save(File file, Location pos1, Location pos2, Plugin plugin) {
|
||||
List<Block> blocks = getBlocks(Locations.min(pos1, pos2), Locations.max(pos1, pos2));
|
||||
|
||||
Map<String, Integer> palette = getPalette(blocks);
|
||||
Map<String, Integer> offsetData = getOffsetData(blocks, palette);
|
||||
|
||||
// write to file
|
||||
try (ObjectOutputStream stream = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(file)))) {
|
||||
stream.writeObject(Schematic.VERSION);
|
||||
stream.writeObject(palette);
|
||||
stream.writeObject(offsetData);
|
||||
stream.flush();
|
||||
} catch (IOException ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// returns all blocks between the min location (minL) and max location (maxL)
|
||||
private List<Block> getBlocks(Location minL, Location maxL) {
|
||||
List<Block> blocks = new ArrayList<>();
|
||||
Location location = new Location(minL.getWorld() == null ? maxL.getWorld() : minL.getWorld(), 0, 0, 0);
|
||||
|
||||
for (int x = minL.getBlockX(); x <= maxL.getBlockX(); x++) {
|
||||
for (int y = minL.getBlockY(); y <= maxL.getBlockY(); y++) {
|
||||
for (int z = minL.getBlockZ(); z <= maxL.getBlockZ(); z++) {
|
||||
location.setX(x);
|
||||
location.setY(y);
|
||||
location.setZ(z);
|
||||
|
||||
if (location.getBlock().getType() == Material.AIR) {
|
||||
continue;
|
||||
}
|
||||
|
||||
blocks.add(location.getBlock());
|
||||
}
|
||||
}
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
|
||||
// returns block palette where ids are used instead of the full blockdata to reduce file space
|
||||
private Map<String, Integer> getPalette(List<Block> blocks) {
|
||||
Map<String, Integer> palette = new HashMap<>();
|
||||
|
||||
blocks.stream()
|
||||
.map(block -> block.getBlockData().getAsString())
|
||||
.distinct()
|
||||
.forEach(datum -> palette.put(datum, palette.size()));
|
||||
|
||||
return palette;
|
||||
}
|
||||
|
||||
// returns the offset data, where each block offset (position relative to minimum location)
|
||||
// is mapped to the data at that position.
|
||||
// the first item in the array is the generic BlockData of that block.
|
||||
// the second item in the array is the special data of that block, as encoded by the State instances.
|
||||
private Map<String, Integer> getOffsetData(List<Block> blocks, Map<String, Integer> palette) {
|
||||
List<Location> locations = blocks.stream().map(Block::getLocation).toList();
|
||||
Location min = locations.stream().reduce(Locations::min).orElseThrow();
|
||||
|
||||
Map<String, Integer> offsetData = new HashMap<>();
|
||||
|
||||
for (Location loc : locations) {
|
||||
offsetData.put(loc.clone().subtract(min).toVector().toString(),
|
||||
palette.get(loc.clone().getBlock().getBlockData().getAsString()));
|
||||
}
|
||||
|
||||
return offsetData;
|
||||
}
|
||||
}
|
||||
173
loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Colls.java
Normal file
173
loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Colls.java
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
package dev.efnilite.vilib.util;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
public final class Colls {
|
||||
|
||||
/**
|
||||
* Creates a util class to simplify map threading.
|
||||
*
|
||||
* @param map The map.
|
||||
* @param <K> The key type.
|
||||
* @param <V> The value type.
|
||||
* @return A new {@link MapThreader}.
|
||||
*/
|
||||
public static <K, V> MapThreader<K, V> thread(Map<K, V> map) {
|
||||
return new MapThreader<>(map);
|
||||
}
|
||||
|
||||
/**
|
||||
* Threads through maps. Modifications return {@link HashMap}.
|
||||
*
|
||||
* @param <K> The key type.
|
||||
* @param <V> The value type.
|
||||
*/
|
||||
public static class MapThreader<K, V> {
|
||||
|
||||
private Map<K, V> map;
|
||||
|
||||
public MapThreader(Map<K, V> map) {
|
||||
this.map = map;
|
||||
}
|
||||
|
||||
public Map<K, V> get() {
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param f The function to apply.
|
||||
* @return Threader with map, only with the items where f returns true. Returns {@link HashMap}.
|
||||
*/
|
||||
public MapThreader<K, V> filter(BiFunction<K, V, Boolean> f) {
|
||||
Map<K, V> newMap = new HashMap<>();
|
||||
|
||||
for (Map.Entry<K, V> entry : map.entrySet()) {
|
||||
if (f.apply(entry.getKey(), entry.getValue())) {
|
||||
newMap.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
map = newMap;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param f The function to apply to each value in collection. Arguments are the key and original value.
|
||||
* @param <NV> The new list type.
|
||||
* @return Threader with map, where f has been applied to each value in map. Returns {@link HashMap}.
|
||||
*/
|
||||
public <NV> MapThreader<K, NV> mapv(BiFunction<K, V, NV> f) {
|
||||
Map<K, NV> newMap = new HashMap<>();
|
||||
|
||||
map.forEach((key, value) -> newMap.put(key, f.apply(key, value)));
|
||||
|
||||
return new MapThreader<>(newMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param f The function to apply to each key in collection. Arguments are the original key and value.
|
||||
* @param <NK> The new key type.
|
||||
* @return Threader with map, where f has been applied to each key in map. Returns {@link HashMap}.
|
||||
*/
|
||||
public <NK> MapThreader<NK, V> mapk(BiFunction<K, V, NK> f) {
|
||||
Map<NK, V> newMap = new HashMap<>();
|
||||
|
||||
map.forEach((key, value) -> newMap.put(f.apply(key, value), value));
|
||||
|
||||
return new MapThreader<>(newMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps kf to each key and vf to each value. Modifications of k/v are independent of each other but executed at the same time.
|
||||
*
|
||||
* @param kf The function to apply to each key in collection. Argument is the original key.
|
||||
* @param vf The function to apply to each value in collection. Argument is the original value.
|
||||
* @param <NK> The new key type.
|
||||
* @param <NV> The new value type.
|
||||
* @return Threader with map, where kf has been applied to each key and vf to each value. Returns {@link HashMap}.
|
||||
*/
|
||||
public <NK, NV> MapThreader<NK, NV> mapkv(Function<K, NK> kf, Function<V, NV> vf) {
|
||||
Map<NK, NV> newMap = new HashMap<>();
|
||||
|
||||
map.forEach((key, value) -> newMap.put(kf.apply(key), vf.apply(value)));
|
||||
|
||||
return new MapThreader<>(newMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs consumer for every key and value.
|
||||
*
|
||||
* @param consumer The consumer.
|
||||
*/
|
||||
public void forEach(BiConsumer<K, V> consumer) {
|
||||
map.forEach(consumer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps all keys to values.
|
||||
*
|
||||
* @return Threader with map, where every value is the key to each key. Returns {@link HashMap}.
|
||||
*/
|
||||
public MapThreader<V, K> inverse() {
|
||||
Map<V, K> newMap = new HashMap<>();
|
||||
|
||||
map.forEach((k, v) -> newMap.put(v, k));
|
||||
|
||||
return new MapThreader<>(newMap);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param coll The collection.
|
||||
* @param <T> The type.
|
||||
* @return A random item from coll.
|
||||
*/
|
||||
public static <T> T random(List<T> coll) {
|
||||
return coll.get(ThreadLocalRandom.current().nextInt(coll.size()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param coll The collection.
|
||||
* @param n The amount of random items.
|
||||
* @param <T> The type.
|
||||
* @return n random items from coll.
|
||||
*/
|
||||
public static <T> List<T> random(List<T> coll, int n) {
|
||||
return IntStream.range(0, n).mapToObj(i -> random(coll)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param start The start. Inclusive.
|
||||
* @param end The end. Exclusive.
|
||||
* @param step The increment between values.
|
||||
* @return List with all ints from start to end with increment step.
|
||||
*/
|
||||
public static List<Integer> range(int start, int end, int step) {
|
||||
return IntStream.iterate(start, i -> i < end, i -> i + step).boxed().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param start The start. Inclusive.
|
||||
* @param end The end. Exclusive.
|
||||
* @return List with all ints from start to end with increment 1.
|
||||
*/
|
||||
public static List<Integer> range(int start, int end) {
|
||||
return range(start, end, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param end The end. Exclusive.
|
||||
* @return List with all ints from 0 to end with increment 1.
|
||||
*/
|
||||
public static List<Integer> range(int end) {
|
||||
return range(0, end, 1);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
package dev.efnilite.vilib.util;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Utility class for handling cooldowns. Cooldowns are stored by key (and optionally by player).
|
||||
* The keys are a way of identifying which action to use in the cooldown.
|
||||
* When a player performs action A, action B and then action B again, the system needs to be able to differentiate action A and B,
|
||||
* since a player can be on cooldown for multiple things.
|
||||
* <p>Using the {@link #canPerform(String, long)} or {@link #canPerform(Player, String, long)} methods allows you to check
|
||||
* whether a player can perform the action specified in the arguments. These methods return true when the player
|
||||
* is allowed to perform the action, and false when the player is not. When the player is allowed to perform the action,
|
||||
* the last execution time of the player is automatically updated to now. The {@code cooldown} argument is used to specify
|
||||
* the cooldown for the provided key. Code example:</p>
|
||||
* <blockquote><code>if (!canPerform(player, "right click wand", 1000)) { return; }</code></blockquote>
|
||||
* <p>In the above example, the {@code return} statement will be executed when the player has performed the action {@code "right click wand"}
|
||||
* in the last 1000 ms (1 second). If the action was last performed more than 1000 ms ago, the {@code return} statement will not be called.</p>
|
||||
*/
|
||||
public class Cooldowns {
|
||||
|
||||
private static final Map<String, Long> EXECUTION_TIMES = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Returns whether the provided action {@code key} can be performed with the provided cooldown {@code cooldown}.
|
||||
* <strong>This method is only recommended to be used with global actions</strong>, e.g. requiring no player.
|
||||
* Returns:
|
||||
* <ul>
|
||||
* <li>{@code true} - When the action with the provided key can be performed.
|
||||
* The last execution was more than {@code cooldown} milliseconds ago.</li>
|
||||
* <li>{@code false} - When the action with the provided key cannot be performed.
|
||||
* The last execution was less than {@code cooldown} milliseconds ago.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param key The key by which the system can differentiate actions.
|
||||
* @param cooldown The cooldown in milliseconds.
|
||||
* @return true if the action {@code key} was more than {@code cooldown} ms ago, false if not.
|
||||
*/
|
||||
public static boolean canPerform(@NotNull String key, long cooldown) {
|
||||
Objects.requireNonNull(key);
|
||||
|
||||
Long lastTime = EXECUTION_TIMES.get(key);
|
||||
|
||||
// if no value has been previously registered, player is executing for the first time, so return true
|
||||
if (lastTime == null) {
|
||||
EXECUTION_TIMES.put(key, System.currentTimeMillis());
|
||||
return true;
|
||||
}
|
||||
|
||||
// get the difference between now and the last time the command was executed
|
||||
long dt = System.currentTimeMillis() - lastTime;
|
||||
|
||||
// if the last time execution difference is higher than cooldown, allow execution
|
||||
if (dt > cooldown) {
|
||||
EXECUTION_TIMES.put(key, System.currentTimeMillis());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the provided action {@code key}, belonging to player {@code player}, can be performed
|
||||
* with the provided cooldown {@code cooldown}. Returns:
|
||||
* <ul>
|
||||
* <li>{@code true} - When the action with the provided player's key can be performed.
|
||||
* The last execution was more than {@code cooldown} milliseconds ago.</li>
|
||||
* <li>{@code false} - When the action with the provided player's key cannot be performed.
|
||||
* The last execution was less than {@code cooldown} milliseconds ago.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param key The key by which the system can differentiate actions.
|
||||
* @param cooldown The cooldown in milliseconds.
|
||||
* @return true if the action {@code key} was more than {@code cooldown} ms ago, false if not.
|
||||
*/
|
||||
public static boolean canPerform(@NotNull Player player, @NotNull String key, long cooldown) {
|
||||
Objects.requireNonNull(player);
|
||||
Objects.requireNonNull(key);
|
||||
|
||||
return canPerform(player.getUniqueId() + "-" + key, cooldown);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last registered execution time (in ms) of the specified string key. Can be null.
|
||||
* <strong>This method is only recommended to be used with global actions</strong>, e.g. requiring no player.
|
||||
*
|
||||
* @param key The key by which the system can differentiate actions.
|
||||
* @return the last execution time (in ms) of the global action. Can be null.
|
||||
*/
|
||||
@Nullable
|
||||
public static Long getLastExecutionTime(@NotNull String key) {
|
||||
Objects.requireNonNull(key);
|
||||
|
||||
return EXECUTION_TIMES.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last registered execution time (in ms) of the specified string key that's associated to the provided player.
|
||||
* Can be null.
|
||||
*
|
||||
* @param player The player.
|
||||
* @param key The key by which the system can differentiate actions.
|
||||
* @return the last execution time (in ms) of the player. Can be null.
|
||||
*/
|
||||
@Nullable
|
||||
public static Long getLastExecutionTime(@NotNull Player player, @NotNull String key) {
|
||||
Objects.requireNonNull(player);
|
||||
Objects.requireNonNull(key);
|
||||
|
||||
return EXECUTION_TIMES.get(player.getUniqueId() + "-" + key);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
package dev.efnilite.vilib.util;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.block.data.type.Fence;
|
||||
import org.bukkit.block.data.type.Wall;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Asynchronously gets the {@link Block} instances between the two specified {@link Location} instances.
|
||||
* Executes the provided {@link Consumer} with the gathered block list.
|
||||
*/
|
||||
public class Cuboid {
|
||||
|
||||
/**
|
||||
* The amount of changes per tick.
|
||||
*/
|
||||
public static final int CHANGES_PER_TICK = 2500;
|
||||
|
||||
/**
|
||||
* Sets blocks in <code>blocks</code> to their respective {@link BlockData}.
|
||||
* Performs <code>onComplete</code> when block setting has finished.
|
||||
*
|
||||
* @param blocks The block map.
|
||||
* @param onComplete What to do on completion.
|
||||
*/
|
||||
public static void set(@NotNull Map<Block, BlockData> blocks, Plugin plugin, @Nullable Runnable onComplete) {
|
||||
Queue<Block> queue = new LinkedList<>(blocks.keySet());
|
||||
|
||||
Task.create(plugin).repeat(1).execute(new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (int i = 0; i < CHANGES_PER_TICK; i++) {
|
||||
Block block = queue.poll();
|
||||
|
||||
if (block == null) {
|
||||
// no blocks left, so cancel task
|
||||
cancel();
|
||||
|
||||
if (onComplete != null) {
|
||||
onComplete.run();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setBlock(block, blocks.get(block));
|
||||
}
|
||||
}
|
||||
}).run();
|
||||
}
|
||||
|
||||
private static void setBlock(Block block, BlockData data) {
|
||||
block.setBlockData(data, data instanceof Fence || data instanceof Wall);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all blocks between the provided locations.
|
||||
*
|
||||
* @param pos1 The first location
|
||||
* @param pos2 The second location
|
||||
*/
|
||||
public static List<Block> get(@NotNull Location pos1, @NotNull Location pos2, boolean ignoreAir) {
|
||||
List<Block> blocks = new ArrayList<>();
|
||||
Location max = Locations.max(pos1, pos2);
|
||||
Location min = Locations.min(pos1, pos2);
|
||||
Location location = max.clone();
|
||||
|
||||
location.setWorld(pos1.getWorld() == null ? pos2.getWorld() : pos1.getWorld());
|
||||
for (int x = min.getBlockX(); x <= max.getBlockX(); x++) {
|
||||
for (int y = min.getBlockY(); y <= max.getBlockY(); y++) {
|
||||
for (int z = min.getBlockZ(); z <= max.getBlockZ(); z++) {
|
||||
location.setX(x);
|
||||
location.setY(y);
|
||||
location.setZ(z);
|
||||
|
||||
if (ignoreAir) {
|
||||
if (location.getBlock().getType() == Material.AIR) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
blocks.add(location.getBlock());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all blocks between the provided locations asynchronously.
|
||||
*
|
||||
* @param pos1 The first location
|
||||
* @param pos2 The second location
|
||||
* @param onComplete A {@link Consumer} with the list of gathered blocks.
|
||||
*/
|
||||
public static void getAsync(@NotNull Location pos1, @NotNull Location pos2, boolean ignoreAir,
|
||||
Plugin plugin, @NotNull Consumer<List<Block>> onComplete) {
|
||||
Task.create(plugin).async().execute(() -> onComplete.accept(get(pos1, pos2, ignoreAir))).run();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package dev.efnilite.vilib.util;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
|
||||
/**
|
||||
* Utilities for Locations
|
||||
*/
|
||||
public class Locations {
|
||||
|
||||
/**
|
||||
* Gets the max of the locations
|
||||
*
|
||||
* @param pos1 The first location
|
||||
* @param pos2 The second location
|
||||
* @return the max values of the locations
|
||||
*/
|
||||
public static Location max(Location pos1, Location pos2) {
|
||||
World world = pos1.getWorld() == null ? pos2.getWorld() : pos1.getWorld();
|
||||
return new Location(world, Math.max(pos1.getX(), pos2.getX()), Math.max(pos1.getY(), pos2.getY()), Math.max(pos1.getZ(), pos2.getZ()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the min of the locations
|
||||
*
|
||||
* @param pos1 The first location
|
||||
* @param pos2 The second location
|
||||
* @return the min values of the locations
|
||||
*/
|
||||
public static Location min(Location pos1, Location pos2) {
|
||||
World world = pos1.getWorld() == null ? pos2.getWorld() : pos1.getWorld();
|
||||
return new Location(world, Math.min(pos1.getX(), pos2.getX()), Math.min(pos1.getY(), pos2.getY()), Math.min(pos1.getZ(), pos2.getZ()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a string version of a Location.
|
||||
*
|
||||
* @param location The location
|
||||
* @return string version
|
||||
*/
|
||||
public static String toString(Location location, boolean formatted) {
|
||||
if (!formatted) {
|
||||
return "(%s,%s,%s,%s)".formatted(location.getX(), location.getY(), location.getZ(), location.getWorld().getName());
|
||||
} else {
|
||||
return "(%d, %d, %d)".formatted(location.getBlockX(), location.getBlockY(), location.getBlockZ());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a location from a string
|
||||
*
|
||||
* @param string The string
|
||||
* @return the location from the string
|
||||
*/
|
||||
public static Location parseLocation(String string) {
|
||||
String[] values = string.replaceAll("[()]", "")
|
||||
.replace(", ", " ")
|
||||
.replace(",", " ")
|
||||
.split(" ");
|
||||
|
||||
World world = Bukkit.getWorld(values[3]);
|
||||
|
||||
// if world is null, get first world
|
||||
if (world == null) {
|
||||
world = Bukkit.getWorlds().get(0);
|
||||
}
|
||||
|
||||
return new Location(world, Double.parseDouble(values[0]), Double.parseDouble(values[1]), Double.parseDouble(values[2]));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package dev.efnilite.vilib.util;
|
||||
|
||||
import dev.efnilite.vilib.ViPlugin;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Adds useful data for later (e.g. game testing)
|
||||
*/
|
||||
public class Logging {
|
||||
|
||||
private final Logger logger;
|
||||
private final ViPlugin plugin;
|
||||
|
||||
public Logging(ViPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
this.logger = plugin.getLogger();
|
||||
}
|
||||
|
||||
public void info(String info) {
|
||||
logger.info(info);
|
||||
}
|
||||
|
||||
public void warn(String warn) {
|
||||
logger.warning(warn);
|
||||
}
|
||||
|
||||
public void error(String error) {
|
||||
logger.severe(error);
|
||||
}
|
||||
|
||||
public void stack(String error, String fix) {
|
||||
stack(error, fix, null);
|
||||
}
|
||||
|
||||
public void stack(String error, Throwable throwable) {
|
||||
stack(error, null, throwable);
|
||||
}
|
||||
|
||||
public void stack(String error, @Nullable String fix, @Nullable Throwable throwable) {
|
||||
error("##");
|
||||
error("## %s has encountered a critical error!".formatted(plugin.getName()));
|
||||
error("## %s".formatted(error));
|
||||
error("##");
|
||||
|
||||
if (throwable == null) {
|
||||
error("## No stack trace provided");
|
||||
} else {
|
||||
error("## Stack trace:");
|
||||
error("## %s".formatted(throwable));
|
||||
StackTraceElement[] stack = throwable.getStackTrace();
|
||||
for (StackTraceElement stackTraceElement : stack) {
|
||||
error("##\t" + stackTraceElement.toString());
|
||||
}
|
||||
}
|
||||
|
||||
error("##");
|
||||
if (fix == null) {
|
||||
error("## This is probably not your fault.");
|
||||
error("## Contact the developer to fix it.");
|
||||
error("## Be sure to send the entire error while reporting.");
|
||||
} else {
|
||||
error("## This is probably not your fault, but you may be able to fix it.");
|
||||
error("## You should try: %s".formatted(fix));
|
||||
error("## Contact the developer if this doesn't work.");
|
||||
error("## Be sure to send the entire error while reporting.");
|
||||
}
|
||||
error("##");
|
||||
error("## Version information:");
|
||||
error("##\tPlugin Version: %s".formatted(plugin.getDescription().getVersion()));
|
||||
error("##\tMinecraft: %s".formatted(Version.getVersion()));
|
||||
error("##");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package dev.efnilite.vilib.util;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
/**
|
||||
* Number utils
|
||||
*/
|
||||
public class Numbers {
|
||||
|
||||
/**
|
||||
* Returns a random real number between two bounds.
|
||||
*
|
||||
* @param lower The lower bound (inclusive)
|
||||
* @param upper The upper bound (exclusive)
|
||||
* @return a random real number between the two bounds
|
||||
*/
|
||||
public static int random(int lower, int upper) {
|
||||
return ThreadLocalRandom.current().nextInt(lower, upper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the min int from an Array of ints
|
||||
*
|
||||
* @param ints All ints
|
||||
* @return the smallest of the values of parameter ints
|
||||
*/
|
||||
public static int min(int... ints) {
|
||||
return Arrays.stream(ints).min().orElseThrow();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the max int from an array of ints
|
||||
*
|
||||
* @param ints All ints
|
||||
* @return the int with the biggest value
|
||||
*/
|
||||
public static int max(int... ints) {
|
||||
return Arrays.stream(ints).max().orElseThrow();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all real numbers between two bounds
|
||||
*
|
||||
* @param from The lower bound (inclusive)
|
||||
* @param to The upper bound (inclusive)
|
||||
* @return a list of all real numbers between the two bounds
|
||||
*/
|
||||
public static List<Integer> getFromTo(int from, int to) {
|
||||
return IntStream.rangeClosed(min(from, to), max(from, to)).boxed().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all ints from 0 (inclusive) to n (inclusive)
|
||||
*
|
||||
* @param n The max value
|
||||
* @return a list with all ints from 0 to n
|
||||
*/
|
||||
public static List<Integer> getFromZero(int n) {
|
||||
return getFromTo(0, n);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package dev.efnilite.vilib.util;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
public final class Probs {
|
||||
|
||||
/**
|
||||
* @return A random chance with range 0 (inclusive) to 1 (exclusive).
|
||||
*/
|
||||
public static double random() {
|
||||
return random(ThreadLocalRandom.current());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param random The random instance.
|
||||
* @return A random chance with range 0 (inclusive) to 1 (exclusive).
|
||||
*/
|
||||
public static double random(Random random) {
|
||||
return random.nextDouble();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mean The average value.
|
||||
* @param sd The standard deviation.
|
||||
* @param x The x-coordinate.
|
||||
* @return The normal distributed chance at x.
|
||||
*/
|
||||
public static double normalpdf(double mean, double sd, double x) {
|
||||
double a = (x - mean) / sd;
|
||||
|
||||
return (1 / (sd * Math.sqrt(2 * Math.PI))) * Math.exp(-0.5 * (a * a));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param distribution A map where each key is mapped to a probability.
|
||||
* @param random The random instance.
|
||||
* @param <K> The key type.
|
||||
* @return A random item from the list, based on the probabilities.
|
||||
*/
|
||||
public static <K> K random(@NotNull Map<K, Double> distribution, Random random) {
|
||||
if (distribution.isEmpty()) {
|
||||
throw new NoSuchElementException("No elements in distribution map");
|
||||
}
|
||||
|
||||
double total = 0;
|
||||
|
||||
Map<K, Double> cumulative = new LinkedHashMap<>();
|
||||
for (Map.Entry<K, Double> entry : distribution.entrySet()) {
|
||||
total += entry.getValue();
|
||||
cumulative.put(entry.getKey(), total);
|
||||
}
|
||||
|
||||
double randomNum = random(random) * total;
|
||||
for (Map.Entry<K, Double> entry : cumulative.entrySet()) {
|
||||
if (entry.getValue() >= randomNum) {
|
||||
return entry.getKey();
|
||||
}
|
||||
}
|
||||
|
||||
return distribution.keySet().stream().findFirst().orElseThrow();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param distribution A map where each key is mapped to a probability.
|
||||
* @param <K> The key type.
|
||||
* @return A random item from the list, based on the probabilities.
|
||||
*/
|
||||
public static <K> K random(@NotNull Map<K, Double> distribution) {
|
||||
return random(distribution, ThreadLocalRandom.current());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package dev.efnilite.vilib.util;
|
||||
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.meta.SkullMeta;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class SkullSetter {
|
||||
|
||||
private static boolean isPaper;
|
||||
private static Method getPlayerProfileMethod;
|
||||
private static Method hasTexturesMethod;
|
||||
private static Method setPlayerProfileMethod;
|
||||
|
||||
static {
|
||||
try {
|
||||
Class<?> playerProfileClass = Class.forName("com.destroystokyo.paper.profile.PlayerProfile");
|
||||
getPlayerProfileMethod = Player.class.getDeclaredMethod("getPlayerProfile");
|
||||
hasTexturesMethod = playerProfileClass.getDeclaredMethod("hasTextures");
|
||||
setPlayerProfileMethod = SkullMeta.class.getDeclaredMethod("setPlayerProfile", playerProfileClass);
|
||||
isPaper = true;
|
||||
} catch (Exception ex) {
|
||||
isPaper = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void setPlayerHead(OfflinePlayer player, SkullMeta meta) {
|
||||
if (!isPaper) { // ew
|
||||
meta.setOwningPlayer(player);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Object playerProfile = getPlayerProfileMethod.invoke(player);
|
||||
boolean hasTexture = (boolean) hasTexturesMethod.invoke(playerProfile);
|
||||
if (hasTexture) {
|
||||
setPlayerProfileMethod.invoke(meta, playerProfile);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
meta.setOwningPlayer(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
package dev.efnilite.vilib.util;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.standard.StandardTags;
|
||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class Strings {
|
||||
|
||||
private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage();
|
||||
private static final LegacyComponentSerializer LEGACY_COMPONENT_SERIALIZER = LegacyComponentSerializer.builder()
|
||||
.extractUrls()
|
||||
.hexColors()
|
||||
.character(LegacyComponentSerializer.SECTION_CHAR)
|
||||
.useUnusualXRepeatedCharacterHexFormat() // spigot makes me sad :(
|
||||
.build();
|
||||
|
||||
/**
|
||||
* Colours a list of strings using {@link MiniMessage} and {@link LegacyComponentSerializer}
|
||||
*
|
||||
* @param strings The list of strings
|
||||
* @return the coloured list of strings
|
||||
*/
|
||||
public static List<String> colour(@NotNull List<String> strings) {
|
||||
return strings.stream().map(Strings::colour).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Colours an array of strings using {@link MiniMessage} and {@link LegacyComponentSerializer}
|
||||
*
|
||||
* @param strings The array of strings
|
||||
* @return the array of strings, but coloured
|
||||
*/
|
||||
@NotNull
|
||||
public static String[] colour(@NotNull String... strings) {
|
||||
String[] coloured = new String[strings.length];
|
||||
|
||||
int index = 0;
|
||||
for (String string : strings) {
|
||||
coloured[index] = colour(string);
|
||||
index++;
|
||||
}
|
||||
|
||||
return coloured;
|
||||
}
|
||||
|
||||
/**
|
||||
* Colours a string using {@link MiniMessage} and {@link LegacyComponentSerializer}
|
||||
*
|
||||
* @param string The string
|
||||
* @return the coloured string
|
||||
*/
|
||||
@NotNull
|
||||
public static String colour(@NotNull String string) {
|
||||
Component component = MINI_MESSAGE.deserialize(string); // sanitize input
|
||||
|
||||
return ChatColor.translateAlternateColorCodes(LegacyComponentSerializer.SECTION_CHAR, LEGACY_COMPONENT_SERIALIZER.serialize(component));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the closest matching string
|
||||
*
|
||||
* @param source The source string
|
||||
* @param strings Strings which will be compared to this string
|
||||
* @return the closest matching string from parameter strings
|
||||
*/
|
||||
public static String getClosestMatching(String source, List<String> strings) {
|
||||
int min = Integer.MAX_VALUE;
|
||||
String closest = "";
|
||||
|
||||
for (String string : strings) {
|
||||
int distance = getLevenshteinDistance(source, string);
|
||||
if (distance < min) {
|
||||
closest = string;
|
||||
}
|
||||
}
|
||||
return closest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the levenshtein distance between two strings
|
||||
* Source: <a href="https://www.stephenenright.com/java-levenshtein-distance">https://www.stephenenright.com/java-levenshtein-distance</a>
|
||||
*
|
||||
* @param source The source string
|
||||
* @param other The other string
|
||||
* @return the distance required
|
||||
*/
|
||||
public static int getLevenshteinDistance(String source, String other) {
|
||||
int sourceLength = source.length();
|
||||
int otherLength = other.length();
|
||||
|
||||
int[][] minDistanceMatrix = new int[sourceLength + 1][otherLength + 1]; // init the minimum distance matrix and add one to account for default values
|
||||
minDistanceMatrix[0][0] = 0;
|
||||
|
||||
for (int row = 1; row <= sourceLength; row++) { // enter default edit values for the source string (in rows)
|
||||
minDistanceMatrix[row][0] = row;
|
||||
}
|
||||
|
||||
for (int col = 1; col <= otherLength; col++) { // enter default edit values for the other string (in cols)
|
||||
minDistanceMatrix[0][col] = col;
|
||||
}
|
||||
|
||||
for (int row = 1; row <= sourceLength; row++) {
|
||||
for (int col = 1; col <= otherLength; col++) { // go through every value and get the min value
|
||||
minDistanceMatrix[row][col] = getMinLevenshteinCost(source, other, minDistanceMatrix, row, col);
|
||||
}
|
||||
}
|
||||
|
||||
return minDistanceMatrix[sourceLength][otherLength]; // get the last value
|
||||
}
|
||||
|
||||
private static int getMinLevenshteinCost(String source, String other, int[][] minDistanceMatrix, int row, int col) {
|
||||
int insertion = minDistanceMatrix[row][col - 1] + 1;
|
||||
int deletion = minDistanceMatrix[row - 1][col] + 1;
|
||||
int substition = minDistanceMatrix[row - 1][col - 1];
|
||||
|
||||
if (source.charAt(row - 1) != other.charAt(col - 1)) { // if the letters are the same skip adding a cost
|
||||
substition += 1;
|
||||
}
|
||||
|
||||
return Numbers.min(insertion, deletion, substition);
|
||||
}
|
||||
}
|
||||
164
loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Task.java
Normal file
164
loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Task.java
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
package dev.efnilite.vilib.util;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
import org.bukkit.scheduler.BukkitScheduler;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
/**
|
||||
* Class for easily constructing tasks.
|
||||
*
|
||||
* @author Efnilite
|
||||
*/
|
||||
public class Task {
|
||||
|
||||
private int delay = 0;
|
||||
private int repeat = 0;
|
||||
private boolean async = false;
|
||||
private final Plugin plugin;
|
||||
private Runnable defaultRunnable;
|
||||
private BukkitTask task;
|
||||
private BukkitRunnable bukkitRunnable;
|
||||
|
||||
public Task(Plugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new Task instance
|
||||
*
|
||||
* @param plugin The plugin which to register this Task with
|
||||
* @return the created Task instance
|
||||
*/
|
||||
public static Task create(Plugin plugin) {
|
||||
return new Task(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies which Java Runnable should be executed. This supports lambdas.
|
||||
*
|
||||
* @param runnable The Java Runnable that is going to be run
|
||||
* @return the instance of the class
|
||||
*/
|
||||
public Task execute(Runnable runnable) {
|
||||
this.defaultRunnable = runnable;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies which Bukkit Runnable should be executed
|
||||
*
|
||||
* @param runnable The Bukkit Runnable that is going to be run
|
||||
* @return the instance of the class
|
||||
*/
|
||||
public Task execute(BukkitRunnable runnable) {
|
||||
this.bukkitRunnable = runnable;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this task should run async
|
||||
*
|
||||
* @return the instance of this class
|
||||
*/
|
||||
public Task async() {
|
||||
this.async = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The delay this task will run with
|
||||
*
|
||||
* @param delay The delay in ticks
|
||||
* @return the instance of this class
|
||||
*/
|
||||
public Task delay(int delay) {
|
||||
this.delay = delay;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The repeating interval this task will run with
|
||||
*
|
||||
* @param repeat The interval in ticks
|
||||
* @return the instance of this class
|
||||
*/
|
||||
public Task repeat(int repeat) {
|
||||
this.repeat = repeat;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels this task
|
||||
*
|
||||
* @return the instance of this class
|
||||
*/
|
||||
public Task cancel() {
|
||||
task.cancel();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the active/waiting task and runs the task immediately
|
||||
*/
|
||||
public void cancelAndRunImmediately() {
|
||||
task.cancel();
|
||||
|
||||
if (bukkitRunnable != null) {
|
||||
bukkitRunnable.run();
|
||||
}
|
||||
if (defaultRunnable != null) {
|
||||
defaultRunnable.run();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs this task
|
||||
*
|
||||
* @return the BukkitTask instance returned from running this task
|
||||
*/
|
||||
public BukkitTask run() {
|
||||
if (bukkitRunnable != null) {
|
||||
if (async) { // async
|
||||
if (repeat > 0) {
|
||||
task = bukkitRunnable.runTaskTimerAsynchronously(plugin, delay, repeat);
|
||||
} else if (delay > 0) {
|
||||
task = bukkitRunnable.runTaskLaterAsynchronously(plugin, delay);
|
||||
} else {
|
||||
task = bukkitRunnable.runTaskAsynchronously(plugin);
|
||||
}
|
||||
} else {
|
||||
if (repeat > 0) {
|
||||
task = bukkitRunnable.runTaskTimer(plugin, delay, repeat);
|
||||
} else if (delay > 0) {
|
||||
task = bukkitRunnable.runTaskLater(plugin, delay);
|
||||
} else {
|
||||
task = bukkitRunnable.runTask(plugin);
|
||||
}
|
||||
}
|
||||
} else if (defaultRunnable != null) {
|
||||
BukkitScheduler scheduler = Bukkit.getScheduler();
|
||||
if (async) { // async
|
||||
if (repeat > 0) {
|
||||
task = scheduler.runTaskTimerAsynchronously(plugin, defaultRunnable, delay, repeat);
|
||||
} else if (delay > 0) {
|
||||
task = scheduler.runTaskLaterAsynchronously(plugin, defaultRunnable, delay);
|
||||
} else {
|
||||
task = scheduler.runTaskAsynchronously(plugin, defaultRunnable);
|
||||
}
|
||||
} else {
|
||||
if (repeat > 0) {
|
||||
task = scheduler.runTaskTimer(plugin, defaultRunnable, delay, repeat);
|
||||
} else if (delay > 0) {
|
||||
task = scheduler.runTaskLater(plugin, defaultRunnable, delay);
|
||||
} else {
|
||||
task = scheduler.runTask(plugin, defaultRunnable);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new IllegalStateException("Both runnable types are null!");
|
||||
}
|
||||
return task;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
package dev.efnilite.vilib.util;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* Time utils
|
||||
*
|
||||
* @author Efnilite
|
||||
*/
|
||||
public class Time {
|
||||
|
||||
private static final HashMap<String, Long> timings = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Hours per day.
|
||||
*/
|
||||
public static final int HOURS_PER_DAY = 24;
|
||||
|
||||
/**
|
||||
* Minutes per hour.
|
||||
*/
|
||||
public static final int MINUTES_PER_HOUR = 60;
|
||||
|
||||
/**
|
||||
* Seconds per minute.
|
||||
*/
|
||||
public static final int SECONDS_PER_MINUTE = 60;
|
||||
|
||||
/**
|
||||
* Seconds per hour.
|
||||
*/
|
||||
public static final int SECONDS_PER_HOUR = 3600;
|
||||
|
||||
/**
|
||||
* Seconds per day.
|
||||
*/
|
||||
public static final int SECONDS_PER_DAY = 86400;
|
||||
|
||||
/**
|
||||
* Converts an amount of seconds to millis
|
||||
*
|
||||
* @param secs The second count
|
||||
* @return the second count to millis
|
||||
*/
|
||||
public static long toMillis(long secs) {
|
||||
return secs * 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the timer.
|
||||
*
|
||||
* @param key The name of the operation which will be timed. These need to be unique!
|
||||
*/
|
||||
public static void timerStart(String key) {
|
||||
timings.put(key, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* End the timer and get the time between start and finish in ms.
|
||||
*
|
||||
* @param key The name of the operation.
|
||||
* @return the time it took between starting and finishing.
|
||||
*/
|
||||
public static long timerEnd(String key) {
|
||||
long startTime = timings.get(key);
|
||||
timings.remove(key);
|
||||
return System.currentTimeMillis() - startTime;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package dev.efnilite.vilib.util;
|
||||
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class UpdateChecker {
|
||||
|
||||
public static void check(Plugin plugin, int resourceId) {
|
||||
Task.create(plugin)
|
||||
.async()
|
||||
.execute(() -> {
|
||||
HttpClient client = HttpClient.newHttpClient();
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create("https://api.spiget.org/v2/resources/%s/versions/latest".formatted(resourceId)))
|
||||
.build();
|
||||
|
||||
plugin.getLogger().info("Checking for updates");
|
||||
|
||||
var tag = client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
|
||||
.thenApply(HttpResponse::body)
|
||||
.thenApply(UpdateChecker::parseLatestTag)
|
||||
.join();
|
||||
|
||||
if (tag.isEmpty()) {
|
||||
plugin.getLogger().info("Failed to check for updates");
|
||||
return;
|
||||
}
|
||||
|
||||
var version = plugin.getDescription().getVersion();
|
||||
if (!version.equals(tag)) {
|
||||
plugin.getLogger().info("A new version is available: %s -> %s".formatted(version, tag));
|
||||
plugin.getLogger().info("Download at https://spigotmc.org/resources/%s".formatted(resourceId));
|
||||
} else {
|
||||
plugin.getLogger().info("No new version found");
|
||||
}
|
||||
})
|
||||
.run();
|
||||
}
|
||||
|
||||
private static String parseLatestTag(String responseBody) {
|
||||
Pattern pattern = Pattern.compile("\"name\": ?\"(.*?)\"");
|
||||
Matcher matcher = pattern.matcher(responseBody);
|
||||
|
||||
if (matcher.find()) {
|
||||
return matcher.group(1);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
package dev.efnilite.vilib.util;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Version class, useful for checking whether certain features on legacy can be executed.
|
||||
*
|
||||
* @author Efnilite
|
||||
*/
|
||||
public enum Version {
|
||||
|
||||
V1_16(16), V1_17(17), V1_18(18), V1_19(19), V1_20(20), V1_20_5(20, 5),
|
||||
V1_21(21), V1_22(22);
|
||||
|
||||
public static Version VERSION;
|
||||
public final int major;
|
||||
public final int minor;
|
||||
|
||||
Version(int major) {
|
||||
this.major = major;
|
||||
this.minor = 0;
|
||||
}
|
||||
|
||||
Version(int major, int minor) {
|
||||
this.major = major;
|
||||
this.minor = minor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the version is higher or equal to a given version.
|
||||
*
|
||||
* @param compareTo The version to compare to
|
||||
* @return true if the current version is higher or equal to the given version, false if not
|
||||
*/
|
||||
public static boolean isHigherOrEqual(Version compareTo) {
|
||||
if (VERSION.major == compareTo.major) {
|
||||
return VERSION.minor >= compareTo.minor;
|
||||
} else {
|
||||
return VERSION.major > compareTo.major;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current version as a String which can be displayed to users.
|
||||
*
|
||||
* @return the pretty version as a String
|
||||
*/
|
||||
public static String getPrettyVersion() {
|
||||
return Bukkit.getBukkitVersion().split("-")[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current version as an instance of this enum.
|
||||
*
|
||||
* @return the version.
|
||||
*/
|
||||
public static Version getVersion() {
|
||||
var parts = getPrettyVersion().split("\\.");
|
||||
var major = Integer.parseInt(parts[1]);
|
||||
var minor = parts.length == 3 ? Integer.parseInt(parts[2]) : 0;
|
||||
|
||||
var lowerVersions = Arrays.stream(Version.values()).filter(version -> {
|
||||
if (version.major == major) {
|
||||
return version.minor <= minor;
|
||||
} else {
|
||||
return version.major < major;
|
||||
}
|
||||
}).toList();
|
||||
|
||||
VERSION = lowerVersions.get(lowerVersions.size() - 1);
|
||||
|
||||
return VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the internal version from the Bukkit package.
|
||||
* Format: "v1_20_R1"
|
||||
*
|
||||
* @return the internal version with format "v1_20_R1"
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
public static String getInternalVersion() {
|
||||
return Bukkit.getBukkitVersion().split("-")[0];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
package dev.efnilite.vilib.util;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.generator.BiomeProvider;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
import org.bukkit.generator.WorldInfo;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Wrapper class of the specific version void generators
|
||||
* Inspired by the VoidGen plugin.
|
||||
*/
|
||||
public abstract class VoidGenerator extends ChunkGenerator {
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateNoise() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateSurface() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateBedrock() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateCaves() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateDecorations() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateMobs() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateStructures() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static VoidGenerator getGenerator() {
|
||||
if (Version.getVersion().major > 16) {
|
||||
return new VoidGenerator_v1_17();
|
||||
} else {
|
||||
return new VoidGenerator_v1_16();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the active void generator plugin
|
||||
*
|
||||
* @return the void generator currently in use
|
||||
*/
|
||||
public static @Nullable String getMultiverseGenerator() {
|
||||
return Bukkit.getPluginManager().isPluginEnabled("VoidGen") ? "VoidGen" : null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
protected static class VoidGenerator_v1_16 extends VoidGenerator {
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ChunkData generateChunkData(@NotNull World world, @NotNull Random random, int x, int z, @NotNull ChunkGenerator.BiomeGrid biome) {
|
||||
return createChunkData(world);
|
||||
}
|
||||
}
|
||||
|
||||
protected static class VoidGenerator_v1_17 extends VoidGenerator {
|
||||
|
||||
@Override
|
||||
public BiomeProvider getDefaultBiomeProvider(@NotNull WorldInfo worldInfo) {
|
||||
return new VoidGenerator_v1_17.VoidBiomeProvider();
|
||||
}
|
||||
|
||||
private static class VoidBiomeProvider extends BiomeProvider {
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Biome getBiome(@NotNull WorldInfo worldInfo, int x, int y, int z) {
|
||||
return Biome.PLAINS;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public List<Biome> getBiomes(@NotNull WorldInfo worldInfo) {
|
||||
return List.of(Biome.PLAINS);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +1,3 @@
|
|||
rootProject.name = 'LoParkour'
|
||||
|
||||
include 'loparkour-vilib'
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ import dev.loki.loparkour.mode.MultiMode;
|
|||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.loki.loparkour.player.data.InventoryData;
|
||||
import dev.loki.loparkour.schematic.lpschem.LPSchematic;
|
||||
import dev.loki.loparkour.schematic.lpschem.LPSchematicBuilder;
|
||||
import dev.loki.loparkour.schematic.lpschem.SchematicConverter;
|
||||
import dev.loki.loparkour.session.Session;
|
||||
import dev.efnilite.vilib.command.ViCommand;
|
||||
import dev.efnilite.vilib.inventory.item.Item;
|
||||
|
|
@ -227,8 +230,11 @@ public class Command extends ViCommand {
|
|||
send(player, "<red>/LoParkour schematic wand <dark_gray>- <gray>Get the schematic wand");
|
||||
send(player, "<red>/LoParkour schematic pos1 <dark_gray>- <gray>Set the first position of your selection");
|
||||
send(player, "<red>/LoParkour schematic pos2 <dark_gray>- <gray>Set the second position of your selection");
|
||||
send(player, "<red>/LoParkour schematic save <dark_gray>- <gray>Save your selection to a schematic file");
|
||||
send(player, "<red>/LoParkour schematic paste <file> <dark_gray>- <gray>Paste a schematic file");
|
||||
send(player, "<red>/LoParkour schematic save <name> <dark_gray>- <gray>Save selection to .lpschem format");
|
||||
send(player, "<red>/LoParkour schematic paste <name> <dark_gray>- <gray>Paste a .lpschem schematic");
|
||||
send(player, "<red>/LoParkour schematic list <dark_gray>- <gray>List all loaded schematics");
|
||||
send(player, "<red>/LoParkour schematic reload <dark_gray>- <gray>Reload all schematics");
|
||||
send(player, "<red>/LoParkour schematic convert <dark_gray>- <gray>Convert old schematics to .lpschem");
|
||||
send(player, "");
|
||||
send(player, "<dark_gray><underlined>Have any questions or need help? Join the Discord.");
|
||||
}
|
||||
|
|
@ -510,6 +516,27 @@ public class Command extends ViCommand {
|
|||
|
||||
Schematic.save(LoParkour.getInFolder("schematics/parkour-%s".formatted(code)), existingSelection[0], existingSelection[1], LoParkour.getPlugin());
|
||||
}
|
||||
case "list" -> {
|
||||
send(player, "<dark_red><bold>Schematics <reset><gray>Loaded schematics:");
|
||||
LoParkour.getSchematicManager().getAllSchematics().forEach((name, schem) -> {
|
||||
send(player, "<gray>- <red>" + name + " <dark_gray>(" + schem.getMetadata().getDifficulty() + ")");
|
||||
});
|
||||
}
|
||||
case "reload" -> {
|
||||
if (!cooldown(sender, "LoParkour reload schematics", 2500)) {
|
||||
return;
|
||||
}
|
||||
LoParkour.getSchematicManager().reload();
|
||||
send(player, "<dark_red><bold>Schematics <reset><gray>Reloaded all schematics.");
|
||||
}
|
||||
case "convert" -> {
|
||||
if (!cooldown(sender, "LoParkour convert schematics", 5000)) {
|
||||
return;
|
||||
}
|
||||
send(player, "<dark_red><bold>Schematics <reset><gray>Converting old schematics to .lpschem format...");
|
||||
SchematicConverter.convertAll();
|
||||
send(player, "<dark_red><bold>Schematics <reset><gray>Conversion complete! Check console for details.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -521,22 +548,56 @@ public class Command extends ViCommand {
|
|||
}
|
||||
|
||||
if (arg1.equalsIgnoreCase("schematic")) {
|
||||
if (!arg2.equalsIgnoreCase("paste")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!player.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schematic schematic = Schematics.getSchematic(LoParkour.getPlugin(), arg3);
|
||||
if (schematic == null) {
|
||||
send(sender, "%sCouldn't find %s".formatted(LoParkour.PREFIX, arg3));
|
||||
return;
|
||||
}
|
||||
switch (arg2.toLowerCase()) {
|
||||
case "save" -> {
|
||||
if (!cooldown(sender, "LoParkour save schematic", 2500)) {
|
||||
return;
|
||||
}
|
||||
|
||||
schematic.paste(player.getLocation());
|
||||
send(sender, "%sPasted schematic %s".formatted(LoParkour.PREFIX, arg3));
|
||||
Location[] selection = selections.get(player);
|
||||
if (selection == null || selection[0] == null || selection[1] == null) {
|
||||
send(player, "<dark_red><bold>Schematics <reset><gray>Your schematic isn't complete yet. Make sure you've set the first and second position.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
int width = Math.abs(selection[1].getBlockX() - selection[0].getBlockX()) + 1;
|
||||
int height = Math.abs(selection[1].getBlockY() - selection[0].getBlockY()) + 1;
|
||||
int length = Math.abs(selection[1].getBlockZ() - selection[0].getBlockZ()) + 1;
|
||||
|
||||
LPSchematic schematic = new LPSchematicBuilder(arg3, player.getName(), 0.5, width, height, length)
|
||||
.fromWorld(player.getWorld(), selection[0], selection[1])
|
||||
.build();
|
||||
|
||||
LoParkour.getSchematicManager().saveSchematic(schematic);
|
||||
send(player, "<dark_red><bold>Schematics <reset><gray>Saved schematic as <red>" + arg3 + "<gray>!");
|
||||
} catch (Exception e) {
|
||||
send(player, "<dark_red><bold>Schematics <reset><gray>Failed to save schematic: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
case "paste" -> {
|
||||
LPSchematic lpSchematic = LoParkour.getSchematicManager().getSchematic(arg3);
|
||||
if (lpSchematic != null) {
|
||||
lpSchematic.paste(player.getLocation(), player.getWorld());
|
||||
send(sender, "%sPasted .lpschem schematic %s".formatted(LoParkour.PREFIX, arg3));
|
||||
return;
|
||||
}
|
||||
|
||||
Schematic oldSchematic = Schematics.getSchematic(LoParkour.getPlugin(), arg3);
|
||||
if (oldSchematic == null) {
|
||||
send(sender, "%sCouldn't find %s".formatted(LoParkour.PREFIX, arg3));
|
||||
return;
|
||||
}
|
||||
|
||||
oldSchematic.paste(player.getLocation());
|
||||
send(sender, "%sPasted old schematic %s".formatted(LoParkour.PREFIX, arg3));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,16 +7,20 @@ import dev.loki.loparkour.config.Option;
|
|||
import dev.loki.loparkour.hook.HoloHook;
|
||||
import dev.loki.loparkour.hook.PAPIHook;
|
||||
import dev.loki.loparkour.mode.DefaultMode;
|
||||
import dev.loki.loparkour.mode.GravityShiftMode;
|
||||
import dev.loki.loparkour.mode.HardcoreMode;
|
||||
import dev.loki.loparkour.mode.Modes;
|
||||
import dev.loki.loparkour.mode.SpectatorMode;
|
||||
import dev.loki.loparkour.mode.SpeedrunMode;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.loki.loparkour.reward.Rewards;
|
||||
import dev.loki.loparkour.schematic.lpschem.LPSchematicManager;
|
||||
import dev.loki.loparkour.storage.Storage;
|
||||
import dev.loki.loparkour.world.World;
|
||||
import dev.efnilite.vilib.ViPlugin;
|
||||
import dev.efnilite.vilib.bstats.bukkit.Metrics;
|
||||
import dev.efnilite.vilib.bstats.charts.SimplePie;
|
||||
import dev.efnilite.vilib.bstats.charts.SingleLineChart;
|
||||
import org.bstats.bukkit.Metrics;
|
||||
import org.bstats.charts.SimplePie;
|
||||
import org.bstats.charts.SingleLineChart;
|
||||
import dev.efnilite.vilib.inventory.Menu;
|
||||
import dev.efnilite.vilib.util.Logging;
|
||||
import dev.efnilite.vilib.util.UpdateChecker;
|
||||
|
|
@ -36,6 +40,7 @@ public final class LoParkour extends ViPlugin {
|
|||
|
||||
private static Logging logging;
|
||||
private static LoParkour instance;
|
||||
private static LPSchematicManager schematicManager;
|
||||
|
||||
@Nullable
|
||||
private static PAPIHook placeholderHook;
|
||||
|
|
@ -73,6 +78,10 @@ public final class LoParkour extends ViPlugin {
|
|||
return placeholderHook;
|
||||
}
|
||||
|
||||
public static LPSchematicManager getSchematicManager() {
|
||||
return schematicManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
instance = this;
|
||||
|
|
@ -86,10 +95,18 @@ public final class LoParkour extends ViPlugin {
|
|||
|
||||
Config.reload(true);
|
||||
|
||||
// ----- Schematics -----
|
||||
|
||||
schematicManager = new LPSchematicManager();
|
||||
schematicManager.loadAll();
|
||||
|
||||
// ----- Registry -----
|
||||
|
||||
Registry.register(new DefaultMode());
|
||||
Registry.register(new SpectatorMode());
|
||||
Registry.register(new SpeedrunMode());
|
||||
Registry.register(new GravityShiftMode());
|
||||
Registry.register(new HardcoreMode());
|
||||
|
||||
Modes.init();
|
||||
Menu.init(this);
|
||||
|
|
@ -124,7 +141,7 @@ public final class LoParkour extends ViPlugin {
|
|||
|
||||
// ----- Metrics -----
|
||||
|
||||
Metrics metrics = new Metrics(this, 9272);
|
||||
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.locales.size())));
|
||||
|
|
|
|||
|
|
@ -182,6 +182,32 @@ public class Option {
|
|||
SQL_PREFIX = Config.CONFIG.getString("sql.prefix");
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------
|
||||
// Jump Validation
|
||||
|
||||
public static boolean JUMP_VALIDATION_ENABLED;
|
||||
public static double MAX_JUMP_DISTANCE;
|
||||
public static double MAX_HORIZONTAL_DISTANCE;
|
||||
public static double MAX_VERTICAL_UP;
|
||||
public static double MAX_VERTICAL_DOWN;
|
||||
|
||||
// Jump Types
|
||||
|
||||
public static boolean JUMP_TYPES_ENABLED;
|
||||
public static Map<String, Boolean> JUMP_TYPE_ENABLED;
|
||||
public static Map<String, Double> JUMP_TYPE_CHANCE;
|
||||
|
||||
// Memory Optimization
|
||||
|
||||
public static int BLOCK_CLEANUP_DISTANCE;
|
||||
public static int CLEANUP_INTERVAL;
|
||||
|
||||
// Ghost Mode
|
||||
|
||||
public static boolean GHOST_MODE_ENABLED;
|
||||
public static int GHOST_SHOW_TOP;
|
||||
public static double GHOST_TRANSPARENCY;
|
||||
|
||||
// --------------------------------------------------------------
|
||||
// Generation
|
||||
|
||||
|
|
@ -208,6 +234,33 @@ public class Option {
|
|||
public static int MIN_Y;
|
||||
|
||||
private static void initGeneration() {
|
||||
// Jump validation
|
||||
JUMP_VALIDATION_ENABLED = Config.CONFIG.getBoolean("jump-validation.enabled");
|
||||
MAX_JUMP_DISTANCE = Config.CONFIG.getDouble("jump-validation.max-distance");
|
||||
MAX_HORIZONTAL_DISTANCE = Config.CONFIG.getDouble("jump-validation.max-horizontal");
|
||||
MAX_VERTICAL_UP = Config.CONFIG.getDouble("jump-validation.max-vertical-up");
|
||||
MAX_VERTICAL_DOWN = Config.CONFIG.getDouble("jump-validation.max-vertical-down");
|
||||
|
||||
// Jump types
|
||||
JUMP_TYPES_ENABLED = Config.CONFIG.getBoolean("jump-types.enabled");
|
||||
JUMP_TYPE_ENABLED = new HashMap<>();
|
||||
JUMP_TYPE_CHANCE = new HashMap<>();
|
||||
|
||||
for (String type : Config.CONFIG.getChildren("jump-types.types", false)) {
|
||||
String path = "jump-types.types." + type;
|
||||
JUMP_TYPE_ENABLED.put(type, Config.CONFIG.getBoolean(path + ".enabled"));
|
||||
JUMP_TYPE_CHANCE.put(type, Config.CONFIG.getDouble(path + ".chance"));
|
||||
}
|
||||
|
||||
// Memory optimization
|
||||
BLOCK_CLEANUP_DISTANCE = Config.CONFIG.getInt("memory.block-cleanup-distance");
|
||||
CLEANUP_INTERVAL = Config.CONFIG.getInt("memory.cleanup-interval");
|
||||
|
||||
// Ghost mode
|
||||
GHOST_MODE_ENABLED = Config.CONFIG.getBoolean("ghost-mode.enabled");
|
||||
GHOST_SHOW_TOP = Config.CONFIG.getInt("ghost-mode.show-top");
|
||||
GHOST_TRANSPARENCY = Config.CONFIG.getDouble("ghost-mode.transparency");
|
||||
|
||||
TYPE_NORMAL = Config.GENERATION.getInt("generation.type.normal") / 100.0;
|
||||
TYPE_SPECIAL = Config.GENERATION.getInt("generation.type.schematic") / 100.0;
|
||||
TYPE_SCHEMATICS = Config.GENERATION.getInt("generation.type.special") / 100.0;
|
||||
|
|
|
|||
53
src/main/java/dev/loki/loparkour/generator/JumpType.java
Normal file
53
src/main/java/dev/loki/loparkour/generator/JumpType.java
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package dev.loki.loparkour.generator;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public enum JumpType {
|
||||
|
||||
NORMAL(1.0, "Normal jump", Material.STONE),
|
||||
NEO_JUMP(2.5, "Jump around fence/wall", Material.OAK_FENCE, Material.COBBLESTONE_WALL, Material.NETHER_BRICK_FENCE),
|
||||
HEAD_HITTER(2.0, "Jump with block overhead", Material.STONE_SLAB),
|
||||
FENCE_JUMP(1.8, "Jump onto fence", Material.OAK_FENCE, Material.BIRCH_FENCE, Material.SPRUCE_FENCE),
|
||||
TRAPDOOR_JUMP(1.5, "Jump onto trapdoor", Material.OAK_TRAPDOOR, Material.IRON_TRAPDOOR),
|
||||
LADDER_JUMP(1.7, "Jump onto ladder", Material.LADDER);
|
||||
|
||||
private final double difficulty;
|
||||
private final String description;
|
||||
private final List<Material> materials;
|
||||
|
||||
JumpType(double difficulty, String description, Material... materials) {
|
||||
this.difficulty = difficulty;
|
||||
this.description = description;
|
||||
this.materials = Arrays.asList(materials);
|
||||
}
|
||||
|
||||
public double getDifficulty() {
|
||||
return difficulty;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public List<Material> getMaterials() {
|
||||
return materials;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Material getRandomMaterial() {
|
||||
if (materials.isEmpty()) {
|
||||
return Material.STONE;
|
||||
}
|
||||
return materials.get((int) (Math.random() * materials.size()));
|
||||
}
|
||||
|
||||
public boolean usesMaterial(@NotNull Material material) {
|
||||
return materials.contains(material);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package dev.loki.loparkour.generator;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.util.Vector;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class JumpValidator {
|
||||
|
||||
private static final double MAX_JUMP_DISTANCE = 4.5;
|
||||
private static final double MAX_HORIZONTAL_DISTANCE = 4.1;
|
||||
private static final double MAX_VERTICAL_UP = 1.25;
|
||||
private static final double MAX_VERTICAL_DOWN = 3.0;
|
||||
|
||||
private final double maxDistance;
|
||||
private final double maxHorizontal;
|
||||
private final double maxVerticalUp;
|
||||
private final double maxVerticalDown;
|
||||
|
||||
public JumpValidator() {
|
||||
this(MAX_JUMP_DISTANCE, MAX_HORIZONTAL_DISTANCE, MAX_VERTICAL_UP, MAX_VERTICAL_DOWN);
|
||||
}
|
||||
|
||||
public JumpValidator(double maxDistance, double maxHorizontal, double maxVerticalUp, double maxVerticalDown) {
|
||||
this.maxDistance = maxDistance;
|
||||
this.maxHorizontal = maxHorizontal;
|
||||
this.maxVerticalUp = maxVerticalUp;
|
||||
this.maxVerticalDown = maxVerticalDown;
|
||||
}
|
||||
|
||||
public boolean canJump(@NotNull Location from, @NotNull Location to) {
|
||||
return canJump(from.toVector(), to.toVector());
|
||||
}
|
||||
|
||||
// Проверка возможности прыжка: sqrt(dx² + dy² + dz²) <= maxDistance
|
||||
public boolean canJump(@NotNull Vector from, @NotNull Vector to) {
|
||||
double dx = to.getX() - from.getX();
|
||||
double dy = to.getY() - from.getY();
|
||||
double dz = to.getZ() - from.getZ();
|
||||
|
||||
double horizontalDistance = Math.sqrt(dx * dx + dz * dz);
|
||||
if (horizontalDistance > maxHorizontal) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (dy > maxVerticalUp || dy < -maxVerticalDown) {
|
||||
return false;
|
||||
}
|
||||
|
||||
double totalDistance = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
return totalDistance <= maxDistance;
|
||||
}
|
||||
|
||||
public double calculateDistance(@NotNull Location from, @NotNull Location to) {
|
||||
return calculateDistance(from.toVector(), to.toVector());
|
||||
}
|
||||
|
||||
public double calculateDistance(@NotNull Vector from, @NotNull Vector to) {
|
||||
double dx = to.getX() - from.getX();
|
||||
double dy = to.getY() - from.getY();
|
||||
double dz = to.getZ() - from.getZ();
|
||||
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
}
|
||||
|
||||
public double calculateHorizontalDistance(@NotNull Vector from, @NotNull Vector to) {
|
||||
double dx = to.getX() - from.getX();
|
||||
double dz = to.getZ() - from.getZ();
|
||||
return Math.sqrt(dx * dx + dz * dz);
|
||||
}
|
||||
|
||||
public double getMaxDistance() {
|
||||
return maxDistance;
|
||||
}
|
||||
|
||||
public double getMaxHorizontal() {
|
||||
return maxHorizontal;
|
||||
}
|
||||
|
||||
public double getMaxVerticalUp() {
|
||||
return maxVerticalUp;
|
||||
}
|
||||
|
||||
public double getMaxVerticalDown() {
|
||||
return maxVerticalDown;
|
||||
}
|
||||
}
|
||||
|
|
@ -52,49 +52,19 @@ import java.util.*;
|
|||
*/
|
||||
public class ParkourGenerator {
|
||||
|
||||
/**
|
||||
* The amount of blocks that trail behind the player.
|
||||
*/
|
||||
public static final int BLOCK_TRAIL = 2;
|
||||
|
||||
/**
|
||||
* This generator's score
|
||||
*/
|
||||
public int score = 0;
|
||||
|
||||
/**
|
||||
* The total score achieved in this Generator instance
|
||||
*/
|
||||
public int totalScore = 0;
|
||||
|
||||
/**
|
||||
* The schematic cooldown
|
||||
*/
|
||||
public int schematicCooldown = Config.GENERATION.getInt("advanced.schematic-cooldown");
|
||||
|
||||
/**
|
||||
* Whether this generator has been stopped
|
||||
*/
|
||||
public boolean stopped = false;
|
||||
|
||||
/**
|
||||
* The zone in which the parkour can take place. (playable area)
|
||||
*/
|
||||
public Location[] zone;
|
||||
|
||||
/**
|
||||
* The player
|
||||
*/
|
||||
public ParkourPlayer player;
|
||||
|
||||
/**
|
||||
* The task used in checking the player's current location
|
||||
*/
|
||||
public BukkitTask task;
|
||||
|
||||
/**
|
||||
* Where blocks from schematics spawn
|
||||
*/
|
||||
private BukkitTask cleanupTask;
|
||||
|
||||
public Location blockSpawn;
|
||||
|
||||
/**
|
||||
|
|
@ -127,64 +97,19 @@ public class ParkourGenerator {
|
|||
*/
|
||||
public final Profile profile = new Profile();
|
||||
|
||||
/**
|
||||
* The island instance.
|
||||
*/
|
||||
public final Island island;
|
||||
|
||||
/**
|
||||
* The chances of which distance the jump should have
|
||||
*/
|
||||
public final Map<Integer, Double> distanceChances = new HashMap<>();
|
||||
|
||||
/**
|
||||
* The chances of which height the jump should have
|
||||
*/
|
||||
public final Map<Integer, Double> heightChances = new HashMap<>();
|
||||
|
||||
/**
|
||||
* The chances of which type of special jump
|
||||
*/
|
||||
public final Map<BlockData, Double> specialChances = new HashMap<>();
|
||||
public final Map<BlockGenerationType, Double> defaultChances = new HashMap<>();
|
||||
|
||||
/**
|
||||
* The chances of default jump types: schematic, 'special' (ice, etc.) or normal
|
||||
*/
|
||||
public final Map<JumpType, Double> defaultChances = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Whether the schematic should be deleted on the next jump.
|
||||
*/
|
||||
protected boolean deleteSchematic = false;
|
||||
protected boolean waitForSchematicCompletion = false;
|
||||
|
||||
/**
|
||||
* The last location the player was found standing in
|
||||
*/
|
||||
protected Location lastStandingPlayerLocation;
|
||||
|
||||
/**
|
||||
* A list of blocks from the (possibly) spawned structure
|
||||
*/
|
||||
protected List<Block> schematicBlocks = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* The player's current position index.
|
||||
*/
|
||||
protected int lastPositionIndexPlayer = -1;
|
||||
protected List<Block> history = new LinkedList<>();
|
||||
|
||||
/**
|
||||
* The history of generated blocks. The most recently generated block is the last item in the list.
|
||||
*/
|
||||
protected List<Block> history = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Creates a new ParkourGenerator instance
|
||||
*
|
||||
* @param session The session.
|
||||
* @param schematic The schematic to use for the spawn island.
|
||||
* @param generatorOptions The options.
|
||||
*/
|
||||
public ParkourGenerator(@NotNull Session session, @Nullable Schematic schematic, GeneratorOption... generatorOptions) {
|
||||
this.session = session;
|
||||
this.generatorOptions = Arrays.asList(generatorOptions);
|
||||
|
|
@ -196,30 +121,17 @@ public class ParkourGenerator {
|
|||
calculateChances();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ParkourGenerator instance.
|
||||
*
|
||||
* @param session The session.
|
||||
* @param generatorOptions The options.
|
||||
*/
|
||||
public ParkourGenerator(@NotNull Session session, GeneratorOption... generatorOptions) {
|
||||
this(session, Schematics.getSchematic(LoParkour.getPlugin(), "spawn-island"), generatorOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures generator preferences in profile can't be overridden by the player changing settings.
|
||||
*/
|
||||
public void overrideProfile() { }
|
||||
|
||||
/**
|
||||
* Calculates all chances for every variable.
|
||||
* Modification is possible in the generator constructor or through external map changes.
|
||||
*/
|
||||
protected void calculateChances() {
|
||||
defaultChances.clear();
|
||||
defaultChances.put(JumpType.DEFAULT, Option.TYPE_NORMAL);
|
||||
defaultChances.put(JumpType.SCHEMATIC, Option.TYPE_SCHEMATICS);
|
||||
defaultChances.put(JumpType.SPECIAL, Option.TYPE_SPECIAL);
|
||||
defaultChances.put(BlockGenerationType.DEFAULT, Option.TYPE_NORMAL);
|
||||
defaultChances.put(BlockGenerationType.SCHEMATIC, Option.TYPE_SCHEMATICS);
|
||||
defaultChances.put(BlockGenerationType.SPECIAL, Option.TYPE_SPECIAL);
|
||||
|
||||
heightChances.clear();
|
||||
heightChances.put(1, Option.NORMAL_HEIGHT_1);
|
||||
|
|
@ -388,6 +300,11 @@ public class ParkourGenerator {
|
|||
.repeat(1)
|
||||
.execute(this::tick)
|
||||
.run();
|
||||
|
||||
cleanupTask = Task.create(LoParkour.getPlugin())
|
||||
.repeat(Option.CLEANUP_INTERVAL)
|
||||
.execute(this::cleanupDistantBlocks)
|
||||
.run();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -396,6 +313,9 @@ public class ParkourGenerator {
|
|||
public void tick() {
|
||||
if (stopped) {
|
||||
task.cancel();
|
||||
if (cleanupTask != null) {
|
||||
cleanupTask.cancel();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -462,7 +382,6 @@ public class ParkourGenerator {
|
|||
lastPositionIndexPlayer = currentIndex;
|
||||
|
||||
for (int i = currentIndex - BLOCK_TRAIL - 1; i >= currentIndex - 4 * BLOCK_TRAIL; i--) {
|
||||
// avoid setting beginning block to air
|
||||
if (i <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -470,6 +389,8 @@ public class ParkourGenerator {
|
|||
history.get(i).setType(Material.AIR);
|
||||
}
|
||||
|
||||
cleanupDistantBlocks();
|
||||
|
||||
deleteSchematic();
|
||||
|
||||
for (int i = 0; i < (Config.CONFIG.getBoolean("scoring.all-points") ? deltaFromLast : 1); i++) { // score the difference
|
||||
|
|
@ -597,19 +518,19 @@ public class ParkourGenerator {
|
|||
return;
|
||||
}
|
||||
|
||||
Map<JumpType, Double> chances = new HashMap<>(defaultChances);
|
||||
Map<BlockGenerationType, Double> chances = new HashMap<>(defaultChances);
|
||||
if (schematicCooldown > 0 || generatorOptions.contains(GeneratorOption.DISABLE_SCHEMATICS) || profile.get("schematicDifficulty").asDouble() == 0.0 || !schematicBlocks.isEmpty()) {
|
||||
chances.remove(JumpType.SCHEMATIC);
|
||||
chances.remove(BlockGenerationType.SCHEMATIC);
|
||||
}
|
||||
if (!profile.get("useSpecialBlocks").asBoolean()) {
|
||||
chances.remove(JumpType.SPECIAL);
|
||||
chances.remove(BlockGenerationType.SPECIAL);
|
||||
}
|
||||
if (chances.isEmpty()) {
|
||||
chances.put(JumpType.DEFAULT, 1.0);
|
||||
chances.put(BlockGenerationType.DEFAULT, 1.0);
|
||||
}
|
||||
|
||||
JumpType jump = Probs.random(chances);
|
||||
if (jump == JumpType.SCHEMATIC) {
|
||||
BlockGenerationType jump = Probs.random(chances);
|
||||
if (jump == BlockGenerationType.SCHEMATIC) {
|
||||
LoParkour.log("Generating schematic jump at index %s".formatted(history.size() + 1));
|
||||
|
||||
double difficulty = profile.get("schematicDifficulty").asDouble();
|
||||
|
|
@ -647,7 +568,7 @@ public class ParkourGenerator {
|
|||
|
||||
List<Block> movedBlocks = new ArrayList<>();
|
||||
for (Block block : blocks) {
|
||||
BlockData data = (jump == JumpType.SPECIAL && !generatorOptions.contains(GeneratorOption.DISABLE_SPECIAL)) ? Probs.random(specialChances) : selectBlockData();
|
||||
BlockData data = (jump == BlockGenerationType.SPECIAL && !generatorOptions.contains(GeneratorOption.DISABLE_SPECIAL)) ? Probs.random(specialChances) : selectBlockData();
|
||||
|
||||
if (data instanceof Fence) {
|
||||
block = block.getLocation().subtract(0, 1, 0).getBlock();
|
||||
|
|
@ -731,6 +652,33 @@ public class ParkourGenerator {
|
|||
return history.get(history.size() - 1);
|
||||
}
|
||||
|
||||
// Очистка блоков на большом расстоянии от игрока для экономии памяти
|
||||
protected void cleanupDistantBlocks() {
|
||||
if (history.size() < Option.BLOCK_CLEANUP_DISTANCE * 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
Location playerLoc = player.getLocation();
|
||||
int removed = 0;
|
||||
|
||||
Iterator<Block> iterator = history.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Block block = iterator.next();
|
||||
|
||||
if (block.getLocation().distance(playerLoc) > Option.BLOCK_CLEANUP_DISTANCE) {
|
||||
block.setType(Material.AIR, false);
|
||||
iterator.remove();
|
||||
removed++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
lastPositionIndexPlayer = Math.max(0, lastPositionIndexPlayer - removed);
|
||||
}
|
||||
}
|
||||
|
||||
private double getDifficulty(String fileName) {
|
||||
String path = "difficulty.%s".formatted(fileName.split("[-.]")[1]);
|
||||
|
||||
|
|
@ -837,7 +785,7 @@ public class ParkourGenerator {
|
|||
return session.getSpectators();
|
||||
}
|
||||
|
||||
public enum JumpType {
|
||||
public enum BlockGenerationType {
|
||||
DEFAULT, SCHEMATIC, SPECIAL
|
||||
}
|
||||
}
|
||||
|
|
|
|||
71
src/main/java/dev/loki/loparkour/ghost/GhostData.java
Normal file
71
src/main/java/dev/loki/loparkour/ghost/GhostData.java
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
package dev.loki.loparkour.ghost;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class GhostData {
|
||||
|
||||
private final String playerName;
|
||||
private final int score;
|
||||
private final List<GhostFrame> frames;
|
||||
|
||||
public GhostData(@NotNull String playerName, int score, @NotNull List<GhostFrame> frames) {
|
||||
this.playerName = playerName;
|
||||
this.score = score;
|
||||
this.frames = new ArrayList<>(frames);
|
||||
}
|
||||
|
||||
public String getPlayerName() {
|
||||
return playerName;
|
||||
}
|
||||
|
||||
public int getScore() {
|
||||
return score;
|
||||
}
|
||||
|
||||
public List<GhostFrame> getFrames() {
|
||||
return frames;
|
||||
}
|
||||
|
||||
public void saveToFile(@NotNull File file) throws IOException {
|
||||
try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) {
|
||||
out.writeUTF(playerName);
|
||||
out.writeInt(score);
|
||||
out.writeInt(frames.size());
|
||||
|
||||
for (GhostFrame frame : frames) {
|
||||
out.writeLong(frame.getTimestamp());
|
||||
out.writeDouble(frame.getX());
|
||||
out.writeDouble(frame.getY());
|
||||
out.writeDouble(frame.getZ());
|
||||
out.writeFloat(frame.getYaw());
|
||||
out.writeFloat(frame.getPitch());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static GhostData loadFromFile(@NotNull File file) throws IOException {
|
||||
try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
|
||||
String playerName = in.readUTF();
|
||||
int score = in.readInt();
|
||||
int frameCount = in.readInt();
|
||||
|
||||
List<GhostFrame> frames = new ArrayList<>(frameCount);
|
||||
for (int i = 0; i < frameCount; i++) {
|
||||
long timestamp = in.readLong();
|
||||
double x = in.readDouble();
|
||||
double y = in.readDouble();
|
||||
double z = in.readDouble();
|
||||
float yaw = in.readFloat();
|
||||
float pitch = in.readFloat();
|
||||
|
||||
frames.add(new GhostFrame(timestamp, x, y, z, yaw, pitch));
|
||||
}
|
||||
|
||||
return new GhostData(playerName, score, frames);
|
||||
}
|
||||
}
|
||||
}
|
||||
60
src/main/java/dev/loki/loparkour/ghost/GhostFrame.java
Normal file
60
src/main/java/dev/loki/loparkour/ghost/GhostFrame.java
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
package dev.loki.loparkour.ghost;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class GhostFrame {
|
||||
|
||||
private final long timestamp;
|
||||
private final double x;
|
||||
private final double y;
|
||||
private final double z;
|
||||
private final float yaw;
|
||||
private final float pitch;
|
||||
|
||||
public GhostFrame(long timestamp, @NotNull Location location) {
|
||||
this.timestamp = timestamp;
|
||||
this.x = location.getX();
|
||||
this.y = location.getY();
|
||||
this.z = location.getZ();
|
||||
this.yaw = location.getYaw();
|
||||
this.pitch = location.getPitch();
|
||||
}
|
||||
|
||||
public GhostFrame(long timestamp, double x, double y, double z, float yaw, float pitch) {
|
||||
this.timestamp = timestamp;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.yaw = yaw;
|
||||
this.pitch = pitch;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public double getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public double getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
public double getZ() {
|
||||
return z;
|
||||
}
|
||||
|
||||
public float getYaw() {
|
||||
return yaw;
|
||||
}
|
||||
|
||||
public float getPitch() {
|
||||
return pitch;
|
||||
}
|
||||
|
||||
public Location toLocation(org.bukkit.World world) {
|
||||
return new Location(world, x, y, z, yaw, pitch);
|
||||
}
|
||||
}
|
||||
131
src/main/java/dev/loki/loparkour/ghost/GhostManager.java
Normal file
131
src/main/java/dev/loki/loparkour/ghost/GhostManager.java
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package dev.loki.loparkour.ghost;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
public class GhostManager {
|
||||
|
||||
private static final File GHOST_FOLDER = LoParkour.getInFolder("ghosts");
|
||||
private static final int MAX_GHOSTS_PER_MODE = 3;
|
||||
|
||||
private final Map<String, List<GhostData>> ghostsByMode = new HashMap<>();
|
||||
private final List<GhostPlayer> activeGhosts = new ArrayList<>();
|
||||
|
||||
public GhostManager() {
|
||||
if (!GHOST_FOLDER.exists()) {
|
||||
GHOST_FOLDER.mkdirs();
|
||||
}
|
||||
}
|
||||
|
||||
public void loadGhosts(@NotNull String mode) {
|
||||
File modeFolder = new File(GHOST_FOLDER, mode);
|
||||
if (!modeFolder.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<GhostData> ghosts = new ArrayList<>();
|
||||
File[] files = modeFolder.listFiles((dir, name) -> name.endsWith(".ghost"));
|
||||
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
try {
|
||||
GhostData data = GhostData.loadFromFile(file);
|
||||
ghosts.add(data);
|
||||
} catch (IOException e) {
|
||||
LoParkour.logging().error("Failed to load ghost: " + file.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ghosts.sort((a, b) -> Integer.compare(b.getScore(), a.getScore()));
|
||||
ghostsByMode.put(mode, ghosts);
|
||||
}
|
||||
|
||||
public void saveGhost(@NotNull String mode, @NotNull GhostData data) {
|
||||
File modeFolder = new File(GHOST_FOLDER, mode);
|
||||
if (!modeFolder.exists()) {
|
||||
modeFolder.mkdirs();
|
||||
}
|
||||
|
||||
List<GhostData> ghosts = ghostsByMode.getOrDefault(mode, new ArrayList<>());
|
||||
ghosts.add(data);
|
||||
ghosts.sort((a, b) -> Integer.compare(b.getScore(), a.getScore()));
|
||||
|
||||
while (ghosts.size() > MAX_GHOSTS_PER_MODE) {
|
||||
GhostData removed = ghosts.remove(ghosts.size() - 1);
|
||||
File file = new File(modeFolder, removed.getPlayerName() + ".ghost");
|
||||
file.delete();
|
||||
}
|
||||
|
||||
ghostsByMode.put(mode, ghosts);
|
||||
|
||||
File file = new File(modeFolder, data.getPlayerName() + ".ghost");
|
||||
try {
|
||||
data.saveToFile(file);
|
||||
} catch (IOException e) {
|
||||
LoParkour.logging().error("Failed to save ghost: " + file.getName());
|
||||
}
|
||||
}
|
||||
|
||||
public void spawnGhosts(@NotNull String mode, @NotNull Location startLocation, @NotNull World world) {
|
||||
if (!Config.CONFIG.getBoolean("ghost-mode.enabled")) {
|
||||
return;
|
||||
}
|
||||
|
||||
stopAllGhosts();
|
||||
|
||||
List<GhostData> ghosts = ghostsByMode.get(mode);
|
||||
if (ghosts == null || ghosts.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int showTop = Math.min(Config.CONFIG.getInt("ghost-mode.show-top"), ghosts.size());
|
||||
for (int i = 0; i < showTop; i++) {
|
||||
GhostPlayer ghost = new GhostPlayer(ghosts.get(i), world);
|
||||
ghost.spawn(startLocation);
|
||||
activeGhosts.add(ghost);
|
||||
}
|
||||
}
|
||||
|
||||
public void stopAllGhosts() {
|
||||
for (GhostPlayer ghost : activeGhosts) {
|
||||
ghost.stop();
|
||||
}
|
||||
activeGhosts.clear();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public GhostData getTopGhost(@NotNull String mode) {
|
||||
List<GhostData> ghosts = ghostsByMode.get(mode);
|
||||
if (ghosts == null || ghosts.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return ghosts.get(0);
|
||||
}
|
||||
|
||||
public List<GhostData> getTopGhosts(@NotNull String mode, int count) {
|
||||
List<GhostData> ghosts = ghostsByMode.get(mode);
|
||||
if (ghosts == null || ghosts.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return ghosts.subList(0, Math.min(count, ghosts.size()));
|
||||
}
|
||||
|
||||
public boolean shouldRecordGhost(@NotNull String mode, int score) {
|
||||
List<GhostData> ghosts = ghostsByMode.get(mode);
|
||||
if (ghosts == null || ghosts.size() < MAX_GHOSTS_PER_MODE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
GhostData lowestGhost = ghosts.get(ghosts.size() - 1);
|
||||
return score > lowestGhost.getScore();
|
||||
}
|
||||
}
|
||||
95
src/main/java/dev/loki/loparkour/ghost/GhostPlayer.java
Normal file
95
src/main/java/dev/loki/loparkour/ghost/GhostPlayer.java
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
package dev.loki.loparkour.ghost;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.efnilite.vilib.util.Task;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.ArmorStand;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class GhostPlayer {
|
||||
|
||||
private final GhostData data;
|
||||
private final World world;
|
||||
private ArmorStand entity;
|
||||
private BukkitTask task;
|
||||
private int currentFrame = 0;
|
||||
private long startTime;
|
||||
|
||||
public GhostPlayer(@NotNull GhostData data, @NotNull World world) {
|
||||
this.data = data;
|
||||
this.world = world;
|
||||
}
|
||||
|
||||
public void spawn(@NotNull Location startLocation) {
|
||||
if (data.getFrames().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
entity = (ArmorStand) world.spawnEntity(startLocation, EntityType.ARMOR_STAND);
|
||||
entity.setVisible(false);
|
||||
entity.setGravity(false);
|
||||
entity.setInvulnerable(true);
|
||||
entity.setCustomName("§7" + data.getPlayerName() + " §8(Ghost)");
|
||||
entity.setCustomNameVisible(true);
|
||||
|
||||
startTime = System.currentTimeMillis();
|
||||
currentFrame = 0;
|
||||
|
||||
task = Task.create(LoParkour.getPlugin())
|
||||
.repeat(1)
|
||||
.execute(this::update)
|
||||
.run();
|
||||
}
|
||||
|
||||
private void update() {
|
||||
if (entity == null || !entity.isValid()) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
|
||||
long elapsed = System.currentTimeMillis() - startTime;
|
||||
|
||||
while (currentFrame < data.getFrames().size()) {
|
||||
GhostFrame frame = data.getFrames().get(currentFrame);
|
||||
|
||||
if (frame.getTimestamp() > elapsed) {
|
||||
break;
|
||||
}
|
||||
|
||||
Location loc = frame.toLocation(world);
|
||||
entity.teleport(loc);
|
||||
currentFrame++;
|
||||
}
|
||||
|
||||
if (currentFrame >= data.getFrames().size()) {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (task != null) {
|
||||
task.cancel();
|
||||
task = null;
|
||||
}
|
||||
|
||||
if (entity != null) {
|
||||
entity.remove();
|
||||
entity = null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return entity != null && entity.isValid();
|
||||
}
|
||||
|
||||
public String getPlayerName() {
|
||||
return data.getPlayerName();
|
||||
}
|
||||
|
||||
public int getScore() {
|
||||
return data.getScore();
|
||||
}
|
||||
}
|
||||
53
src/main/java/dev/loki/loparkour/ghost/GhostRecorder.java
Normal file
53
src/main/java/dev/loki/loparkour/ghost/GhostRecorder.java
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package dev.loki.loparkour.ghost;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class GhostRecorder {
|
||||
|
||||
private static final double MIN_MOVEMENT = 0.1;
|
||||
|
||||
private final List<GhostFrame> frames = new ArrayList<>();
|
||||
private Location lastLocation;
|
||||
private long startTime;
|
||||
private boolean recording = false;
|
||||
|
||||
public void startRecording(@NotNull Location startLocation) {
|
||||
frames.clear();
|
||||
lastLocation = startLocation.clone();
|
||||
startTime = System.currentTimeMillis();
|
||||
recording = true;
|
||||
|
||||
frames.add(new GhostFrame(0, startLocation.clone()));
|
||||
}
|
||||
|
||||
public void recordFrame(@NotNull Location currentLocation) {
|
||||
if (!recording) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastLocation.distance(currentLocation) < MIN_MOVEMENT) {
|
||||
return;
|
||||
}
|
||||
|
||||
long timestamp = System.currentTimeMillis() - startTime;
|
||||
frames.add(new GhostFrame(timestamp, currentLocation.clone()));
|
||||
lastLocation = currentLocation.clone();
|
||||
}
|
||||
|
||||
public GhostData stopRecording(String playerName, int score) {
|
||||
recording = false;
|
||||
return new GhostData(playerName, score, frames);
|
||||
}
|
||||
|
||||
public boolean isRecording() {
|
||||
return recording;
|
||||
}
|
||||
|
||||
public int getFrameCount() {
|
||||
return frames.size();
|
||||
}
|
||||
}
|
||||
172
src/main/java/dev/loki/loparkour/mode/GravityShiftMode.java
Normal file
172
src/main/java/dev/loki/loparkour/mode/GravityShiftMode.java
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
package dev.loki.loparkour.mode;
|
||||
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.session.Session;
|
||||
import dev.efnilite.vilib.inventory.item.Item;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.bukkit.potion.PotionEffectType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
public class GravityShiftMode implements Mode {
|
||||
|
||||
private final Leaderboard leaderboard = new Leaderboard(getName(), Leaderboard.Sort.SCORE);
|
||||
private final Random random = new Random();
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public String getName() {
|
||||
return "gravity-shift";
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Item getItem(String locale) {
|
||||
return Locales.getItem(locale, "play.single.gravity-shift");
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Leaderboard getLeaderboard() {
|
||||
return leaderboard;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void create(Player player) {
|
||||
if (!Config.CONFIG.getBoolean("joining")) {
|
||||
player.sendMessage("Joining is currently disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
ParkourPlayer pp = ParkourPlayer.getPlayer(player);
|
||||
if (pp != null && pp.session.generator != null && pp.session.generator.getMode() instanceof GravityShiftMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
player.closeInventory();
|
||||
|
||||
Session.create(session -> new GravityShiftGenerator(session), null, null, player);
|
||||
}
|
||||
|
||||
private static class GravityShiftGenerator extends ParkourGenerator {
|
||||
private int jumpsUntilShift;
|
||||
private final int shiftInterval;
|
||||
private PotionEffect currentEffect;
|
||||
|
||||
public GravityShiftGenerator(@NotNull Session session) {
|
||||
super(session);
|
||||
this.shiftInterval = Config.CONFIG.getInt("modes.gravity-shift.interval");
|
||||
this.jumpsUntilShift = shiftInterval;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void score() {
|
||||
super.score();
|
||||
|
||||
jumpsUntilShift--;
|
||||
|
||||
if (jumpsUntilShift <= 0) {
|
||||
applyRandomEffect();
|
||||
jumpsUntilShift = shiftInterval;
|
||||
}
|
||||
}
|
||||
|
||||
private void applyRandomEffect() {
|
||||
if (currentEffect != null) {
|
||||
player.player.removePotionEffect(currentEffect.getType());
|
||||
}
|
||||
|
||||
List<EffectData> effects = new ArrayList<>();
|
||||
|
||||
if (Config.CONFIG.getBoolean("modes.gravity-shift.effects.jump-boost.enabled")) {
|
||||
effects.add(new EffectData(
|
||||
PotionEffectType.JUMP,
|
||||
Config.CONFIG.getInt("modes.gravity-shift.effects.jump-boost.amplifier"),
|
||||
Config.CONFIG.getInt("modes.gravity-shift.effects.jump-boost.duration"),
|
||||
"Jump Boost"
|
||||
));
|
||||
}
|
||||
|
||||
if (Config.CONFIG.getBoolean("modes.gravity-shift.effects.speed.enabled")) {
|
||||
effects.add(new EffectData(
|
||||
PotionEffectType.SPEED,
|
||||
Config.CONFIG.getInt("modes.gravity-shift.effects.speed.amplifier"),
|
||||
Config.CONFIG.getInt("modes.gravity-shift.effects.speed.duration"),
|
||||
"Speed"
|
||||
));
|
||||
}
|
||||
|
||||
if (Config.CONFIG.getBoolean("modes.gravity-shift.effects.slowness.enabled")) {
|
||||
effects.add(new EffectData(
|
||||
PotionEffectType.SLOW,
|
||||
Config.CONFIG.getInt("modes.gravity-shift.effects.slowness.amplifier"),
|
||||
Config.CONFIG.getInt("modes.gravity-shift.effects.slowness.duration"),
|
||||
"Slowness"
|
||||
));
|
||||
}
|
||||
|
||||
if (Config.CONFIG.getBoolean("modes.gravity-shift.effects.levitation.enabled")) {
|
||||
effects.add(new EffectData(
|
||||
PotionEffectType.LEVITATION,
|
||||
Config.CONFIG.getInt("modes.gravity-shift.effects.levitation.amplifier"),
|
||||
Config.CONFIG.getInt("modes.gravity-shift.effects.levitation.duration"),
|
||||
"Levitation"
|
||||
));
|
||||
}
|
||||
|
||||
if (effects.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
EffectData selected = effects.get(new Random().nextInt(effects.size()));
|
||||
currentEffect = new PotionEffect(
|
||||
selected.type,
|
||||
selected.duration * 20,
|
||||
selected.amplifier - 1,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
player.player.addPotionEffect(currentEffect);
|
||||
player.sendTranslated("modes.gravity-shift.effect-applied", selected.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset(boolean regenerate) {
|
||||
if (currentEffect != null && player != null && player.player != null) {
|
||||
player.player.removePotionEffect(currentEffect.getType());
|
||||
}
|
||||
jumpsUntilShift = shiftInterval;
|
||||
super.reset(regenerate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mode getMode() {
|
||||
return Modes.GRAVITY_SHIFT;
|
||||
}
|
||||
}
|
||||
|
||||
private static class EffectData {
|
||||
final PotionEffectType type;
|
||||
final int amplifier;
|
||||
final int duration;
|
||||
final String name;
|
||||
|
||||
EffectData(PotionEffectType type, int amplifier, int duration, String name) {
|
||||
this.type = type;
|
||||
this.amplifier = amplifier;
|
||||
this.duration = duration;
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
77
src/main/java/dev/loki/loparkour/mode/HardcoreMode.java
Normal file
77
src/main/java/dev/loki/loparkour/mode/HardcoreMode.java
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
package dev.loki.loparkour.mode;
|
||||
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.session.Session;
|
||||
import dev.efnilite.vilib.inventory.item.Item;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class HardcoreMode implements Mode {
|
||||
|
||||
private final Leaderboard leaderboard = new Leaderboard(getName(), Leaderboard.Sort.SCORE);
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public String getName() {
|
||||
return "hardcore";
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Item getItem(String locale) {
|
||||
return Locales.getItem(locale, "play.single.hardcore");
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Leaderboard getLeaderboard() {
|
||||
return leaderboard;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void create(Player player) {
|
||||
if (!Config.CONFIG.getBoolean("joining")) {
|
||||
player.sendMessage("Joining is currently disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
ParkourPlayer pp = ParkourPlayer.getPlayer(player);
|
||||
if (pp != null && pp.session.generator != null && pp.session.generator.getMode() instanceof HardcoreMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
player.closeInventory();
|
||||
|
||||
Session.create(session -> new HardcoreGenerator(session), null, null, player);
|
||||
}
|
||||
|
||||
private static class HardcoreGenerator extends ParkourGenerator {
|
||||
|
||||
public HardcoreGenerator(@NotNull Session session) {
|
||||
super(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void fall() {
|
||||
boolean resetRewards = Config.CONFIG.getBoolean("modes.hardcore.reset-rewards");
|
||||
|
||||
if (resetRewards) {
|
||||
player.sendTranslated("modes.hardcore.rewards-lost");
|
||||
}
|
||||
|
||||
player.sendTranslated("modes.hardcore.full-reset");
|
||||
|
||||
super.fall();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mode getMode() {
|
||||
return Modes.HARDCORE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,9 +6,15 @@ public class Modes {
|
|||
|
||||
public static DefaultMode DEFAULT;
|
||||
public static SpectatorMode SPECTATOR;
|
||||
public static SpeedrunMode SPEEDRUN;
|
||||
public static GravityShiftMode GRAVITY_SHIFT;
|
||||
public static HardcoreMode HARDCORE;
|
||||
|
||||
public static void init() {
|
||||
DEFAULT = (DefaultMode) Registry.getMode("default");
|
||||
SPECTATOR = (SpectatorMode) Registry.getMode("spectator");
|
||||
SPEEDRUN = (SpeedrunMode) Registry.getMode("speedrun");
|
||||
GRAVITY_SHIFT = (GravityShiftMode) Registry.getMode("gravity-shift");
|
||||
HARDCORE = (HardcoreMode) Registry.getMode("hardcore");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
112
src/main/java/dev/loki/loparkour/mode/SpeedrunMode.java
Normal file
112
src/main/java/dev/loki/loparkour/mode/SpeedrunMode.java
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
package dev.loki.loparkour.mode;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.session.Session;
|
||||
import dev.efnilite.vilib.inventory.item.Item;
|
||||
import dev.efnilite.vilib.util.Task;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class SpeedrunMode implements Mode {
|
||||
|
||||
private final Leaderboard leaderboard = new Leaderboard(getName(), Leaderboard.Sort.SCORE);
|
||||
private final Map<Block, Long> blockTimestamps = new HashMap<>();
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public String getName() {
|
||||
return "speedrun";
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Item getItem(String locale) {
|
||||
return Locales.getItem(locale, "play.single.speedrun");
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Leaderboard getLeaderboard() {
|
||||
return leaderboard;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void create(Player player) {
|
||||
if (!Config.CONFIG.getBoolean("joining")) {
|
||||
player.sendMessage("Joining is currently disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
ParkourPlayer pp = ParkourPlayer.getPlayer(player);
|
||||
if (pp != null && pp.session.generator != null && pp.session.generator.getMode() instanceof SpeedrunMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
player.closeInventory();
|
||||
|
||||
Session.create(session -> new SpeedrunGenerator(session), null, null, player);
|
||||
}
|
||||
|
||||
public void onBlockTouch(@NotNull Block block, @NotNull ParkourPlayer player) {
|
||||
if (!blockTimestamps.containsKey(block)) {
|
||||
blockTimestamps.put(block, System.currentTimeMillis());
|
||||
scheduleBlockRemoval(player, block);
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleBlockRemoval(@NotNull ParkourPlayer player, @NotNull Block block) {
|
||||
double blockLifetime = Config.CONFIG.getDouble("modes.speedrun.block-lifetime");
|
||||
double warningTime = Config.CONFIG.getDouble("modes.speedrun.warning-time");
|
||||
|
||||
long removalTime = (long) (blockLifetime * 1000);
|
||||
long warningTimeMs = (long) (warningTime * 1000);
|
||||
|
||||
Task.create(LoParkour.getPlugin())
|
||||
.delay((int) ((removalTime - warningTimeMs) / 50))
|
||||
.execute(() -> {
|
||||
if (block.getType() != Material.AIR) {
|
||||
player.player.playSound(block.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 1.0f, 2.0f);
|
||||
player.player.spawnParticle(Particle.FLAME, block.getLocation().add(0.5, 1, 0.5), 10, 0.3, 0.3, 0.3, 0.01);
|
||||
}
|
||||
})
|
||||
.run();
|
||||
|
||||
Task.create(LoParkour.getPlugin())
|
||||
.delay((int) (removalTime / 50))
|
||||
.execute(() -> {
|
||||
if (block.getType() != Material.AIR) {
|
||||
block.setType(Material.AIR);
|
||||
blockTimestamps.remove(block);
|
||||
}
|
||||
})
|
||||
.run();
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
blockTimestamps.clear();
|
||||
}
|
||||
|
||||
private static class SpeedrunGenerator extends ParkourGenerator {
|
||||
public SpeedrunGenerator(@NotNull Session session) {
|
||||
super(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mode getMode() {
|
||||
return Modes.SPEEDRUN;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
package dev.loki.loparkour.schematic.lpschem;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
public class LPSchematic {
|
||||
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
private static final int FORMAT_VERSION = 2;
|
||||
|
||||
private final SchematicMetadata metadata;
|
||||
private final SchematicDimensions dimensions;
|
||||
private final List<String> palette;
|
||||
private final int[] blocks;
|
||||
private final SchematicMarkers markers;
|
||||
private SchematicVisuals visuals;
|
||||
private SchematicLogic logic;
|
||||
|
||||
public LPSchematic(@NotNull SchematicMetadata metadata,
|
||||
@NotNull SchematicDimensions dimensions,
|
||||
@NotNull List<String> palette,
|
||||
@NotNull int[] blocks,
|
||||
@NotNull SchematicMarkers markers) {
|
||||
this.metadata = metadata;
|
||||
this.dimensions = dimensions;
|
||||
this.palette = palette;
|
||||
this.blocks = blocks;
|
||||
this.markers = markers;
|
||||
}
|
||||
|
||||
public void save(@NotNull File file) throws IOException {
|
||||
SchematicData data = new SchematicData();
|
||||
data.format_version = FORMAT_VERSION;
|
||||
data.metadata = metadata;
|
||||
data.dimensions = dimensions;
|
||||
data.palette = palette;
|
||||
data.blocks = blocks;
|
||||
data.markers = markers;
|
||||
data.visuals = visuals;
|
||||
data.logic = logic;
|
||||
|
||||
String json = GSON.toJson(data);
|
||||
|
||||
try (GZIPOutputStream gzip = new GZIPOutputStream(new FileOutputStream(file))) {
|
||||
gzip.write(json.getBytes("UTF-8"));
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static LPSchematic load(@NotNull File file) throws IOException {
|
||||
StringBuilder json = new StringBuilder();
|
||||
|
||||
try (GZIPInputStream gzip = new GZIPInputStream(new FileInputStream(file));
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(gzip, "UTF-8"))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
json.append(line);
|
||||
}
|
||||
}
|
||||
|
||||
SchematicData data = GSON.fromJson(json.toString(), SchematicData.class);
|
||||
|
||||
if (data.format_version != FORMAT_VERSION) {
|
||||
throw new IOException("Unsupported format version: " + data.format_version);
|
||||
}
|
||||
|
||||
LPSchematic schematic = new LPSchematic(
|
||||
data.metadata,
|
||||
data.dimensions,
|
||||
data.palette,
|
||||
data.blocks,
|
||||
data.markers
|
||||
);
|
||||
|
||||
schematic.visuals = data.visuals;
|
||||
schematic.logic = data.logic;
|
||||
|
||||
return schematic;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public List<Block> paste(@NotNull Location origin, @NotNull World world) {
|
||||
List<Block> placedBlocks = new ArrayList<>();
|
||||
|
||||
int width = dimensions.width;
|
||||
int height = dimensions.height;
|
||||
int length = dimensions.length;
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int z = 0; z < length; z++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
int index = x + (z * width) + (y * width * length);
|
||||
int paletteIndex = blocks[index];
|
||||
|
||||
if (paletteIndex == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String blockDataString = palette.get(paletteIndex);
|
||||
BlockData blockData = parseBlockData(blockDataString);
|
||||
|
||||
if (blockData == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Block block = world.getBlockAt(
|
||||
origin.getBlockX() + x,
|
||||
origin.getBlockY() + y,
|
||||
origin.getBlockZ() + z
|
||||
);
|
||||
|
||||
block.setBlockData(blockData);
|
||||
placedBlocks.add(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return placedBlocks;
|
||||
}
|
||||
|
||||
private BlockData parseBlockData(@NotNull String blockDataString) {
|
||||
try {
|
||||
return org.bukkit.Bukkit.createBlockData(blockDataString);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Material.MAGENTA_GLAZED_TERRACOTTA.createBlockData();
|
||||
}
|
||||
}
|
||||
|
||||
public SchematicMetadata getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
public SchematicDimensions getDimensions() {
|
||||
return dimensions;
|
||||
}
|
||||
|
||||
public SchematicMarkers getMarkers() {
|
||||
return markers;
|
||||
}
|
||||
|
||||
public SchematicVisuals getVisuals() {
|
||||
return visuals;
|
||||
}
|
||||
|
||||
public void setVisuals(SchematicVisuals visuals) {
|
||||
this.visuals = visuals;
|
||||
}
|
||||
|
||||
public SchematicLogic getLogic() {
|
||||
return logic;
|
||||
}
|
||||
|
||||
public void setLogic(SchematicLogic logic) {
|
||||
this.logic = logic;
|
||||
}
|
||||
|
||||
private static class SchematicData {
|
||||
int format_version;
|
||||
SchematicMetadata metadata;
|
||||
SchematicDimensions dimensions;
|
||||
List<String> palette;
|
||||
int[] blocks;
|
||||
SchematicMarkers markers;
|
||||
SchematicVisuals visuals;
|
||||
SchematicLogic logic;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
package dev.loki.loparkour.schematic.lpschem;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class LPSchematicBuilder {
|
||||
|
||||
private final SchematicMetadata metadata;
|
||||
private final SchematicDimensions dimensions;
|
||||
private final Map<String, Integer> paletteMap;
|
||||
private final List<String> palette;
|
||||
private final int[] blocks;
|
||||
private SchematicMarkers.Vector3i start;
|
||||
private SchematicMarkers.Vector3i end;
|
||||
private final List<SchematicMarkers.Vector3i> checkpoints;
|
||||
|
||||
public LPSchematicBuilder(@NotNull String name, @NotNull String author, double difficulty,
|
||||
int width, int height, int length) {
|
||||
this.metadata = new SchematicMetadata(name, author, difficulty);
|
||||
this.dimensions = new SchematicDimensions(width, height, length);
|
||||
this.paletteMap = new HashMap<>();
|
||||
this.palette = new ArrayList<>();
|
||||
this.blocks = new int[width * height * length];
|
||||
this.checkpoints = new ArrayList<>();
|
||||
|
||||
palette.add("minecraft:air");
|
||||
paletteMap.put("minecraft:air", 0);
|
||||
}
|
||||
|
||||
public LPSchematicBuilder setBlock(int x, int y, int z, @NotNull BlockData blockData) {
|
||||
String blockString = blockData.getAsString();
|
||||
int paletteIndex = getOrAddToPalette(blockString);
|
||||
int index = x + (z * dimensions.width) + (y * dimensions.width * dimensions.length);
|
||||
blocks[index] = paletteIndex;
|
||||
return this;
|
||||
}
|
||||
|
||||
public LPSchematicBuilder setBlock(int x, int y, int z, @NotNull Material material) {
|
||||
return setBlock(x, y, z, material.createBlockData());
|
||||
}
|
||||
|
||||
public LPSchematicBuilder setStart(int x, int y, int z) {
|
||||
this.start = new SchematicMarkers.Vector3i(x, y, z);
|
||||
return this;
|
||||
}
|
||||
|
||||
public LPSchematicBuilder setEnd(int x, int y, int z) {
|
||||
this.end = new SchematicMarkers.Vector3i(x, y, z);
|
||||
return this;
|
||||
}
|
||||
|
||||
public LPSchematicBuilder addCheckpoint(int x, int y, int z) {
|
||||
checkpoints.add(new SchematicMarkers.Vector3i(x, y, z));
|
||||
return this;
|
||||
}
|
||||
|
||||
public LPSchematicBuilder addTag(@NotNull String tag) {
|
||||
metadata.addTag(tag);
|
||||
return this;
|
||||
}
|
||||
|
||||
public LPSchematicBuilder fromWorld(@NotNull World world, @NotNull Location corner1, @NotNull Location corner2) {
|
||||
int minX = Math.min(corner1.getBlockX(), corner2.getBlockX());
|
||||
int minY = Math.min(corner1.getBlockY(), corner2.getBlockY());
|
||||
int minZ = Math.min(corner1.getBlockZ(), corner2.getBlockZ());
|
||||
|
||||
int maxX = Math.max(corner1.getBlockX(), corner2.getBlockX());
|
||||
int maxY = Math.max(corner1.getBlockY(), corner2.getBlockY());
|
||||
int maxZ = Math.max(corner1.getBlockZ(), corner2.getBlockZ());
|
||||
|
||||
for (int y = minY; y <= maxY; y++) {
|
||||
for (int z = minZ; z <= maxZ; z++) {
|
||||
for (int x = minX; x <= maxX; x++) {
|
||||
Block block = world.getBlockAt(x, y, z);
|
||||
if (block.getType() != Material.AIR) {
|
||||
int relX = x - minX;
|
||||
int relY = y - minY;
|
||||
int relZ = z - minZ;
|
||||
setBlock(relX, relY, relZ, block.getBlockData());
|
||||
|
||||
if (block.getType() == Material.LIME_WOOL) {
|
||||
setStart(relX, relY, relZ);
|
||||
} else if (block.getType() == Material.RED_WOOL) {
|
||||
setEnd(relX, relY, relZ);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public LPSchematic build() {
|
||||
if (start == null || end == null) {
|
||||
throw new IllegalStateException("Start and end markers must be set");
|
||||
}
|
||||
|
||||
SchematicMarkers markers = new SchematicMarkers(start, end);
|
||||
checkpoints.forEach(markers::addCheckpoint);
|
||||
|
||||
return new LPSchematic(metadata, dimensions, palette, blocks, markers);
|
||||
}
|
||||
|
||||
private int getOrAddToPalette(@NotNull String blockString) {
|
||||
if (paletteMap.containsKey(blockString)) {
|
||||
return paletteMap.get(blockString);
|
||||
}
|
||||
|
||||
int index = palette.size();
|
||||
palette.add(blockString);
|
||||
paletteMap.put(blockString, index);
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package dev.loki.loparkour.schematic.lpschem;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class LPSchematicManager {
|
||||
|
||||
private static final File SCHEMATIC_FOLDER = LoParkour.getInFolder("schematics-new");
|
||||
private final Map<String, LPSchematic> loadedSchematics = new HashMap<>();
|
||||
|
||||
public LPSchematicManager() {
|
||||
if (!SCHEMATIC_FOLDER.exists()) {
|
||||
SCHEMATIC_FOLDER.mkdirs();
|
||||
}
|
||||
}
|
||||
|
||||
public void loadAll() {
|
||||
File[] files = SCHEMATIC_FOLDER.listFiles((dir, name) -> name.endsWith(".lpschem"));
|
||||
|
||||
if (files == null || files.length == 0) {
|
||||
LoParkour.log("No .lpschem files found");
|
||||
return;
|
||||
}
|
||||
|
||||
for (File file : files) {
|
||||
try {
|
||||
LPSchematic schematic = LPSchematic.load(file);
|
||||
String name = schematic.getMetadata().getName();
|
||||
loadedSchematics.put(name, schematic);
|
||||
LoParkour.log("Loaded schematic: " + name);
|
||||
} catch (IOException e) {
|
||||
LoParkour.logging().error("Failed to load schematic: " + file.getName());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
LoParkour.log("Loaded " + loadedSchematics.size() + " schematics");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public LPSchematic getSchematic(@NotNull String name) {
|
||||
return loadedSchematics.get(name);
|
||||
}
|
||||
|
||||
public void saveSchematic(@NotNull LPSchematic schematic) throws IOException {
|
||||
String fileName = schematic.getMetadata().getName().replaceAll("[^a-zA-Z0-9-_]", "_") + ".lpschem";
|
||||
File file = new File(SCHEMATIC_FOLDER, fileName);
|
||||
schematic.save(file);
|
||||
loadedSchematics.put(schematic.getMetadata().getName(), schematic);
|
||||
LoParkour.log("Saved schematic: " + schematic.getMetadata().getName());
|
||||
}
|
||||
|
||||
public Map<String, LPSchematic> getAllSchematics() {
|
||||
return new HashMap<>(loadedSchematics);
|
||||
}
|
||||
|
||||
public void reload() {
|
||||
loadedSchematics.clear();
|
||||
loadAll();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package dev.loki.loparkour.schematic.lpschem;
|
||||
|
||||
import dev.efnilite.vilib.schematic.Schematic;
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class SchematicConverter {
|
||||
|
||||
public static LPSchematic fromVilib(@NotNull Schematic oldSchematic, @NotNull String name, @NotNull String author) {
|
||||
var dimensions = oldSchematic.getDimensions();
|
||||
int width = (int) dimensions.getX();
|
||||
int height = (int) dimensions.getY();
|
||||
int length = (int) dimensions.getZ();
|
||||
|
||||
LPSchematicBuilder builder = new LPSchematicBuilder(name, author, 0.5, width, height, length);
|
||||
|
||||
var blocks = oldSchematic.getVectorBlockMap();
|
||||
for (var entry : blocks.entrySet()) {
|
||||
var pos = entry.getKey();
|
||||
BlockData data = entry.getValue();
|
||||
|
||||
if (data != null && data.getMaterial() != Material.AIR) {
|
||||
int x = (int) pos.getX();
|
||||
int y = (int) pos.getY();
|
||||
int z = (int) pos.getZ();
|
||||
|
||||
builder.setBlock(x, y, z, data);
|
||||
|
||||
if (data.getMaterial() == Material.LIME_WOOL) {
|
||||
builder.setStart(x, y, z);
|
||||
} else if (data.getMaterial() == Material.RED_WOOL) {
|
||||
builder.setEnd(x, y, z);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasMarkers(builder)) {
|
||||
builder.setStart(0, 0, 0);
|
||||
builder.setEnd(width - 1, 0, length - 1);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private static boolean hasMarkers(LPSchematicBuilder builder) {
|
||||
try {
|
||||
builder.build();
|
||||
return true;
|
||||
} catch (IllegalStateException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void convertAll() {
|
||||
LoParkour.log("Starting conversion of old schematics to .lpschem format");
|
||||
int converted = 0;
|
||||
int failed = 0;
|
||||
|
||||
try {
|
||||
for (String name : dev.efnilite.vilib.schematic.Schematics.getSchematicNames(LoParkour.getPlugin())) {
|
||||
try {
|
||||
Schematic oldSchematic = dev.efnilite.vilib.schematic.Schematics.getSchematic(LoParkour.getPlugin(), name);
|
||||
if (oldSchematic != null) {
|
||||
LPSchematic newSchematic = fromVilib(oldSchematic, name, "Converted");
|
||||
LoParkour.getSchematicManager().saveSchematic(newSchematic);
|
||||
converted++;
|
||||
LoParkour.log("Converted: " + name);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
failed++;
|
||||
LoParkour.logging().error("Failed to convert schematic: " + name);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LoParkour.logging().error("Error during schematic conversion");
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
LoParkour.log("Conversion complete: " + converted + " converted, " + failed + " failed");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package dev.loki.loparkour.schematic.lpschem;
|
||||
|
||||
public class SchematicDimensions {
|
||||
|
||||
public int width;
|
||||
public int height;
|
||||
public int length;
|
||||
|
||||
public SchematicDimensions(int width, int height, int length) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public int getTotalBlocks() {
|
||||
return width * height * length;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package dev.loki.loparkour.schematic.lpschem;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class SchematicLogic {
|
||||
|
||||
private List<int[]> ghost_path;
|
||||
|
||||
public SchematicLogic() {
|
||||
this.ghost_path = new ArrayList<>();
|
||||
}
|
||||
|
||||
public List<int[]> getGhostPath() {
|
||||
return ghost_path;
|
||||
}
|
||||
|
||||
public void setGhostPath(List<int[]> ghost_path) {
|
||||
this.ghost_path = ghost_path;
|
||||
}
|
||||
|
||||
public void addGhostPoint(int x, int y, int z) {
|
||||
ghost_path.add(new int[]{x, y, z});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package dev.loki.loparkour.schematic.lpschem;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class SchematicMarkers {
|
||||
|
||||
private Vector3i start;
|
||||
private Vector3i end;
|
||||
private List<Vector3i> checkpoints;
|
||||
|
||||
public SchematicMarkers(@NotNull Vector3i start, @NotNull Vector3i end) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.checkpoints = new ArrayList<>();
|
||||
}
|
||||
|
||||
public Vector3i getStart() {
|
||||
return start;
|
||||
}
|
||||
|
||||
public void setStart(Vector3i start) {
|
||||
this.start = start;
|
||||
}
|
||||
|
||||
public Vector3i getEnd() {
|
||||
return end;
|
||||
}
|
||||
|
||||
public void setEnd(Vector3i end) {
|
||||
this.end = end;
|
||||
}
|
||||
|
||||
public List<Vector3i> getCheckpoints() {
|
||||
return checkpoints;
|
||||
}
|
||||
|
||||
public void addCheckpoint(Vector3i checkpoint) {
|
||||
checkpoints.add(checkpoint);
|
||||
}
|
||||
|
||||
public static class Vector3i {
|
||||
public int x;
|
||||
public int y;
|
||||
public int z;
|
||||
|
||||
public Vector3i(int x, int y, int z) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package dev.loki.loparkour.schematic.lpschem;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class SchematicMetadata {
|
||||
private String name;
|
||||
private String author;
|
||||
private double difficulty;
|
||||
private List<String> tags;
|
||||
|
||||
public SchematicMetadata(@NotNull String name, @NotNull String author, double difficulty) {
|
||||
this.name = name;
|
||||
this.author = author;
|
||||
this.difficulty = difficulty;
|
||||
this.tags = new ArrayList<>();
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public double getDifficulty() {
|
||||
return difficulty;
|
||||
}
|
||||
|
||||
public List<String> getTags() {
|
||||
return tags;
|
||||
}
|
||||
|
||||
public void addTag(@NotNull String tag) {
|
||||
tags.add(tag);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package dev.loki.loparkour.schematic.lpschem;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class SchematicVisuals {
|
||||
|
||||
private List<BlockDisplay> displays;
|
||||
private List<ParticleEffect> particles;
|
||||
|
||||
public SchematicVisuals() {
|
||||
this.displays = new ArrayList<>();
|
||||
this.particles = new ArrayList<>();
|
||||
}
|
||||
|
||||
public List<BlockDisplay> getDisplays() {
|
||||
return displays;
|
||||
}
|
||||
|
||||
public void addDisplay(BlockDisplay display) {
|
||||
displays.add(display);
|
||||
}
|
||||
|
||||
public List<ParticleEffect> getParticles() {
|
||||
return particles;
|
||||
}
|
||||
|
||||
public void addParticle(ParticleEffect particle) {
|
||||
particles.add(particle);
|
||||
}
|
||||
|
||||
public static class BlockDisplay {
|
||||
public String type;
|
||||
public String block;
|
||||
public float[] pos;
|
||||
public float[] scale;
|
||||
public Animation animation;
|
||||
|
||||
public BlockDisplay(String block, float[] pos, float[] scale) {
|
||||
this.type = "block";
|
||||
this.block = block;
|
||||
this.pos = pos;
|
||||
this.scale = scale;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Animation {
|
||||
public String type;
|
||||
public String axis;
|
||||
public float speed;
|
||||
|
||||
public Animation(String type, String axis, float speed) {
|
||||
this.type = type;
|
||||
this.axis = axis;
|
||||
this.speed = speed;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ParticleEffect {
|
||||
public String type;
|
||||
public float[] pos;
|
||||
public int amount;
|
||||
public float spread;
|
||||
|
||||
public ParticleEffect(String type, float[] pos, int amount, float spread) {
|
||||
this.type = type;
|
||||
this.pos = pos;
|
||||
this.amount = amount;
|
||||
this.spread = spread;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -227,6 +227,124 @@ styles:
|
|||
rainbow: [red_wool, blue_wool, orange_wool, magenta_wool, lime_wool, purple_wool, yellow_concrete, orange_concrete, lime_concrete, light_blue_concrete, yellow_stained_glass, white_stained_glass, magenta_stained_glass, light_blue_stained_glass]
|
||||
glow: [glowstone, jack_o_lantern, beacon, sea_lantern, crying_obsidian, shroomlight]
|
||||
|
||||
# -= Jump validation options =-
|
||||
jump-validation:
|
||||
|
||||
# -= Jump validation enabled =-
|
||||
# Validates generated jumps using Minecraft physics.
|
||||
# Ensures all jumps are physically possible.
|
||||
enabled: true
|
||||
|
||||
# -= Maximum jump distance =-
|
||||
# Maximum 3D distance for a jump (Pythagorean theorem).
|
||||
max-distance: 4.5
|
||||
|
||||
# -= Maximum horizontal distance =-
|
||||
# Maximum horizontal distance (X and Z axes only).
|
||||
max-horizontal: 4.1
|
||||
|
||||
# -= Maximum vertical up =-
|
||||
# Maximum height a player can jump up.
|
||||
max-vertical-up: 1.25
|
||||
|
||||
# -= Maximum vertical down =-
|
||||
# Maximum height a player can jump down.
|
||||
max-vertical-down: 3.0
|
||||
|
||||
# -= Jump types options =-
|
||||
jump-types:
|
||||
|
||||
# -= Jump types enabled =-
|
||||
# Enables special jump types (neo-jumps, fence jumps, etc.).
|
||||
enabled: false
|
||||
|
||||
# -= Jump types list =-
|
||||
# Configure which jump types are enabled and their spawn chances.
|
||||
types:
|
||||
neo-jump:
|
||||
enabled: true
|
||||
chance: 0.05
|
||||
head-hitter:
|
||||
enabled: true
|
||||
chance: 0.08
|
||||
fence-jump:
|
||||
enabled: true
|
||||
chance: 0.10
|
||||
trapdoor-jump:
|
||||
enabled: true
|
||||
chance: 0.12
|
||||
ladder-jump:
|
||||
enabled: true
|
||||
chance: 0.07
|
||||
|
||||
# -= Memory optimization =-
|
||||
memory:
|
||||
|
||||
# -= Block cleanup distance =-
|
||||
# Distance in blocks after which old parkour blocks are removed.
|
||||
# Lower values save memory but might remove blocks too early.
|
||||
block-cleanup-distance: 20
|
||||
|
||||
# -= Cleanup interval =-
|
||||
# How often (in ticks) to check for blocks to clean up.
|
||||
# 20 ticks = 1 second
|
||||
cleanup-interval: 100
|
||||
|
||||
# -= Ghost mode options =-
|
||||
ghost-mode:
|
||||
|
||||
# -= Ghost mode enabled =-
|
||||
# Shows ghosts of top players running alongside you.
|
||||
enabled: false
|
||||
|
||||
# -= Show top =-
|
||||
# How many top ghosts to show (1-3).
|
||||
show-top: 3
|
||||
|
||||
# -= Ghost transparency =-
|
||||
# Transparency of ghost entities (0.0 = invisible, 1.0 = fully visible).
|
||||
transparency: 0.5
|
||||
|
||||
# -= Game modes options =-
|
||||
modes:
|
||||
|
||||
# -= Speedrun mode =-
|
||||
speedrun:
|
||||
# Block lifetime in seconds before disappearing
|
||||
block-lifetime: 1.5
|
||||
# Warning time in seconds before block disappears
|
||||
warning-time: 0.5
|
||||
|
||||
# -= Gravity shift mode =-
|
||||
gravity-shift:
|
||||
# Interval in jumps before effect changes
|
||||
interval: 10
|
||||
|
||||
effects:
|
||||
jump-boost:
|
||||
enabled: true
|
||||
amplifier: 2
|
||||
duration: 30
|
||||
speed:
|
||||
enabled: true
|
||||
amplifier: 1
|
||||
duration: 30
|
||||
slowness:
|
||||
enabled: true
|
||||
amplifier: 1
|
||||
duration: 15
|
||||
levitation:
|
||||
enabled: true
|
||||
amplifier: 1
|
||||
duration: 5
|
||||
|
||||
# -= Hardcore mode =-
|
||||
hardcore:
|
||||
# Reset all rewards on fall
|
||||
reset-rewards: true
|
||||
|
||||
|
||||
|
||||
# -= Scoring options =-
|
||||
scoring:
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue