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.
This commit is contained in:
parent
3546d7f9e3
commit
ccf64b7dfd
5 changed files with 277 additions and 369 deletions
|
|
@ -3,45 +3,55 @@ package dev.loki.loparkour.mode.elytra;
|
||||||
import dev.loki.loparkour.config.Config;
|
import dev.loki.loparkour.config.Config;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration settings for Elytra mode.
|
* Configuration for Elytra mode loaded from config.yml
|
||||||
*/
|
*/
|
||||||
public class ElytraConfig {
|
public class ElytraConfig {
|
||||||
|
|
||||||
private final double ringDistanceMin;
|
private final int ringDistanceMin;
|
||||||
private final double ringDistanceMax;
|
private final int ringDistanceMax;
|
||||||
private final double ringSize;
|
private final int ringSize;
|
||||||
private final int ringLead;
|
private final int ringLead;
|
||||||
private final double maxHeadingChangeHorizontal;
|
private final int maxHeadingChangeHorizontal;
|
||||||
private final double maxHeadingChangeVertical;
|
private final int maxHeadingChangeVertical;
|
||||||
private final double maxHeightAboveSpawn;
|
private final int maxHeightAboveSpawn;
|
||||||
private final double maxDeviation;
|
private final int maxDeviation;
|
||||||
private final double boostPower;
|
private final double boostPower;
|
||||||
private final long fireworkCooldownMs;
|
private final long fireworkCooldownMs;
|
||||||
private final int startingFireworks;
|
private final int startingFireworks;
|
||||||
|
|
||||||
public ElytraConfig() {
|
public ElytraConfig() {
|
||||||
this.ringDistanceMin = Config.CONFIG.getDouble("modes.elytra.ring-distance-min");
|
this.ringDistanceMin = Config.CONFIG.isPath("modes.elytra.ring-distance-min")
|
||||||
this.ringDistanceMax = Config.CONFIG.getDouble("modes.elytra.ring-distance-max");
|
? Config.CONFIG.getInt("modes.elytra.ring-distance-min") : 20;
|
||||||
this.ringSize = Config.CONFIG.getDouble("modes.elytra.ring-size");
|
this.ringDistanceMax = Config.CONFIG.isPath("modes.elytra.ring-distance-max")
|
||||||
this.ringLead = Config.CONFIG.getInt("modes.elytra.ring-lead");
|
? Config.CONFIG.getInt("modes.elytra.ring-distance-max") : 40;
|
||||||
this.maxHeadingChangeHorizontal = Config.CONFIG.getDouble("modes.elytra.max-heading-change-horizontal");
|
this.ringSize = Config.CONFIG.isPath("modes.elytra.ring-size")
|
||||||
this.maxHeadingChangeVertical = Config.CONFIG.getDouble("modes.elytra.max-heading-change-vertical");
|
? Config.CONFIG.getInt("modes.elytra.ring-size") : 5;
|
||||||
this.maxHeightAboveSpawn = Config.CONFIG.getDouble("modes.elytra.max-height-above-spawn");
|
this.ringLead = Config.CONFIG.isPath("modes.elytra.ring-lead")
|
||||||
this.maxDeviation = Config.CONFIG.getDouble("modes.elytra.max-deviation");
|
? Config.CONFIG.getInt("modes.elytra.ring-lead") : 8;
|
||||||
this.boostPower = Config.CONFIG.getDouble("modes.elytra.boost-power");
|
this.maxHeadingChangeHorizontal = Config.CONFIG.isPath("modes.elytra.max-heading-change-horizontal")
|
||||||
this.fireworkCooldownMs = (long) Config.CONFIG.getDouble("modes.elytra.firework-cooldown-ms");
|
? Config.CONFIG.getInt("modes.elytra.max-heading-change-horizontal") : 30;
|
||||||
this.startingFireworks = Config.CONFIG.getInt("modes.elytra.starting-fireworks");
|
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 int getRingDistanceMin() { return ringDistanceMin; }
|
||||||
public double getRingDistanceMin() { return ringDistanceMin; }
|
public int getRingDistanceMax() { return ringDistanceMax; }
|
||||||
public double getRingDistanceMax() { return ringDistanceMax; }
|
public int getRingSize() { return ringSize; }
|
||||||
public double getRingSize() { return ringSize; }
|
|
||||||
public int getRingLead() { return ringLead; }
|
public int getRingLead() { return ringLead; }
|
||||||
public double getMaxHeadingChangeHorizontal() { return maxHeadingChangeHorizontal; }
|
public int getMaxHeadingChangeHorizontal() { return maxHeadingChangeHorizontal; }
|
||||||
public double getMaxHeadingChangeVertical() { return maxHeadingChangeVertical; }
|
public int getMaxHeadingChangeVertical() { return maxHeadingChangeVertical; }
|
||||||
public double getMaxHeightAboveSpawn() { return maxHeightAboveSpawn; }
|
public int getMaxHeightAboveSpawn() { return maxHeightAboveSpawn; }
|
||||||
public double getMaxDeviation() { return maxDeviation; }
|
public int getMaxDeviation() { return maxDeviation; }
|
||||||
public double getBoostPower() { return boostPower; }
|
public double getBoostPower() { return boostPower; }
|
||||||
public long getFireworkCooldownMs() { return fireworkCooldownMs; }
|
public long getFireworkCooldownMs() { return fireworkCooldownMs; }
|
||||||
public int getStartingFireworks() { return startingFireworks; }
|
public int getStartingFireworks() { return startingFireworks; }
|
||||||
|
|
|
||||||
|
|
@ -12,129 +12,126 @@ import java.util.Map;
|
||||||
import java.util.UUID;
|
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 {
|
public class ElytraPhysics {
|
||||||
|
|
||||||
private final ElytraConfig config;
|
private final ElytraConfig config;
|
||||||
private final Map<UUID, Long> lastFireworkTime = new HashMap<>();
|
private final Map<UUID, Long> lastFireworkBoost = new HashMap<>();
|
||||||
|
|
||||||
public ElytraPhysics(@NotNull ElytraConfig config) {
|
public ElytraPhysics(@NotNull ElytraConfig config) {
|
||||||
this.config = 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<ElytraRing> rings, int currentRingIndex) {
|
public int checkRingCrossings(@NotNull ParkourPlayer player, @NotNull List<ElytraRing> rings, int currentIndex) {
|
||||||
Player p = player.player;
|
if (currentIndex >= rings.size()) {
|
||||||
Location playerLoc = p.getLocation();
|
return currentIndex;
|
||||||
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
|
Location playerLoc = player.player.getLocation();
|
||||||
|
|
||||||
|
// 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
|
@NotNull
|
||||||
public FallCheckResult checkFall(@NotNull ParkourPlayer player, @NotNull List<ElytraRing> rings, int currentRingIndex) {
|
public FallCheckResult checkFall(@NotNull ParkourPlayer player, @NotNull List<ElytraRing> rings, int currentIndex) {
|
||||||
Player p = player.player;
|
if (rings.isEmpty()) {
|
||||||
Location playerLoc = p.getLocation();
|
return FallCheckResult.noFall();
|
||||||
|
|
||||||
if (rings.isEmpty() || currentRingIndex >= rings.size()) {
|
|
||||||
return new FallCheckResult(false, "No rings available");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check distance to current ring
|
Location playerLoc = player.player.getLocation();
|
||||||
ElytraRing currentRing = rings.get(currentRingIndex);
|
|
||||||
double distanceToRing = playerLoc.distance(currentRing.getCenter());
|
|
||||||
|
|
||||||
if (distanceToRing > config.getMaxDeviation()) {
|
// Check if player is too far from nearest ring
|
||||||
return new FallCheckResult(true, "Too far from course (distance: " + String.format("%.1f", distanceToRing) + ")");
|
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)
|
// Check if player is gliding
|
||||||
if (!p.isGliding()) {
|
if (!player.player.isGliding()) {
|
||||||
return new FallCheckResult(true, "Not gliding");
|
return FallCheckResult.fall("Not gliding");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new FallCheckResult(false, null);
|
return FallCheckResult.noFall();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply firework boost to player if cooldown allows.
|
* Check if player passed through center of ring (for bonus points)
|
||||||
|
*/
|
||||||
|
public boolean isPlayerCentered(@NotNull Location playerLoc, @NotNull ElytraRing ring) {
|
||||||
|
return ring.isCentered(playerLoc, 0.5); // 50% of radius
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply firework boost if cooldown allows
|
||||||
*/
|
*/
|
||||||
public boolean applyFireworkBoost(@NotNull ParkourPlayer player) {
|
public boolean applyFireworkBoost(@NotNull ParkourPlayer player) {
|
||||||
Player p = player.player;
|
UUID uuid = player.getUUID();
|
||||||
UUID playerId = p.getUniqueId();
|
long now = System.currentTimeMillis();
|
||||||
long currentTime = System.currentTimeMillis();
|
Long lastBoost = lastFireworkBoost.get(uuid);
|
||||||
|
|
||||||
// Check cooldown
|
if (lastBoost != null && (now - lastBoost) < config.getFireworkCooldownMs()) {
|
||||||
Long lastBoost = lastFireworkTime.get(playerId);
|
|
||||||
if (lastBoost != null && (currentTime - lastBoost) < config.getFireworkCooldownMs()) {
|
|
||||||
return false; // Still on cooldown
|
return false; // Still on cooldown
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply boost
|
// Apply boost
|
||||||
|
Player p = player.player;
|
||||||
Vector velocity = p.getVelocity();
|
Vector velocity = p.getVelocity();
|
||||||
Vector boostedVelocity = velocity.multiply(config.getBoostPower());
|
Vector boost = p.getLocation().getDirection().multiply(config.getBoostPower());
|
||||||
p.setVelocity(boostedVelocity);
|
p.setVelocity(velocity.add(boost));
|
||||||
|
|
||||||
// Update cooldown
|
|
||||||
lastFireworkTime.put(playerId, currentTime);
|
|
||||||
|
|
||||||
|
lastFireworkBoost.put(uuid, now);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if player is centered when passing through a ring.
|
* Cleanup player data
|
||||||
*/
|
*/
|
||||||
public boolean isPlayerCentered(@NotNull Location playerLoc, @NotNull ElytraRing ring) {
|
public void cleanup(@NotNull UUID uuid) {
|
||||||
Vector toPlayer = playerLoc.toVector().subtract(ring.getCenter().toVector());
|
lastFireworkBoost.remove(uuid);
|
||||||
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.
|
* Result of fall check
|
||||||
*/
|
|
||||||
public void cleanup(@NotNull UUID playerId) {
|
|
||||||
lastFireworkTime.remove(playerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Result of fall check.
|
|
||||||
*/
|
*/
|
||||||
public static class FallCheckResult {
|
public static class FallCheckResult {
|
||||||
private final boolean shouldFall;
|
private final boolean shouldFall;
|
||||||
private final String reason;
|
private final String reason;
|
||||||
|
|
||||||
public FallCheckResult(boolean shouldFall, String reason) {
|
private FallCheckResult(boolean shouldFall, String reason) {
|
||||||
this.shouldFall = shouldFall;
|
this.shouldFall = shouldFall;
|
||||||
this.reason = reason;
|
this.reason = reason;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean shouldFall() { return shouldFall; }
|
public static FallCheckResult fall(String reason) {
|
||||||
public String getReason() { return reason; }
|
return new FallCheckResult(true, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FallCheckResult noFall() {
|
||||||
|
return new FallCheckResult(false, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shouldFall() {
|
||||||
|
return shouldFall;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getReason() {
|
||||||
|
return reason;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,101 +1,58 @@
|
||||||
package dev.loki.loparkour.mode.elytra;
|
package dev.loki.loparkour.mode.elytra;
|
||||||
|
|
||||||
import org.bukkit.Color;
|
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.Particle;
|
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.bukkit.util.Vector;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles visual rendering of Elytra rings and effects.
|
* Renders rings using particles for Elytra mode
|
||||||
*/
|
*/
|
||||||
public class ElytraRenderer {
|
public class ElytraRenderer {
|
||||||
|
|
||||||
private static final int RING_PARTICLES = 32;
|
private static final int PARTICLES_PER_RING = 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.
|
* Render rings starting from currentIndex
|
||||||
*/
|
*/
|
||||||
public void renderRings(@NotNull List<ElytraRing> rings, int currentRingIndex) {
|
public void renderRings(@NotNull List<ElytraRing> rings, int currentIndex) {
|
||||||
for (int i = 0; i < rings.size(); i++) {
|
// Render next few rings ahead of player
|
||||||
ElytraRing ring = rings.get(i);
|
int renderCount = Math.min(5, rings.size() - currentIndex);
|
||||||
Color color = getRingColor(i, currentRingIndex, ring.isPassed());
|
|
||||||
renderRing(ring, color);
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private void renderRing(@NotNull ElytraRing ring, boolean isNext) {
|
||||||
* Render a single ring with the specified color.
|
|
||||||
*/
|
|
||||||
public void renderRing(@NotNull ElytraRing ring, @NotNull Color color) {
|
|
||||||
Location center = ring.getCenter();
|
Location center = ring.getCenter();
|
||||||
Vector normal = ring.getNormal();
|
int radius = ring.getRadius();
|
||||||
double radius = ring.getRadius();
|
Vector direction = ring.getDirection();
|
||||||
|
|
||||||
// Create two perpendicular vectors to the normal
|
// Calculate perpendicular vectors for ring plane
|
||||||
Vector u = findPerpendicular(normal);
|
Vector up = new Vector(0, 1, 0);
|
||||||
Vector v = normal.getCrossProduct(u).normalize();
|
Vector right = direction.clone().crossProduct(up).normalize();
|
||||||
|
Vector actualUp = right.clone().crossProduct(direction).normalize();
|
||||||
|
|
||||||
// Draw circle using particles
|
// Particle color based on if it's the next ring
|
||||||
World world = center.getWorld();
|
Particle particle = isNext ? Particle.VILLAGER_HAPPY : Particle.END_ROD;
|
||||||
if (world == null) return;
|
|
||||||
|
|
||||||
for (int i = 0; i < RING_PARTICLES; i++) {
|
// Draw ring circle
|
||||||
double angle = 2 * Math.PI * i / RING_PARTICLES;
|
for (int i = 0; i < PARTICLES_PER_RING; i++) {
|
||||||
double x = radius * Math.cos(angle);
|
double angle = 2 * Math.PI * i / PARTICLES_PER_RING;
|
||||||
double y = radius * Math.sin(angle);
|
double x = Math.cos(angle) * radius;
|
||||||
|
double y = Math.sin(angle) * radius;
|
||||||
|
|
||||||
Vector offset = u.clone().multiply(x).add(v.clone().multiply(y));
|
Vector offset = right.clone().multiply(x).add(actualUp.clone().multiply(y));
|
||||||
Location particlePos = center.clone().add(offset);
|
Location particleLoc = center.clone().add(offset);
|
||||||
|
|
||||||
// Spawn colored particle
|
center.getWorld().spawnParticle(particle, particleLoc, 1, 0, 0, 0, 0);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,80 +1,77 @@
|
||||||
package dev.loki.loparkour.mode.elytra;
|
package dev.loki.loparkour.mode.elytra;
|
||||||
|
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.entity.ArmorStand;
|
|
||||||
import org.bukkit.util.Vector;
|
import org.bukkit.util.Vector;
|
||||||
import org.jetbrains.annotations.NotNull;
|
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 {
|
public class ElytraRing {
|
||||||
|
|
||||||
private final int id;
|
|
||||||
private final Location center;
|
private final Location center;
|
||||||
private final Vector normal;
|
private final int radius;
|
||||||
private final double radius;
|
private final Vector direction;
|
||||||
private ArmorStand entity;
|
private final int index;
|
||||||
private boolean passed = false;
|
|
||||||
|
|
||||||
public ElytraRing(int id, @NotNull Location center, @NotNull Vector normal, double radius) {
|
public ElytraRing(@NotNull Location center, int radius, @NotNull Vector direction, int index) {
|
||||||
this.id = id;
|
|
||||||
this.center = center.clone();
|
this.center = center.clone();
|
||||||
this.normal = normal.clone().normalize();
|
|
||||||
this.radius = radius;
|
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) {
|
public boolean contains(@NotNull Location location) {
|
||||||
if (passed) return false;
|
if (!location.getWorld().equals(center.getWorld())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Vector toPlayer = playerLoc.toVector().subtract(center.toVector());
|
Vector toLocation = location.toVector().subtract(center.toVector());
|
||||||
double distanceToPlane = toPlayer.dot(normal);
|
double distanceFromCenter = toLocation.length();
|
||||||
|
|
||||||
// Check if player is close to the ring plane
|
return distanceFromCenter <= radius;
|
||||||
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.
|
* Check if a location is centered (within inner radius) in this ring
|
||||||
*/
|
*/
|
||||||
public boolean isInside(@NotNull Location playerLoc) {
|
public boolean isCentered(@NotNull Location location, double innerRadiusPercent) {
|
||||||
Vector toPlayer = playerLoc.toVector().subtract(center.toVector());
|
if (!location.getWorld().equals(center.getWorld())) {
|
||||||
double distanceToPlane = Math.abs(toPlayer.dot(normal));
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (distanceToPlane > 1.0) return false;
|
Vector toLocation = location.toVector().subtract(center.toVector());
|
||||||
|
double distanceFromCenter = toLocation.length();
|
||||||
|
double innerRadius = radius * innerRadiusPercent;
|
||||||
|
|
||||||
Vector projectedPos = toPlayer.subtract(normal.clone().multiply(toPlayer.dot(normal)));
|
return distanceFromCenter <= innerRadius;
|
||||||
return projectedPos.length() <= radius;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove the ring's visual representation.
|
* Remove visual representation (called on cleanup)
|
||||||
*/
|
*/
|
||||||
public void remove() {
|
public void remove() {
|
||||||
if (entity != null && !entity.isDead()) {
|
// Particles are temporary, nothing to clean up
|
||||||
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; }
|
|
||||||
}
|
}
|
||||||
|
|
@ -6,134 +6,81 @@ import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
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 {
|
public class ElytraRingGenerator {
|
||||||
|
|
||||||
private final ElytraConfig config;
|
private final ElytraConfig config;
|
||||||
private int nextRingId = 0;
|
private final Random random = new Random();
|
||||||
|
private int ringCounter = 0;
|
||||||
|
|
||||||
public ElytraRingGenerator(@NotNull ElytraConfig config) {
|
public ElytraRingGenerator(@NotNull ElytraConfig config) {
|
||||||
this.config = 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
|
@NotNull
|
||||||
public List<ElytraRing> generateRings(@NotNull Location origin, @NotNull Vector direction, int count) {
|
public List<ElytraRing> generateRings(@NotNull Location start, @NotNull Vector direction, int count) {
|
||||||
List<ElytraRing> rings = new ArrayList<>();
|
List<ElytraRing> rings = new ArrayList<>();
|
||||||
|
Location current = start.clone();
|
||||||
Location currentPos = origin.clone();
|
|
||||||
Vector currentDir = direction.clone().normalize();
|
Vector currentDir = direction.clone().normalize();
|
||||||
|
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
// Calculate distance to next ring
|
// Random distance between min and max
|
||||||
double distance = calculateRingDistance();
|
int distance = config.getRingDistanceMin() +
|
||||||
|
random.nextInt(config.getRingDistanceMax() - config.getRingDistanceMin() + 1);
|
||||||
|
|
||||||
|
// Apply random heading changes
|
||||||
|
currentDir = applyHeadingChange(currentDir);
|
||||||
|
|
||||||
// Move to next ring position
|
// Move to next ring position
|
||||||
currentPos.add(currentDir.clone().multiply(distance));
|
current.add(currentDir.clone().multiply(distance));
|
||||||
|
|
||||||
// Apply course variation
|
// Clamp height
|
||||||
currentDir = applyDirectionVariation(currentDir);
|
double spawnY = start.getY();
|
||||||
|
double maxY = spawnY + config.getMaxHeightAboveSpawn();
|
||||||
// Ensure we don't go too high or too low
|
if (current.getY() > maxY) {
|
||||||
currentPos = constrainHeight(currentPos, origin);
|
current.setY(maxY);
|
||||||
|
}
|
||||||
// Create ring with perpendicular normal
|
if (current.getY() < spawnY - 20) {
|
||||||
Vector normal = findPerpendicularVector(currentDir);
|
current.setY(spawnY - 20);
|
||||||
ElytraRing ring = new ElytraRing(nextRingId++, currentPos.clone(), normal, config.getRingSize());
|
}
|
||||||
|
|
||||||
|
ElytraRing ring = new ElytraRing(current, config.getRingSize(), currentDir, ringCounter++);
|
||||||
rings.add(ring);
|
rings.add(ring);
|
||||||
}
|
}
|
||||||
|
|
||||||
return rings;
|
return rings;
|
||||||
}
|
}
|
||||||
|
|
||||||
private double calculateRingDistance() {
|
|
||||||
return ThreadLocalRandom.current().nextDouble(
|
|
||||||
config.getRingDistanceMin(),
|
|
||||||
config.getRingDistanceMax()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private Vector applyDirectionVariation(@NotNull Vector currentDir) {
|
private Vector applyHeadingChange(@NotNull Vector direction) {
|
||||||
double maxHorizontalChange = Math.toRadians(config.getMaxHeadingChangeHorizontal());
|
// Random horizontal angle change
|
||||||
double maxVerticalChange = Math.toRadians(config.getMaxHeadingChangeVertical());
|
double horizontalChange = (random.nextDouble() * 2 - 1) * config.getMaxHeadingChangeHorizontal();
|
||||||
|
|
||||||
// Random horizontal rotation
|
// Random vertical angle change
|
||||||
double yawChange = ThreadLocalRandom.current().nextDouble(-maxHorizontalChange, maxHorizontalChange);
|
double verticalChange = (random.nextDouble() * 2 - 1) * config.getMaxHeadingChangeVertical();
|
||||||
|
|
||||||
// Random vertical rotation
|
// Apply rotation
|
||||||
double pitchChange = ThreadLocalRandom.current().nextDouble(-maxVerticalChange, maxVerticalChange);
|
Vector result = direction.clone();
|
||||||
|
|
||||||
return rotateVector(currentDir, yawChange, pitchChange);
|
// 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);
|
||||||
|
|
||||||
@NotNull
|
// Vertical rotation (pitch)
|
||||||
private Location constrainHeight(@NotNull Location pos, @NotNull Location origin) {
|
double pitch = Math.toRadians(verticalChange);
|
||||||
double maxHeight = origin.getY() + config.getMaxHeightAboveSpawn();
|
result.setY(result.getY() + Math.sin(pitch) * 0.5);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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();
|
return result.normalize();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue