From ccf64b7dfd012d4809320a0ca9ddcda1bf693940 Mon Sep 17 00:00:00 2001 From: loki Date: Thu, 9 Apr 2026 13:59:46 +0200 Subject: [PATCH] Implement ElytraMode support classes Created missing classes for ElytraMode: - ElytraConfig: loads settings from config.yml - ElytraRing: ring model with center, radius, direction - ElytraRingGenerator: generates rings along curved path - ElytraPhysics: checks ring crossings, falls, firework boosts - ElytraRenderer: renders rings with particles ElytraGenerator now has all dependencies and should work. --- .../loparkour/mode/elytra/ElytraConfig.java | 72 ++++--- .../loparkour/mode/elytra/ElytraPhysics.java | 181 +++++++++--------- .../loparkour/mode/elytra/ElytraRenderer.java | 121 ++++-------- .../loparkour/mode/elytra/ElytraRing.java | 111 ++++++----- .../mode/elytra/ElytraRingGenerator.java | 161 ++++++---------- 5 files changed, 277 insertions(+), 369 deletions(-) diff --git a/src/main/java/dev/loki/loparkour/mode/elytra/ElytraConfig.java b/src/main/java/dev/loki/loparkour/mode/elytra/ElytraConfig.java index 5f77d50..fa34731 100644 --- a/src/main/java/dev/loki/loparkour/mode/elytra/ElytraConfig.java +++ b/src/main/java/dev/loki/loparkour/mode/elytra/ElytraConfig.java @@ -3,46 +3,56 @@ package dev.loki.loparkour.mode.elytra; import dev.loki.loparkour.config.Config; /** - * Configuration settings for Elytra mode. + * Configuration for Elytra mode loaded from config.yml */ public class ElytraConfig { - - private final double ringDistanceMin; - private final double ringDistanceMax; - private final double ringSize; + + private final int ringDistanceMin; + private final int ringDistanceMax; + private final int ringSize; private final int ringLead; - private final double maxHeadingChangeHorizontal; - private final double maxHeadingChangeVertical; - private final double maxHeightAboveSpawn; - private final double maxDeviation; + private final int maxHeadingChangeHorizontal; + private final int maxHeadingChangeVertical; + private final int maxHeightAboveSpawn; + private final int 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"); + this.ringDistanceMin = Config.CONFIG.isPath("modes.elytra.ring-distance-min") + ? Config.CONFIG.getInt("modes.elytra.ring-distance-min") : 20; + this.ringDistanceMax = Config.CONFIG.isPath("modes.elytra.ring-distance-max") + ? Config.CONFIG.getInt("modes.elytra.ring-distance-max") : 40; + this.ringSize = Config.CONFIG.isPath("modes.elytra.ring-size") + ? Config.CONFIG.getInt("modes.elytra.ring-size") : 5; + this.ringLead = Config.CONFIG.isPath("modes.elytra.ring-lead") + ? Config.CONFIG.getInt("modes.elytra.ring-lead") : 8; + this.maxHeadingChangeHorizontal = Config.CONFIG.isPath("modes.elytra.max-heading-change-horizontal") + ? Config.CONFIG.getInt("modes.elytra.max-heading-change-horizontal") : 30; + this.maxHeadingChangeVertical = Config.CONFIG.isPath("modes.elytra.max-heading-change-vertical") + ? Config.CONFIG.getInt("modes.elytra.max-heading-change-vertical") : 15; + this.maxHeightAboveSpawn = Config.CONFIG.isPath("modes.elytra.max-height-above-spawn") + ? Config.CONFIG.getInt("modes.elytra.max-height-above-spawn") : 60; + this.maxDeviation = Config.CONFIG.isPath("modes.elytra.max-deviation") + ? Config.CONFIG.getInt("modes.elytra.max-deviation") : 25; + this.boostPower = Config.CONFIG.isPath("modes.elytra.boost-power") + ? Config.CONFIG.getDouble("modes.elytra.boost-power") : 1.5; + this.fireworkCooldownMs = Config.CONFIG.isPath("modes.elytra.firework-cooldown-ms") + ? Config.CONFIG.getInt("modes.elytra.firework-cooldown-ms") : 3000; + this.startingFireworks = Config.CONFIG.isPath("modes.elytra.starting-fireworks") + ? Config.CONFIG.getInt("modes.elytra.starting-fireworks") : 64; } - - // Getters - public double getRingDistanceMin() { return ringDistanceMin; } - public double getRingDistanceMax() { return ringDistanceMax; } - public double getRingSize() { return ringSize; } + + public int getRingDistanceMin() { return ringDistanceMin; } + public int getRingDistanceMax() { return ringDistanceMax; } + public int 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 int getMaxHeadingChangeHorizontal() { return maxHeadingChangeHorizontal; } + public int getMaxHeadingChangeVertical() { return maxHeadingChangeVertical; } + public int getMaxHeightAboveSpawn() { return maxHeightAboveSpawn; } + public int getMaxDeviation() { return maxDeviation; } public double getBoostPower() { return boostPower; } public long getFireworkCooldownMs() { return fireworkCooldownMs; } public int getStartingFireworks() { return startingFireworks; } -} \ No newline at end of file +} diff --git a/src/main/java/dev/loki/loparkour/mode/elytra/ElytraPhysics.java b/src/main/java/dev/loki/loparkour/mode/elytra/ElytraPhysics.java index 91745bf..36e5b8b 100644 --- a/src/main/java/dev/loki/loparkour/mode/elytra/ElytraPhysics.java +++ b/src/main/java/dev/loki/loparkour/mode/elytra/ElytraPhysics.java @@ -12,129 +12,126 @@ import java.util.Map; import java.util.UUID; /** - * Handles physics calculations and validation for Elytra mode. + * Handles physics checks for Elytra mode (ring crossings, falls, boosts) */ public class ElytraPhysics { - + private final ElytraConfig config; - private final Map lastFireworkTime = new HashMap<>(); - + private final Map lastFireworkBoost = new HashMap<>(); + public ElytraPhysics(@NotNull ElytraConfig config) { this.config = config; } - + /** - * Check if any rings have been crossed by the player. + * Check if player has crossed any new rings + * @return new ring index if crossed, otherwise current index */ - public int checkRingCrossings(@NotNull ParkourPlayer player, @NotNull List 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; + public int checkRingCrossings(@NotNull ParkourPlayer player, @NotNull List rings, int currentIndex) { + if (currentIndex >= rings.size()) { + return currentIndex; } + + Location playerLoc = player.player.getLocation(); - return currentRingIndex; // No change + // Check next ring + ElytraRing nextRing = rings.get(currentIndex); + if (nextRing.contains(playerLoc)) { + return currentIndex + 1; + } + + return currentIndex; } - + /** - * Check if player should fall due to deviation from course. + * Check if player should fall (too far from course) */ @NotNull - public FallCheckResult checkFall(@NotNull ParkourPlayer player, @NotNull List rings, int currentRingIndex) { - Player p = player.player; - Location playerLoc = p.getLocation(); - - if (rings.isEmpty() || currentRingIndex >= rings.size()) { - return new FallCheckResult(false, "No rings available"); + public FallCheckResult checkFall(@NotNull ParkourPlayer player, @NotNull List rings, int currentIndex) { + if (rings.isEmpty()) { + return FallCheckResult.noFall(); } - - // 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) + ")"); + + Location playerLoc = player.player.getLocation(); + + // Check if player is too far from nearest ring + ElytraRing nearestRing = currentIndex < rings.size() ? rings.get(currentIndex) : rings.get(rings.size() - 1); + double distance = playerLoc.distance(nearestRing.getCenter()); + + if (distance > config.getMaxDeviation()) { + return FallCheckResult.fall("Too far from course"); } - - // Check if player is flying (has elytra deployed) - if (!p.isGliding()) { - return new FallCheckResult(true, "Not gliding"); + + // Check if player is gliding + if (!player.player.isGliding()) { + return FallCheckResult.fall("Not gliding"); } - - return new FallCheckResult(false, null); + + return FallCheckResult.noFall(); } - + /** - * 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. + * Check if player passed through center of ring (for bonus points) */ 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; + return ring.isCentered(playerLoc, 0.5); // 50% of radius } - + /** - * Clean up physics data for a player. + * Apply firework boost if cooldown allows */ - public void cleanup(@NotNull UUID playerId) { - lastFireworkTime.remove(playerId); + public boolean applyFireworkBoost(@NotNull ParkourPlayer player) { + UUID uuid = player.getUUID(); + long now = System.currentTimeMillis(); + Long lastBoost = lastFireworkBoost.get(uuid); + + if (lastBoost != null && (now - lastBoost) < config.getFireworkCooldownMs()) { + return false; // Still on cooldown + } + + // Apply boost + Player p = player.player; + Vector velocity = p.getVelocity(); + Vector boost = p.getLocation().getDirection().multiply(config.getBoostPower()); + p.setVelocity(velocity.add(boost)); + + lastFireworkBoost.put(uuid, now); + return true; } - + /** - * Result of fall check. + * Cleanup player data + */ + public void cleanup(@NotNull UUID uuid) { + lastFireworkBoost.remove(uuid); + } + + /** + * Result of fall check */ public static class FallCheckResult { private final boolean shouldFall; private final String reason; - - public FallCheckResult(boolean shouldFall, String reason) { + + private FallCheckResult(boolean shouldFall, String reason) { this.shouldFall = shouldFall; this.reason = reason; } - - public boolean shouldFall() { return shouldFall; } - public String getReason() { return reason; } + + public static FallCheckResult fall(String reason) { + return new FallCheckResult(true, reason); + } + + public static FallCheckResult noFall() { + return new FallCheckResult(false, ""); + } + + public boolean shouldFall() { + return shouldFall; + } + + public String getReason() { + return reason; + } } -} \ No newline at end of file +} diff --git a/src/main/java/dev/loki/loparkour/mode/elytra/ElytraRenderer.java b/src/main/java/dev/loki/loparkour/mode/elytra/ElytraRenderer.java index 613fecd..c7c4ca8 100644 --- a/src/main/java/dev/loki/loparkour/mode/elytra/ElytraRenderer.java +++ b/src/main/java/dev/loki/loparkour/mode/elytra/ElytraRenderer.java @@ -1,101 +1,58 @@ 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. + * Renders rings using particles for Elytra mode */ 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; - + + private static final int PARTICLES_PER_RING = 32; + /** - * Render all rings with appropriate colors. + * Render rings starting from currentIndex */ - public void renderRings(@NotNull List 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); + public void renderRings(@NotNull List rings, int currentIndex) { + // Render next few rings ahead of player + int renderCount = Math.min(5, rings.size() - currentIndex); + + for (int i = 0; i < renderCount; i++) { + int index = currentIndex + i; + if (index >= rings.size()) break; + + ElytraRing ring = rings.get(index); + renderRing(ring, i == 0); // Highlight next ring } } - - /** - * Render a single ring with the specified color. - */ - public void renderRing(@NotNull ElytraRing ring, @NotNull Color color) { + + private void renderRing(@NotNull ElytraRing ring, boolean isNext) { 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()); + int radius = ring.getRadius(); + Vector direction = ring.getDirection(); + + // Calculate perpendicular vectors for ring plane + Vector up = new Vector(0, 1, 0); + Vector right = direction.clone().crossProduct(up).normalize(); + Vector actualUp = right.clone().crossProduct(direction).normalize(); + + // Particle color based on if it's the next ring + Particle particle = isNext ? Particle.VILLAGER_HAPPY : Particle.END_ROD; + + // Draw ring circle + for (int i = 0; i < PARTICLES_PER_RING; i++) { + double angle = 2 * Math.PI * i / PARTICLES_PER_RING; + double x = Math.cos(angle) * radius; + double y = Math.sin(angle) * radius; + + Vector offset = right.clone().multiply(x).add(actualUp.clone().multiply(y)); + Location particleLoc = center.clone().add(offset); + + center.getWorld().spawnParticle(particle, particleLoc, 1, 0, 0, 0, 0); } } - - @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(); - } - } -} \ No newline at end of file +} diff --git a/src/main/java/dev/loki/loparkour/mode/elytra/ElytraRing.java b/src/main/java/dev/loki/loparkour/mode/elytra/ElytraRing.java index 7e853a6..097be16 100644 --- a/src/main/java/dev/loki/loparkour/mode/elytra/ElytraRing.java +++ b/src/main/java/dev/loki/loparkour/mode/elytra/ElytraRing.java @@ -1,80 +1,77 @@ 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. + * 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; + private final int radius; + private final Vector direction; + private final int index; + + public ElytraRing(@NotNull Location center, int radius, @NotNull Vector direction, int index) { this.center = center.clone(); - this.normal = normal.clone().normalize(); this.radius = radius; + this.direction = direction.clone().normalize(); + this.index = index; } - + + @NotNull + public Location getCenter() { + return center.clone(); + } + + public int getRadius() { + return radius; + } + + @NotNull + public Vector getDirection() { + return direction.clone(); + } + + public int getIndex() { + return index; + } + /** - * Check if player has crossed through this ring. + * Check if a location is inside 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; + public boolean contains(@NotNull Location location) { + if (!location.getWorld().equals(center.getWorld())) { + return false; + } + + Vector toLocation = location.toVector().subtract(center.toVector()); + double distanceFromCenter = toLocation.length(); + + return distanceFromCenter <= radius; } - + /** - * Check if player is currently inside the ring. + * Check if a location is centered (within inner radius) in this 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; + public boolean isCentered(@NotNull Location location, double innerRadiusPercent) { + if (!location.getWorld().equals(center.getWorld())) { + return false; + } + + Vector toLocation = location.toVector().subtract(center.toVector()); + double distanceFromCenter = toLocation.length(); + double innerRadius = radius * innerRadiusPercent; + + return distanceFromCenter <= innerRadius; } - + /** - * Remove the ring's visual representation. + * Remove visual representation (called on cleanup) */ public void remove() { - if (entity != null && !entity.isDead()) { - entity.remove(); - } + // Particles are temporary, nothing to clean up } - - // 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; } -} \ No newline at end of file +} diff --git a/src/main/java/dev/loki/loparkour/mode/elytra/ElytraRingGenerator.java b/src/main/java/dev/loki/loparkour/mode/elytra/ElytraRingGenerator.java index d835552..7454a63 100644 --- a/src/main/java/dev/loki/loparkour/mode/elytra/ElytraRingGenerator.java +++ b/src/main/java/dev/loki/loparkour/mode/elytra/ElytraRingGenerator.java @@ -6,135 +6,82 @@ import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ThreadLocalRandom; +import java.util.Random; /** - * Generates rings for Elytra mode parkour. + * Generates rings along a curved aerial path for Elytra mode */ public class ElytraRingGenerator { - + private final ElytraConfig config; - private int nextRingId = 0; - + private final Random random = new Random(); + private int ringCounter = 0; + public ElytraRingGenerator(@NotNull ElytraConfig config) { this.config = config; } - + /** - * Generate a series of rings starting from origin in the given direction. + * Generate multiple rings starting from a location in a direction */ @NotNull - public List generateRings(@NotNull Location origin, @NotNull Vector direction, int count) { + public List generateRings(@NotNull Location start, @NotNull Vector direction, int count) { List rings = new ArrayList<>(); - - Location currentPos = origin.clone(); + Location current = start.clone(); Vector currentDir = direction.clone().normalize(); - + for (int i = 0; i < count; i++) { - // Calculate distance to next ring - double distance = calculateRingDistance(); - + // Random distance between min and max + int distance = config.getRingDistanceMin() + + random.nextInt(config.getRingDistanceMax() - config.getRingDistanceMin() + 1); + + // Apply random heading changes + currentDir = applyHeadingChange(currentDir); + // 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()); - + current.add(currentDir.clone().multiply(distance)); + + // Clamp height + double spawnY = start.getY(); + double maxY = spawnY + config.getMaxHeightAboveSpawn(); + if (current.getY() > maxY) { + current.setY(maxY); + } + if (current.getY() < spawnY - 20) { + current.setY(spawnY - 20); + } + + ElytraRing ring = new ElytraRing(current, config.getRingSize(), currentDir, ringCounter++); 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 = Math.max( - origin.getWorld().getMinHeight() + 10, // World minimum + safety margin - origin.getY() - 20 // Don't go too far below spawn - ); - double worldMaxHeight = origin.getWorld().getMaxHeight() - 10; // Safety margin from world ceiling - // Constrain to configured limits - if (pos.getY() > maxHeight) { - pos.setY(Math.min(maxHeight, worldMaxHeight)); - } else if (pos.getY() < minHeight) { - pos.setY(minHeight); - } - - // Final world bounds check - if (pos.getY() > worldMaxHeight) { - pos.setY(worldMaxHeight); - } else if (pos.getY() < origin.getWorld().getMinHeight()) { - pos.setY(origin.getWorld().getMinHeight() + 10); - } - - 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); - } + private Vector applyHeadingChange(@NotNull Vector direction) { + // Random horizontal angle change + double horizontalChange = (random.nextDouble() * 2 - 1) * config.getMaxHeadingChangeHorizontal(); - // 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))); + // Random vertical angle change + double verticalChange = (random.nextDouble() * 2 - 1) * config.getMaxHeadingChangeVertical(); + + // Apply rotation + Vector result = direction.clone(); + // Horizontal rotation (yaw) + double yaw = Math.toRadians(horizontalChange); + double cos = Math.cos(yaw); + double sin = Math.sin(yaw); + double x = result.getX() * cos - result.getZ() * sin; + double z = result.getX() * sin + result.getZ() * cos; + result.setX(x); + result.setZ(z); + + // Vertical rotation (pitch) + double pitch = Math.toRadians(verticalChange); + result.setY(result.getY() + Math.sin(pitch) * 0.5); + return result.normalize(); } -} \ No newline at end of file +}