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
|
gradle-app.setting
|
||||||
!**/src/main/**/build/
|
!**/src/main/**/build/
|
||||||
!**/src/test/**/build/
|
!**/src/test/**/build/
|
||||||
|
run/
|
||||||
# OS files
|
# OS files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
plugins {
|
plugins {
|
||||||
id("java")
|
id("java")
|
||||||
id("io.github.goooler.shadow") version("8.1.8")
|
id("io.github.goooler.shadow") version("8.1.8")
|
||||||
|
id("xyz.jpenilla.run-paper") version("3.0.2")
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "dev.loki"
|
group = "dev.loki"
|
||||||
|
|
@ -90,8 +91,8 @@ dependencies {
|
||||||
implementation("org.bstats:bstats-bukkit:3.2.1")
|
implementation("org.bstats:bstats-bukkit:3.2.1")
|
||||||
implementation("com.zaxxer:HikariCP:5.1.0")
|
implementation("com.zaxxer:HikariCP:5.1.0")
|
||||||
implementation("com.mysql:mysql-connector-j:8.3.0")
|
implementation("com.mysql:mysql-connector-j:8.3.0")
|
||||||
implementation("com.sk89q.worldedit:worldedit-core:7.3.6")
|
compileOnly("com.sk89q.worldedit:worldedit-core:7.3.6")
|
||||||
implementation("com.sk89q.worldedit:worldedit-bukkit:7.3.6")
|
compileOnly("com.sk89q.worldedit:worldedit-bukkit:7.3.6")
|
||||||
|
|
||||||
// Provided dependencies (plugins)
|
// Provided dependencies (plugins)
|
||||||
compileOnly("me.clip:placeholderapi:2.11.6")
|
compileOnly("me.clip:placeholderapi:2.11.6")
|
||||||
|
|
@ -119,8 +120,6 @@ tasks.shadowJar {
|
||||||
relocate("io.papermc.lib", "dev.loki.loparkour.lib.paperlib")
|
relocate("io.papermc.lib", "dev.loki.loparkour.lib.paperlib")
|
||||||
relocate("com.google.gson", "dev.loki.loparkour.lib.gson")
|
relocate("com.google.gson", "dev.loki.loparkour.lib.gson")
|
||||||
relocate("org.bstats", "dev.loki.loparkour.lib.bstats")
|
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
|
// Don't minimize - causes issues with Caffeine's dynamically generated classes
|
||||||
// minimize {
|
// minimize {
|
||||||
|
|
@ -156,5 +155,18 @@ tasks.withType<Javadoc> {
|
||||||
options.encoding = "UTF-8"
|
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
|
// Default task
|
||||||
defaultTasks("clean", "build")
|
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
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
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
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,8 @@ public class GeneratorProfileManager {
|
||||||
generator.state.specialChances.clear();
|
generator.state.specialChances.clear();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
double difficulty = getDifficultyScore();
|
||||||
|
|
||||||
for (String specialType : Config.GENERATION.getChildren("advanced.special")) {
|
for (String specialType : Config.GENERATION.getChildren("advanced.special")) {
|
||||||
String materialPath = "advanced.special." + specialType + ".material";
|
String materialPath = "advanced.special." + specialType + ".material";
|
||||||
String chancePath = "advanced.special." + specialType + ".chance";
|
String chancePath = "advanced.special." + specialType + ".chance";
|
||||||
|
|
@ -132,6 +134,17 @@ public class GeneratorProfileManager {
|
||||||
generator.state.specialChances.put(blockData, (double) chance);
|
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);
|
normalizeMap(generator.state.specialChances);
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
// special blocks config optional
|
// special blocks config optional
|
||||||
|
|
|
||||||
|
|
@ -48,9 +48,9 @@ public class JumpCalculator {
|
||||||
distance = Math.min(distance, constraints.maxDistance);
|
distance = Math.min(distance, constraints.maxDistance);
|
||||||
|
|
||||||
// Clamp values to valid ranges
|
// 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));
|
distance = Math.max(1, Math.min(4, distance));
|
||||||
|
|
||||||
if (height > 0) distance = Math.max(distance - height, 1);
|
if (height > 0) distance = Math.max(distance - height, 1);
|
||||||
|
|
||||||
// Calculate offset
|
// Calculate offset
|
||||||
|
|
@ -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;
|
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);
|
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) {
|
if (offset.getX() == 0) {
|
||||||
offset.setX(randomOffset);
|
offset.setX(randomOffset);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -133,17 +133,21 @@ public class JumpCalculator {
|
||||||
|
|
||||||
private int getSpecialMaterialMaxHeight(@NotNull Material material) {
|
private int getSpecialMaterialMaxHeight(@NotNull Material material) {
|
||||||
return switch (material) {
|
return switch (material) {
|
||||||
case PACKED_ICE -> 1; // Ice allows normal height
|
case PACKED_ICE -> 1; // Ice allows normal height
|
||||||
case LADDER -> 1; // Ladder allows some height
|
case BLUE_ICE -> 1; // Blue ice same
|
||||||
default -> 0; // Most special materials restrict height
|
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) {
|
private int getSpecialMaterialMaxDistance(@NotNull Material material) {
|
||||||
return switch (material) {
|
return switch (material) {
|
||||||
case PACKED_ICE -> 3; // Ice is slippery
|
case PACKED_ICE -> 4; // Ice — full distance, slippery
|
||||||
case LADDER -> 2; // Ladder is very restrictive
|
case BLUE_ICE -> 4; // Blue ice — full distance
|
||||||
default -> 3; // Default restriction
|
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) {
|
if (recommendedHeading.lengthSquared() == 0) {
|
||||||
return current;
|
return current;
|
||||||
} else {
|
} 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.schematic.core.SchematicManager;
|
||||||
import dev.loki.loparkour.util.misc.Probs;
|
import dev.loki.loparkour.util.misc.Probs;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Material;
|
||||||
import org.bukkit.Particle;
|
import org.bukkit.Particle;
|
||||||
import org.bukkit.Sound;
|
import org.bukkit.Sound;
|
||||||
import org.bukkit.block.Block;
|
import org.bukkit.block.Block;
|
||||||
|
|
@ -40,7 +41,7 @@ public class BlockPlacer {
|
||||||
public void generate() {
|
public void generate() {
|
||||||
generate(1);
|
generate(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate specified amount of blocks.
|
* Generate specified amount of blocks.
|
||||||
*/
|
*/
|
||||||
|
|
@ -49,6 +50,27 @@ public class BlockPlacer {
|
||||||
generateSingleBlock();
|
generateSingleBlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.
|
* Generate the first block at specified locations.
|
||||||
|
|
@ -92,30 +114,33 @@ public class BlockPlacer {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void placeNormalBlock() {
|
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;
|
if (blocks.isEmpty()) return;
|
||||||
|
|
||||||
Block selectedBlock = blocks.get(0); // Use first block for simplicity
|
Block selectedBlock = blocks.get(0);
|
||||||
BlockData blockData = blockSelector.selectBlockData();
|
|
||||||
if (blockData == null) {
|
|
||||||
return; // Skip if no valid block data available
|
|
||||||
}
|
|
||||||
|
|
||||||
placeBlockData(selectedBlock, blockData);
|
placeBlockData(selectedBlock, blockData);
|
||||||
generator.state.history.add(selectedBlock);
|
generator.state.history.add(selectedBlock);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void placeSpecialBlock() {
|
private void placeSpecialBlock() {
|
||||||
// Select special block type based on probabilities
|
|
||||||
BlockData specialBlockData = Probs.random(generator.state.specialChances);
|
BlockData specialBlockData = Probs.random(generator.state.specialChances);
|
||||||
if (specialBlockData == null) {
|
if (specialBlockData == null) {
|
||||||
placeNormalBlock();
|
placeNormalBlock();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Block> blocks = selectBlocks();
|
Material mat = specialBlockData.getMaterial();
|
||||||
|
boolean tall = isTallMaterial(mat);
|
||||||
|
|
||||||
|
List<Block> blocks = tall ? selectBlocks(-1) : selectBlocks();
|
||||||
if (blocks.isEmpty()) return;
|
if (blocks.isEmpty()) return;
|
||||||
|
|
||||||
Block selectedBlock = blocks.get(0);
|
Block selectedBlock = blocks.get(0);
|
||||||
placeBlockData(selectedBlock, specialBlockData);
|
placeBlockData(selectedBlock, specialBlockData);
|
||||||
generator.state.history.add(selectedBlock);
|
generator.state.history.add(selectedBlock);
|
||||||
|
|
|
||||||
|
|
@ -107,12 +107,14 @@ public class BlockSelector {
|
||||||
return isSlabMaterial(material) ||
|
return isSlabMaterial(material) ||
|
||||||
material == Material.GLASS_PANE ||
|
material == Material.GLASS_PANE ||
|
||||||
material == Material.PACKED_ICE ||
|
material == Material.PACKED_ICE ||
|
||||||
|
material == Material.BLUE_ICE ||
|
||||||
|
material == Material.ICE ||
|
||||||
isFenceMaterial(material) ||
|
isFenceMaterial(material) ||
|
||||||
isTrapdoorMaterial(material) ||
|
isTrapdoorMaterial(material) ||
|
||||||
material == Material.LADDER;
|
material == Material.LADDER;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isFenceMaterial(@NotNull Material material) {
|
public boolean isFenceMaterial(@NotNull Material material) {
|
||||||
return material == Material.OAK_FENCE ||
|
return material == Material.OAK_FENCE ||
|
||||||
material == Material.BIRCH_FENCE ||
|
material == Material.BIRCH_FENCE ||
|
||||||
material == Material.SPRUCE_FENCE ||
|
material == Material.SPRUCE_FENCE ||
|
||||||
|
|
@ -126,7 +128,7 @@ public class BlockSelector {
|
||||||
material == Material.NETHER_BRICK_FENCE;
|
material == Material.NETHER_BRICK_FENCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isTrapdoorMaterial(@NotNull Material material) {
|
public boolean isTrapdoorMaterial(@NotNull Material material) {
|
||||||
return material == Material.OAK_TRAPDOOR ||
|
return material == Material.OAK_TRAPDOOR ||
|
||||||
material == Material.BIRCH_TRAPDOOR ||
|
material == Material.BIRCH_TRAPDOOR ||
|
||||||
material == Material.SPRUCE_TRAPDOOR ||
|
material == Material.SPRUCE_TRAPDOOR ||
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import org.jetbrains.annotations.NotNull;
|
||||||
public class LifecycleTickManager {
|
public class LifecycleTickManager {
|
||||||
|
|
||||||
private static final int TIME_UI_INTERVAL = 20; // 1 second — time display tick
|
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 ParkourGenerator generator;
|
||||||
private final GeneratorCleanup cleanup;
|
private final GeneratorCleanup cleanup;
|
||||||
|
|
@ -32,12 +32,10 @@ public class LifecycleTickManager {
|
||||||
* Start the tick cycle.
|
* Start the tick cycle.
|
||||||
*/
|
*/
|
||||||
public void startTick() {
|
public void startTick() {
|
||||||
if (generator.state.start == null) {
|
// Timer starts on first block jump, not here — see PlayerInteractionHandler.handleScore()
|
||||||
generator.state.start = java.time.Instant.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate initial blocks (reduced from GENERATION_LEAD to avoid too far generation)
|
// Generate initial blocks ahead of the player
|
||||||
generator.generate(3);
|
generator.generate(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ public class GeneratorCleanup {
|
||||||
for (ParkourPlayer pp : generator.getPlayers()) {
|
for (ParkourPlayer pp : generator.getPlayers()) {
|
||||||
Block below = blockBelowPlayer(pp);
|
Block below = blockBelowPlayer(pp);
|
||||||
if (below == null) continue;
|
if (below == null) continue;
|
||||||
int idx = history.indexOf(below);
|
int idx = indexOfBlock(history, below);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
minStandingIndex = Math.min(minStandingIndex, idx);
|
minStandingIndex = Math.min(minStandingIndex, idx);
|
||||||
}
|
}
|
||||||
|
|
@ -56,6 +56,17 @@ public class GeneratorCleanup {
|
||||||
history.subList(0, removeCount).clear();
|
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) {
|
private static Block blockBelowPlayer(@NotNull ParkourPlayer pp) {
|
||||||
Location loc = pp.player.getLocation().clone();
|
Location loc = pp.player.getLocation().clone();
|
||||||
Block below = loc.subtract(0, 1, 0).getBlock();
|
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.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles player interactions with parkour blocks.
|
* Handles player interactions with parkour blocks.
|
||||||
*/
|
*/
|
||||||
|
|
@ -42,11 +45,29 @@ 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.
|
* Handle player scoring on a block.
|
||||||
*/
|
*/
|
||||||
public void handleScore() {
|
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++;
|
generator.state.score++;
|
||||||
eventManager.handleScore();
|
eventManager.handleScore();
|
||||||
|
|
||||||
|
|
@ -91,8 +112,8 @@ public class PlayerInteractionHandler {
|
||||||
handleSchematicEndBlock(block);
|
handleSchematicEndBlock(block);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for scoring
|
// Check for scoring (compare by coordinates, not reference — Bukkit Block uses identity)
|
||||||
if (generator.state.history.contains(block) && !player.hasScored(block)) {
|
if (historyContains(generator.state.history, block) && !player.hasScored(block)) {
|
||||||
player.markScored(block);
|
player.markScored(block);
|
||||||
handleScore();
|
handleScore();
|
||||||
}
|
}
|
||||||
|
|
@ -111,7 +132,7 @@ public class PlayerInteractionHandler {
|
||||||
|
|
||||||
private boolean isSchematicEndBlock(@NotNull Block block) {
|
private boolean isSchematicEndBlock(@NotNull Block block) {
|
||||||
return generator.state.schematicBlocks != null
|
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. */
|
/** Block directly under feet; {@code null} in air — scoring runs when landed. */
|
||||||
|
|
|
||||||
|
|
@ -67,17 +67,22 @@ public class Leaderboard {
|
||||||
// ── CRUD operations ───────────────────────────────────────────────────────
|
// ── 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 uuid The player's uuid
|
||||||
* @param score The {@link Score} instance associated with a player's run
|
* @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
|
@Nullable
|
||||||
public Score put(@NotNull UUID uuid, @NotNull Score score) {
|
public Score put(@NotNull UUID uuid, @NotNull Score score) {
|
||||||
Score previous;
|
Score previous;
|
||||||
synchronized (scores) {
|
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);
|
sorter.sortInPlace(scores);
|
||||||
|
|
@ -85,6 +90,15 @@ public class Leaderboard {
|
||||||
return previous;
|
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
|
* 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();
|
public static final Map<String, PlayerSettingsManager.OptionContainer> PLAYER_COLUMNS = PlayerSettingsManager.getColumnMappings();
|
||||||
|
|
||||||
// Track scored blocks to prevent duplicate scoring
|
// Track scored blocks to prevent duplicate scoring (String key = "x,y,z" to avoid Bukkit Block reference equality)
|
||||||
private final Set<Block> scoredBlocks = new HashSet<>();
|
private final Set<String> scoredBlocks = new HashSet<>();
|
||||||
|
|
||||||
public @Expose Double schematicDifficulty;
|
public @Expose Double schematicDifficulty;
|
||||||
public @Expose Integer blockLead;
|
public @Expose Integer blockLead;
|
||||||
|
|
@ -115,18 +115,22 @@ public class ParkourPlayer extends ParkourUser {
|
||||||
save(LoParkour.getPlugin().isEnabled());
|
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.
|
* Check if player has already scored on this block.
|
||||||
*/
|
*/
|
||||||
public boolean hasScored(Block block) {
|
public boolean hasScored(Block block) {
|
||||||
return scoredBlocks.contains(block);
|
return scoredBlocks.contains(blockKey(block));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark block as scored to prevent duplicate scoring.
|
* Mark block as scored to prevent duplicate scoring.
|
||||||
*/
|
*/
|
||||||
public void markScored(Block block) {
|
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.core.Leaderboard;
|
||||||
import dev.loki.loparkour.leaderboard.model.Score;
|
import dev.loki.loparkour.leaderboard.model.Score;
|
||||||
import dev.loki.loparkour.util.text.ColorUtil;
|
import dev.loki.loparkour.util.text.ColorUtil;
|
||||||
import me.clip.placeholderapi.PlaceholderAPI;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.scoreboard.DisplaySlot;
|
import org.bukkit.scoreboard.DisplaySlot;
|
||||||
import org.bukkit.scoreboard.Objective;
|
import org.bukkit.scoreboard.Objective;
|
||||||
|
|
@ -124,10 +124,13 @@ public class ScoreboardManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
private String applyPlaceholderAPI(String text) {
|
private String applyPlaceholderAPI(String text) {
|
||||||
try {
|
if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) {
|
||||||
return PlaceholderAPI.setPlaceholders(player, text);
|
try {
|
||||||
} catch (Exception e) {
|
return me.clip.placeholderapi.PlaceholderAPI.setPlaceholders(player, text);
|
||||||
return text;
|
} catch (Exception e) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,12 @@ public class SessionStateManager {
|
||||||
|
|
||||||
tickTask = Scheduler.get(LoParkour.getPlugin()).runTimer(() -> {
|
tickTask = Scheduler.get(LoParkour.getPlugin()).runTimer(() -> {
|
||||||
if (session.generator != null) {
|
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)
|
}, 0, 1); // Run every tick (1 = 50ms)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue