Update dependencies and improve configuration management

- Updated LoLib dependency to version 3.0.0 in build.gradle.kts
- Added HikariCP and MySQL connector dependencies for improved database handling
- Enhanced jar minimization process to exclude JDBC pool and driver
- Refactored configuration management by introducing ConfigAccessor and ConfigLoader for better file handling and validation
- Removed deprecated GenerationOptions class and integrated its functionality into the new structure
- Improved command error handling and added cooldown hints for admin commands
This commit is contained in:
loki 2026-04-01 16:21:09 +02:00
parent 978f739045
commit 3100c53229
86 changed files with 4324 additions and 3248 deletions

View file

@ -0,0 +1,48 @@
package dev.loki.loparkour.mode.elytra;
import dev.loki.loparkour.config.Config;
/**
* Configuration settings for Elytra mode.
*/
public class ElytraConfig {
private final double ringDistanceMin;
private final double ringDistanceMax;
private final double ringSize;
private final int ringLead;
private final double maxHeadingChangeHorizontal;
private final double maxHeadingChangeVertical;
private final double maxHeightAboveSpawn;
private final double maxDeviation;
private final double boostPower;
private final long fireworkCooldownMs;
private final int startingFireworks;
public ElytraConfig() {
this.ringDistanceMin = Config.CONFIG.getDouble("modes.elytra.ring-distance-min");
this.ringDistanceMax = Config.CONFIG.getDouble("modes.elytra.ring-distance-max");
this.ringSize = Config.CONFIG.getDouble("modes.elytra.ring-size");
this.ringLead = Config.CONFIG.getInt("modes.elytra.ring-lead");
this.maxHeadingChangeHorizontal = Config.CONFIG.getDouble("modes.elytra.max-heading-change-horizontal");
this.maxHeadingChangeVertical = Config.CONFIG.getDouble("modes.elytra.max-heading-change-vertical");
this.maxHeightAboveSpawn = Config.CONFIG.getDouble("modes.elytra.max-height-above-spawn");
this.maxDeviation = Config.CONFIG.getDouble("modes.elytra.max-deviation");
this.boostPower = Config.CONFIG.getDouble("modes.elytra.boost-power");
this.fireworkCooldownMs = (long) Config.CONFIG.getDouble("modes.elytra.firework-cooldown-ms");
this.startingFireworks = Config.CONFIG.getInt("modes.elytra.starting-fireworks");
}
// Getters
public double getRingDistanceMin() { return ringDistanceMin; }
public double getRingDistanceMax() { return ringDistanceMax; }
public double getRingSize() { return ringSize; }
public int getRingLead() { return ringLead; }
public double getMaxHeadingChangeHorizontal() { return maxHeadingChangeHorizontal; }
public double getMaxHeadingChangeVertical() { return maxHeadingChangeVertical; }
public double getMaxHeightAboveSpawn() { return maxHeightAboveSpawn; }
public double getMaxDeviation() { return maxDeviation; }
public double getBoostPower() { return boostPower; }
public long getFireworkCooldownMs() { return fireworkCooldownMs; }
public int getStartingFireworks() { return startingFireworks; }
}

View file

@ -0,0 +1,140 @@
package dev.loki.loparkour.mode.elytra;
import dev.loki.loparkour.player.ParkourPlayer;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* Handles physics calculations and validation for Elytra mode.
*/
public class ElytraPhysics {
private final ElytraConfig config;
private final Map<UUID, Long> lastFireworkTime = new HashMap<>();
public ElytraPhysics(@NotNull ElytraConfig config) {
this.config = config;
}
/**
* Check if any rings have been crossed by the player.
*/
public int checkRingCrossings(@NotNull ParkourPlayer player, @NotNull List<ElytraRing> rings, int currentRingIndex) {
Player p = player.player;
Location playerLoc = p.getLocation();
Vector velocity = p.getVelocity();
// Check rings starting from current index
for (int i = currentRingIndex; i < rings.size(); i++) {
ElytraRing ring = rings.get(i);
if (ring.hasCrossed(playerLoc, velocity)) {
ring.setPassed(true);
return i + 1; // Return new current ring index
}
// Only check a few rings ahead to avoid skipping
if (i > currentRingIndex + 2) break;
}
return currentRingIndex; // No change
}
/**
* Check if player should fall due to deviation from course.
*/
@NotNull
public FallCheckResult checkFall(@NotNull ParkourPlayer player, @NotNull List<ElytraRing> rings, int currentRingIndex) {
Player p = player.player;
Location playerLoc = p.getLocation();
if (rings.isEmpty() || currentRingIndex >= rings.size()) {
return new FallCheckResult(false, "No rings available");
}
// Check distance to current ring
ElytraRing currentRing = rings.get(currentRingIndex);
double distanceToRing = playerLoc.distance(currentRing.getCenter());
if (distanceToRing > config.getMaxDeviation()) {
return new FallCheckResult(true, "Too far from course (distance: " + String.format("%.1f", distanceToRing) + ")");
}
// Check if player is flying (has elytra deployed)
if (!p.isGliding()) {
return new FallCheckResult(true, "Not gliding");
}
return new FallCheckResult(false, null);
}
/**
* Apply firework boost to player if cooldown allows.
*/
public boolean applyFireworkBoost(@NotNull ParkourPlayer player) {
Player p = player.player;
UUID playerId = p.getUniqueId();
long currentTime = System.currentTimeMillis();
// Check cooldown
Long lastBoost = lastFireworkTime.get(playerId);
if (lastBoost != null && (currentTime - lastBoost) < config.getFireworkCooldownMs()) {
return false; // Still on cooldown
}
// Apply boost
Vector velocity = p.getVelocity();
Vector boostedVelocity = velocity.multiply(config.getBoostPower());
p.setVelocity(boostedVelocity);
// Update cooldown
lastFireworkTime.put(playerId, currentTime);
return true;
}
/**
* Check if player is centered when passing through a ring.
*/
public boolean isPlayerCentered(@NotNull Location playerLoc, @NotNull ElytraRing ring) {
Vector toPlayer = playerLoc.toVector().subtract(ring.getCenter().toVector());
Vector normal = ring.getNormal();
// Project player position onto ring plane
Vector projectedPos = toPlayer.subtract(normal.clone().multiply(toPlayer.dot(normal)));
// Check if within center threshold (25% of ring radius)
double centerThreshold = ring.getRadius() * 0.25;
return projectedPos.length() <= centerThreshold;
}
/**
* Clean up physics data for a player.
*/
public void cleanup(@NotNull UUID playerId) {
lastFireworkTime.remove(playerId);
}
/**
* Result of fall check.
*/
public static class FallCheckResult {
private final boolean shouldFall;
private final String reason;
public FallCheckResult(boolean shouldFall, String reason) {
this.shouldFall = shouldFall;
this.reason = reason;
}
public boolean shouldFall() { return shouldFall; }
public String getReason() { return reason; }
}
}

View file

@ -0,0 +1,101 @@
package dev.loki.loparkour.mode.elytra;
import org.bukkit.Color;
import org.bukkit.Location;
import org.bukkit.Particle;
import org.bukkit.World;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.EntityType;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import java.util.List;
/**
* Handles visual rendering of Elytra rings and effects.
*/
public class ElytraRenderer {
private static final int RING_PARTICLES = 32;
private static final Color ACTIVE_RING_COLOR = Color.LIME;
private static final Color PASSED_RING_COLOR = Color.GRAY;
private static final Color UPCOMING_RING_COLOR = Color.YELLOW;
/**
* Render all rings with appropriate colors.
*/
public void renderRings(@NotNull List<ElytraRing> rings, int currentRingIndex) {
for (int i = 0; i < rings.size(); i++) {
ElytraRing ring = rings.get(i);
Color color = getRingColor(i, currentRingIndex, ring.isPassed());
renderRing(ring, color);
}
}
/**
* Render a single ring with the specified color.
*/
public void renderRing(@NotNull ElytraRing ring, @NotNull Color color) {
Location center = ring.getCenter();
Vector normal = ring.getNormal();
double radius = ring.getRadius();
// Create two perpendicular vectors to the normal
Vector u = findPerpendicular(normal);
Vector v = normal.getCrossProduct(u).normalize();
// Draw circle using particles
World world = center.getWorld();
if (world == null) return;
for (int i = 0; i < RING_PARTICLES; i++) {
double angle = 2 * Math.PI * i / RING_PARTICLES;
double x = radius * Math.cos(angle);
double y = radius * Math.sin(angle);
Vector offset = u.clone().multiply(x).add(v.clone().multiply(y));
Location particlePos = center.clone().add(offset);
// Spawn colored particle
Particle.DustOptions dustOptions = new Particle.DustOptions(color, 1.0f);
world.spawnParticle(Particle.REDSTONE, particlePos, 1, dustOptions);
}
// Create or update armor stand for ring center (optional visual aid)
if (ring.getEntity() == null) {
ArmorStand stand = (ArmorStand) world.spawnEntity(center, EntityType.ARMOR_STAND);
stand.setVisible(false);
stand.setGravity(false);
stand.setMarker(true);
stand.setCustomNameVisible(true);
ring.setEntity(stand);
}
// Update armor stand name with ring info
ArmorStand stand = ring.getEntity();
if (stand != null && !stand.isDead()) {
stand.setCustomName("Ring #" + ring.getId());
}
}
@NotNull
private Color getRingColor(int ringIndex, int currentRingIndex, boolean passed) {
if (passed) {
return PASSED_RING_COLOR;
} else if (ringIndex == currentRingIndex) {
return ACTIVE_RING_COLOR;
} else {
return UPCOMING_RING_COLOR;
}
}
@NotNull
private Vector findPerpendicular(@NotNull Vector v) {
// Find a vector perpendicular to v
if (Math.abs(v.getX()) < 0.9) {
return new Vector(1, 0, 0).getCrossProduct(v).normalize();
} else {
return new Vector(0, 1, 0).getCrossProduct(v).normalize();
}
}
}

View file

@ -0,0 +1,80 @@
package dev.loki.loparkour.mode.elytra;
import org.bukkit.Location;
import org.bukkit.entity.ArmorStand;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
/**
* Represents a ring that players must fly through in Elytra mode.
*/
public class ElytraRing {
private final int id;
private final Location center;
private final Vector normal;
private final double radius;
private ArmorStand entity;
private boolean passed = false;
public ElytraRing(int id, @NotNull Location center, @NotNull Vector normal, double radius) {
this.id = id;
this.center = center.clone();
this.normal = normal.clone().normalize();
this.radius = radius;
}
/**
* Check if player has crossed through this ring.
*/
public boolean hasCrossed(@NotNull Location playerLoc, @NotNull Vector velocity) {
if (passed) return false;
Vector toPlayer = playerLoc.toVector().subtract(center.toVector());
double distanceToPlane = toPlayer.dot(normal);
// Check if player is close to the ring plane
if (Math.abs(distanceToPlane) > 2.0) return false;
// Check if moving towards the ring
if (velocity.dot(normal) <= 0) return false;
// Check if within ring radius
Vector projectedPos = toPlayer.subtract(normal.clone().multiply(distanceToPlane));
return projectedPos.length() <= radius;
}
/**
* Check if player is currently inside the ring.
*/
public boolean isInside(@NotNull Location playerLoc) {
Vector toPlayer = playerLoc.toVector().subtract(center.toVector());
double distanceToPlane = Math.abs(toPlayer.dot(normal));
if (distanceToPlane > 1.0) return false;
Vector projectedPos = toPlayer.subtract(normal.clone().multiply(toPlayer.dot(normal)));
return projectedPos.length() <= radius;
}
/**
* Remove the ring's visual representation.
*/
public void remove() {
if (entity != null && !entity.isDead()) {
entity.remove();
}
}
// Getters
public int getId() { return id; }
public Location getCenter() { return center.clone(); }
public Vector getNormal() { return normal.clone(); }
public double getRadius() { return radius; }
public ArmorStand getEntity() { return entity; }
public boolean isPassed() { return passed; }
// Setters
public void setEntity(ArmorStand entity) { this.entity = entity; }
public void setPassed(boolean passed) { this.passed = passed; }
}

View file

@ -0,0 +1,128 @@
package dev.loki.loparkour.mode.elytra;
import org.bukkit.Location;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
/**
* Generates rings for Elytra mode parkour.
*/
public class ElytraRingGenerator {
private final ElytraConfig config;
private int nextRingId = 0;
public ElytraRingGenerator(@NotNull ElytraConfig config) {
this.config = config;
}
/**
* Generate a series of rings starting from origin in the given direction.
*/
@NotNull
public List<ElytraRing> generateRings(@NotNull Location origin, @NotNull Vector direction, int count) {
List<ElytraRing> rings = new ArrayList<>();
Location currentPos = origin.clone();
Vector currentDir = direction.clone().normalize();
for (int i = 0; i < count; i++) {
// Calculate distance to next ring
double distance = calculateRingDistance();
// Move to next ring position
currentPos.add(currentDir.clone().multiply(distance));
// Apply course variation
currentDir = applyDirectionVariation(currentDir);
// Ensure we don't go too high or too low
currentPos = constrainHeight(currentPos, origin);
// Create ring with perpendicular normal
Vector normal = findPerpendicularVector(currentDir);
ElytraRing ring = new ElytraRing(nextRingId++, currentPos.clone(), normal, config.getRingSize());
rings.add(ring);
}
return rings;
}
private double calculateRingDistance() {
return ThreadLocalRandom.current().nextDouble(
config.getRingDistanceMin(),
config.getRingDistanceMax()
);
}
@NotNull
private Vector applyDirectionVariation(@NotNull Vector currentDir) {
double maxHorizontalChange = Math.toRadians(config.getMaxHeadingChangeHorizontal());
double maxVerticalChange = Math.toRadians(config.getMaxHeadingChangeVertical());
// Random horizontal rotation
double yawChange = ThreadLocalRandom.current().nextDouble(-maxHorizontalChange, maxHorizontalChange);
// Random vertical rotation
double pitchChange = ThreadLocalRandom.current().nextDouble(-maxVerticalChange, maxVerticalChange);
return rotateVector(currentDir, yawChange, pitchChange);
}
@NotNull
private Location constrainHeight(@NotNull Location pos, @NotNull Location origin) {
double maxHeight = origin.getY() + config.getMaxHeightAboveSpawn();
double minHeight = origin.getY() - 20; // Don't go too low
if (pos.getY() > maxHeight) {
pos.setY(maxHeight);
} else if (pos.getY() < minHeight) {
pos.setY(minHeight);
}
return pos;
}
@NotNull
private Vector findPerpendicularVector(@NotNull Vector v) {
// Find a vector perpendicular to v
Vector perp;
if (Math.abs(v.getX()) < 0.9) {
perp = new Vector(1, 0, 0);
} else {
perp = new Vector(0, 1, 0);
}
// Make it truly perpendicular using cross product
return v.getCrossProduct(perp).normalize();
}
@NotNull
private Vector rotateVector(@NotNull Vector v, double yawRad, double pitchRad) {
// Rotate around Y axis (yaw)
double cosYaw = Math.cos(yawRad);
double sinYaw = Math.sin(yawRad);
double newX = v.getX() * cosYaw - v.getZ() * sinYaw;
double newZ = v.getX() * sinYaw + v.getZ() * cosYaw;
Vector rotated = new Vector(newX, v.getY(), newZ);
// Rotate around perpendicular axis (pitch)
Vector axis = findPerpendicularVector(rotated);
double cosPitch = Math.cos(pitchRad);
double sinPitch = Math.sin(pitchRad);
// Rodrigues' rotation formula
Vector result = rotated.clone().multiply(cosPitch);
result.add(axis.getCrossProduct(rotated).multiply(sinPitch));
result.add(axis.clone().multiply(axis.dot(rotated) * (1 - cosPitch)));
return result.normalize();
}
}