feat: add Elytra mode (basic S-level)
- elytra/section/: ElytraSection (5 knots + cubic spline via commons-math3), ElytraKnotDirector (dx~N(75,15), dy bias, dz~N(0,35)), ElytraPointType (CIRCLE Bresenham / FLAT), ClientBlockChanger (client-side sendBlockChanges, forward-limit min(vd,8)) - elytra/generator/ElytraGenerator extends ParkourGenerator: own tick() (score by velocity.x while gliding, section lifecycle, fall detection, sustained boost in ascending sections), reset, spawn with elytra - elytra/mode/ElytraMode: registered in Modes/PluginBootstrap, locale items ru/en - build.gradle: commons-math3 3.6.1 + relocate - checkstyle 0/0, PMD 0, build green (tests + shadowJar)
This commit is contained in:
parent
6f71a47ab3
commit
8fde1b579b
12 changed files with 759 additions and 4 deletions
|
|
@ -12,6 +12,7 @@ import dev.loki.loparkour.hook.papi.PAPIHook;
|
|||
import dev.loki.loparkour.listener.gameplay.ParkourRestrictionListener;
|
||||
import dev.loki.loparkour.listener.schematic.SchematicWandListener;
|
||||
import dev.loki.loparkour.listener.player.PlayerConnectionListener;
|
||||
import dev.loki.loparkour.elytra.mode.ElytraMode;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
import dev.loki.loparkour.mode.impl.barrier.InvisibleBarrierMode;
|
||||
import dev.loki.loparkour.mode.impl.multi.CoopMode;
|
||||
|
|
@ -76,6 +77,7 @@ public final class PluginBootstrap {
|
|||
Registry.register(new InvisibleBarrierMode());
|
||||
Registry.register(new CoopMode());
|
||||
Registry.register(new GravityShiftMode());
|
||||
Registry.register(new ElytraMode());
|
||||
Modes.init();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,200 @@
|
|||
package dev.loki.loparkour.elytra.generator;
|
||||
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.elytra.section.ClientBlockChanger;
|
||||
import dev.loki.loparkour.elytra.section.ElytraKnotDirector;
|
||||
import dev.loki.loparkour.elytra.section.ElytraPointType;
|
||||
import dev.loki.loparkour.elytra.section.ElytraSection;
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
import dev.loki.loparkour.mode.base.Mode;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
import dev.loki.loparkour.session.core.Session;
|
||||
import dev.loki.loparkour.util.item.Materials;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.util.Vector;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
public class ElytraGenerator extends ParkourGenerator {
|
||||
|
||||
private static final int ASCEND_THRESHOLD_Y = 50;
|
||||
private static final double SUSTAINED_BOOST = 0.08;
|
||||
private static final int DEFAULT_RADIUS = 3;
|
||||
private static final double GENERATE_DISTANCE = 6.0;
|
||||
private static final double EPSILON = 1e-6;
|
||||
public final Map<Integer, ElytraSection> sections = new LinkedHashMap<>();
|
||||
public final ClientBlockChanger blockChanger = new ClientBlockChanger();
|
||||
private double movementScore = 0.0;
|
||||
private Long startTime = null;
|
||||
private Vector playerSpawn;
|
||||
private Vector blockSpawn;
|
||||
private Random random;
|
||||
private final ElytraPointType pointType = ElytraPointType.CIRCLE;
|
||||
private final int radius;
|
||||
private final Material material;
|
||||
private int boostedSectionIdx = Integer.MIN_VALUE;
|
||||
|
||||
public ElytraGenerator(Session session) {
|
||||
super(session);
|
||||
this.radius = Config.GENERATION.getInt("elytra.radius", DEFAULT_RADIUS);
|
||||
this.material = Materials.parseOr(Config.GENERATION.getString("advanced.island.parkour.first-block-material"), Material.WHITE_STAINED_GLASS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void generateFirst(Location spawn, Location block) {
|
||||
this.playerSpawn = spawn.toVector();
|
||||
this.blockSpawn = block.toVector();
|
||||
this.random = new Random(System.currentTimeMillis());
|
||||
final Player player = getPlayer();
|
||||
if (player != null) {
|
||||
if (player.getInventory().getChestplate() == null || player.getInventory().getChestplate().getType() != Material.ELYTRA) {
|
||||
player.getInventory().setChestplate(new ItemStack(Material.ELYTRA));
|
||||
}
|
||||
player.setGliding(true);
|
||||
player.setVelocity(new Vector(1.0, 0.0, 0.0));
|
||||
}
|
||||
generate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void generate() {
|
||||
final int idx;
|
||||
final ElytraSection section;
|
||||
if (sections.isEmpty()) {
|
||||
idx = 0;
|
||||
section = new ElytraSection(blockSpawn.clone().add(new Vector(0, pointType.heightOffset, 0)), random,
|
||||
ElytraKnotDirector.DEFAULT_VERTICAL_BIAS);
|
||||
} else {
|
||||
final int lastIdx = sections.keySet().stream().max(Integer::compareTo).orElse(0);
|
||||
final ElytraSection previous = sections.get(lastIdx);
|
||||
final int bias = previous.end().getY() < ASCEND_THRESHOLD_Y ? ElytraKnotDirector.ASCENDING_VERTICAL_BIAS
|
||||
: ElytraKnotDirector.DEFAULT_VERTICAL_BIAS;
|
||||
idx = lastIdx + 1;
|
||||
section = new ElytraSection(previous.end(), random, bias);
|
||||
}
|
||||
sections.put(idx, section);
|
||||
blockChanger.queue(section.generateBlocks(pointType, radius));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startTick() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
final Player player = getPlayer();
|
||||
if (player == null || sections.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
final int idx = sections.keySet().stream().max(Integer::compareTo).orElse(-1);
|
||||
final ElytraSection section = sections.get(idx);
|
||||
if (section == null) {
|
||||
return;
|
||||
}
|
||||
final Vector pos = player.getLocation().toVector();
|
||||
applyScoring(player, pos);
|
||||
blockChanger.check(player, material);
|
||||
removeOldest(player);
|
||||
if (shouldReset(player, pos)) {
|
||||
reset(true);
|
||||
return;
|
||||
}
|
||||
if (section.isAscending()) {
|
||||
sustainAscendingPush(player, section, idx);
|
||||
}
|
||||
if (section.isNearKnot(pos, 2, GENERATE_DISTANCE)) {
|
||||
generate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset(boolean regenerate) {
|
||||
final Player player = getPlayer();
|
||||
for (final ElytraSection section : sections.values()) {
|
||||
if (player != null) {
|
||||
section.clear(player);
|
||||
}
|
||||
}
|
||||
sections.clear();
|
||||
blockChanger.clear();
|
||||
movementScore = 0;
|
||||
startTime = null;
|
||||
boostedSectionIdx = Integer.MIN_VALUE;
|
||||
if (regenerate) {
|
||||
if (player != null && playerSpawn != null) {
|
||||
player.teleport(playerSpawn.toLocation(player.getWorld()));
|
||||
player.setGliding(true);
|
||||
}
|
||||
generate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mode getMode() {
|
||||
return Modes.ELYTRA;
|
||||
}
|
||||
|
||||
public int getElytraScore() {
|
||||
return (int) Math.round(movementScore);
|
||||
}
|
||||
|
||||
private Player getPlayer() {
|
||||
return session.getPlayers().isEmpty() ? null : session.getPlayers().get(0).player;
|
||||
}
|
||||
|
||||
private void removeOldest(Player player) {
|
||||
if (sections.size() <= 2) {
|
||||
return;
|
||||
}
|
||||
final ElytraSection old = sections.remove(sections.keySet().stream().min(Integer::compareTo).orElse(-1));
|
||||
if (old != null) {
|
||||
old.clear(player);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyScoring(Player player, Vector pos) {
|
||||
if (player.isGliding() && pos.getX() - blockSpawn.getX() > 0) {
|
||||
movementScore += player.getVelocity().getX();
|
||||
}
|
||||
if (startTime == null && movementScore > 0) {
|
||||
startTime = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldReset(Player player, Vector pos) {
|
||||
Map.Entry<Integer, ElytraSection> target = null;
|
||||
for (final Map.Entry<Integer, ElytraSection> entry : sections.entrySet()) {
|
||||
if (pos.getX() < entry.getValue().end().getX() && (target == null || entry.getKey() < target.getKey())) {
|
||||
target = entry;
|
||||
}
|
||||
}
|
||||
if (target == null) {
|
||||
return false;
|
||||
}
|
||||
final double progress = pos.getX() - target.getValue().beginning().getX();
|
||||
if (progress < 0) {
|
||||
return target.getKey() == 0 && pos.getY() < blockSpawn.getY() - radius;
|
||||
}
|
||||
return progress > 0 && (!player.isGliding() || !target.getValue().isNearPoint(pos, (int) progress, radius));
|
||||
}
|
||||
|
||||
private void sustainAscendingPush(Player player, ElytraSection section, int idx) {
|
||||
final Vector pos = player.getLocation().toVector();
|
||||
if (pos.getX() < section.beginning().getX() || pos.getX() >= section.end().getX()) {
|
||||
return;
|
||||
}
|
||||
if (boostedSectionIdx != idx) {
|
||||
boostedSectionIdx = idx;
|
||||
player.setGliding(true);
|
||||
}
|
||||
final Vector direction = section.end().clone().subtract(section.beginning()).normalize();
|
||||
if (direction.lengthSquared() < EPSILON) {
|
||||
return;
|
||||
}
|
||||
player.setVelocity(player.getVelocity().add(direction.multiply(SUSTAINED_BOOST)));
|
||||
}
|
||||
}
|
||||
55
src/main/java/dev/loki/loparkour/elytra/mode/ElytraMode.java
Normal file
55
src/main/java/dev/loki/loparkour/elytra/mode/ElytraMode.java
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
package dev.loki.loparkour.elytra.mode;
|
||||
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.elytra.generator.ElytraGenerator;
|
||||
import dev.loki.loparkour.leaderboard.core.Leaderboard;
|
||||
import dev.loki.loparkour.mode.base.Mode;
|
||||
import dev.loki.loparkour.mode.base.ModeMessages;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import dev.loki.loparkour.session.core.Session;
|
||||
import dev.loki.loparkour.util.item.Item;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Elytra gliding mode.
|
||||
*/
|
||||
public class ElytraMode implements Mode {
|
||||
|
||||
private final Leaderboard leaderboard = new Leaderboard(getName(), Leaderboard.Sort.SCORE);
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public String getName() {
|
||||
return "elytra";
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Item getItem(String locale) {
|
||||
return Locales.getItem(locale, "play.elytra.elytra");
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Leaderboard getLeaderboard() {
|
||||
return leaderboard;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void create(Player player) {
|
||||
if (!ModeMessages.checkJoiningEnabled(player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final ParkourPlayer pp = ParkourPlayer.getPlayer(player);
|
||||
if (pp != null && pp.session.generator != null && pp.session.generator.getMode() instanceof ElytraMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
player.closeInventory();
|
||||
|
||||
Session.create(ElytraGenerator::new, null, null, player);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
package dev.loki.loparkour.elytra.section;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.BlockState;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
/**
|
||||
* Pipes pending client-side block updates to the player without leaking server
|
||||
* memory. Positions are stored as {@link Vector}s rather than Bukkit Blocks to
|
||||
* avoid pinning chunks. At send time each batch is resolved and shipped as one
|
||||
* MultiBlockChange per chunk via {@link Player#sendBlockChanges}.
|
||||
*/
|
||||
public class ClientBlockChanger {
|
||||
|
||||
private static final int DEFAULT_MIN_VIEW_DISTANCE = 4;
|
||||
private static final int MAX_FORWARD_CHUNKS = 8;
|
||||
|
||||
private final Map<Integer, Set<Vector>> toChange = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Adds chunked block positions to the queue.
|
||||
*
|
||||
* @param blocks chunk X to block positions to add.
|
||||
*/
|
||||
public synchronized void queue(Map<Integer, Set<Vector>> blocks) {
|
||||
for (final Map.Entry<Integer, Set<Vector>> entry : blocks.entrySet()) {
|
||||
toChange.computeIfAbsent(entry.getKey(), k -> new HashSet<>()).addAll(entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends all queued chunks up to the player's forward render limit and removes
|
||||
* them from the queue. MUST be called on the main thread.
|
||||
*
|
||||
* @param player the player whose client should be updated.
|
||||
* @param material material to display the blocks as.
|
||||
*/
|
||||
public void check(Player player, Material material) {
|
||||
final int playerCx = player.getLocation().getChunk().getX();
|
||||
|
||||
final int rawVd = player.getClientViewDistance();
|
||||
final int vd = rawVd > 0 ? rawVd : Math.max(DEFAULT_MIN_VIEW_DISTANCE, player.getWorld().getViewDistance());
|
||||
final int forwardLimit = playerCx + Math.min(vd, MAX_FORWARD_CHUNKS);
|
||||
|
||||
final List<Map.Entry<Integer, Set<Vector>>> toSend = new ArrayList<>();
|
||||
synchronized (this) {
|
||||
final Iterator<Map.Entry<Integer, Set<Vector>>> iterator = toChange.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
final Map.Entry<Integer, Set<Vector>> entry = iterator.next();
|
||||
if (entry.getKey() <= forwardLimit) {
|
||||
toSend.add(Map.entry(entry.getKey(), new HashSet<>(entry.getValue())));
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (toSend.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
renderBatches(player, toSend, material);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drains and sends everything currently queued, then clears the queue.
|
||||
*
|
||||
* @param player the player whose client should be updated.
|
||||
* @param material material to display the blocks as.
|
||||
*/
|
||||
public void flush(Player player, Material material) {
|
||||
final List<Map.Entry<Integer, Set<Vector>>> toSend = new ArrayList<>();
|
||||
synchronized (this) {
|
||||
for (final Map.Entry<Integer, Set<Vector>> entry : toChange.entrySet()) {
|
||||
toSend.add(Map.entry(entry.getKey(), new HashSet<>(entry.getValue())));
|
||||
}
|
||||
toChange.clear();
|
||||
}
|
||||
|
||||
if (toSend.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
renderBatches(player, toSend, material);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the queue without sending anything.
|
||||
*/
|
||||
public synchronized void clear() {
|
||||
toChange.clear();
|
||||
}
|
||||
|
||||
private void renderBatches(Player player, List<Map.Entry<Integer, Set<Vector>>> batches, Material material) {
|
||||
final World world = player.getWorld();
|
||||
for (final Map.Entry<Integer, Set<Vector>> batch : batches) {
|
||||
final Set<Vector> vectors = batch.getValue();
|
||||
if (vectors.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
final List<BlockState> states = new ArrayList<>(vectors.size());
|
||||
for (final Vector v : vectors) {
|
||||
final BlockState state = v.toLocation(world).getBlock().getState();
|
||||
state.setType(material);
|
||||
states.add(state);
|
||||
}
|
||||
player.sendBlockChanges(states);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package dev.loki.loparkour.elytra.section;
|
||||
|
||||
import java.util.Random;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
/**
|
||||
* Picks per-knot offsets for the spline-based parkour pipe.
|
||||
*
|
||||
* <p>The Y bias is parameterised: default is -15 (gentle descent). Pass a positive
|
||||
* value (e.g. +15) to make the pipe climb instead.</p>
|
||||
*/
|
||||
public class ElytraKnotDirector {
|
||||
|
||||
public static final int DEFAULT_VERTICAL_BIAS = -15;
|
||||
public static final int ASCENDING_VERTICAL_BIAS = 15;
|
||||
|
||||
private static final int DX_MEAN = 75;
|
||||
private static final int DX_SD = 15;
|
||||
private static final int DY_SD = 3;
|
||||
private static final int DZ_SD = 35;
|
||||
|
||||
private final Random random;
|
||||
private final int verticalBias;
|
||||
|
||||
/**
|
||||
* @param random source of randomness for the offsets.
|
||||
* @param verticalBias centre of the normal distribution used for the Y offset.
|
||||
*/
|
||||
public ElytraKnotDirector(Random random, int verticalBias) {
|
||||
this.random = random;
|
||||
this.verticalBias = verticalBias;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a random offset vector. The Y component is centred on the vertical
|
||||
* bias so the caller controls whether the section descends, ascends or hovers.
|
||||
*
|
||||
* @return a random offset vector.
|
||||
*/
|
||||
public Vector nextOffset() {
|
||||
final int dx = nextOffset(DX_MEAN, DX_SD);
|
||||
final int dy = nextOffset(verticalBias, DY_SD);
|
||||
final int dz = nextOffset(0, DZ_SD);
|
||||
|
||||
return new Vector(dx, dy, dz);
|
||||
}
|
||||
|
||||
private int nextOffset(int mean, int sd) {
|
||||
final int start = mean - 2 * sd;
|
||||
final int end = mean + 2 * sd;
|
||||
final double[] weights = new double[end - start + 1];
|
||||
double total = 0.0;
|
||||
|
||||
for (int i = 0; i < weights.length; i++) {
|
||||
final int value = start + i;
|
||||
weights[i] = normalPdf(mean, sd, value);
|
||||
total += weights[i];
|
||||
}
|
||||
|
||||
final double roll = random.nextDouble() * total;
|
||||
double cumulative = 0.0;
|
||||
for (int i = 0; i < weights.length; i++) {
|
||||
cumulative += weights[i];
|
||||
if (roll <= cumulative) {
|
||||
return start + i;
|
||||
}
|
||||
}
|
||||
return end;
|
||||
}
|
||||
|
||||
private static double normalPdf(double mean, double sd, double value) {
|
||||
final double variance = sd * sd;
|
||||
final double diff = value - mean;
|
||||
return Math.exp(-(diff * diff) / (2 * variance)) / Math.sqrt(2 * Math.PI * variance);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package dev.loki.loparkour.elytra.section;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
/**
|
||||
* Defines how a single spline point expands into a set of display block positions.
|
||||
*/
|
||||
public enum ElytraPointType {
|
||||
|
||||
CIRCLE(-1) {
|
||||
@Override
|
||||
public List<Vector> getPoints(Vector center, int radius) {
|
||||
if (radius == 0) {
|
||||
return List.of(center);
|
||||
}
|
||||
|
||||
final List<Vector> blocks = new ArrayList<>();
|
||||
final double centerX = Math.floor(center.getX()) + HALF_OFFSET;
|
||||
final double centerY = Math.floor(center.getY()) + HALF_OFFSET;
|
||||
final double centerZ = Math.floor(center.getZ()) + HALF_OFFSET;
|
||||
|
||||
int y = radius;
|
||||
int z = 0;
|
||||
int d = CIRCLE_START_D - 2 * radius;
|
||||
|
||||
while (z <= y) {
|
||||
blocks.add(new Vector(centerX, centerY + y, centerZ + z));
|
||||
blocks.add(new Vector(centerX, centerY + y, centerZ - z));
|
||||
blocks.add(new Vector(centerX, centerY - y, centerZ + z));
|
||||
blocks.add(new Vector(centerX, centerY - y, centerZ - z));
|
||||
blocks.add(new Vector(centerX, centerY + z, centerZ + y));
|
||||
blocks.add(new Vector(centerX, centerY + z, centerZ - y));
|
||||
blocks.add(new Vector(centerX, centerY - z, centerZ + y));
|
||||
blocks.add(new Vector(centerX, centerY - z, centerZ - y));
|
||||
|
||||
if (d < 0) {
|
||||
d += CIRCLE_STEP_MULTIPLIER * z + CIRCLE_INNER_INCREMENT;
|
||||
} else {
|
||||
d += CIRCLE_STEP_MULTIPLIER * (z - y) + CIRCLE_OUTER_INCREMENT;
|
||||
y--;
|
||||
}
|
||||
z++;
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
},
|
||||
FLAT(-5) {
|
||||
@Override
|
||||
public List<Vector> getPoints(Vector center, int radius) {
|
||||
final double x = center.getX();
|
||||
final double y = center.getY();
|
||||
final double z = center.getZ();
|
||||
|
||||
return List.of(
|
||||
new Vector(x, y, z - 1),
|
||||
new Vector(x, y, z),
|
||||
new Vector(x, y, z + 1)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private static final double HALF_OFFSET = 0.5;
|
||||
private static final int CIRCLE_START_D = 3;
|
||||
private static final int CIRCLE_STEP_MULTIPLIER = 4;
|
||||
private static final int CIRCLE_INNER_INCREMENT = 6;
|
||||
private static final int CIRCLE_OUTER_INCREMENT = 10;
|
||||
|
||||
public final int heightOffset;
|
||||
|
||||
ElytraPointType(int heightOffset) {
|
||||
this.heightOffset = heightOffset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands a spline center point into display block positions.
|
||||
*
|
||||
* @param center the spline point to expand.
|
||||
* @param radius the radius of the pipe.
|
||||
* @return the block positions to display.
|
||||
*/
|
||||
public abstract List<Vector> getPoints(Vector center, int radius);
|
||||
}
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
package dev.loki.loparkour.elytra.section;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import org.apache.commons.math3.analysis.interpolation.SplineInterpolator;
|
||||
import org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.BlockState;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
/**
|
||||
* Represents a section of the total parkour of size KNOTS.
|
||||
*
|
||||
* <p>Stores {@link Vector} coordinates instead of Bukkit Block instances to avoid
|
||||
* pinning chunks in memory. Display blocks are sent client-side via
|
||||
* {@link Player#sendBlockChanges} and undone in {@link #clear(Player)}.</p>
|
||||
*/
|
||||
public class ElytraSection {
|
||||
|
||||
private static final int KNOTS = 5;
|
||||
private static final int EXTRA_POINTS_OFFSET = 1;
|
||||
private static final int CHUNK_SHIFT = 4;
|
||||
|
||||
private final ElytraKnotDirector director;
|
||||
private final SplineInterpolator interpolator;
|
||||
private final List<Vector> knots;
|
||||
private final List<Vector> points;
|
||||
private final boolean ascending;
|
||||
|
||||
private Map<Integer, Set<Vector>> blockMap;
|
||||
|
||||
/**
|
||||
* @param start first knot of the section.
|
||||
* @param random source of randomness for the knot offsets.
|
||||
* @param verticalBias vertical bias of the knot director.
|
||||
*/
|
||||
public ElytraSection(Vector start, Random random, int verticalBias) {
|
||||
director = new ElytraKnotDirector(random, verticalBias);
|
||||
interpolator = new SplineInterpolator();
|
||||
knots = generateKnots(start);
|
||||
points = generatePoints();
|
||||
ascending = verticalBias > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param idx index of the knot.
|
||||
* @return the knot at the given index.
|
||||
*/
|
||||
public Vector getKnot(int idx) {
|
||||
return knots.get(idx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param idx index of the point.
|
||||
* @return the spline point at the given index.
|
||||
*/
|
||||
public Vector getPoint(int idx) {
|
||||
return points.get(idx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param pos position to test against.
|
||||
* @param idx index of the knot.
|
||||
* @param distance maximum allowed distance.
|
||||
* @return whether the position is near the given knot.
|
||||
*/
|
||||
public boolean isNearKnot(Vector pos, int idx, double distance) {
|
||||
return knots.get(idx).distanceSquared(pos) < distance * distance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param pos position to test against.
|
||||
* @param idx index of the point.
|
||||
* @param distance maximum allowed distance.
|
||||
* @return whether the position is near the given point.
|
||||
*/
|
||||
public boolean isNearPoint(Vector pos, int idx, double distance) {
|
||||
return points.get(idx).distanceSquared(pos) < distance * distance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a copy of the first knot.
|
||||
*/
|
||||
public Vector beginning() {
|
||||
return knots.get(0).clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a copy of the last knot.
|
||||
*/
|
||||
public Vector end() {
|
||||
return knots.get(knots.size() - 1).clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true when this section was generated with an upward vertical bias.
|
||||
*/
|
||||
public boolean isAscending() {
|
||||
return ascending;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the block positions for this section synchronously, grouped by chunk X.
|
||||
* Results are also stashed in {@link #blockMap} so {@link #clear(Player)} can undo them.
|
||||
*
|
||||
* @param pointType how each spline point expands into display blocks.
|
||||
* @param radius radius of the pipe.
|
||||
* @return the generated block map, chunk X to block positions.
|
||||
*/
|
||||
public Map<Integer, Set<Vector>> generateBlocks(ElytraPointType pointType, int radius) {
|
||||
final Map<Integer, Set<Vector>> currentBlocks = new HashMap<>();
|
||||
|
||||
for (final Vector point : points) {
|
||||
final List<Vector> pts = pointType.getPoints(point, radius);
|
||||
for (final Vector pt : pts) {
|
||||
final int cx = pt.getBlockX() >> CHUNK_SHIFT;
|
||||
currentBlocks.computeIfAbsent(cx, k -> new HashSet<>()).add(pt);
|
||||
}
|
||||
}
|
||||
|
||||
blockMap = currentBlocks;
|
||||
return currentBlocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends AIR packets for every position this section had drawn on the client,
|
||||
* then drops the stored map so the GC can reclaim it. MUST be called on the
|
||||
* main thread.
|
||||
*
|
||||
* @param player the player whose client should be updated.
|
||||
*/
|
||||
public void clear(Player player) {
|
||||
final Map<Integer, Set<Vector>> localBlocks = blockMap;
|
||||
blockMap = null;
|
||||
if (localBlocks == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final World world = player.getWorld();
|
||||
for (final Map.Entry<Integer, Set<Vector>> entry : localBlocks.entrySet()) {
|
||||
final Set<Vector> vectors = entry.getValue();
|
||||
if (vectors.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
final List<BlockState> states = new ArrayList<>(vectors.size());
|
||||
for (final Vector v : vectors) {
|
||||
final BlockState state = v.toLocation(world).getBlock().getState();
|
||||
state.setType(Material.AIR);
|
||||
states.add(state);
|
||||
}
|
||||
player.sendBlockChanges(states);
|
||||
}
|
||||
localBlocks.clear();
|
||||
}
|
||||
|
||||
private List<Vector> generatePoints() {
|
||||
final List<Vector> splineKnots = new ArrayList<>(knots);
|
||||
splineKnots.add(0, knots.get(0).clone().subtract(new Vector(EXTRA_POINTS_OFFSET, 0, 0)));
|
||||
splineKnots.add(knots.get(knots.size() - 1).clone().add(new Vector(EXTRA_POINTS_OFFSET, 0, 0)));
|
||||
|
||||
final double[] xs = new double[splineKnots.size()];
|
||||
final double[] ys = new double[splineKnots.size()];
|
||||
final double[] zs = new double[splineKnots.size()];
|
||||
for (int i = 0; i < splineKnots.size(); i++) {
|
||||
xs[i] = splineKnots.get(i).getX();
|
||||
ys[i] = splineKnots.get(i).getY();
|
||||
zs[i] = splineKnots.get(i).getZ();
|
||||
}
|
||||
|
||||
final PolynomialSplineFunction splineY = interpolator.interpolate(xs, ys);
|
||||
final PolynomialSplineFunction splineZ = interpolator.interpolate(xs, zs);
|
||||
|
||||
final List<Vector> result = new ArrayList<>();
|
||||
final int actualXFirst = (int) xs[EXTRA_POINTS_OFFSET];
|
||||
final int actualXLast = (int) xs[xs.length - 1 - EXTRA_POINTS_OFFSET];
|
||||
for (int x = actualXFirst; x <= actualXLast; x++) {
|
||||
result.add(new Vector(x, (int) splineY.value(x), (int) splineZ.value(x)));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<Vector> generateKnots(Vector start) {
|
||||
final List<Vector> result = new ArrayList<>(KNOTS);
|
||||
result.add(start);
|
||||
|
||||
for (int i = 0; i < KNOTS - 1; i++) {
|
||||
result.add(result.get(result.size() - 1).clone().add(director.nextOffset()));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package dev.loki.loparkour.mode.base;
|
||||
|
||||
import dev.loki.loparkour.api.core.Registry;
|
||||
import dev.loki.loparkour.elytra.mode.ElytraMode;
|
||||
import dev.loki.loparkour.mode.impl.barrier.InvisibleBarrierMode;
|
||||
import dev.loki.loparkour.mode.impl.multi.CoopMode;
|
||||
import dev.loki.loparkour.mode.impl.multi.RaceMode;
|
||||
|
|
@ -21,6 +22,7 @@ public final class Modes {
|
|||
public static RaceMode RACE;
|
||||
public static CoopMode COOP;
|
||||
public static GravityShiftMode GRAVITY_SHIFT;
|
||||
public static ElytraMode ELYTRA;
|
||||
|
||||
public static void init() {
|
||||
DEFAULT = (DefaultMode) Registry.getMode("default");
|
||||
|
|
@ -30,5 +32,6 @@ public final class Modes {
|
|||
RACE = (RaceMode) Registry.getMode("race");
|
||||
COOP = (CoopMode) Registry.getMode("coop");
|
||||
GRAVITY_SHIFT = (GravityShiftMode) Registry.getMode("gravity-shift");
|
||||
ELYTRA = (ElytraMode) Registry.getMode("elytra");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue