v1.2.4: Remove roguelike perks, update bStats to 3.2.1, add new game modes and lpschem system

- Removed roguelike perk system (double jump, magnet, shield, second chance)
- Updated bStats dependency to 3.2.1 with proper relocation
- Changed bStats plugin ID to 29754
- Added new game modes: Speedrun, Gravity Shift, Hardcore
- Implemented .lpschem format with GZIP + JSON compression
- Added JumpValidator for physics-based jump validation
- Added JumpType system (neo-jump, head-hitter, fence, trapdoor, ladder)
- Implemented Ghost system for recording/playback top players
- Added loparkour-vilib as local dependency
- Updated config with new mode settings and removed roguelike section
- Cleaned up TODO.md roadmap
This commit is contained in:
loki 2026-02-25 00:55:24 +01:00
parent aecb03ccf8
commit 8aeb547897
68 changed files with 6186 additions and 163 deletions

View file

@ -12,6 +12,9 @@ import dev.loki.loparkour.mode.MultiMode;
import dev.loki.loparkour.player.ParkourPlayer;
import dev.loki.loparkour.player.ParkourUser;
import dev.loki.loparkour.player.data.InventoryData;
import dev.loki.loparkour.schematic.lpschem.LPSchematic;
import dev.loki.loparkour.schematic.lpschem.LPSchematicBuilder;
import dev.loki.loparkour.schematic.lpschem.SchematicConverter;
import dev.loki.loparkour.session.Session;
import dev.efnilite.vilib.command.ViCommand;
import dev.efnilite.vilib.inventory.item.Item;
@ -227,8 +230,11 @@ public class Command extends ViCommand {
send(player, "<red>/LoParkour schematic wand <dark_gray>- <gray>Get the schematic wand");
send(player, "<red>/LoParkour schematic pos1 <dark_gray>- <gray>Set the first position of your selection");
send(player, "<red>/LoParkour schematic pos2 <dark_gray>- <gray>Set the second position of your selection");
send(player, "<red>/LoParkour schematic save <dark_gray>- <gray>Save your selection to a schematic file");
send(player, "<red>/LoParkour schematic paste <file> <dark_gray>- <gray>Paste a schematic file");
send(player, "<red>/LoParkour schematic save <name> <dark_gray>- <gray>Save selection to .lpschem format");
send(player, "<red>/LoParkour schematic paste <name> <dark_gray>- <gray>Paste a .lpschem schematic");
send(player, "<red>/LoParkour schematic list <dark_gray>- <gray>List all loaded schematics");
send(player, "<red>/LoParkour schematic reload <dark_gray>- <gray>Reload all schematics");
send(player, "<red>/LoParkour schematic convert <dark_gray>- <gray>Convert old schematics to .lpschem");
send(player, "");
send(player, "<dark_gray><underlined>Have any questions or need help? Join the Discord.");
}
@ -510,6 +516,27 @@ public class Command extends ViCommand {
Schematic.save(LoParkour.getInFolder("schematics/parkour-%s".formatted(code)), existingSelection[0], existingSelection[1], LoParkour.getPlugin());
}
case "list" -> {
send(player, "<dark_red><bold>Schematics <reset><gray>Loaded schematics:");
LoParkour.getSchematicManager().getAllSchematics().forEach((name, schem) -> {
send(player, "<gray>- <red>" + name + " <dark_gray>(" + schem.getMetadata().getDifficulty() + ")");
});
}
case "reload" -> {
if (!cooldown(sender, "LoParkour reload schematics", 2500)) {
return;
}
LoParkour.getSchematicManager().reload();
send(player, "<dark_red><bold>Schematics <reset><gray>Reloaded all schematics.");
}
case "convert" -> {
if (!cooldown(sender, "LoParkour convert schematics", 5000)) {
return;
}
send(player, "<dark_red><bold>Schematics <reset><gray>Converting old schematics to .lpschem format...");
SchematicConverter.convertAll();
send(player, "<dark_red><bold>Schematics <reset><gray>Conversion complete! Check console for details.");
}
}
}
}
@ -521,22 +548,56 @@ public class Command extends ViCommand {
}
if (arg1.equalsIgnoreCase("schematic")) {
if (!arg2.equalsIgnoreCase("paste")) {
return;
}
if (!player.hasPermission(ParkourOption.ADMIN.permission)) {
return;
}
Schematic schematic = Schematics.getSchematic(LoParkour.getPlugin(), arg3);
if (schematic == null) {
send(sender, "%sCouldn't find %s".formatted(LoParkour.PREFIX, arg3));
return;
}
switch (arg2.toLowerCase()) {
case "save" -> {
if (!cooldown(sender, "LoParkour save schematic", 2500)) {
return;
}
schematic.paste(player.getLocation());
send(sender, "%sPasted schematic %s".formatted(LoParkour.PREFIX, arg3));
Location[] selection = selections.get(player);
if (selection == null || selection[0] == null || selection[1] == null) {
send(player, "<dark_red><bold>Schematics <reset><gray>Your schematic isn't complete yet. Make sure you've set the first and second position.");
return;
}
try {
int width = Math.abs(selection[1].getBlockX() - selection[0].getBlockX()) + 1;
int height = Math.abs(selection[1].getBlockY() - selection[0].getBlockY()) + 1;
int length = Math.abs(selection[1].getBlockZ() - selection[0].getBlockZ()) + 1;
LPSchematic schematic = new LPSchematicBuilder(arg3, player.getName(), 0.5, width, height, length)
.fromWorld(player.getWorld(), selection[0], selection[1])
.build();
LoParkour.getSchematicManager().saveSchematic(schematic);
send(player, "<dark_red><bold>Schematics <reset><gray>Saved schematic as <red>" + arg3 + "<gray>!");
} catch (Exception e) {
send(player, "<dark_red><bold>Schematics <reset><gray>Failed to save schematic: " + e.getMessage());
e.printStackTrace();
}
}
case "paste" -> {
LPSchematic lpSchematic = LoParkour.getSchematicManager().getSchematic(arg3);
if (lpSchematic != null) {
lpSchematic.paste(player.getLocation(), player.getWorld());
send(sender, "%sPasted .lpschem schematic %s".formatted(LoParkour.PREFIX, arg3));
return;
}
Schematic oldSchematic = Schematics.getSchematic(LoParkour.getPlugin(), arg3);
if (oldSchematic == null) {
send(sender, "%sCouldn't find %s".formatted(LoParkour.PREFIX, arg3));
return;
}
oldSchematic.paste(player.getLocation());
send(sender, "%sPasted old schematic %s".formatted(LoParkour.PREFIX, arg3));
}
}
}
}

View file

@ -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())));

View file

@ -182,6 +182,32 @@ public class Option {
SQL_PREFIX = Config.CONFIG.getString("sql.prefix");
}
// --------------------------------------------------------------
// Jump Validation
public static boolean JUMP_VALIDATION_ENABLED;
public static double MAX_JUMP_DISTANCE;
public static double MAX_HORIZONTAL_DISTANCE;
public static double MAX_VERTICAL_UP;
public static double MAX_VERTICAL_DOWN;
// Jump Types
public static boolean JUMP_TYPES_ENABLED;
public static Map<String, Boolean> JUMP_TYPE_ENABLED;
public static Map<String, Double> JUMP_TYPE_CHANCE;
// Memory Optimization
public static int BLOCK_CLEANUP_DISTANCE;
public static int CLEANUP_INTERVAL;
// Ghost Mode
public static boolean GHOST_MODE_ENABLED;
public static int GHOST_SHOW_TOP;
public static double GHOST_TRANSPARENCY;
// --------------------------------------------------------------
// Generation
@ -208,6 +234,33 @@ public class Option {
public static int MIN_Y;
private static void initGeneration() {
// Jump validation
JUMP_VALIDATION_ENABLED = Config.CONFIG.getBoolean("jump-validation.enabled");
MAX_JUMP_DISTANCE = Config.CONFIG.getDouble("jump-validation.max-distance");
MAX_HORIZONTAL_DISTANCE = Config.CONFIG.getDouble("jump-validation.max-horizontal");
MAX_VERTICAL_UP = Config.CONFIG.getDouble("jump-validation.max-vertical-up");
MAX_VERTICAL_DOWN = Config.CONFIG.getDouble("jump-validation.max-vertical-down");
// Jump types
JUMP_TYPES_ENABLED = Config.CONFIG.getBoolean("jump-types.enabled");
JUMP_TYPE_ENABLED = new HashMap<>();
JUMP_TYPE_CHANCE = new HashMap<>();
for (String type : Config.CONFIG.getChildren("jump-types.types", false)) {
String path = "jump-types.types." + type;
JUMP_TYPE_ENABLED.put(type, Config.CONFIG.getBoolean(path + ".enabled"));
JUMP_TYPE_CHANCE.put(type, Config.CONFIG.getDouble(path + ".chance"));
}
// Memory optimization
BLOCK_CLEANUP_DISTANCE = Config.CONFIG.getInt("memory.block-cleanup-distance");
CLEANUP_INTERVAL = Config.CONFIG.getInt("memory.cleanup-interval");
// Ghost mode
GHOST_MODE_ENABLED = Config.CONFIG.getBoolean("ghost-mode.enabled");
GHOST_SHOW_TOP = Config.CONFIG.getInt("ghost-mode.show-top");
GHOST_TRANSPARENCY = Config.CONFIG.getDouble("ghost-mode.transparency");
TYPE_NORMAL = Config.GENERATION.getInt("generation.type.normal") / 100.0;
TYPE_SPECIAL = Config.GENERATION.getInt("generation.type.schematic") / 100.0;
TYPE_SCHEMATICS = Config.GENERATION.getInt("generation.type.special") / 100.0;

View file

@ -0,0 +1,53 @@
package dev.loki.loparkour.generator;
import org.bukkit.Material;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.List;
public enum JumpType {
NORMAL(1.0, "Normal jump", Material.STONE),
NEO_JUMP(2.5, "Jump around fence/wall", Material.OAK_FENCE, Material.COBBLESTONE_WALL, Material.NETHER_BRICK_FENCE),
HEAD_HITTER(2.0, "Jump with block overhead", Material.STONE_SLAB),
FENCE_JUMP(1.8, "Jump onto fence", Material.OAK_FENCE, Material.BIRCH_FENCE, Material.SPRUCE_FENCE),
TRAPDOOR_JUMP(1.5, "Jump onto trapdoor", Material.OAK_TRAPDOOR, Material.IRON_TRAPDOOR),
LADDER_JUMP(1.7, "Jump onto ladder", Material.LADDER);
private final double difficulty;
private final String description;
private final List<Material> materials;
JumpType(double difficulty, String description, Material... materials) {
this.difficulty = difficulty;
this.description = description;
this.materials = Arrays.asList(materials);
}
public double getDifficulty() {
return difficulty;
}
@NotNull
public String getDescription() {
return description;
}
@NotNull
public List<Material> getMaterials() {
return materials;
}
@NotNull
public Material getRandomMaterial() {
if (materials.isEmpty()) {
return Material.STONE;
}
return materials.get((int) (Math.random() * materials.size()));
}
public boolean usesMaterial(@NotNull Material material) {
return materials.contains(material);
}
}

View file

@ -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;
}
}

View file

@ -52,49 +52,19 @@ import java.util.*;
*/
public class ParkourGenerator {
/**
* The amount of blocks that trail behind the player.
*/
public static final int BLOCK_TRAIL = 2;
/**
* This generator's score
*/
public int score = 0;
/**
* The total score achieved in this Generator instance
*/
public int totalScore = 0;
/**
* The schematic cooldown
*/
public int schematicCooldown = Config.GENERATION.getInt("advanced.schematic-cooldown");
/**
* Whether this generator has been stopped
*/
public boolean stopped = false;
/**
* The zone in which the parkour can take place. (playable area)
*/
public Location[] zone;
/**
* The player
*/
public ParkourPlayer player;
/**
* The task used in checking the player's current location
*/
public BukkitTask task;
/**
* Where blocks from schematics spawn
*/
private BukkitTask cleanupTask;
public Location blockSpawn;
/**
@ -127,64 +97,19 @@ public class ParkourGenerator {
*/
public final Profile profile = new Profile();
/**
* The island instance.
*/
public final Island island;
/**
* The chances of which distance the jump should have
*/
public final Map<Integer, Double> distanceChances = new HashMap<>();
/**
* The chances of which height the jump should have
*/
public final Map<Integer, Double> heightChances = new HashMap<>();
/**
* The chances of which type of special jump
*/
public final Map<BlockData, Double> specialChances = new HashMap<>();
public final Map<BlockGenerationType, Double> defaultChances = new HashMap<>();
/**
* The chances of default jump types: schematic, 'special' (ice, etc.) or normal
*/
public final Map<JumpType, Double> defaultChances = new HashMap<>();
/**
* Whether the schematic should be deleted on the next jump.
*/
protected boolean deleteSchematic = false;
protected boolean waitForSchematicCompletion = false;
/**
* The last location the player was found standing in
*/
protected Location lastStandingPlayerLocation;
/**
* A list of blocks from the (possibly) spawned structure
*/
protected List<Block> schematicBlocks = new ArrayList<>();
/**
* The player's current position index.
*/
protected int lastPositionIndexPlayer = -1;
protected List<Block> history = new LinkedList<>();
/**
* The history of generated blocks. The most recently generated block is the last item in the list.
*/
protected List<Block> history = new ArrayList<>();
/**
* Creates a new ParkourGenerator instance
*
* @param session The session.
* @param schematic The schematic to use for the spawn island.
* @param generatorOptions The options.
*/
public ParkourGenerator(@NotNull Session session, @Nullable Schematic schematic, GeneratorOption... generatorOptions) {
this.session = session;
this.generatorOptions = Arrays.asList(generatorOptions);
@ -196,30 +121,17 @@ public class ParkourGenerator {
calculateChances();
}
/**
* Creates a new ParkourGenerator instance.
*
* @param session The session.
* @param generatorOptions The options.
*/
public ParkourGenerator(@NotNull Session session, GeneratorOption... generatorOptions) {
this(session, Schematics.getSchematic(LoParkour.getPlugin(), "spawn-island"), generatorOptions);
}
/**
* Ensures generator preferences in profile can't be overridden by the player changing settings.
*/
public void overrideProfile() { }
/**
* Calculates all chances for every variable.
* Modification is possible in the generator constructor or through external map changes.
*/
protected void calculateChances() {
defaultChances.clear();
defaultChances.put(JumpType.DEFAULT, Option.TYPE_NORMAL);
defaultChances.put(JumpType.SCHEMATIC, Option.TYPE_SCHEMATICS);
defaultChances.put(JumpType.SPECIAL, Option.TYPE_SPECIAL);
defaultChances.put(BlockGenerationType.DEFAULT, Option.TYPE_NORMAL);
defaultChances.put(BlockGenerationType.SCHEMATIC, Option.TYPE_SCHEMATICS);
defaultChances.put(BlockGenerationType.SPECIAL, Option.TYPE_SPECIAL);
heightChances.clear();
heightChances.put(1, Option.NORMAL_HEIGHT_1);
@ -388,6 +300,11 @@ public class ParkourGenerator {
.repeat(1)
.execute(this::tick)
.run();
cleanupTask = Task.create(LoParkour.getPlugin())
.repeat(Option.CLEANUP_INTERVAL)
.execute(this::cleanupDistantBlocks)
.run();
}
/**
@ -396,6 +313,9 @@ public class ParkourGenerator {
public void tick() {
if (stopped) {
task.cancel();
if (cleanupTask != null) {
cleanupTask.cancel();
}
return;
}
@ -462,7 +382,6 @@ public class ParkourGenerator {
lastPositionIndexPlayer = currentIndex;
for (int i = currentIndex - BLOCK_TRAIL - 1; i >= currentIndex - 4 * BLOCK_TRAIL; i--) {
// avoid setting beginning block to air
if (i <= 0) {
continue;
}
@ -470,6 +389,8 @@ public class ParkourGenerator {
history.get(i).setType(Material.AIR);
}
cleanupDistantBlocks();
deleteSchematic();
for (int i = 0; i < (Config.CONFIG.getBoolean("scoring.all-points") ? deltaFromLast : 1); i++) { // score the difference
@ -597,19 +518,19 @@ public class ParkourGenerator {
return;
}
Map<JumpType, Double> chances = new HashMap<>(defaultChances);
Map<BlockGenerationType, Double> chances = new HashMap<>(defaultChances);
if (schematicCooldown > 0 || generatorOptions.contains(GeneratorOption.DISABLE_SCHEMATICS) || profile.get("schematicDifficulty").asDouble() == 0.0 || !schematicBlocks.isEmpty()) {
chances.remove(JumpType.SCHEMATIC);
chances.remove(BlockGenerationType.SCHEMATIC);
}
if (!profile.get("useSpecialBlocks").asBoolean()) {
chances.remove(JumpType.SPECIAL);
chances.remove(BlockGenerationType.SPECIAL);
}
if (chances.isEmpty()) {
chances.put(JumpType.DEFAULT, 1.0);
chances.put(BlockGenerationType.DEFAULT, 1.0);
}
JumpType jump = Probs.random(chances);
if (jump == JumpType.SCHEMATIC) {
BlockGenerationType jump = Probs.random(chances);
if (jump == BlockGenerationType.SCHEMATIC) {
LoParkour.log("Generating schematic jump at index %s".formatted(history.size() + 1));
double difficulty = profile.get("schematicDifficulty").asDouble();
@ -647,7 +568,7 @@ public class ParkourGenerator {
List<Block> movedBlocks = new ArrayList<>();
for (Block block : blocks) {
BlockData data = (jump == JumpType.SPECIAL && !generatorOptions.contains(GeneratorOption.DISABLE_SPECIAL)) ? Probs.random(specialChances) : selectBlockData();
BlockData data = (jump == BlockGenerationType.SPECIAL && !generatorOptions.contains(GeneratorOption.DISABLE_SPECIAL)) ? Probs.random(specialChances) : selectBlockData();
if (data instanceof Fence) {
block = block.getLocation().subtract(0, 1, 0).getBlock();
@ -731,6 +652,33 @@ public class ParkourGenerator {
return history.get(history.size() - 1);
}
// Очистка блоков на большом расстоянии от игрока для экономии памяти
protected void cleanupDistantBlocks() {
if (history.size() < Option.BLOCK_CLEANUP_DISTANCE * 2) {
return;
}
Location playerLoc = player.getLocation();
int removed = 0;
Iterator<Block> iterator = history.iterator();
while (iterator.hasNext()) {
Block block = iterator.next();
if (block.getLocation().distance(playerLoc) > Option.BLOCK_CLEANUP_DISTANCE) {
block.setType(Material.AIR, false);
iterator.remove();
removed++;
} else {
break;
}
}
if (removed > 0) {
lastPositionIndexPlayer = Math.max(0, lastPositionIndexPlayer - removed);
}
}
private double getDifficulty(String fileName) {
String path = "difficulty.%s".formatted(fileName.split("[-.]")[1]);
@ -837,7 +785,7 @@ public class ParkourGenerator {
return session.getSpectators();
}
public enum JumpType {
public enum BlockGenerationType {
DEFAULT, SCHEMATIC, SPECIAL
}
}

View file

@ -0,0 +1,71 @@
package dev.loki.loparkour.ghost;
import org.jetbrains.annotations.NotNull;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
public class GhostData {
private final String playerName;
private final int score;
private final List<GhostFrame> frames;
public GhostData(@NotNull String playerName, int score, @NotNull List<GhostFrame> frames) {
this.playerName = playerName;
this.score = score;
this.frames = new ArrayList<>(frames);
}
public String getPlayerName() {
return playerName;
}
public int getScore() {
return score;
}
public List<GhostFrame> getFrames() {
return frames;
}
public void saveToFile(@NotNull File file) throws IOException {
try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) {
out.writeUTF(playerName);
out.writeInt(score);
out.writeInt(frames.size());
for (GhostFrame frame : frames) {
out.writeLong(frame.getTimestamp());
out.writeDouble(frame.getX());
out.writeDouble(frame.getY());
out.writeDouble(frame.getZ());
out.writeFloat(frame.getYaw());
out.writeFloat(frame.getPitch());
}
}
}
public static GhostData loadFromFile(@NotNull File file) throws IOException {
try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
String playerName = in.readUTF();
int score = in.readInt();
int frameCount = in.readInt();
List<GhostFrame> frames = new ArrayList<>(frameCount);
for (int i = 0; i < frameCount; i++) {
long timestamp = in.readLong();
double x = in.readDouble();
double y = in.readDouble();
double z = in.readDouble();
float yaw = in.readFloat();
float pitch = in.readFloat();
frames.add(new GhostFrame(timestamp, x, y, z, yaw, pitch));
}
return new GhostData(playerName, score, frames);
}
}
}

View file

@ -0,0 +1,60 @@
package dev.loki.loparkour.ghost;
import org.bukkit.Location;
import org.jetbrains.annotations.NotNull;
public class GhostFrame {
private final long timestamp;
private final double x;
private final double y;
private final double z;
private final float yaw;
private final float pitch;
public GhostFrame(long timestamp, @NotNull Location location) {
this.timestamp = timestamp;
this.x = location.getX();
this.y = location.getY();
this.z = location.getZ();
this.yaw = location.getYaw();
this.pitch = location.getPitch();
}
public GhostFrame(long timestamp, double x, double y, double z, float yaw, float pitch) {
this.timestamp = timestamp;
this.x = x;
this.y = y;
this.z = z;
this.yaw = yaw;
this.pitch = pitch;
}
public long getTimestamp() {
return timestamp;
}
public double getX() {
return x;
}
public double getY() {
return y;
}
public double getZ() {
return z;
}
public float getYaw() {
return yaw;
}
public float getPitch() {
return pitch;
}
public Location toLocation(org.bukkit.World world) {
return new Location(world, x, y, z, yaw, pitch);
}
}

View file

@ -0,0 +1,131 @@
package dev.loki.loparkour.ghost;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.config.Config;
import org.bukkit.Location;
import org.bukkit.World;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.*;
public class GhostManager {
private static final File GHOST_FOLDER = LoParkour.getInFolder("ghosts");
private static final int MAX_GHOSTS_PER_MODE = 3;
private final Map<String, List<GhostData>> ghostsByMode = new HashMap<>();
private final List<GhostPlayer> activeGhosts = new ArrayList<>();
public GhostManager() {
if (!GHOST_FOLDER.exists()) {
GHOST_FOLDER.mkdirs();
}
}
public void loadGhosts(@NotNull String mode) {
File modeFolder = new File(GHOST_FOLDER, mode);
if (!modeFolder.exists()) {
return;
}
List<GhostData> ghosts = new ArrayList<>();
File[] files = modeFolder.listFiles((dir, name) -> name.endsWith(".ghost"));
if (files != null) {
for (File file : files) {
try {
GhostData data = GhostData.loadFromFile(file);
ghosts.add(data);
} catch (IOException e) {
LoParkour.logging().error("Failed to load ghost: " + file.getName());
}
}
}
ghosts.sort((a, b) -> Integer.compare(b.getScore(), a.getScore()));
ghostsByMode.put(mode, ghosts);
}
public void saveGhost(@NotNull String mode, @NotNull GhostData data) {
File modeFolder = new File(GHOST_FOLDER, mode);
if (!modeFolder.exists()) {
modeFolder.mkdirs();
}
List<GhostData> ghosts = ghostsByMode.getOrDefault(mode, new ArrayList<>());
ghosts.add(data);
ghosts.sort((a, b) -> Integer.compare(b.getScore(), a.getScore()));
while (ghosts.size() > MAX_GHOSTS_PER_MODE) {
GhostData removed = ghosts.remove(ghosts.size() - 1);
File file = new File(modeFolder, removed.getPlayerName() + ".ghost");
file.delete();
}
ghostsByMode.put(mode, ghosts);
File file = new File(modeFolder, data.getPlayerName() + ".ghost");
try {
data.saveToFile(file);
} catch (IOException e) {
LoParkour.logging().error("Failed to save ghost: " + file.getName());
}
}
public void spawnGhosts(@NotNull String mode, @NotNull Location startLocation, @NotNull World world) {
if (!Config.CONFIG.getBoolean("ghost-mode.enabled")) {
return;
}
stopAllGhosts();
List<GhostData> ghosts = ghostsByMode.get(mode);
if (ghosts == null || ghosts.isEmpty()) {
return;
}
int showTop = Math.min(Config.CONFIG.getInt("ghost-mode.show-top"), ghosts.size());
for (int i = 0; i < showTop; i++) {
GhostPlayer ghost = new GhostPlayer(ghosts.get(i), world);
ghost.spawn(startLocation);
activeGhosts.add(ghost);
}
}
public void stopAllGhosts() {
for (GhostPlayer ghost : activeGhosts) {
ghost.stop();
}
activeGhosts.clear();
}
@Nullable
public GhostData getTopGhost(@NotNull String mode) {
List<GhostData> ghosts = ghostsByMode.get(mode);
if (ghosts == null || ghosts.isEmpty()) {
return null;
}
return ghosts.get(0);
}
public List<GhostData> getTopGhosts(@NotNull String mode, int count) {
List<GhostData> ghosts = ghostsByMode.get(mode);
if (ghosts == null || ghosts.isEmpty()) {
return Collections.emptyList();
}
return ghosts.subList(0, Math.min(count, ghosts.size()));
}
public boolean shouldRecordGhost(@NotNull String mode, int score) {
List<GhostData> ghosts = ghostsByMode.get(mode);
if (ghosts == null || ghosts.size() < MAX_GHOSTS_PER_MODE) {
return true;
}
GhostData lowestGhost = ghosts.get(ghosts.size() - 1);
return score > lowestGhost.getScore();
}
}

View file

@ -0,0 +1,95 @@
package dev.loki.loparkour.ghost;
import dev.loki.loparkour.LoParkour;
import dev.efnilite.vilib.util.Task;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.EntityType;
import org.bukkit.scheduler.BukkitTask;
import org.jetbrains.annotations.NotNull;
public class GhostPlayer {
private final GhostData data;
private final World world;
private ArmorStand entity;
private BukkitTask task;
private int currentFrame = 0;
private long startTime;
public GhostPlayer(@NotNull GhostData data, @NotNull World world) {
this.data = data;
this.world = world;
}
public void spawn(@NotNull Location startLocation) {
if (data.getFrames().isEmpty()) {
return;
}
entity = (ArmorStand) world.spawnEntity(startLocation, EntityType.ARMOR_STAND);
entity.setVisible(false);
entity.setGravity(false);
entity.setInvulnerable(true);
entity.setCustomName("§7" + data.getPlayerName() + " §8(Ghost)");
entity.setCustomNameVisible(true);
startTime = System.currentTimeMillis();
currentFrame = 0;
task = Task.create(LoParkour.getPlugin())
.repeat(1)
.execute(this::update)
.run();
}
private void update() {
if (entity == null || !entity.isValid()) {
stop();
return;
}
long elapsed = System.currentTimeMillis() - startTime;
while (currentFrame < data.getFrames().size()) {
GhostFrame frame = data.getFrames().get(currentFrame);
if (frame.getTimestamp() > elapsed) {
break;
}
Location loc = frame.toLocation(world);
entity.teleport(loc);
currentFrame++;
}
if (currentFrame >= data.getFrames().size()) {
stop();
}
}
public void stop() {
if (task != null) {
task.cancel();
task = null;
}
if (entity != null) {
entity.remove();
entity = null;
}
}
public boolean isActive() {
return entity != null && entity.isValid();
}
public String getPlayerName() {
return data.getPlayerName();
}
public int getScore() {
return data.getScore();
}
}

View file

@ -0,0 +1,53 @@
package dev.loki.loparkour.ghost;
import org.bukkit.Location;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
public class GhostRecorder {
private static final double MIN_MOVEMENT = 0.1;
private final List<GhostFrame> frames = new ArrayList<>();
private Location lastLocation;
private long startTime;
private boolean recording = false;
public void startRecording(@NotNull Location startLocation) {
frames.clear();
lastLocation = startLocation.clone();
startTime = System.currentTimeMillis();
recording = true;
frames.add(new GhostFrame(0, startLocation.clone()));
}
public void recordFrame(@NotNull Location currentLocation) {
if (!recording) {
return;
}
if (lastLocation.distance(currentLocation) < MIN_MOVEMENT) {
return;
}
long timestamp = System.currentTimeMillis() - startTime;
frames.add(new GhostFrame(timestamp, currentLocation.clone()));
lastLocation = currentLocation.clone();
}
public GhostData stopRecording(String playerName, int score) {
recording = false;
return new GhostData(playerName, score, frames);
}
public boolean isRecording() {
return recording;
}
public int getFrameCount() {
return frames.size();
}
}

View file

@ -0,0 +1,172 @@
package dev.loki.loparkour.mode;
import dev.loki.loparkour.config.Config;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.generator.ParkourGenerator;
import dev.loki.loparkour.leaderboard.Leaderboard;
import dev.loki.loparkour.player.ParkourPlayer;
import dev.loki.loparkour.session.Session;
import dev.efnilite.vilib.inventory.item.Item;
import org.bukkit.entity.Player;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class GravityShiftMode implements Mode {
private final Leaderboard leaderboard = new Leaderboard(getName(), Leaderboard.Sort.SCORE);
private final Random random = new Random();
@Override
@NotNull
public String getName() {
return "gravity-shift";
}
@Override
@Nullable
public Item getItem(String locale) {
return Locales.getItem(locale, "play.single.gravity-shift");
}
@Override
@NotNull
public Leaderboard getLeaderboard() {
return leaderboard;
}
@Override
public void create(Player player) {
if (!Config.CONFIG.getBoolean("joining")) {
player.sendMessage("Joining is currently disabled.");
return;
}
ParkourPlayer pp = ParkourPlayer.getPlayer(player);
if (pp != null && pp.session.generator != null && pp.session.generator.getMode() instanceof GravityShiftMode) {
return;
}
player.closeInventory();
Session.create(session -> new GravityShiftGenerator(session), null, null, player);
}
private static class GravityShiftGenerator extends ParkourGenerator {
private int jumpsUntilShift;
private final int shiftInterval;
private PotionEffect currentEffect;
public GravityShiftGenerator(@NotNull Session session) {
super(session);
this.shiftInterval = Config.CONFIG.getInt("modes.gravity-shift.interval");
this.jumpsUntilShift = shiftInterval;
}
@Override
protected void score() {
super.score();
jumpsUntilShift--;
if (jumpsUntilShift <= 0) {
applyRandomEffect();
jumpsUntilShift = shiftInterval;
}
}
private void applyRandomEffect() {
if (currentEffect != null) {
player.player.removePotionEffect(currentEffect.getType());
}
List<EffectData> effects = new ArrayList<>();
if (Config.CONFIG.getBoolean("modes.gravity-shift.effects.jump-boost.enabled")) {
effects.add(new EffectData(
PotionEffectType.JUMP,
Config.CONFIG.getInt("modes.gravity-shift.effects.jump-boost.amplifier"),
Config.CONFIG.getInt("modes.gravity-shift.effects.jump-boost.duration"),
"Jump Boost"
));
}
if (Config.CONFIG.getBoolean("modes.gravity-shift.effects.speed.enabled")) {
effects.add(new EffectData(
PotionEffectType.SPEED,
Config.CONFIG.getInt("modes.gravity-shift.effects.speed.amplifier"),
Config.CONFIG.getInt("modes.gravity-shift.effects.speed.duration"),
"Speed"
));
}
if (Config.CONFIG.getBoolean("modes.gravity-shift.effects.slowness.enabled")) {
effects.add(new EffectData(
PotionEffectType.SLOW,
Config.CONFIG.getInt("modes.gravity-shift.effects.slowness.amplifier"),
Config.CONFIG.getInt("modes.gravity-shift.effects.slowness.duration"),
"Slowness"
));
}
if (Config.CONFIG.getBoolean("modes.gravity-shift.effects.levitation.enabled")) {
effects.add(new EffectData(
PotionEffectType.LEVITATION,
Config.CONFIG.getInt("modes.gravity-shift.effects.levitation.amplifier"),
Config.CONFIG.getInt("modes.gravity-shift.effects.levitation.duration"),
"Levitation"
));
}
if (effects.isEmpty()) {
return;
}
EffectData selected = effects.get(new Random().nextInt(effects.size()));
currentEffect = new PotionEffect(
selected.type,
selected.duration * 20,
selected.amplifier - 1,
false,
true,
true
);
player.player.addPotionEffect(currentEffect);
player.sendTranslated("modes.gravity-shift.effect-applied", selected.name);
}
@Override
public void reset(boolean regenerate) {
if (currentEffect != null && player != null && player.player != null) {
player.player.removePotionEffect(currentEffect.getType());
}
jumpsUntilShift = shiftInterval;
super.reset(regenerate);
}
@Override
public Mode getMode() {
return Modes.GRAVITY_SHIFT;
}
}
private static class EffectData {
final PotionEffectType type;
final int amplifier;
final int duration;
final String name;
EffectData(PotionEffectType type, int amplifier, int duration, String name) {
this.type = type;
this.amplifier = amplifier;
this.duration = duration;
this.name = name;
}
}
}

View file

@ -0,0 +1,77 @@
package dev.loki.loparkour.mode;
import dev.loki.loparkour.config.Config;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.generator.ParkourGenerator;
import dev.loki.loparkour.leaderboard.Leaderboard;
import dev.loki.loparkour.player.ParkourPlayer;
import dev.loki.loparkour.session.Session;
import dev.efnilite.vilib.inventory.item.Item;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class HardcoreMode implements Mode {
private final Leaderboard leaderboard = new Leaderboard(getName(), Leaderboard.Sort.SCORE);
@Override
@NotNull
public String getName() {
return "hardcore";
}
@Override
@Nullable
public Item getItem(String locale) {
return Locales.getItem(locale, "play.single.hardcore");
}
@Override
@NotNull
public Leaderboard getLeaderboard() {
return leaderboard;
}
@Override
public void create(Player player) {
if (!Config.CONFIG.getBoolean("joining")) {
player.sendMessage("Joining is currently disabled.");
return;
}
ParkourPlayer pp = ParkourPlayer.getPlayer(player);
if (pp != null && pp.session.generator != null && pp.session.generator.getMode() instanceof HardcoreMode) {
return;
}
player.closeInventory();
Session.create(session -> new HardcoreGenerator(session), null, null, player);
}
private static class HardcoreGenerator extends ParkourGenerator {
public HardcoreGenerator(@NotNull Session session) {
super(session);
}
@Override
protected void fall() {
boolean resetRewards = Config.CONFIG.getBoolean("modes.hardcore.reset-rewards");
if (resetRewards) {
player.sendTranslated("modes.hardcore.rewards-lost");
}
player.sendTranslated("modes.hardcore.full-reset");
super.fall();
}
@Override
public Mode getMode() {
return Modes.HARDCORE;
}
}
}

View file

@ -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");
}
}

View file

@ -0,0 +1,112 @@
package dev.loki.loparkour.mode;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.config.Config;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.generator.ParkourGenerator;
import dev.loki.loparkour.leaderboard.Leaderboard;
import dev.loki.loparkour.player.ParkourPlayer;
import dev.loki.loparkour.session.Session;
import dev.efnilite.vilib.inventory.item.Item;
import dev.efnilite.vilib.util.Task;
import org.bukkit.Material;
import org.bukkit.Particle;
import org.bukkit.Sound;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
public class SpeedrunMode implements Mode {
private final Leaderboard leaderboard = new Leaderboard(getName(), Leaderboard.Sort.SCORE);
private final Map<Block, Long> blockTimestamps = new HashMap<>();
@Override
@NotNull
public String getName() {
return "speedrun";
}
@Override
@Nullable
public Item getItem(String locale) {
return Locales.getItem(locale, "play.single.speedrun");
}
@Override
@NotNull
public Leaderboard getLeaderboard() {
return leaderboard;
}
@Override
public void create(Player player) {
if (!Config.CONFIG.getBoolean("joining")) {
player.sendMessage("Joining is currently disabled.");
return;
}
ParkourPlayer pp = ParkourPlayer.getPlayer(player);
if (pp != null && pp.session.generator != null && pp.session.generator.getMode() instanceof SpeedrunMode) {
return;
}
player.closeInventory();
Session.create(session -> new SpeedrunGenerator(session), null, null, player);
}
public void onBlockTouch(@NotNull Block block, @NotNull ParkourPlayer player) {
if (!blockTimestamps.containsKey(block)) {
blockTimestamps.put(block, System.currentTimeMillis());
scheduleBlockRemoval(player, block);
}
}
private void scheduleBlockRemoval(@NotNull ParkourPlayer player, @NotNull Block block) {
double blockLifetime = Config.CONFIG.getDouble("modes.speedrun.block-lifetime");
double warningTime = Config.CONFIG.getDouble("modes.speedrun.warning-time");
long removalTime = (long) (blockLifetime * 1000);
long warningTimeMs = (long) (warningTime * 1000);
Task.create(LoParkour.getPlugin())
.delay((int) ((removalTime - warningTimeMs) / 50))
.execute(() -> {
if (block.getType() != Material.AIR) {
player.player.playSound(block.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 1.0f, 2.0f);
player.player.spawnParticle(Particle.FLAME, block.getLocation().add(0.5, 1, 0.5), 10, 0.3, 0.3, 0.3, 0.01);
}
})
.run();
Task.create(LoParkour.getPlugin())
.delay((int) (removalTime / 50))
.execute(() -> {
if (block.getType() != Material.AIR) {
block.setType(Material.AIR);
blockTimestamps.remove(block);
}
})
.run();
}
public void reset() {
blockTimestamps.clear();
}
private static class SpeedrunGenerator extends ParkourGenerator {
public SpeedrunGenerator(@NotNull Session session) {
super(session);
}
@Override
public Mode getMode() {
return Modes.SPEEDRUN;
}
}
}

View file

@ -0,0 +1,179 @@
package dev.loki.loparkour.schematic.lpschem;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.jetbrains.annotations.NotNull;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
public class LPSchematic {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static final int FORMAT_VERSION = 2;
private final SchematicMetadata metadata;
private final SchematicDimensions dimensions;
private final List<String> palette;
private final int[] blocks;
private final SchematicMarkers markers;
private SchematicVisuals visuals;
private SchematicLogic logic;
public LPSchematic(@NotNull SchematicMetadata metadata,
@NotNull SchematicDimensions dimensions,
@NotNull List<String> palette,
@NotNull int[] blocks,
@NotNull SchematicMarkers markers) {
this.metadata = metadata;
this.dimensions = dimensions;
this.palette = palette;
this.blocks = blocks;
this.markers = markers;
}
public void save(@NotNull File file) throws IOException {
SchematicData data = new SchematicData();
data.format_version = FORMAT_VERSION;
data.metadata = metadata;
data.dimensions = dimensions;
data.palette = palette;
data.blocks = blocks;
data.markers = markers;
data.visuals = visuals;
data.logic = logic;
String json = GSON.toJson(data);
try (GZIPOutputStream gzip = new GZIPOutputStream(new FileOutputStream(file))) {
gzip.write(json.getBytes("UTF-8"));
}
}
@NotNull
public static LPSchematic load(@NotNull File file) throws IOException {
StringBuilder json = new StringBuilder();
try (GZIPInputStream gzip = new GZIPInputStream(new FileInputStream(file));
BufferedReader reader = new BufferedReader(new InputStreamReader(gzip, "UTF-8"))) {
String line;
while ((line = reader.readLine()) != null) {
json.append(line);
}
}
SchematicData data = GSON.fromJson(json.toString(), SchematicData.class);
if (data.format_version != FORMAT_VERSION) {
throw new IOException("Unsupported format version: " + data.format_version);
}
LPSchematic schematic = new LPSchematic(
data.metadata,
data.dimensions,
data.palette,
data.blocks,
data.markers
);
schematic.visuals = data.visuals;
schematic.logic = data.logic;
return schematic;
}
@NotNull
public List<Block> paste(@NotNull Location origin, @NotNull World world) {
List<Block> placedBlocks = new ArrayList<>();
int width = dimensions.width;
int height = dimensions.height;
int length = dimensions.length;
for (int y = 0; y < height; y++) {
for (int z = 0; z < length; z++) {
for (int x = 0; x < width; x++) {
int index = x + (z * width) + (y * width * length);
int paletteIndex = blocks[index];
if (paletteIndex == 0) {
continue;
}
String blockDataString = palette.get(paletteIndex);
BlockData blockData = parseBlockData(blockDataString);
if (blockData == null) {
continue;
}
Block block = world.getBlockAt(
origin.getBlockX() + x,
origin.getBlockY() + y,
origin.getBlockZ() + z
);
block.setBlockData(blockData);
placedBlocks.add(block);
}
}
}
return placedBlocks;
}
private BlockData parseBlockData(@NotNull String blockDataString) {
try {
return org.bukkit.Bukkit.createBlockData(blockDataString);
} catch (IllegalArgumentException e) {
return Material.MAGENTA_GLAZED_TERRACOTTA.createBlockData();
}
}
public SchematicMetadata getMetadata() {
return metadata;
}
public SchematicDimensions getDimensions() {
return dimensions;
}
public SchematicMarkers getMarkers() {
return markers;
}
public SchematicVisuals getVisuals() {
return visuals;
}
public void setVisuals(SchematicVisuals visuals) {
this.visuals = visuals;
}
public SchematicLogic getLogic() {
return logic;
}
public void setLogic(SchematicLogic logic) {
this.logic = logic;
}
private static class SchematicData {
int format_version;
SchematicMetadata metadata;
SchematicDimensions dimensions;
List<String> palette;
int[] blocks;
SchematicMarkers markers;
SchematicVisuals visuals;
SchematicLogic logic;
}
}

View file

@ -0,0 +1,122 @@
package dev.loki.loparkour.schematic.lpschem;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.jetbrains.annotations.NotNull;
import java.util.*;
public class LPSchematicBuilder {
private final SchematicMetadata metadata;
private final SchematicDimensions dimensions;
private final Map<String, Integer> paletteMap;
private final List<String> palette;
private final int[] blocks;
private SchematicMarkers.Vector3i start;
private SchematicMarkers.Vector3i end;
private final List<SchematicMarkers.Vector3i> checkpoints;
public LPSchematicBuilder(@NotNull String name, @NotNull String author, double difficulty,
int width, int height, int length) {
this.metadata = new SchematicMetadata(name, author, difficulty);
this.dimensions = new SchematicDimensions(width, height, length);
this.paletteMap = new HashMap<>();
this.palette = new ArrayList<>();
this.blocks = new int[width * height * length];
this.checkpoints = new ArrayList<>();
palette.add("minecraft:air");
paletteMap.put("minecraft:air", 0);
}
public LPSchematicBuilder setBlock(int x, int y, int z, @NotNull BlockData blockData) {
String blockString = blockData.getAsString();
int paletteIndex = getOrAddToPalette(blockString);
int index = x + (z * dimensions.width) + (y * dimensions.width * dimensions.length);
blocks[index] = paletteIndex;
return this;
}
public LPSchematicBuilder setBlock(int x, int y, int z, @NotNull Material material) {
return setBlock(x, y, z, material.createBlockData());
}
public LPSchematicBuilder setStart(int x, int y, int z) {
this.start = new SchematicMarkers.Vector3i(x, y, z);
return this;
}
public LPSchematicBuilder setEnd(int x, int y, int z) {
this.end = new SchematicMarkers.Vector3i(x, y, z);
return this;
}
public LPSchematicBuilder addCheckpoint(int x, int y, int z) {
checkpoints.add(new SchematicMarkers.Vector3i(x, y, z));
return this;
}
public LPSchematicBuilder addTag(@NotNull String tag) {
metadata.addTag(tag);
return this;
}
public LPSchematicBuilder fromWorld(@NotNull World world, @NotNull Location corner1, @NotNull Location corner2) {
int minX = Math.min(corner1.getBlockX(), corner2.getBlockX());
int minY = Math.min(corner1.getBlockY(), corner2.getBlockY());
int minZ = Math.min(corner1.getBlockZ(), corner2.getBlockZ());
int maxX = Math.max(corner1.getBlockX(), corner2.getBlockX());
int maxY = Math.max(corner1.getBlockY(), corner2.getBlockY());
int maxZ = Math.max(corner1.getBlockZ(), corner2.getBlockZ());
for (int y = minY; y <= maxY; y++) {
for (int z = minZ; z <= maxZ; z++) {
for (int x = minX; x <= maxX; x++) {
Block block = world.getBlockAt(x, y, z);
if (block.getType() != Material.AIR) {
int relX = x - minX;
int relY = y - minY;
int relZ = z - minZ;
setBlock(relX, relY, relZ, block.getBlockData());
if (block.getType() == Material.LIME_WOOL) {
setStart(relX, relY, relZ);
} else if (block.getType() == Material.RED_WOOL) {
setEnd(relX, relY, relZ);
}
}
}
}
}
return this;
}
@NotNull
public LPSchematic build() {
if (start == null || end == null) {
throw new IllegalStateException("Start and end markers must be set");
}
SchematicMarkers markers = new SchematicMarkers(start, end);
checkpoints.forEach(markers::addCheckpoint);
return new LPSchematic(metadata, dimensions, palette, blocks, markers);
}
private int getOrAddToPalette(@NotNull String blockString) {
if (paletteMap.containsKey(blockString)) {
return paletteMap.get(blockString);
}
int index = palette.size();
palette.add(blockString);
paletteMap.put(blockString, index);
return index;
}
}

View file

@ -0,0 +1,67 @@
package dev.loki.loparkour.schematic.lpschem;
import dev.loki.loparkour.LoParkour;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class LPSchematicManager {
private static final File SCHEMATIC_FOLDER = LoParkour.getInFolder("schematics-new");
private final Map<String, LPSchematic> loadedSchematics = new HashMap<>();
public LPSchematicManager() {
if (!SCHEMATIC_FOLDER.exists()) {
SCHEMATIC_FOLDER.mkdirs();
}
}
public void loadAll() {
File[] files = SCHEMATIC_FOLDER.listFiles((dir, name) -> name.endsWith(".lpschem"));
if (files == null || files.length == 0) {
LoParkour.log("No .lpschem files found");
return;
}
for (File file : files) {
try {
LPSchematic schematic = LPSchematic.load(file);
String name = schematic.getMetadata().getName();
loadedSchematics.put(name, schematic);
LoParkour.log("Loaded schematic: " + name);
} catch (IOException e) {
LoParkour.logging().error("Failed to load schematic: " + file.getName());
e.printStackTrace();
}
}
LoParkour.log("Loaded " + loadedSchematics.size() + " schematics");
}
@Nullable
public LPSchematic getSchematic(@NotNull String name) {
return loadedSchematics.get(name);
}
public void saveSchematic(@NotNull LPSchematic schematic) throws IOException {
String fileName = schematic.getMetadata().getName().replaceAll("[^a-zA-Z0-9-_]", "_") + ".lpschem";
File file = new File(SCHEMATIC_FOLDER, fileName);
schematic.save(file);
loadedSchematics.put(schematic.getMetadata().getName(), schematic);
LoParkour.log("Saved schematic: " + schematic.getMetadata().getName());
}
public Map<String, LPSchematic> getAllSchematics() {
return new HashMap<>(loadedSchematics);
}
public void reload() {
loadedSchematics.clear();
loadAll();
}
}

View file

@ -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");
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,25 @@
package dev.loki.loparkour.schematic.lpschem;
import java.util.ArrayList;
import java.util.List;
public class SchematicLogic {
private List<int[]> ghost_path;
public SchematicLogic() {
this.ghost_path = new ArrayList<>();
}
public List<int[]> getGhostPath() {
return ghost_path;
}
public void setGhostPath(List<int[]> ghost_path) {
this.ghost_path = ghost_path;
}
public void addGhostPoint(int x, int y, int z) {
ghost_path.add(new int[]{x, y, z});
}
}

View file

@ -0,0 +1,55 @@
package dev.loki.loparkour.schematic.lpschem;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
public class SchematicMarkers {
private Vector3i start;
private Vector3i end;
private List<Vector3i> checkpoints;
public SchematicMarkers(@NotNull Vector3i start, @NotNull Vector3i end) {
this.start = start;
this.end = end;
this.checkpoints = new ArrayList<>();
}
public Vector3i getStart() {
return start;
}
public void setStart(Vector3i start) {
this.start = start;
}
public Vector3i getEnd() {
return end;
}
public void setEnd(Vector3i end) {
this.end = end;
}
public List<Vector3i> getCheckpoints() {
return checkpoints;
}
public void addCheckpoint(Vector3i checkpoint) {
checkpoints.add(checkpoint);
}
public static class Vector3i {
public int x;
public int y;
public int z;
public Vector3i(int x, int y, int z) {
this.x = x;
this.y = y;
this.z = z;
}
}
}

View file

@ -0,0 +1,39 @@
package dev.loki.loparkour.schematic.lpschem;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
public class SchematicMetadata {
private String name;
private String author;
private double difficulty;
private List<String> tags;
public SchematicMetadata(@NotNull String name, @NotNull String author, double difficulty) {
this.name = name;
this.author = author;
this.difficulty = difficulty;
this.tags = new ArrayList<>();
}
public String getName() {
return name;
}
public String getAuthor() {
return author;
}
public double getDifficulty() {
return difficulty;
}
public List<String> getTags() {
return tags;
}
public void addTag(@NotNull String tag) {
tags.add(tag);
}
}

View file

@ -0,0 +1,72 @@
package dev.loki.loparkour.schematic.lpschem;
import java.util.ArrayList;
import java.util.List;
public class SchematicVisuals {
private List<BlockDisplay> displays;
private List<ParticleEffect> particles;
public SchematicVisuals() {
this.displays = new ArrayList<>();
this.particles = new ArrayList<>();
}
public List<BlockDisplay> getDisplays() {
return displays;
}
public void addDisplay(BlockDisplay display) {
displays.add(display);
}
public List<ParticleEffect> getParticles() {
return particles;
}
public void addParticle(ParticleEffect particle) {
particles.add(particle);
}
public static class BlockDisplay {
public String type;
public String block;
public float[] pos;
public float[] scale;
public Animation animation;
public BlockDisplay(String block, float[] pos, float[] scale) {
this.type = "block";
this.block = block;
this.pos = pos;
this.scale = scale;
}
}
public static class Animation {
public String type;
public String axis;
public float speed;
public Animation(String type, String axis, float speed) {
this.type = type;
this.axis = axis;
this.speed = speed;
}
}
public static class ParticleEffect {
public String type;
public float[] pos;
public int amount;
public float spread;
public ParticleEffect(String type, float[] pos, int amount, float spread) {
this.type = type;
this.pos = pos;
this.amount = amount;
this.spread = spread;
}
}
}