feat(generator): improve jump generation and block placement logic
Refactor the parkour generator to improve jump accuracy, block placement, and lifecycle management. - Improve jump calculation by normalizing heading vectors and adjusting height/distance constraints. - Implement height overrides for tall materials (fences, trapdoors, glass panes) during block placement. - Add difficulty-based special block generation (packed/blue ice). - Fix block identity issues by using coordinate-based comparison instead of reference equality for scoring and cleanup. - Optimize generation lead to reduce unnecessary block generation. - Update leaderboard logic to only replace scores if the new score is strictly better (higher score or faster time). - Add error handling for generator ticks to prevent session crashes. build: update build configuration and dependencies - Add `run-paper` plugin for easier local server testing. - Update Gradle wrapper to version 8.14.3. - Change WorldEdit dependencies to `compileOnly`. - Configure `runServer` task with Minecraft 1.21.4 and PlaceholderAPI. fix(ui): improve scoreboard placeholder handling - Add check for PlaceholderAPI plugin enablement before attempting to use it.
This commit is contained in:
parent
fd0ba2cc0e
commit
0b6409397b
15 changed files with 168 additions and 55 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -52,7 +52,7 @@ build/
|
|||
gradle-app.setting
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
run/
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
plugins {
|
||||
id("java")
|
||||
id("io.github.goooler.shadow") version("8.1.8")
|
||||
id("xyz.jpenilla.run-paper") version("3.0.2")
|
||||
}
|
||||
|
||||
group = "dev.loki"
|
||||
|
|
@ -90,8 +91,8 @@ dependencies {
|
|||
implementation("org.bstats:bstats-bukkit:3.2.1")
|
||||
implementation("com.zaxxer:HikariCP:5.1.0")
|
||||
implementation("com.mysql:mysql-connector-j:8.3.0")
|
||||
implementation("com.sk89q.worldedit:worldedit-core:7.3.6")
|
||||
implementation("com.sk89q.worldedit:worldedit-bukkit:7.3.6")
|
||||
compileOnly("com.sk89q.worldedit:worldedit-core:7.3.6")
|
||||
compileOnly("com.sk89q.worldedit:worldedit-bukkit:7.3.6")
|
||||
|
||||
// Provided dependencies (plugins)
|
||||
compileOnly("me.clip:placeholderapi:2.11.6")
|
||||
|
|
@ -119,8 +120,6 @@ tasks.shadowJar {
|
|||
relocate("io.papermc.lib", "dev.loki.loparkour.lib.paperlib")
|
||||
relocate("com.google.gson", "dev.loki.loparkour.lib.gson")
|
||||
relocate("org.bstats", "dev.loki.loparkour.lib.bstats")
|
||||
relocate("com.sk89q.worldedit", "dev.loki.loparkour.lib.worldedit")
|
||||
relocate("org.enginehub", "dev.loki.loparkour.lib.enginehub")
|
||||
|
||||
// Don't minimize - causes issues with Caffeine's dynamically generated classes
|
||||
// minimize {
|
||||
|
|
@ -156,5 +155,18 @@ tasks.withType<Javadoc> {
|
|||
options.encoding = "UTF-8"
|
||||
}
|
||||
|
||||
// Run paper server via gradlew runServer
|
||||
tasks {
|
||||
runServer {
|
||||
minecraftVersion("1.21.4")
|
||||
jvmArgs("-Xms2G", "-Xmx2G")
|
||||
runDirectory.set(layout.projectDirectory.dir("run"))
|
||||
|
||||
downloadPlugins {
|
||||
modrinth("placeholderapi", "2.11.6")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default task
|
||||
defaultTasks("clean", "build")
|
||||
|
|
|
|||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
|||
|
|
@ -121,6 +121,8 @@ public class GeneratorProfileManager {
|
|||
generator.state.specialChances.clear();
|
||||
|
||||
try {
|
||||
double difficulty = getDifficultyScore();
|
||||
|
||||
for (String specialType : Config.GENERATION.getChildren("advanced.special")) {
|
||||
String materialPath = "advanced.special." + specialType + ".material";
|
||||
String chancePath = "advanced.special." + specialType + ".chance";
|
||||
|
|
@ -132,6 +134,17 @@ public class GeneratorProfileManager {
|
|||
generator.state.specialChances.put(blockData, (double) chance);
|
||||
}
|
||||
|
||||
// Add ice variants on higher difficulties
|
||||
// difficulty >= ~1.0 = hard: packed ice, >= ~2.0 = expert: blue ice
|
||||
if (difficulty >= 1.0) {
|
||||
generator.state.specialChances.put(
|
||||
org.bukkit.Bukkit.createBlockData("minecraft:packed_ice"), 15.0);
|
||||
}
|
||||
if (difficulty >= 2.0) {
|
||||
generator.state.specialChances.put(
|
||||
org.bukkit.Bukkit.createBlockData("minecraft:blue_ice"), 10.0);
|
||||
}
|
||||
|
||||
normalizeMap(generator.state.specialChances);
|
||||
} catch (Exception ignored) {
|
||||
// special blocks config optional
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ public class JumpCalculator {
|
|||
distance = Math.min(distance, constraints.maxDistance);
|
||||
|
||||
// Clamp values to valid ranges
|
||||
height = Math.max(-2, Math.min(1, height));
|
||||
height = Math.max(-2, Math.min(2, height));
|
||||
distance = Math.max(1, Math.min(4, distance));
|
||||
|
||||
if (height > 0) distance = Math.max(distance - height, 1);
|
||||
|
|
@ -102,7 +102,7 @@ public class JumpCalculator {
|
|||
double sd = java.util.Arrays.asList(generator.generatorOptions).contains(GeneratorOption.REDUCE_RANDOM_BLOCK_SELECTION_ANGLE) ? 0.5 : 1;
|
||||
int randomOffset = new JumpOffsetGenerator(height, distance).getRandomOffset(0, sd);
|
||||
|
||||
Vector offset = generator.state.heading.clone().multiply(distance + 1).setY(height);
|
||||
Vector offset = generator.state.heading.clone().multiply(distance).setY(height);
|
||||
if (offset.getX() == 0) {
|
||||
offset.setX(randomOffset);
|
||||
} else {
|
||||
|
|
@ -133,17 +133,21 @@ public class JumpCalculator {
|
|||
|
||||
private int getSpecialMaterialMaxHeight(@NotNull Material material) {
|
||||
return switch (material) {
|
||||
case PACKED_ICE -> 1; // Ice allows normal height
|
||||
case LADDER -> 1; // Ladder allows some height
|
||||
default -> 0; // Most special materials restrict height
|
||||
case PACKED_ICE -> 1; // Ice allows normal height
|
||||
case BLUE_ICE -> 1; // Blue ice same
|
||||
case ICE -> 0; // Regular ice melts, slippery
|
||||
case LADDER -> 1; // Ladder allows some height
|
||||
default -> 0; // Most special materials restrict height
|
||||
};
|
||||
}
|
||||
|
||||
private int getSpecialMaterialMaxDistance(@NotNull Material material) {
|
||||
return switch (material) {
|
||||
case PACKED_ICE -> 3; // Ice is slippery
|
||||
case LADDER -> 2; // Ladder is very restrictive
|
||||
default -> 3; // Default restriction
|
||||
case PACKED_ICE -> 4; // Ice — full distance, slippery
|
||||
case BLUE_ICE -> 4; // Blue ice — full distance
|
||||
case ICE -> 3; // Regular ice — reduced
|
||||
case LADDER -> 2; // Ladder is very restrictive
|
||||
default -> 3; // Default restriction
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -101,7 +101,8 @@ public class JumpDirector {
|
|||
if (recommendedHeading.lengthSquared() == 0) {
|
||||
return current;
|
||||
} else {
|
||||
return recommendedHeading;
|
||||
// Normalize so diagonal vectors don't multiply distance by √2
|
||||
return recommendedHeading.normalize();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import dev.loki.loparkour.schematic.core.ParkourSchematic;
|
|||
import dev.loki.loparkour.schematic.core.SchematicManager;
|
||||
import dev.loki.loparkour.util.misc.Probs;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.block.Block;
|
||||
|
|
@ -50,6 +51,27 @@ public class BlockPlacer {
|
|||
}
|
||||
}
|
||||
|
||||
private boolean isTallMaterial(@NotNull Material mat) {
|
||||
return blockSelector.isFenceMaterial(mat) || blockSelector.isTrapdoorMaterial(mat) || mat == Material.GLASS_PANE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select blocks with a forced height override for tall materials (fences, trapdoors).
|
||||
*/
|
||||
@NotNull
|
||||
private List<Block> selectBlocks(int forcedHeight) {
|
||||
List<Block> blocks = new ArrayList<>();
|
||||
if (generator.state.history.isEmpty()) {
|
||||
return blocks;
|
||||
}
|
||||
Block current = generator.getLatest();
|
||||
int distance = Probs.random(generator.state.distanceChances);
|
||||
int height = forcedHeight != Integer.MIN_VALUE ? forcedHeight : Probs.random(generator.state.heightChances);
|
||||
Block nextBlock = jumpCalculator.calculateNextBlock(current, distance, height);
|
||||
blocks.add(nextBlock);
|
||||
return blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the first block at specified locations.
|
||||
*/
|
||||
|
|
@ -92,28 +114,31 @@ public class BlockPlacer {
|
|||
}
|
||||
|
||||
private void placeNormalBlock() {
|
||||
List<Block> blocks = selectBlocks();
|
||||
BlockData blockData = blockSelector.selectBlockData();
|
||||
if (blockData == null) return;
|
||||
|
||||
Material mat = blockData.getMaterial();
|
||||
boolean tall = isTallMaterial(mat);
|
||||
|
||||
List<Block> blocks = tall ? selectBlocks(-1) : selectBlocks();
|
||||
if (blocks.isEmpty()) return;
|
||||
|
||||
Block selectedBlock = blocks.get(0); // Use first block for simplicity
|
||||
BlockData blockData = blockSelector.selectBlockData();
|
||||
if (blockData == null) {
|
||||
return; // Skip if no valid block data available
|
||||
}
|
||||
|
||||
Block selectedBlock = blocks.get(0);
|
||||
placeBlockData(selectedBlock, blockData);
|
||||
generator.state.history.add(selectedBlock);
|
||||
}
|
||||
|
||||
private void placeSpecialBlock() {
|
||||
// Select special block type based on probabilities
|
||||
BlockData specialBlockData = Probs.random(generator.state.specialChances);
|
||||
if (specialBlockData == null) {
|
||||
placeNormalBlock();
|
||||
return;
|
||||
}
|
||||
|
||||
List<Block> blocks = selectBlocks();
|
||||
Material mat = specialBlockData.getMaterial();
|
||||
boolean tall = isTallMaterial(mat);
|
||||
|
||||
List<Block> blocks = tall ? selectBlocks(-1) : selectBlocks();
|
||||
if (blocks.isEmpty()) return;
|
||||
|
||||
Block selectedBlock = blocks.get(0);
|
||||
|
|
|
|||
|
|
@ -107,12 +107,14 @@ public class BlockSelector {
|
|||
return isSlabMaterial(material) ||
|
||||
material == Material.GLASS_PANE ||
|
||||
material == Material.PACKED_ICE ||
|
||||
material == Material.BLUE_ICE ||
|
||||
material == Material.ICE ||
|
||||
isFenceMaterial(material) ||
|
||||
isTrapdoorMaterial(material) ||
|
||||
material == Material.LADDER;
|
||||
}
|
||||
|
||||
private boolean isFenceMaterial(@NotNull Material material) {
|
||||
public boolean isFenceMaterial(@NotNull Material material) {
|
||||
return material == Material.OAK_FENCE ||
|
||||
material == Material.BIRCH_FENCE ||
|
||||
material == Material.SPRUCE_FENCE ||
|
||||
|
|
@ -126,7 +128,7 @@ public class BlockSelector {
|
|||
material == Material.NETHER_BRICK_FENCE;
|
||||
}
|
||||
|
||||
private boolean isTrapdoorMaterial(@NotNull Material material) {
|
||||
public boolean isTrapdoorMaterial(@NotNull Material material) {
|
||||
return material == Material.OAK_TRAPDOOR ||
|
||||
material == Material.BIRCH_TRAPDOOR ||
|
||||
material == Material.SPRUCE_TRAPDOOR ||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import org.jetbrains.annotations.NotNull;
|
|||
public class LifecycleTickManager {
|
||||
|
||||
private static final int TIME_UI_INTERVAL = 20; // 1 second — time display tick
|
||||
private static final int GENERATION_LEAD = 5;
|
||||
private static final int GENERATION_LEAD = 2; // keep only 2 blocks ahead, generate more when player approaches end
|
||||
|
||||
private final ParkourGenerator generator;
|
||||
private final GeneratorCleanup cleanup;
|
||||
|
|
@ -32,12 +32,10 @@ public class LifecycleTickManager {
|
|||
* Start the tick cycle.
|
||||
*/
|
||||
public void startTick() {
|
||||
if (generator.state.start == null) {
|
||||
generator.state.start = java.time.Instant.now();
|
||||
}
|
||||
// Timer starts on first block jump, not here — see PlayerInteractionHandler.handleScore()
|
||||
|
||||
// Generate initial blocks (reduced from GENERATION_LEAD to avoid too far generation)
|
||||
generator.generate(3);
|
||||
// Generate initial blocks ahead of the player
|
||||
generator.generate(2);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ public class GeneratorCleanup {
|
|||
for (ParkourPlayer pp : generator.getPlayers()) {
|
||||
Block below = blockBelowPlayer(pp);
|
||||
if (below == null) continue;
|
||||
int idx = history.indexOf(below);
|
||||
int idx = indexOfBlock(history, below);
|
||||
if (idx >= 0) {
|
||||
minStandingIndex = Math.min(minStandingIndex, idx);
|
||||
}
|
||||
|
|
@ -56,6 +56,17 @@ public class GeneratorCleanup {
|
|||
history.subList(0, removeCount).clear();
|
||||
}
|
||||
|
||||
/** Find index of a block in history by coordinates, not reference. */
|
||||
private static int indexOfBlock(@NotNull List<Block> list, @NotNull Block target) {
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
Block b = list.get(i);
|
||||
if (b != null && b.getX() == target.getX() && b.getY() == target.getY() && b.getZ() == target.getZ()) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static Block blockBelowPlayer(@NotNull ParkourPlayer pp) {
|
||||
Location loc = pp.player.getLocation().clone();
|
||||
Block below = loc.subtract(0, 1, 0).getBlock();
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ import org.bukkit.block.Block;
|
|||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Handles player interactions with parkour blocks.
|
||||
*/
|
||||
|
|
@ -43,10 +46,28 @@ public class PlayerInteractionHandler {
|
|||
}
|
||||
}
|
||||
|
||||
/** Compare two blocks by their world coordinates (Bukkit Block uses reference equality). */
|
||||
private static boolean isSameBlock(@NotNull Block a, @NotNull Block b) {
|
||||
return a.getX() == b.getX() && a.getY() == b.getY() && a.getZ() == b.getZ();
|
||||
}
|
||||
|
||||
/** Check if history contains a block by coordinates, not reference. */
|
||||
private static boolean historyContains(@NotNull List<Block> history, @NotNull Block block) {
|
||||
for (Block b : history) {
|
||||
if (isSameBlock(b, block)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle player scoring on a block.
|
||||
*/
|
||||
public void handleScore() {
|
||||
// Start the timer on the very first scored block
|
||||
if (generator.state.start == null) {
|
||||
generator.state.start = Instant.now();
|
||||
}
|
||||
|
||||
generator.state.score++;
|
||||
eventManager.handleScore();
|
||||
|
||||
|
|
@ -91,8 +112,8 @@ public class PlayerInteractionHandler {
|
|||
handleSchematicEndBlock(block);
|
||||
}
|
||||
|
||||
// Check for scoring
|
||||
if (generator.state.history.contains(block) && !player.hasScored(block)) {
|
||||
// Check for scoring (compare by coordinates, not reference — Bukkit Block uses identity)
|
||||
if (historyContains(generator.state.history, block) && !player.hasScored(block)) {
|
||||
player.markScored(block);
|
||||
handleScore();
|
||||
}
|
||||
|
|
@ -111,7 +132,7 @@ public class PlayerInteractionHandler {
|
|||
|
||||
private boolean isSchematicEndBlock(@NotNull Block block) {
|
||||
return generator.state.schematicBlocks != null
|
||||
&& generator.state.schematicBlocks.contains(block);
|
||||
&& historyContains(generator.state.schematicBlocks, block);
|
||||
}
|
||||
|
||||
/** Block directly under feet; {@code null} in air — scoring runs when landed. */
|
||||
|
|
|
|||
|
|
@ -67,17 +67,22 @@ public class Leaderboard {
|
|||
// ── CRUD operations ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Registers a new score, overriding the old one
|
||||
* Registers a new score — only replaces the old one if it's strictly better
|
||||
* (higher score, or same score with faster time).
|
||||
*
|
||||
* @param uuid The player's uuid
|
||||
* @param score The {@link Score} instance associated with a player's run
|
||||
* @return the previous score, if there was one
|
||||
* @return the previous best score, if there was one
|
||||
*/
|
||||
@Nullable
|
||||
public Score put(@NotNull UUID uuid, @NotNull Score score) {
|
||||
Score previous;
|
||||
synchronized (scores) {
|
||||
previous = scores.put(uuid, score);
|
||||
previous = scores.get(uuid);
|
||||
if (previous != null && !isBetterThan(score, previous)) {
|
||||
return previous; // keep the better score
|
||||
}
|
||||
scores.put(uuid, score);
|
||||
}
|
||||
|
||||
sorter.sortInPlace(scores);
|
||||
|
|
@ -85,6 +90,15 @@ public class Leaderboard {
|
|||
return previous;
|
||||
}
|
||||
|
||||
/** Returns true if {@code candidate} is strictly better than {@code existing}. */
|
||||
private static boolean isBetterThan(@NotNull Score candidate, @NotNull Score existing) {
|
||||
if (candidate.score() != existing.score()) {
|
||||
return candidate.score() > existing.score();
|
||||
}
|
||||
// same score — faster time wins
|
||||
return candidate.getTimeMillis() < existing.getTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the score of a player by deleting it from the internal map
|
||||
*
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@ public class ParkourPlayer extends ParkourUser {
|
|||
|
||||
public static final Map<String, PlayerSettingsManager.OptionContainer> PLAYER_COLUMNS = PlayerSettingsManager.getColumnMappings();
|
||||
|
||||
// Track scored blocks to prevent duplicate scoring
|
||||
private final Set<Block> scoredBlocks = new HashSet<>();
|
||||
// Track scored blocks to prevent duplicate scoring (String key = "x,y,z" to avoid Bukkit Block reference equality)
|
||||
private final Set<String> scoredBlocks = new HashSet<>();
|
||||
|
||||
public @Expose Double schematicDifficulty;
|
||||
public @Expose Integer blockLead;
|
||||
|
|
@ -115,18 +115,22 @@ public class ParkourPlayer extends ParkourUser {
|
|||
save(LoParkour.getPlugin().isEnabled());
|
||||
}
|
||||
|
||||
private static String blockKey(Block block) {
|
||||
return block.getX() + "," + block.getY() + "," + block.getZ();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if player has already scored on this block.
|
||||
*/
|
||||
public boolean hasScored(Block block) {
|
||||
return scoredBlocks.contains(block);
|
||||
return scoredBlocks.contains(blockKey(block));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark block as scored to prevent duplicate scoring.
|
||||
*/
|
||||
public void markScored(Block block) {
|
||||
scoredBlocks.add(block);
|
||||
scoredBlocks.add(blockKey(block));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
|||
import dev.loki.loparkour.leaderboard.core.Leaderboard;
|
||||
import dev.loki.loparkour.leaderboard.model.Score;
|
||||
import dev.loki.loparkour.util.text.ColorUtil;
|
||||
import me.clip.placeholderapi.PlaceholderAPI;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scoreboard.DisplaySlot;
|
||||
import org.bukkit.scoreboard.Objective;
|
||||
|
|
@ -124,10 +124,13 @@ public class ScoreboardManager {
|
|||
}
|
||||
|
||||
private String applyPlaceholderAPI(String text) {
|
||||
try {
|
||||
return PlaceholderAPI.setPlaceholders(player, text);
|
||||
} catch (Exception e) {
|
||||
return text;
|
||||
if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) {
|
||||
try {
|
||||
return me.clip.placeholderapi.PlaceholderAPI.setPlaceholders(player, text);
|
||||
} catch (Exception e) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,12 @@ public class SessionStateManager {
|
|||
|
||||
tickTask = Scheduler.get(LoParkour.getPlugin()).runTimer(() -> {
|
||||
if (session.generator != null) {
|
||||
session.generator.tick();
|
||||
try {
|
||||
session.generator.tick();
|
||||
} catch (Throwable t) {
|
||||
session.generator.session.getPlayers().forEach(p ->
|
||||
p.player.sendMessage("§c[LoParkour] Tick error: " + t.getMessage()));
|
||||
}
|
||||
}
|
||||
}, 0, 1); // Run every tick (1 = 50ms)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue