From 8aeb54789731e26dcb6e21be9e43fb75c5339f3d Mon Sep 17 00:00:00 2001 From: loki Date: Wed, 25 Feb 2026 00:55:24 +0100 Subject: [PATCH] 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 --- CHANGES.md | 38 -- build.gradle | 12 +- docs/CHANGES.md | 74 ++++ docs/LPSCHEM_USAGE.md | 177 ++++++++ docs/sd.md | 220 ++++++++++ loparkour-vilib/build.gradle | 29 ++ .../java/dev/efnilite/vilib/ViPlugin.java | 90 ++++ .../dev/efnilite/vilib/command/ViCommand.java | 118 +++++ .../vilib/configupdater/ConfigUpdater.java | 14 + .../efnilite/vilib/event/EventWatcher.java | 28 ++ .../efnilite/vilib/event/EventWrapper.java | 32 ++ .../efnilite/vilib/fastboard/FastBoard.java | 10 + .../dev/efnilite/vilib/inventory/Menu.java | 337 +++++++++++++++ .../vilib/inventory/MenuClickEvent.java | 15 + .../efnilite/vilib/inventory/PagedMenu.java | 157 +++++++ .../vilib/inventory/item/AutoSliderItem.java | 147 +++++++ .../efnilite/vilib/inventory/item/Item.java | 403 ++++++++++++++++++ .../vilib/inventory/item/MenuItem.java | 68 +++ .../vilib/inventory/item/MovableItem.java | 24 ++ .../vilib/inventory/item/SliderItem.java | 116 +++++ .../vilib/inventory/item/TimedItem.java | 84 ++++ .../efnilite/vilib/particle/ParticleData.java | 117 +++++ .../efnilite/vilib/particle/Particles.java | 230 ++++++++++ .../efnilite/vilib/schematic/Schematic.java | 140 ++++++ .../efnilite/vilib/schematic/Schematics.java | 40 ++ .../vilib/schematic/io/SchematicPaster.java | 102 +++++ .../vilib/schematic/io/SchematicReader.java | 59 +++ .../vilib/schematic/io/SchematicWriter.java | 102 +++++ .../java/dev/efnilite/vilib/util/Colls.java | 173 ++++++++ .../dev/efnilite/vilib/util/Cooldowns.java | 118 +++++ .../java/dev/efnilite/vilib/util/Cuboid.java | 110 +++++ .../dev/efnilite/vilib/util/Locations.java | 72 ++++ .../java/dev/efnilite/vilib/util/Logging.java | 75 ++++ .../java/dev/efnilite/vilib/util/Numbers.java | 65 +++ .../java/dev/efnilite/vilib/util/Probs.java | 77 ++++ .../dev/efnilite/vilib/util/SkullSetter.java | 44 ++ .../java/dev/efnilite/vilib/util/Strings.java | 128 ++++++ .../java/dev/efnilite/vilib/util/Task.java | 164 +++++++ .../java/dev/efnilite/vilib/util/Time.java | 71 +++ .../efnilite/vilib/util/UpdateChecker.java | 56 +++ .../java/dev/efnilite/vilib/util/Version.java | 87 ++++ .../efnilite/vilib/util/VoidGenerator.java | 107 +++++ settings.gradle | 2 + src/main/java/dev/loki/loparkour/Command.java | 87 +++- .../java/dev/loki/loparkour/LoParkour.java | 25 +- .../dev/loki/loparkour/config/Option.java | 53 +++ .../loki/loparkour/generator/JumpType.java | 53 +++ .../loparkour/generator/JumpValidator.java | 85 ++++ .../loparkour/generator/ParkourGenerator.java | 156 +++---- .../dev/loki/loparkour/ghost/GhostData.java | 71 +++ .../dev/loki/loparkour/ghost/GhostFrame.java | 60 +++ .../loki/loparkour/ghost/GhostManager.java | 131 ++++++ .../dev/loki/loparkour/ghost/GhostPlayer.java | 95 +++++ .../loki/loparkour/ghost/GhostRecorder.java | 53 +++ .../loki/loparkour/mode/GravityShiftMode.java | 172 ++++++++ .../dev/loki/loparkour/mode/HardcoreMode.java | 77 ++++ .../java/dev/loki/loparkour/mode/Modes.java | 6 + .../dev/loki/loparkour/mode/SpeedrunMode.java | 112 +++++ .../schematic/lpschem/LPSchematic.java | 179 ++++++++ .../schematic/lpschem/LPSchematicBuilder.java | 122 ++++++ .../schematic/lpschem/LPSchematicManager.java | 67 +++ .../schematic/lpschem/SchematicConverter.java | 86 ++++ .../lpschem/SchematicDimensions.java | 18 + .../schematic/lpschem/SchematicLogic.java | 25 ++ .../schematic/lpschem/SchematicMarkers.java | 55 +++ .../schematic/lpschem/SchematicMetadata.java | 39 ++ .../schematic/lpschem/SchematicVisuals.java | 72 ++++ src/main/resources/config.yml | 118 +++++ 68 files changed, 6186 insertions(+), 163 deletions(-) delete mode 100644 CHANGES.md create mode 100644 docs/CHANGES.md create mode 100644 docs/LPSCHEM_USAGE.md create mode 100644 docs/sd.md create mode 100644 loparkour-vilib/build.gradle create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/ViPlugin.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/command/ViCommand.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/configupdater/ConfigUpdater.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/event/EventWatcher.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/event/EventWrapper.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/fastboard/FastBoard.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/Menu.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/MenuClickEvent.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/PagedMenu.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/item/AutoSliderItem.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/item/Item.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/item/MenuItem.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/item/MovableItem.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/item/SliderItem.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/item/TimedItem.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/particle/ParticleData.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/particle/Particles.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/schematic/Schematic.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/schematic/Schematics.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/schematic/io/SchematicPaster.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/schematic/io/SchematicReader.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/schematic/io/SchematicWriter.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Colls.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Cooldowns.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Cuboid.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Locations.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Logging.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Numbers.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Probs.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/util/SkullSetter.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Strings.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Task.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Time.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/util/UpdateChecker.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Version.java create mode 100644 loparkour-vilib/src/main/java/dev/efnilite/vilib/util/VoidGenerator.java create mode 100644 src/main/java/dev/loki/loparkour/generator/JumpType.java create mode 100644 src/main/java/dev/loki/loparkour/generator/JumpValidator.java create mode 100644 src/main/java/dev/loki/loparkour/ghost/GhostData.java create mode 100644 src/main/java/dev/loki/loparkour/ghost/GhostFrame.java create mode 100644 src/main/java/dev/loki/loparkour/ghost/GhostManager.java create mode 100644 src/main/java/dev/loki/loparkour/ghost/GhostPlayer.java create mode 100644 src/main/java/dev/loki/loparkour/ghost/GhostRecorder.java create mode 100644 src/main/java/dev/loki/loparkour/mode/GravityShiftMode.java create mode 100644 src/main/java/dev/loki/loparkour/mode/HardcoreMode.java create mode 100644 src/main/java/dev/loki/loparkour/mode/SpeedrunMode.java create mode 100644 src/main/java/dev/loki/loparkour/schematic/lpschem/LPSchematic.java create mode 100644 src/main/java/dev/loki/loparkour/schematic/lpschem/LPSchematicBuilder.java create mode 100644 src/main/java/dev/loki/loparkour/schematic/lpschem/LPSchematicManager.java create mode 100644 src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicConverter.java create mode 100644 src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicDimensions.java create mode 100644 src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicLogic.java create mode 100644 src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicMarkers.java create mode 100644 src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicMetadata.java create mode 100644 src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicVisuals.java diff --git a/CHANGES.md b/CHANGES.md deleted file mode 100644 index 9026363..0000000 --- a/CHANGES.md +++ /dev/null @@ -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. diff --git a/build.gradle b/build.gradle index 2a840ac..3c610d5 100644 --- a/build.gradle +++ b/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() diff --git a/docs/CHANGES.md b/docs/CHANGES.md new file mode 100644 index 0000000..93ca328 --- /dev/null +++ b/docs/CHANGES.md @@ -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. diff --git a/docs/LPSCHEM_USAGE.md b/docs/LPSCHEM_USAGE.md new file mode 100644 index 0000000..86f36dc --- /dev/null +++ b/docs/LPSCHEM_USAGE.md @@ -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 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 + 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 ΠΈ Π²ΠΈΠ·ΡƒΠ°Π»ΡŒΠ½Ρ‹Ρ… эффСктов diff --git a/docs/sd.md b/docs/sd.md new file mode 100644 index 0000000..7bf18e1 --- /dev/null +++ b/docs/sd.md @@ -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 Π³.* diff --git a/loparkour-vilib/build.gradle b/loparkour-vilib/build.gradle new file mode 100644 index 0000000..bd23fd5 --- /dev/null +++ b/loparkour-vilib/build.gradle @@ -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' +} diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/ViPlugin.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/ViPlugin.java new file mode 100644 index 0000000..99dcbc1 --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/ViPlugin.java @@ -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); + } +} diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/command/ViCommand.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/command/ViCommand.java new file mode 100644 index 0000000..8107ea6 --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/command/ViCommand.java @@ -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> cooldowns = new HashMap<>(); + + /** + * Execute a command + */ + public abstract boolean execute(CommandSender sender, String[] args); + + /** + * Get what should be suggested + */ + public abstract List 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 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 completions(String typed, List 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 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 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) { + + } +} \ No newline at end of file diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/configupdater/ConfigUpdater.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/configupdater/ConfigUpdater.java new file mode 100644 index 0000000..99d6616 --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/configupdater/ConfigUpdater.java @@ -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 ignoredSections) throws Exception { + // Stub implementation - config updating disabled + // Original library not available, manual config updates required + } +} diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/event/EventWatcher.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/event/EventWatcher.java new file mode 100644 index 0000000..d8c4e62 --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/event/EventWatcher.java @@ -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); + } +} \ No newline at end of file diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/event/EventWrapper.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/event/EventWrapper.java new file mode 100644 index 0000000..a0c58f2 --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/event/EventWrapper.java @@ -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; + } +} \ No newline at end of file diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/fastboard/FastBoard.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/fastboard/FastBoard.java new file mode 100644 index 0000000..231f1cd --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/fastboard/FastBoard.java @@ -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); + } +} diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/Menu.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/Menu.java new file mode 100644 index 0000000..97a6999 --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/Menu.java @@ -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 openMenus = new HashMap<>(); + private final static List 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 items = new HashMap<>(); + protected final List 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 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 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 sortedSlots = itemsInRow.keySet().stream().sorted().toList(); // sort all slots + List slots = getEvenlyDistributedSlots(sortedSlots.size()); // evenly distribute items + List olds = new ArrayList<>(); + List 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, " "); // 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 getItems() { + return items; + } + + /** + * Gets the player + * + * @return the player + */ + public Player getPlayer() { + return player; + } + + public String getTitle() { + return title; + } +} diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/MenuClickEvent.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/MenuClickEvent.java new file mode 100644 index 0000000..26e6e2a --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/MenuClickEvent.java @@ -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(); + } +} \ No newline at end of file diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/PagedMenu.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/PagedMenu.java new file mode 100644 index 0000000..fa68b7f --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/PagedMenu.java @@ -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 displaySlots = new ArrayList<>(); + private final List totalToDisplay = new ArrayList<>(); + private final Map> 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 values = new ArrayList<>(assigned.get(newPage)); + Item missingFiller = new Item(filler, " "); + + 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 total = new ArrayList<>(totalToDisplay); + List 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 items) { + totalToDisplay.addAll(items); + return this; + } + + public void setToDisplay(List 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 getTotalToDisplay() { + return totalToDisplay; + } +} \ No newline at end of file diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/item/AutoSliderItem.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/item/AutoSliderItem.java new file mode 100644 index 0000000..18c382e --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/item/AutoSliderItem.java @@ -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 items = new HashMap<>(); + private final Map> 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 onClick) { + items.put(value, item); + clickFunctions.put(value, onClick); + return this; + } + + @Override + public void handleClick(Menu menu, InventoryClickEvent event, ClickType clickType) { + Consumer 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; + } +} \ No newline at end of file diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/item/Item.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/item/Item.java new file mode 100644 index 0000000..a6173d3 --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/item/Item.java @@ -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 lore = new ArrayList<>(); + private Multimap attributes = HashMultimap.create(); + private Map 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 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 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> 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: + *
+ * + * item.attribute(Attribute.GENERIC_ATTACK_SPEED, -10, AttributeModifier.Operation.ADD_NUMBER) + * + * + * @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 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 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 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; + } +} diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/item/MenuItem.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/item/MenuItem.java new file mode 100644 index 0000000..c0e3788 --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/item/MenuItem.java @@ -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> 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 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 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(); + +} diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/item/MovableItem.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/item/MovableItem.java new file mode 100644 index 0000000..28866fa --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/item/MovableItem.java @@ -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; + } +} \ No newline at end of file diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/item/SliderItem.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/item/SliderItem.java new file mode 100644 index 0000000..0e1da7a --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/item/SliderItem.java @@ -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 items = new HashMap<>(); + private final Map> 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 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 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; + } +} diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/item/TimedItem.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/item/TimedItem.java new file mode 100644 index 0000000..a1f232c --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/inventory/item/TimedItem.java @@ -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, " "); + } + 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; + } +} \ No newline at end of file diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/particle/ParticleData.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/particle/ParticleData.java new file mode 100644 index 0000000..356a689 --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/particle/ParticleData.java @@ -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 { + + 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 size(int size) { + this.size = size; + return this; + } + + public ParticleData speed(double speed) { + this.speed = speed; + return this; + } + + public ParticleData offsetX(double offsetX) { + this.offsetX = offsetX; + return this; + } + + public ParticleData offsetY(double offsetY) { + this.offsetY = offsetY; + return this; + } + + public ParticleData offsetZ(double offsetZ) { + this.offsetZ = offsetZ; + return this; + } + + public ParticleData data(@Nullable T data) { + this.data = data; + return this; + } + + public ParticleData 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; + } +} \ No newline at end of file diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/particle/Particles.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/particle/Particles.java new file mode 100644 index 0000000..e4ee949 --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/particle/Particles.java @@ -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 void draw(Location at, @NotNull ParticleData 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 void draw(Location at, @NotNull ParticleData 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 + *

+ * 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). + *

+ * 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. + *

+ * 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. + *

+ * + * @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 void line(Location one, Location two, ParticleData 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 void box(BoundingBox box, @NotNull World world, ParticleData 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 void box(BoundingBox box, @NotNull World world, ParticleData 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 void line(Location one, Location two, ParticleData 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 void circle(Location location, ParticleData 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 void circle(Location location, ParticleData 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()); + } + } +} \ No newline at end of file diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/schematic/Schematic.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/schematic/Schematic.java new file mode 100644 index 0000000..4777d81 --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/schematic/Schematic.java @@ -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 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 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 paste(Location location, double rotation) { + return new SchematicPaster().paste(location, rotation, vectorBlockMap); + } + + /** + * @return The dimensions of this schematic. + */ + public Vector getDimensions() { + Set 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 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(); + } +} diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/schematic/Schematics.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/schematic/Schematics.java new file mode 100644 index 0000000..f423843 --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/schematic/Schematics.java @@ -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> cache = new HashMap<>(); + + public static void addFromFiles(@NotNull Plugin plugin, @NotNull File... files) throws IOException, ClassNotFoundException { + Map 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 getSchematicNames(@NotNull Plugin plugin) { + return cache.get(plugin).keySet(); + } + + public static Collection getSchematics(@NotNull Plugin plugin) { + return cache.get(plugin).values(); + } +} \ No newline at end of file diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/schematic/io/SchematicPaster.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/schematic/io/SchematicPaster.java new file mode 100644 index 0000000..32a2aaa --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/schematic/io/SchematicPaster.java @@ -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 paste(Location location, Map 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 paste(Location location, double rotation, Map 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 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 paste(Supplier> blocksGetter) { + try { + Map 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(); + } + } +} \ No newline at end of file diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/schematic/io/SchematicReader.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/schematic/io/SchematicReader.java new file mode 100644 index 0000000..d383c87 --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/schematic/io/SchematicReader.java @@ -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 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) stream.readObject(); + var offsets = (Map) stream.readObject(); + + Map 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 -> 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])); + } +} diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/schematic/io/SchematicWriter.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/schematic/io/SchematicWriter.java new file mode 100644 index 0000000..f98d186 --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/schematic/io/SchematicWriter.java @@ -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 blocks = getBlocks(Locations.min(pos1, pos2), Locations.max(pos1, pos2)); + + Map palette = getPalette(blocks); + Map 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 getBlocks(Location minL, Location maxL) { + List 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 getPalette(List blocks) { + Map 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 getOffsetData(List blocks, Map palette) { + List locations = blocks.stream().map(Block::getLocation).toList(); + Location min = locations.stream().reduce(Locations::min).orElseThrow(); + + Map offsetData = new HashMap<>(); + + for (Location loc : locations) { + offsetData.put(loc.clone().subtract(min).toVector().toString(), + palette.get(loc.clone().getBlock().getBlockData().getAsString())); + } + + return offsetData; + } +} diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Colls.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Colls.java new file mode 100644 index 0000000..cb52f55 --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Colls.java @@ -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 The key type. + * @param The value type. + * @return A new {@link MapThreader}. + */ + public static MapThreader thread(Map map) { + return new MapThreader<>(map); + } + + /** + * Threads through maps. Modifications return {@link HashMap}. + * + * @param The key type. + * @param The value type. + */ + public static class MapThreader { + + private Map map; + + public MapThreader(Map map) { + this.map = map; + } + + public Map 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 filter(BiFunction f) { + Map newMap = new HashMap<>(); + + for (Map.Entry 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 The new list type. + * @return Threader with map, where f has been applied to each value in map. Returns {@link HashMap}. + */ + public MapThreader mapv(BiFunction f) { + Map 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 The new key type. + * @return Threader with map, where f has been applied to each key in map. Returns {@link HashMap}. + */ + public MapThreader mapk(BiFunction f) { + Map 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 The new key type. + * @param 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 MapThreader mapkv(Function kf, Function vf) { + Map 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 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 inverse() { + Map newMap = new HashMap<>(); + + map.forEach((k, v) -> newMap.put(v, k)); + + return new MapThreader<>(newMap); + } + } + + /** + * @param coll The collection. + * @param The type. + * @return A random item from coll. + */ + public static T random(List coll) { + return coll.get(ThreadLocalRandom.current().nextInt(coll.size())); + } + + /** + * @param coll The collection. + * @param n The amount of random items. + * @param The type. + * @return n random items from coll. + */ + public static List random(List 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 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 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 range(int end) { + return range(0, end, 1); + } +} \ No newline at end of file diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Cooldowns.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Cooldowns.java new file mode 100644 index 0000000..1bb3520 --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Cooldowns.java @@ -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. + *

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:

+ *
if (!canPerform(player, "right click wand", 1000)) { return; }
+ *

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.

+ */ +public class Cooldowns { + + private static final Map EXECUTION_TIMES = new HashMap<>(); + + /** + * Returns whether the provided action {@code key} can be performed with the provided cooldown {@code cooldown}. + * This method is only recommended to be used with global actions, e.g. requiring no player. + * Returns: + *
    + *
  • {@code true} - When the action with the provided key can be performed. + * The last execution was more than {@code cooldown} milliseconds ago.
  • + *
  • {@code false} - When the action with the provided key cannot be performed. + * The last execution was less than {@code cooldown} milliseconds ago.
  • + *
+ * + * @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: + *
    + *
  • {@code true} - When the action with the provided player's key can be performed. + * The last execution was more than {@code cooldown} milliseconds ago.
  • + *
  • {@code false} - When the action with the provided player's key cannot be performed. + * The last execution was less than {@code cooldown} milliseconds ago.
  • + *
+ * + * @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. + * This method is only recommended to be used with global actions, 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); + } + +} \ No newline at end of file diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Cuboid.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Cuboid.java new file mode 100644 index 0000000..48ee13d --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Cuboid.java @@ -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 blocks to their respective {@link BlockData}. + * Performs onComplete when block setting has finished. + * + * @param blocks The block map. + * @param onComplete What to do on completion. + */ + public static void set(@NotNull Map blocks, Plugin plugin, @Nullable Runnable onComplete) { + Queue 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 get(@NotNull Location pos1, @NotNull Location pos2, boolean ignoreAir) { + List 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> onComplete) { + Task.create(plugin).async().execute(() -> onComplete.accept(get(pos1, pos2, ignoreAir))).run(); + } +} \ No newline at end of file diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Locations.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Locations.java new file mode 100644 index 0000000..329bcdc --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Locations.java @@ -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])); + } + +} diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Logging.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Logging.java new file mode 100644 index 0000000..c76e57f --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Logging.java @@ -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("##"); + } +} diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Numbers.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Numbers.java new file mode 100644 index 0000000..b4bc81d --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Numbers.java @@ -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 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 getFromZero(int n) { + return getFromTo(0, n); + } +} diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Probs.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Probs.java new file mode 100644 index 0000000..d1210a6 --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Probs.java @@ -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 The key type. + * @return A random item from the list, based on the probabilities. + */ + public static K random(@NotNull Map distribution, Random random) { + if (distribution.isEmpty()) { + throw new NoSuchElementException("No elements in distribution map"); + } + + double total = 0; + + Map cumulative = new LinkedHashMap<>(); + for (Map.Entry entry : distribution.entrySet()) { + total += entry.getValue(); + cumulative.put(entry.getKey(), total); + } + + double randomNum = random(random) * total; + for (Map.Entry 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 The key type. + * @return A random item from the list, based on the probabilities. + */ + public static K random(@NotNull Map distribution) { + return random(distribution, ThreadLocalRandom.current()); + } +} \ No newline at end of file diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/SkullSetter.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/SkullSetter.java new file mode 100644 index 0000000..9274d69 --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/SkullSetter.java @@ -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); + } + } +} diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Strings.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Strings.java new file mode 100644 index 0000000..18330c4 --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Strings.java @@ -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 colour(@NotNull List 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 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: https://www.stephenenright.com/java-levenshtein-distance + * + * @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); + } +} \ No newline at end of file diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Task.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Task.java new file mode 100644 index 0000000..8a56327 --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Task.java @@ -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; + } +} \ No newline at end of file diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Time.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Time.java new file mode 100644 index 0000000..a652149 --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Time.java @@ -0,0 +1,71 @@ +package dev.efnilite.vilib.util; + +import java.util.HashMap; + +/** + * Time utils + * + * @author Efnilite + */ +public class Time { + + private static final HashMap 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; + } + +} \ No newline at end of file diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/UpdateChecker.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/UpdateChecker.java new file mode 100644 index 0000000..e75f863 --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/UpdateChecker.java @@ -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 ""; + } + } +} diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Version.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Version.java new file mode 100644 index 0000000..5488ebc --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/Version.java @@ -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]; + } +} diff --git a/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/VoidGenerator.java b/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/VoidGenerator.java new file mode 100644 index 0000000..1cbee79 --- /dev/null +++ b/loparkour-vilib/src/main/java/dev/efnilite/vilib/util/VoidGenerator.java @@ -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 getBiomes(@NotNull WorldInfo worldInfo) { + return List.of(Biome.PLAINS); + } + } + + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 626f456..ce7a9bf 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,3 @@ rootProject.name = 'LoParkour' + +include 'loparkour-vilib' diff --git a/src/main/java/dev/loki/loparkour/Command.java b/src/main/java/dev/loki/loparkour/Command.java index 7ae5200..60b41ff 100644 --- a/src/main/java/dev/loki/loparkour/Command.java +++ b/src/main/java/dev/loki/loparkour/Command.java @@ -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, "/LoParkour schematic wand - Get the schematic wand"); send(player, "/LoParkour schematic pos1 - Set the first position of your selection"); send(player, "/LoParkour schematic pos2 - Set the second position of your selection"); - send(player, "/LoParkour schematic save - Save your selection to a schematic file"); - send(player, "/LoParkour schematic paste - Paste a schematic file"); + send(player, "/LoParkour schematic save - Save selection to .lpschem format"); + send(player, "/LoParkour schematic paste - Paste a .lpschem schematic"); + send(player, "/LoParkour schematic list - List all loaded schematics"); + send(player, "/LoParkour schematic reload - Reload all schematics"); + send(player, "/LoParkour schematic convert - Convert old schematics to .lpschem"); send(player, ""); send(player, "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, "Schematics Loaded schematics:"); + LoParkour.getSchematicManager().getAllSchematics().forEach((name, schem) -> { + send(player, "- " + name + " (" + schem.getMetadata().getDifficulty() + ")"); + }); + } + case "reload" -> { + if (!cooldown(sender, "LoParkour reload schematics", 2500)) { + return; + } + LoParkour.getSchematicManager().reload(); + send(player, "Schematics Reloaded all schematics."); + } + case "convert" -> { + if (!cooldown(sender, "LoParkour convert schematics", 5000)) { + return; + } + send(player, "Schematics Converting old schematics to .lpschem format..."); + SchematicConverter.convertAll(); + send(player, "Schematics 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, "Schematics 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, "Schematics Saved schematic as " + arg3 + "!"); + } catch (Exception e) { + send(player, "Schematics 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)); + } + } } } diff --git a/src/main/java/dev/loki/loparkour/LoParkour.java b/src/main/java/dev/loki/loparkour/LoParkour.java index 4e91680..eb723f7 100644 --- a/src/main/java/dev/loki/loparkour/LoParkour.java +++ b/src/main/java/dev/loki/loparkour/LoParkour.java @@ -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()))); diff --git a/src/main/java/dev/loki/loparkour/config/Option.java b/src/main/java/dev/loki/loparkour/config/Option.java index e38c416..157ad8b 100644 --- a/src/main/java/dev/loki/loparkour/config/Option.java +++ b/src/main/java/dev/loki/loparkour/config/Option.java @@ -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 JUMP_TYPE_ENABLED; + public static Map 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; diff --git a/src/main/java/dev/loki/loparkour/generator/JumpType.java b/src/main/java/dev/loki/loparkour/generator/JumpType.java new file mode 100644 index 0000000..fe06e89 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/generator/JumpType.java @@ -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 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 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); + } +} diff --git a/src/main/java/dev/loki/loparkour/generator/JumpValidator.java b/src/main/java/dev/loki/loparkour/generator/JumpValidator.java new file mode 100644 index 0000000..262a01d --- /dev/null +++ b/src/main/java/dev/loki/loparkour/generator/JumpValidator.java @@ -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; + } +} diff --git a/src/main/java/dev/loki/loparkour/generator/ParkourGenerator.java b/src/main/java/dev/loki/loparkour/generator/ParkourGenerator.java index b31d75d..8555fe2 100644 --- a/src/main/java/dev/loki/loparkour/generator/ParkourGenerator.java +++ b/src/main/java/dev/loki/loparkour/generator/ParkourGenerator.java @@ -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 distanceChances = new HashMap<>(); - - /** - * The chances of which height the jump should have - */ public final Map heightChances = new HashMap<>(); - - /** - * The chances of which type of special jump - */ public final Map specialChances = new HashMap<>(); + public final Map defaultChances = new HashMap<>(); - /** - * The chances of default jump types: schematic, 'special' (ice, etc.) or normal - */ - public final Map 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 schematicBlocks = new ArrayList<>(); - - /** - * The player's current position index. - */ protected int lastPositionIndexPlayer = -1; + protected List history = new LinkedList<>(); - /** - * The history of generated blocks. The most recently generated block is the last item in the list. - */ - protected List 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 chances = new HashMap<>(defaultChances); + Map 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 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 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 } } diff --git a/src/main/java/dev/loki/loparkour/ghost/GhostData.java b/src/main/java/dev/loki/loparkour/ghost/GhostData.java new file mode 100644 index 0000000..b494135 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/ghost/GhostData.java @@ -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 frames; + + public GhostData(@NotNull String playerName, int score, @NotNull List frames) { + this.playerName = playerName; + this.score = score; + this.frames = new ArrayList<>(frames); + } + + public String getPlayerName() { + return playerName; + } + + public int getScore() { + return score; + } + + public List 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 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); + } + } +} diff --git a/src/main/java/dev/loki/loparkour/ghost/GhostFrame.java b/src/main/java/dev/loki/loparkour/ghost/GhostFrame.java new file mode 100644 index 0000000..9e86db6 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/ghost/GhostFrame.java @@ -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); + } +} diff --git a/src/main/java/dev/loki/loparkour/ghost/GhostManager.java b/src/main/java/dev/loki/loparkour/ghost/GhostManager.java new file mode 100644 index 0000000..fcd0311 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/ghost/GhostManager.java @@ -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> ghostsByMode = new HashMap<>(); + private final List 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 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 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 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 ghosts = ghostsByMode.get(mode); + if (ghosts == null || ghosts.isEmpty()) { + return null; + } + return ghosts.get(0); + } + + public List getTopGhosts(@NotNull String mode, int count) { + List 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 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(); + } +} diff --git a/src/main/java/dev/loki/loparkour/ghost/GhostPlayer.java b/src/main/java/dev/loki/loparkour/ghost/GhostPlayer.java new file mode 100644 index 0000000..3e842fd --- /dev/null +++ b/src/main/java/dev/loki/loparkour/ghost/GhostPlayer.java @@ -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(); + } +} diff --git a/src/main/java/dev/loki/loparkour/ghost/GhostRecorder.java b/src/main/java/dev/loki/loparkour/ghost/GhostRecorder.java new file mode 100644 index 0000000..e9c5c62 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/ghost/GhostRecorder.java @@ -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 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(); + } +} diff --git a/src/main/java/dev/loki/loparkour/mode/GravityShiftMode.java b/src/main/java/dev/loki/loparkour/mode/GravityShiftMode.java new file mode 100644 index 0000000..e61ca16 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/mode/GravityShiftMode.java @@ -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 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; + } + } +} diff --git a/src/main/java/dev/loki/loparkour/mode/HardcoreMode.java b/src/main/java/dev/loki/loparkour/mode/HardcoreMode.java new file mode 100644 index 0000000..101ef40 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/mode/HardcoreMode.java @@ -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; + } + } +} diff --git a/src/main/java/dev/loki/loparkour/mode/Modes.java b/src/main/java/dev/loki/loparkour/mode/Modes.java index 41faba0..55a6173 100644 --- a/src/main/java/dev/loki/loparkour/mode/Modes.java +++ b/src/main/java/dev/loki/loparkour/mode/Modes.java @@ -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"); } } diff --git a/src/main/java/dev/loki/loparkour/mode/SpeedrunMode.java b/src/main/java/dev/loki/loparkour/mode/SpeedrunMode.java new file mode 100644 index 0000000..0aba855 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/mode/SpeedrunMode.java @@ -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 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; + } + } +} diff --git a/src/main/java/dev/loki/loparkour/schematic/lpschem/LPSchematic.java b/src/main/java/dev/loki/loparkour/schematic/lpschem/LPSchematic.java new file mode 100644 index 0000000..7ae20ac --- /dev/null +++ b/src/main/java/dev/loki/loparkour/schematic/lpschem/LPSchematic.java @@ -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 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 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 paste(@NotNull Location origin, @NotNull World world) { + List 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 palette; + int[] blocks; + SchematicMarkers markers; + SchematicVisuals visuals; + SchematicLogic logic; + } +} diff --git a/src/main/java/dev/loki/loparkour/schematic/lpschem/LPSchematicBuilder.java b/src/main/java/dev/loki/loparkour/schematic/lpschem/LPSchematicBuilder.java new file mode 100644 index 0000000..ac6720d --- /dev/null +++ b/src/main/java/dev/loki/loparkour/schematic/lpschem/LPSchematicBuilder.java @@ -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 paletteMap; + private final List palette; + private final int[] blocks; + private SchematicMarkers.Vector3i start; + private SchematicMarkers.Vector3i end; + private final List 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; + } +} diff --git a/src/main/java/dev/loki/loparkour/schematic/lpschem/LPSchematicManager.java b/src/main/java/dev/loki/loparkour/schematic/lpschem/LPSchematicManager.java new file mode 100644 index 0000000..d5f5c74 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/schematic/lpschem/LPSchematicManager.java @@ -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 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 getAllSchematics() { + return new HashMap<>(loadedSchematics); + } + + public void reload() { + loadedSchematics.clear(); + loadAll(); + } +} diff --git a/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicConverter.java b/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicConverter.java new file mode 100644 index 0000000..5ce52bd --- /dev/null +++ b/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicConverter.java @@ -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"); + } +} diff --git a/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicDimensions.java b/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicDimensions.java new file mode 100644 index 0000000..cf3d03b --- /dev/null +++ b/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicDimensions.java @@ -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; + } +} diff --git a/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicLogic.java b/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicLogic.java new file mode 100644 index 0000000..798df46 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicLogic.java @@ -0,0 +1,25 @@ +package dev.loki.loparkour.schematic.lpschem; + +import java.util.ArrayList; +import java.util.List; + +public class SchematicLogic { + + private List ghost_path; + + public SchematicLogic() { + this.ghost_path = new ArrayList<>(); + } + + public List getGhostPath() { + return ghost_path; + } + + public void setGhostPath(List ghost_path) { + this.ghost_path = ghost_path; + } + + public void addGhostPoint(int x, int y, int z) { + ghost_path.add(new int[]{x, y, z}); + } +} diff --git a/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicMarkers.java b/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicMarkers.java new file mode 100644 index 0000000..5f9ec05 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicMarkers.java @@ -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 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 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; + } + } +} diff --git a/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicMetadata.java b/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicMetadata.java new file mode 100644 index 0000000..2a15485 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicMetadata.java @@ -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 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 getTags() { + return tags; + } + + public void addTag(@NotNull String tag) { + tags.add(tag); + } +} \ No newline at end of file diff --git a/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicVisuals.java b/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicVisuals.java new file mode 100644 index 0000000..063ee0e --- /dev/null +++ b/src/main/java/dev/loki/loparkour/schematic/lpschem/SchematicVisuals.java @@ -0,0 +1,72 @@ +package dev.loki.loparkour.schematic.lpschem; + +import java.util.ArrayList; +import java.util.List; + +public class SchematicVisuals { + + private List displays; + private List particles; + + public SchematicVisuals() { + this.displays = new ArrayList<>(); + this.particles = new ArrayList<>(); + } + + public List getDisplays() { + return displays; + } + + public void addDisplay(BlockDisplay display) { + displays.add(display); + } + + public List 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; + } + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index d6cd0b2..0509b6e 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -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: