new version 1.3.3
This commit is contained in:
parent
2d5c5eea48
commit
d9eb747c1b
37 changed files with 1021 additions and 471 deletions
|
|
@ -8,8 +8,6 @@ import dev.loki.loparkour.hook.HoloHook;
|
|||
import dev.loki.loparkour.hook.PAPIHook;
|
||||
import dev.loki.loparkour.mode.DefaultMode;
|
||||
import dev.loki.loparkour.mode.ElytraMode;
|
||||
import dev.loki.loparkour.mode.GravityShiftMode;
|
||||
import dev.loki.loparkour.mode.HardcoreMode;
|
||||
import dev.loki.loparkour.mode.Modes;
|
||||
import dev.loki.loparkour.mode.SpectatorMode;
|
||||
import dev.loki.loparkour.mode.SpeedrunMode;
|
||||
|
|
@ -190,8 +188,6 @@ public final class LoParkour extends LoPlugin {
|
|||
Registry.register(new DefaultMode());
|
||||
Registry.register(new SpectatorMode());
|
||||
Registry.register(new SpeedrunMode());
|
||||
Registry.register(new GravityShiftMode());
|
||||
Registry.register(new HardcoreMode());
|
||||
Registry.register(new ElytraMode());
|
||||
Registry.register(new RaceMode());
|
||||
Registry.register(new CoopMode());
|
||||
|
|
@ -260,13 +256,29 @@ public final class LoParkour extends LoPlugin {
|
|||
ParkourUser.leave(user);
|
||||
}
|
||||
|
||||
// write all LoParkour gamemodes
|
||||
Modes.DEFAULT.getLeaderboard().write(false);
|
||||
// Save leaderboards for all modes
|
||||
if (Modes.DEFAULT != null && Modes.DEFAULT.getLeaderboard() != null) {
|
||||
Modes.DEFAULT.getLeaderboard().write(false);
|
||||
}
|
||||
if (Modes.SPEEDRUN != null && Modes.SPEEDRUN.getLeaderboard() != null) {
|
||||
Modes.SPEEDRUN.getLeaderboard().write(false);
|
||||
}
|
||||
if (Modes.ELYTRA != null && Modes.ELYTRA.getLeaderboard() != null) {
|
||||
Modes.ELYTRA.getLeaderboard().write(false);
|
||||
}
|
||||
if (Modes.RACE != null && Modes.RACE.getLeaderboard() != null) {
|
||||
Modes.RACE.getLeaderboard().write(false);
|
||||
}
|
||||
if (Modes.COOP != null && Modes.COOP.getLeaderboard() != null) {
|
||||
Modes.COOP.getLeaderboard().write(false);
|
||||
}
|
||||
|
||||
Storage.close();
|
||||
World.delete();
|
||||
} catch (Throwable ignored) {
|
||||
|
||||
} catch (Throwable ex) {
|
||||
// Log errors instead of silently ignoring them
|
||||
getLogger().severe("Error during plugin disable: " + ex.getMessage());
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,6 +70,9 @@ public enum Config {
|
|||
config.load();
|
||||
}
|
||||
|
||||
// Validate configuration values
|
||||
validateConfigs();
|
||||
|
||||
// read config stuff
|
||||
Rewards.init();
|
||||
Locales.init();
|
||||
|
|
@ -77,6 +80,71 @@ public enum Config {
|
|||
Option.init(initialLoad);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates critical configuration values to prevent crashes and unexpected behavior.
|
||||
*/
|
||||
private static void validateConfigs() {
|
||||
var logger = LoParkour.getPlugin().getLogger();
|
||||
|
||||
// Validate border-size
|
||||
if (CONFIG.isPath("world.border-size")) {
|
||||
int borderSize = CONFIG.getInt("world.border-size");
|
||||
if (borderSize <= 0) {
|
||||
logger.severe("Invalid config: world.border-size must be > 0, found: " + borderSize);
|
||||
logger.severe("Using default value: 1000");
|
||||
}
|
||||
}
|
||||
|
||||
// Validate world dimensions
|
||||
if (CONFIG.isPath("world.max-y") && CONFIG.isPath("world.min-y")) {
|
||||
int maxY = CONFIG.getInt("world.max-y");
|
||||
int minY = CONFIG.getInt("world.min-y");
|
||||
if (maxY <= minY) {
|
||||
logger.severe("Invalid config: world.max-y (" + maxY + ") must be > world.min-y (" + minY + ")");
|
||||
logger.severe("Using default values: max-y=320, min-y=-64");
|
||||
}
|
||||
}
|
||||
|
||||
// Validate section dimensions
|
||||
if (CONFIG.isPath("world.section.width") && CONFIG.isPath("world.section.length")) {
|
||||
int width = CONFIG.getInt("world.section.width");
|
||||
int length = CONFIG.getInt("world.section.length");
|
||||
if (width <= 0 || length <= 0) {
|
||||
logger.severe("Invalid config: world.section dimensions must be > 0, found: width=" + width + ", length=" + length);
|
||||
logger.severe("Using default values: width=50, length=50");
|
||||
}
|
||||
}
|
||||
|
||||
// Validate generation chances (must sum to reasonable values)
|
||||
if (GENERATION.isPath("chances.height")) {
|
||||
double sum = GENERATION.getDouble("chances.height.1") +
|
||||
GENERATION.getDouble("chances.height.0") +
|
||||
GENERATION.getDouble("chances.height.-1") +
|
||||
GENERATION.getDouble("chances.height.-2");
|
||||
if (sum <= 0) {
|
||||
logger.severe("Invalid config: generation height chances sum to " + sum + ", must be > 0");
|
||||
logger.severe("Check generation.yml chances.height section");
|
||||
}
|
||||
}
|
||||
|
||||
// Validate ghost mode settings
|
||||
if (CONFIG.isPath("ghost-mode.show-top")) {
|
||||
int showTop = CONFIG.getInt("ghost-mode.show-top");
|
||||
if (showTop < 0 || showTop > 10) {
|
||||
logger.warning("Config: ghost-mode.show-top should be between 0-10, found: " + showTop);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate cleanup settings
|
||||
if (GENERATION.isPath("advanced.cleanup-distance")) {
|
||||
int cleanupDistance = GENERATION.getInt("advanced.cleanup-distance");
|
||||
if (cleanupDistance <= 0) {
|
||||
logger.severe("Invalid config: advanced.cleanup-distance must be > 0, found: " + cleanupDistance);
|
||||
logger.severe("Using default value: 100");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the file from disk.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -76,9 +76,14 @@ public final class Island {
|
|||
ps.setYaw(Config.GENERATION.getInt("advanced.island.spawn.yaw"));
|
||||
ps.setPitch(Config.GENERATION.getInt("advanced.island.spawn.pitch"));
|
||||
|
||||
// First parkour block should be generated FROM the parkour start block
|
||||
// in the direction of heading, not AT the parkour start block
|
||||
Location parkourStart = parkour.getLocation().add(session.generator.state.heading);
|
||||
// First parkour block position - fixed offset from parkour marker
|
||||
// Always 4 blocks in the direction of heading from center
|
||||
Location parkourStart = parkour.getLocation().clone();
|
||||
parkourStart.add(
|
||||
session.generator.state.heading.getX() * 4,
|
||||
0,
|
||||
session.generator.state.heading.getZ() * 4
|
||||
);
|
||||
|
||||
session.generator.generateFirst(ps, parkourStart);
|
||||
session.generator.startTick();
|
||||
|
|
|
|||
|
|
@ -68,12 +68,30 @@ public class ParkourGenerator {
|
|||
|
||||
private static LPSchematic loadIslandSchematic() {
|
||||
String name = Config.GENERATION.getString("advanced.island.schematic-name");
|
||||
if (name == null || name.isEmpty()) return null;
|
||||
if (name == null || name.isEmpty()) {
|
||||
name = "island"; // Default fallback
|
||||
}
|
||||
|
||||
var manager = LoParkour.getSchematicManager();
|
||||
if (manager == null) return null;
|
||||
if (manager == null) {
|
||||
LoParkour.getPlugin().getLogger().warning("Schematic manager not initialized, using null island");
|
||||
return null;
|
||||
}
|
||||
|
||||
LPSchematic s = manager.getSchematic(name);
|
||||
if (s == null) {
|
||||
LoParkour.getPlugin().getLogger().warning("Island schematic '%s' not found!".formatted(name));
|
||||
|
||||
// Try fallback to default "island" schematic
|
||||
if (!"island".equals(name)) {
|
||||
LoParkour.getPlugin().getLogger().info("Attempting to load default 'island' schematic as fallback...");
|
||||
s = manager.getSchematic("island");
|
||||
if (s != null) {
|
||||
LoParkour.getPlugin().getLogger().info("Successfully loaded fallback 'island' schematic");
|
||||
} else {
|
||||
LoParkour.getPlugin().getLogger().severe("Fallback 'island' schematic also not found! Island generation will be disabled.");
|
||||
}
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
|
@ -121,8 +139,10 @@ public class ParkourGenerator {
|
|||
}
|
||||
state.lastPositionIndexPlayer = 0;
|
||||
if (!state.history.isEmpty()) {
|
||||
state.history.remove(0);
|
||||
state.history.forEach(b -> b.setType(Material.AIR, false));
|
||||
Block first = state.history.remove(0);
|
||||
// Create a copy to avoid ConcurrentModificationException
|
||||
List<Block> historySnapshot = new ArrayList<>(state.history);
|
||||
historySnapshot.forEach(b -> b.setType(Material.AIR, false));
|
||||
state.history.clear();
|
||||
}
|
||||
state.resetSchematicState();
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import java.util.ArrayList;
|
|||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
|
|
@ -32,6 +33,8 @@ import java.util.logging.Level;
|
|||
*/
|
||||
public class BlockPlacer {
|
||||
|
||||
private static final int MAX_JUMP_VALIDATION_RETRIES = 10;
|
||||
|
||||
private final ParkourGenerator g;
|
||||
|
||||
public BlockPlacer(ParkourGenerator generator) {
|
||||
|
|
@ -72,7 +75,29 @@ public class BlockPlacer {
|
|||
g.state.playerSpawn = spawn;
|
||||
g.state.lastStandingPlayerLocation = spawn;
|
||||
g.state.blockSpawn = blockSpawn;
|
||||
g.state.history.add(blockSpawn.getBlock());
|
||||
|
||||
// Place first block - always a full block from config
|
||||
Block firstBlock = blockSpawn.getBlock();
|
||||
|
||||
// Get first block material from config, fallback to stone
|
||||
String firstBlockStr = "stone";
|
||||
try {
|
||||
firstBlockStr = Config.GENERATION.getString("advanced.island.parkour.first-block-material");
|
||||
} catch (Exception ex) {
|
||||
LoParkour.getPlugin().getLogger().warning("Config key 'advanced.island.parkour.first-block-material' not found, using 'stone'");
|
||||
}
|
||||
|
||||
Material firstBlockMaterial = Material.getMaterial(firstBlockStr.toUpperCase());
|
||||
if (firstBlockMaterial == null) {
|
||||
LoParkour.getPlugin().getLogger().warning("Invalid first-block-material: " + firstBlockStr + ", using STONE");
|
||||
firstBlockMaterial = Material.STONE;
|
||||
}
|
||||
|
||||
// Set first block
|
||||
firstBlock.setType(firstBlockMaterial);
|
||||
g.state.history.add(firstBlock);
|
||||
|
||||
// Generate remaining blocks
|
||||
generate(g.profile.get("blockLead").asInt());
|
||||
}
|
||||
|
||||
|
|
@ -100,10 +125,42 @@ public class BlockPlacer {
|
|||
g.state.heading = director.getRecommendedHeading(g.state.heading);
|
||||
height = director.getRecommendedHeight(height);
|
||||
|
||||
switch (g.getLatest().getType()) {
|
||||
case SMOOTH_QUARTZ_SLAB -> height = Math.min(height, 0);
|
||||
case GLASS_PANE -> distance = Math.min(distance, 3);
|
||||
// Restrict jumps after special blocks to prevent impossible jumps
|
||||
Material lastType = g.getLatest().getType();
|
||||
switch (lastType) {
|
||||
case SMOOTH_QUARTZ_SLAB -> {
|
||||
height = Math.min(height, 0); // Can't jump up from slab
|
||||
distance = Math.min(distance, 3); // Limit distance
|
||||
}
|
||||
case GLASS_PANE -> {
|
||||
height = Math.min(height, 0); // Can't jump up from pane
|
||||
distance = Math.min(distance, 3); // Limit distance
|
||||
}
|
||||
case OAK_FENCE, BIRCH_FENCE, SPRUCE_FENCE, DARK_OAK_FENCE,
|
||||
JUNGLE_FENCE, ACACIA_FENCE, MANGROVE_FENCE, CHERRY_FENCE,
|
||||
CRIMSON_FENCE, WARPED_FENCE, NETHER_BRICK_FENCE -> {
|
||||
height = Math.min(height, 0); // Can't jump up from fence
|
||||
distance = Math.min(distance, 3); // Limit distance
|
||||
}
|
||||
case PACKED_ICE -> {
|
||||
distance = Math.min(distance, 3); // Ice is slippery, limit distance
|
||||
}
|
||||
case OAK_TRAPDOOR, BIRCH_TRAPDOOR, SPRUCE_TRAPDOOR, DARK_OAK_TRAPDOOR,
|
||||
JUNGLE_TRAPDOOR, ACACIA_TRAPDOOR, MANGROVE_TRAPDOOR, CHERRY_TRAPDOOR,
|
||||
CRIMSON_TRAPDOOR, WARPED_TRAPDOOR, IRON_TRAPDOOR -> {
|
||||
height = Math.min(height, 0); // Can't jump up from trapdoor
|
||||
distance = Math.min(distance, 2); // Very limited distance
|
||||
}
|
||||
case LADDER -> {
|
||||
height = Math.min(height, 1); // Limited height from ladder
|
||||
distance = Math.min(distance, 2); // Very limited distance
|
||||
}
|
||||
}
|
||||
|
||||
// Clamp values to valid ranges for JumpOffsetGenerator
|
||||
height = Math.max(-2, Math.min(1, height));
|
||||
distance = Math.max(1, Math.min(4, distance));
|
||||
|
||||
if (height > 0) distance = Math.max(distance - height, 1);
|
||||
|
||||
double sd = g.generatorOptions.contains(GeneratorOption.REDUCE_RANDOM_BLOCK_SELECTION_ANGLE) ? 0.5 : 1;
|
||||
|
|
@ -117,10 +174,10 @@ public class BlockPlacer {
|
|||
|
||||
Block candidate = current.getLocation().add(offset).getBlock();
|
||||
|
||||
// Retry with JumpValidator if jump is impossible (up to 10 attempts)
|
||||
// Retry with JumpValidator if jump is impossible (up to MAX_JUMP_VALIDATION_RETRIES attempts)
|
||||
JumpValidator validator = new JumpValidator();
|
||||
int attempts = 0;
|
||||
while (!validator.canJump(current.getLocation(), candidate.getLocation()) && attempts < 10) {
|
||||
while (!validator.canJump(current.getLocation(), candidate.getLocation()) && attempts < MAX_JUMP_VALIDATION_RETRIES) {
|
||||
// Reduce distance and height to make jump easier
|
||||
distance = Math.max(1, distance - 1);
|
||||
height = Math.max(-1, height - 1);
|
||||
|
|
@ -186,14 +243,14 @@ public class BlockPlacer {
|
|||
}
|
||||
|
||||
private JumpType selectJumpType() {
|
||||
// Simple chances: 70% normal, 5% each for special types
|
||||
// Read chances from config
|
||||
Map<JumpType, Double> chances = new HashMap<>();
|
||||
chances.put(JumpType.NORMAL, 70.0);
|
||||
chances.put(JumpType.NEO_JUMP, 5.0);
|
||||
chances.put(JumpType.HEAD_HITTER, 5.0);
|
||||
chances.put(JumpType.FENCE_JUMP, 10.0);
|
||||
chances.put(JumpType.TRAPDOOR_JUMP, 5.0);
|
||||
chances.put(JumpType.LADDER_JUMP, 5.0);
|
||||
chances.put(JumpType.NORMAL, Config.GENERATION.getDouble("generation.jump-types.normal"));
|
||||
chances.put(JumpType.NEO_JUMP, Config.GENERATION.getDouble("generation.jump-types.neo"));
|
||||
chances.put(JumpType.HEAD_HITTER, Config.GENERATION.getDouble("generation.jump-types.head-hitter"));
|
||||
chances.put(JumpType.FENCE_JUMP, Config.GENERATION.getDouble("generation.jump-types.fence"));
|
||||
chances.put(JumpType.TRAPDOOR_JUMP, Config.GENERATION.getDouble("generation.jump-types.trapdoor"));
|
||||
chances.put(JumpType.LADDER_JUMP, Config.GENERATION.getDouble("generation.jump-types.ladder"));
|
||||
|
||||
return Probs.random(chances);
|
||||
}
|
||||
|
|
@ -210,7 +267,7 @@ public class BlockPlacer {
|
|||
.filter(s -> s.getMetadata() != null && Math.abs(s.getMetadata().getDifficulty() - diff) <= 0.26)
|
||||
.toList();
|
||||
var pool = candidates.isEmpty() ? new ArrayList<>(all.values()) : candidates;
|
||||
var schematic = pool.get(new java.util.Random().nextInt(pool.size()));
|
||||
var schematic = pool.get(ThreadLocalRandom.current().nextInt(pool.size()));
|
||||
|
||||
Location origin = g.getLatest().getLocation().add(g.state.heading.clone().multiply(2));
|
||||
List<Block> placed = rotatedPaste(schematic, origin);
|
||||
|
|
@ -228,7 +285,7 @@ public class BlockPlacer {
|
|||
private @NotNull List<Block> rotatedPaste(
|
||||
dev.loki.loparkour.schematic.lpschem.LPSchematic schematic, Location location) {
|
||||
int[] rotations = {0, 90, 180, 270};
|
||||
int rotation = rotations[new java.util.Random().nextInt(rotations.length)];
|
||||
int rotation = rotations[ThreadLocalRandom.current().nextInt(rotations.length)];
|
||||
org.bukkit.World world = location.getWorld();
|
||||
if (world == null) return List.of();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package dev.loki.loparkour.generator.jump;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.util.Probs;
|
||||
|
||||
import java.util.Map;
|
||||
|
|
@ -41,7 +42,11 @@ public class JumpOffsetGenerator {
|
|||
case 1, 2 -> 5;
|
||||
default -> 4;
|
||||
};
|
||||
default -> throw new IllegalArgumentException("Unknown jump height difference %d".formatted(y));
|
||||
default -> {
|
||||
// Fallback for unexpected values
|
||||
LoParkour.getPlugin().getLogger().warning("Unexpected jump height " + y + ", using default maxOffset=2");
|
||||
yield 2;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import java.util.List;
|
|||
public enum JumpType {
|
||||
|
||||
NORMAL(1.0, "Normal jump", Material.STONE),
|
||||
NEO_JUMP(2.5, "Jump around fence/wall", Material.OAK_FENCE, Material.COBBLESTONE_WALL, Material.NETHER_BRICK_FENCE),
|
||||
NEO_JUMP(2.5, "Jump around fence", Material.OAK_FENCE, Material.NETHER_BRICK_FENCE),
|
||||
HEAD_HITTER(2.0, "Jump with block overhead", Material.STONE_SLAB),
|
||||
FENCE_JUMP(1.8, "Jump onto fence", Material.OAK_FENCE, Material.BIRCH_FENCE, Material.SPRUCE_FENCE),
|
||||
TRAPDOOR_JUMP(1.5, "Jump onto trapdoor", Material.OAK_TRAPDOOR, Material.IRON_TRAPDOOR),
|
||||
|
|
|
|||
|
|
@ -28,24 +28,50 @@ public class JumpValidator {
|
|||
}
|
||||
|
||||
public boolean canJump(@NotNull Location from, @NotNull Location to) {
|
||||
// Null safety check
|
||||
if (from == null || to == null) {
|
||||
return false;
|
||||
}
|
||||
return canJump(from.toVector(), to.toVector());
|
||||
}
|
||||
|
||||
// Проверка возможности прыжка: sqrt(dx² + dy² + dz²) <= maxDistance
|
||||
// Check if jump is possible: sqrt(dx² + dy² + dz²) <= maxDistance
|
||||
public boolean canJump(@NotNull Vector from, @NotNull Vector to) {
|
||||
// Null safety check
|
||||
if (from == null || to == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
double dx = to.getX() - from.getX();
|
||||
double dy = to.getY() - from.getY();
|
||||
double dz = to.getZ() - from.getZ();
|
||||
|
||||
// Check if same location (no jump)
|
||||
double horizontalDistance = Math.sqrt(dx * dx + dz * dz);
|
||||
if (horizontalDistance < 0.01 && Math.abs(dy) < 0.01) {
|
||||
return false; // Same location
|
||||
}
|
||||
|
||||
// Check horizontal distance limit
|
||||
if (horizontalDistance > maxHorizontal) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check vertical limits
|
||||
if (dy > maxVerticalUp || dy < -maxVerticalDown) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For upward jumps, reduce max horizontal distance
|
||||
// Player can't jump as far when jumping up
|
||||
if (dy > 0) {
|
||||
double adjustedMaxHorizontal = maxHorizontal - (dy * 0.5); // Reduce by 0.5 blocks per block up
|
||||
if (horizontalDistance > adjustedMaxHorizontal) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check total distance
|
||||
double totalDistance = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
return totalDistance <= maxDistance;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import dev.loki.loparkour.ghost.GhostRecorder;
|
|||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||
import dev.loki.loparkour.leaderboard.Score;
|
||||
import dev.loki.loparkour.mode.Modes;
|
||||
import dev.loki.loparkour.mode.SpeedrunMode;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.ParkourSpectator;
|
||||
import dev.loki.loparkour.reward.Rewards;
|
||||
|
|
@ -29,6 +30,10 @@ import java.util.Iterator;
|
|||
*/
|
||||
public class GeneratorLifecycle {
|
||||
|
||||
private static final int FALL_DISTANCE_THRESHOLD = -10;
|
||||
private static final int SCHEMATIC_CLEANUP_OFFSET = 5;
|
||||
private static final int SCHEMATIC_PROXIMITY_DISTANCE = 15;
|
||||
|
||||
private final ParkourGenerator generator;
|
||||
private dev.lolib.scheduler.ScheduledTask cleanupTask;
|
||||
private GhostRecorder ghostRecorder;
|
||||
|
|
@ -84,7 +89,7 @@ public class GeneratorLifecycle {
|
|||
generator.getSpectators().forEach(ParkourSpectator::update);
|
||||
|
||||
if (generator.player.getLocation().getWorld() != generator.state.lastStandingPlayerLocation.getWorld()) return;
|
||||
if (generator.player.getLocation().subtract(generator.state.lastStandingPlayerLocation).getY() < -10) {
|
||||
if (generator.player.getLocation().subtract(generator.state.lastStandingPlayerLocation).getY() < FALL_DISTANCE_THRESHOLD) {
|
||||
fall();
|
||||
return;
|
||||
}
|
||||
|
|
@ -92,6 +97,11 @@ public class GeneratorLifecycle {
|
|||
Block below = blockBelow();
|
||||
if (below == null) return;
|
||||
|
||||
// Speedrun mode: trigger block disappearing (only for parkour blocks, not spawn platform)
|
||||
if (generator.getMode() instanceof SpeedrunMode && generator.state.history.contains(below)) {
|
||||
((SpeedrunMode.SpeedrunGenerator) generator).onBlockTouch(below);
|
||||
}
|
||||
|
||||
handleSchematicEndBlock(below);
|
||||
|
||||
// Clean up schematic blocks that are behind the player
|
||||
|
|
@ -100,7 +110,7 @@ public class GeneratorLifecycle {
|
|||
if (currentIdx > 0) {
|
||||
generator.state.schematicBlocks.removeIf(b -> {
|
||||
int blockIdx = generator.state.history.indexOf(b);
|
||||
if (blockIdx >= 0 && blockIdx < currentIdx - 5) {
|
||||
if (blockIdx >= 0 && blockIdx < currentIdx - SCHEMATIC_CLEANUP_OFFSET) {
|
||||
b.setType(Material.AIR);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -112,7 +122,7 @@ public class GeneratorLifecycle {
|
|||
// Reset schematic wait if player is far from schematic blocks
|
||||
if (generator.state.waitForSchematicCompletion && !generator.state.schematicBlocks.isEmpty()) {
|
||||
boolean nearSchematic = generator.state.schematicBlocks.stream()
|
||||
.anyMatch(b -> b.getLocation().distance(generator.player.getLocation()) < 15);
|
||||
.anyMatch(b -> b.getLocation().distance(generator.player.getLocation()) < SCHEMATIC_PROXIMITY_DISTANCE);
|
||||
if (!nearSchematic) {
|
||||
generator.state.waitForSchematicCompletion = false;
|
||||
// Force delete schematic blocks
|
||||
|
|
@ -138,7 +148,7 @@ public class GeneratorLifecycle {
|
|||
generator.state.lastPositionIndexPlayer = idx;
|
||||
|
||||
removeTrailBlocks(idx);
|
||||
cleanupDistantBlocks();
|
||||
// cleanupDistantBlocks is called by cleanupTask timer, no need to call here
|
||||
generator.placer.deleteSchematic();
|
||||
|
||||
int pts = Config.CONFIG.getBoolean("scoring.all-points") ? delta : 1;
|
||||
|
|
@ -163,7 +173,16 @@ public class GeneratorLifecycle {
|
|||
}
|
||||
|
||||
private @Nullable Block blockBelow() {
|
||||
Location loc = generator.player.getLocation().subtract(0, 1, 0);
|
||||
Location loc = generator.player.getLocation();
|
||||
|
||||
// Check current block first (for fences, walls, etc with height > 1)
|
||||
Block current = loc.getBlock();
|
||||
if (current.getType().isSolid() && !current.getType().isAir()) {
|
||||
return current;
|
||||
}
|
||||
|
||||
// Check block below
|
||||
loc = loc.subtract(0, 1, 0);
|
||||
Block b = loc.getBlock();
|
||||
if (b.getType() == Material.AIR) {
|
||||
if (loc.subtract(0, 0.5, 0).getBlock().getType() == Material.AIR) return null;
|
||||
|
|
@ -227,6 +246,7 @@ public class GeneratorLifecycle {
|
|||
if (ghostManager.shouldRecordGhost(modeName, score)) {
|
||||
GhostData ghostData = ghostRecorder.stopRecording(
|
||||
generator.player.getName(),
|
||||
generator.player.getUUID().toString(),
|
||||
score
|
||||
);
|
||||
ghostManager.saveGhost(modeName, ghostData);
|
||||
|
|
|
|||
|
|
@ -9,11 +9,13 @@ import java.util.List;
|
|||
public class GhostData {
|
||||
|
||||
private final String playerName;
|
||||
private final String playerUUID;
|
||||
private final int score;
|
||||
private final List<GhostFrame> frames;
|
||||
|
||||
public GhostData(@NotNull String playerName, int score, @NotNull List<GhostFrame> frames) {
|
||||
public GhostData(@NotNull String playerName, @NotNull String playerUUID, int score, @NotNull List<GhostFrame> frames) {
|
||||
this.playerName = playerName;
|
||||
this.playerUUID = playerUUID;
|
||||
this.score = score;
|
||||
this.frames = new ArrayList<>(frames);
|
||||
}
|
||||
|
|
@ -22,6 +24,10 @@ public class GhostData {
|
|||
return playerName;
|
||||
}
|
||||
|
||||
public String getPlayerUUID() {
|
||||
return playerUUID;
|
||||
}
|
||||
|
||||
public int getScore() {
|
||||
return score;
|
||||
}
|
||||
|
|
@ -33,6 +39,7 @@ public class GhostData {
|
|||
public void saveToFile(@NotNull File file) throws IOException {
|
||||
try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) {
|
||||
out.writeUTF(playerName);
|
||||
out.writeUTF(playerUUID);
|
||||
out.writeInt(score);
|
||||
out.writeInt(frames.size());
|
||||
|
||||
|
|
@ -50,6 +57,7 @@ public class GhostData {
|
|||
public static GhostData loadFromFile(@NotNull File file) throws IOException {
|
||||
try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
|
||||
String playerName = in.readUTF();
|
||||
String playerUUID = in.readUTF();
|
||||
int score = in.readInt();
|
||||
int frameCount = in.readInt();
|
||||
|
||||
|
|
@ -65,7 +73,7 @@ public class GhostData {
|
|||
frames.add(new GhostFrame(timestamp, x, y, z, yaw, pitch));
|
||||
}
|
||||
|
||||
return new GhostData(playerName, score, frames);
|
||||
return new GhostData(playerName, playerUUID, score, frames);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,15 +61,18 @@ public class GhostManager {
|
|||
ghosts.add(data);
|
||||
ghosts.sort((a, b) -> Integer.compare(b.getScore(), a.getScore()));
|
||||
|
||||
// Remove old ghosts beyond MAX limit
|
||||
while (ghosts.size() > MAX_GHOSTS_PER_MODE) {
|
||||
GhostData removed = ghosts.remove(ghosts.size() - 1);
|
||||
File file = new File(modeFolder, removed.getPlayerName() + ".ghost");
|
||||
// Use UUID for filename to avoid collisions and path traversal
|
||||
File file = new File(modeFolder, removed.getPlayerUUID() + ".ghost");
|
||||
file.delete();
|
||||
}
|
||||
|
||||
ghostsByMode.put(mode, ghosts);
|
||||
|
||||
File file = new File(modeFolder, data.getPlayerName() + ".ghost");
|
||||
// Use UUID for filename instead of player name
|
||||
File file = new File(modeFolder, data.getPlayerUUID() + ".ghost");
|
||||
try {
|
||||
data.saveToFile(file);
|
||||
} catch (IOException e) {
|
||||
|
|
|
|||
|
|
@ -38,9 +38,9 @@ public class GhostRecorder {
|
|||
lastLocation = currentLocation.clone();
|
||||
}
|
||||
|
||||
public GhostData stopRecording(String playerName, int score) {
|
||||
public GhostData stopRecording(String playerName, String playerUUID, int score) {
|
||||
recording = false;
|
||||
return new GhostData(playerName, score, frames);
|
||||
return new GhostData(playerName, playerUUID, score, frames);
|
||||
}
|
||||
|
||||
public boolean isRecording() {
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ public abstract class LPMenu {
|
|||
ItemStack item = new ItemStack(Material.RED_DYE);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setDisplayName(ColorUtil.color("<#DE1F1F><bold>«"));
|
||||
meta.setDisplayName(ColorUtil.color("<#DE1F1F><bold>Назад"));
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
return item;
|
||||
|
|
@ -53,7 +53,7 @@ public abstract class LPMenu {
|
|||
ItemStack item = new ItemStack(Material.LIME_DYE);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setDisplayName(ColorUtil.color("<#0DCB07><bold>»"));
|
||||
meta.setDisplayName(ColorUtil.color("<#0DCB07><bold>Далее"));
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
return item;
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ public class MainMenu extends LPMenu {
|
|||
|
||||
int count = visibleItems.size();
|
||||
if (count == 0) {
|
||||
player.sendMessage("§cNo menu items available. Check your permissions and locale files.");
|
||||
player.sendMessage(Locales.getString(player, "other.no_menu_items"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -68,8 +68,8 @@ public class LobbyMenu extends LPMenu {
|
|||
ItemStack item = new ItemStack(mat);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setDisplayName(ColorUtil.color(
|
||||
Locales.getString(locale, "lobby.visibility") + " §7» §f" + label));
|
||||
String visibilityName = Locales.getString(locale, "lobby.visibility.name");
|
||||
meta.setDisplayName(ColorUtil.color(visibilityName + " §7» §f" + label));
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
return item;
|
||||
|
|
|
|||
|
|
@ -28,11 +28,19 @@ public class SingleMenu extends LPMenu {
|
|||
List<ItemStack> items = new ArrayList<>();
|
||||
|
||||
for (Mode mode : Registry.getModes()) {
|
||||
// Skip spectator mode in single menu
|
||||
if (mode.getName().equals("spectator")) continue;
|
||||
|
||||
// Check if mode is enabled in config
|
||||
if (!Config.CONFIG.getBoolean("modes." + mode.getName() + ".enabled")) continue;
|
||||
|
||||
boolean blocked = Config.CONFIG.getBoolean("permissions.enabled")
|
||||
&& !player.hasPermission("LoParkour.gamemode." + mode.getName());
|
||||
if (blocked || mode instanceof MultiMode) continue;
|
||||
if (blocked) continue;
|
||||
|
||||
var item = mode.getItem(locale);
|
||||
if (item == null) continue;
|
||||
|
||||
availableModes.add(mode);
|
||||
items.add(item.build());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ public class LangMenu extends LPMenu {
|
|||
gui = gui.setItem(10 + i, item, e -> {
|
||||
pp.locale = lang;
|
||||
pp._locale = lang;
|
||||
pp.updateHotbar(); // Update hotbar with new language
|
||||
Menus.SETTINGS.open(player);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,8 +62,7 @@ public class ParkourSettingsMenu extends LPMenu {
|
|||
pp.updateGeneratorSettings(pp.session.generator);
|
||||
open(pp);
|
||||
})
|
||||
.setItem(19, localeItem(player, "settings.parkour_settings.styles.item"),
|
||||
e -> openStyleMenu(pp))
|
||||
.setItem(19, styleItem(pp), e -> openStyleMenu(pp))
|
||||
.setItem(20, schemDiffItem(pp), e -> {
|
||||
if (pp.session.generator.state.score == 0) {
|
||||
List<Double> diffs = List.of(0.0, 0.25, 0.5, 0.75, 1.0);
|
||||
|
|
@ -80,11 +79,20 @@ public class ParkourSettingsMenu extends LPMenu {
|
|||
private ItemStack toggleItem(String locale, String key, boolean value) {
|
||||
Material mat = value ? Material.LIME_STAINED_GLASS_PANE : Material.RED_STAINED_GLASS_PANE;
|
||||
String symbol = Locales.getString(locale, "settings.parkour_settings." + (value ? "enabled" : "disabled"));
|
||||
String label = Locales.getString(locale, "settings.parkour_settings." + key + ".item");
|
||||
String name = Locales.getString(locale, "settings.parkour_settings.items." + key + ".name");
|
||||
String lore = Locales.getString(locale, "settings.parkour_settings.items." + key + ".lore");
|
||||
|
||||
ItemStack item = new ItemStack(mat);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setDisplayName(ColorUtil.color(label + " §7[" + symbol + "§7]"));
|
||||
meta.setDisplayName(ColorUtil.color(name + " §7[" + symbol + "§7]"));
|
||||
if (lore != null && !lore.isEmpty()) {
|
||||
List<String> loreLines = new ArrayList<>();
|
||||
for (String line : lore.split("\\|\\|")) {
|
||||
loreLines.add(ColorUtil.color(line.replace("%s", symbol)));
|
||||
}
|
||||
meta.setLore(loreLines);
|
||||
}
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
return item;
|
||||
|
|
@ -92,7 +100,7 @@ public class ParkourSettingsMenu extends LPMenu {
|
|||
|
||||
private ItemStack schemDiffItem(ParkourPlayer pp) {
|
||||
List<Double> diffs = List.of(0.0, 0.25, 0.5, 0.75, 1.0);
|
||||
List<String> values = Locales.getStringList(pp.locale, "settings.parkour_settings.schematics.values");
|
||||
List<String> values = Locales.getStringList(pp.locale, "settings.parkour_settings.items.schematics.values");
|
||||
int idx = Math.max(0, diffs.indexOf(pp.schematicDifficulty));
|
||||
String label = idx < values.size() ? values.get(idx) : String.valueOf(pp.schematicDifficulty);
|
||||
Material[] mats = {Material.RED_STAINED_GLASS_PANE, Material.LIME_STAINED_GLASS_PANE,
|
||||
|
|
@ -100,7 +108,35 @@ public class ParkourSettingsMenu extends LPMenu {
|
|||
ItemStack item = new ItemStack(idx < mats.length ? mats[idx] : Material.PAPER);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setDisplayName(ColorUtil.color("§eSchematics §7» §f" + label));
|
||||
String name = Locales.getString(pp.locale, "settings.parkour_settings.items.schematics.name");
|
||||
String lore = Locales.getString(pp.locale, "settings.parkour_settings.items.schematics.lore");
|
||||
meta.setDisplayName(ColorUtil.color(name.replace("%s", label)));
|
||||
if (lore != null && !lore.isEmpty()) {
|
||||
List<String> loreLines = new ArrayList<>();
|
||||
for (String line : lore.split("\\|\\|")) {
|
||||
loreLines.add(ColorUtil.color(line.replace("%s", label)));
|
||||
}
|
||||
meta.setLore(loreLines);
|
||||
}
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
private ItemStack styleItem(ParkourPlayer pp) {
|
||||
String name = Locales.getString(pp.locale, "settings.parkour_settings.items.styles.item.name");
|
||||
String lore = Locales.getString(pp.locale, "settings.parkour_settings.items.styles.item.lore");
|
||||
ItemStack item = new ItemStack(Material.CRAFTING_TABLE);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setDisplayName(ColorUtil.color(name.replace("%s", pp.style)));
|
||||
if (lore != null && !lore.isEmpty()) {
|
||||
List<String> loreLines = new ArrayList<>();
|
||||
for (String line : lore.split("\\|\\|")) {
|
||||
loreLines.add(ColorUtil.color(line.replace("%s", pp.style)));
|
||||
}
|
||||
meta.setLore(loreLines);
|
||||
}
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
return item;
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ public class CoopMode implements Mode {
|
|||
@Override
|
||||
public void create(Player player) {
|
||||
if (!Config.CONFIG.getBoolean("joining")) {
|
||||
player.sendMessage("§cJoining is currently disabled.");
|
||||
player.sendMessage(Locales.getString(player, "other.joining_disabled"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -66,6 +66,8 @@ public class CoopMode implements Mode {
|
|||
// Initialize contributions
|
||||
for (ParkourPlayer pp : session.getPlayers()) {
|
||||
playerContributions.put(pp.getUUID(), 0);
|
||||
pp.player.sendTitle("§a§lCOOP MODE", "§7Work together for a shared score!", 10, 70, 20);
|
||||
pp.player.sendMessage("§a§lCoop Mode §7activated! Team score is shared.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,8 +5,11 @@ import dev.loki.loparkour.util.Item;
|
|||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||
import dev.loki.loparkour.ghost.GhostManager;
|
||||
import dev.loki.loparkour.ghost.GhostRecorder;
|
||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.ParkourSpectator;
|
||||
import dev.loki.loparkour.session.Session;
|
||||
|
||||
import org.bukkit.*;
|
||||
|
|
@ -17,6 +20,7 @@ import org.bukkit.util.Vector;
|
|||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
|
|
@ -47,7 +51,7 @@ public class ElytraMode implements Mode {
|
|||
@Override
|
||||
public void create(Player player) {
|
||||
if (!Config.CONFIG.getBoolean("joining")) {
|
||||
player.sendMessage("§cJoining is currently disabled.");
|
||||
player.sendMessage(Locales.getString(player, "other.joining_disabled"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -81,18 +85,30 @@ public class ElytraMode implements Mode {
|
|||
p.getInventory().addItem(new ItemStack(Material.FIREWORK_ROCKET, fireworks));
|
||||
|
||||
ringsCollected.put(p.getUniqueId(), 0);
|
||||
|
||||
// Notify player
|
||||
p.sendTitle("§b§lELYTRA MODE", "§7Fly through rings for bonus points!", 10, 70, 20);
|
||||
p.sendMessage("§b§lElytra Mode §7activated! Use fireworks to boost.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
super.tick();
|
||||
// Don't call super.tick() - we don't want normal block generation
|
||||
tickCounter++;
|
||||
|
||||
// Spawn rings every 5 blocks
|
||||
if (tickCounter % 100 == 0 && !state.history.isEmpty()) {
|
||||
Location ringLoc = getLatest().getLocation().clone().add(0, 3, 0);
|
||||
rings.add(new RingCheckpoint(ringLoc));
|
||||
// Update spectators
|
||||
getSpectators().forEach(ParkourSpectator::update);
|
||||
|
||||
// Check for fall (much lower threshold for elytra)
|
||||
if (player.getLocation().getY() < state.lastStandingPlayerLocation.getY() - 50) {
|
||||
lifecycle.fall();
|
||||
return;
|
||||
}
|
||||
|
||||
// Spawn rings every 2 seconds
|
||||
if (tickCounter % 40 == 0) {
|
||||
spawnNextRing();
|
||||
}
|
||||
|
||||
// Display rings and check for player passing through
|
||||
|
|
@ -116,20 +132,40 @@ public class ElytraMode implements Mode {
|
|||
// Bonus points for passing through ring
|
||||
lifecycle.score();
|
||||
lifecycle.score();
|
||||
lifecycle.score(); // 3 points per ring
|
||||
|
||||
pp.player.playSound(pp.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1f, 1.5f);
|
||||
pp.player.spawnParticle(Particle.TOTEM, ring.location, 20, 0.5, 0.5, 0.5, 0.1);
|
||||
|
||||
// Update last standing location to ring location
|
||||
state.lastStandingPlayerLocation = ring.location.clone();
|
||||
}
|
||||
}
|
||||
|
||||
// Remove old rings
|
||||
if (ring.location.distance(player.getLocation()) > 50) {
|
||||
if (ring.location.distance(player.getLocation()) > 100) {
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Check firework cooldown
|
||||
checkFireworkCooldown();
|
||||
|
||||
// Start timer if not started
|
||||
if (state.start == null) state.start = Instant.now();
|
||||
}
|
||||
|
||||
private void spawnNextRing() {
|
||||
Location playerLoc = player.getLocation();
|
||||
|
||||
// Spawn ring ahead of player in flight direction
|
||||
Vector direction = player.getLocation().getDirection();
|
||||
Location ringLoc = playerLoc.clone().add(direction.multiply(20)).add(0, 5, 0);
|
||||
|
||||
// Add some randomness
|
||||
ringLoc.add((Math.random() - 0.5) * 10, (Math.random() - 0.5) * 5, (Math.random() - 0.5) * 10);
|
||||
|
||||
rings.add(new RingCheckpoint(ringLoc));
|
||||
}
|
||||
|
||||
private void displayRing(Location center) {
|
||||
|
|
|
|||
|
|
@ -1,180 +0,0 @@
|
|||
package dev.loki.loparkour.mode;
|
||||
|
||||
import dev.loki.loparkour.util.Item;
|
||||
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.session.Session;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.bukkit.potion.PotionEffectType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
public class GravityShiftMode implements Mode {
|
||||
|
||||
private final Leaderboard leaderboard = new Leaderboard(getName(), Leaderboard.Sort.SCORE);
|
||||
private final Random random = new Random();
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public String getName() {
|
||||
return "gravity-shift";
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public dev.loki.loparkour.util.Item getItem(String locale) {
|
||||
return Locales.getItem(locale, "play.single.gravity-shift");
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Leaderboard getLeaderboard() {
|
||||
return leaderboard;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void create(Player player) {
|
||||
if (!Config.CONFIG.getBoolean("joining")) {
|
||||
player.sendMessage("Joining is currently disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
ParkourPlayer pp = ParkourPlayer.getPlayer(player);
|
||||
if (pp != null && pp.session.generator != null && pp.session.generator.getMode() instanceof GravityShiftMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
player.closeInventory();
|
||||
|
||||
Session.create(session -> new GravityShiftGenerator(session), null, null, player);
|
||||
}
|
||||
|
||||
private static class GravityShiftGenerator extends ParkourGenerator {
|
||||
private int jumpsUntilShift;
|
||||
private final int shiftInterval;
|
||||
private PotionEffect currentEffect;
|
||||
|
||||
public GravityShiftGenerator(@NotNull Session session) {
|
||||
super(session);
|
||||
this.shiftInterval = Config.CONFIG.getInt("modes.gravity-shift.interval");
|
||||
this.jumpsUntilShift = shiftInterval;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void score() {
|
||||
super.score();
|
||||
|
||||
jumpsUntilShift--;
|
||||
|
||||
if (jumpsUntilShift <= 0) {
|
||||
applyRandomEffect();
|
||||
jumpsUntilShift = shiftInterval;
|
||||
}
|
||||
}
|
||||
|
||||
private void applyRandomEffect() {
|
||||
if (currentEffect != null) {
|
||||
player.player.removePotionEffect(currentEffect.getType());
|
||||
}
|
||||
|
||||
List<EffectData> effects = new ArrayList<>();
|
||||
|
||||
if (Config.CONFIG.getBoolean("modes.gravity-shift.effects.jump-boost.enabled")) {
|
||||
effects.add(new EffectData(
|
||||
PotionEffectType.JUMP,
|
||||
Config.CONFIG.getInt("modes.gravity-shift.effects.jump-boost.amplifier"),
|
||||
Config.CONFIG.getInt("modes.gravity-shift.effects.jump-boost.duration"),
|
||||
"Jump Boost"
|
||||
));
|
||||
}
|
||||
|
||||
if (Config.CONFIG.getBoolean("modes.gravity-shift.effects.speed.enabled")) {
|
||||
effects.add(new EffectData(
|
||||
PotionEffectType.SPEED,
|
||||
Config.CONFIG.getInt("modes.gravity-shift.effects.speed.amplifier"),
|
||||
Config.CONFIG.getInt("modes.gravity-shift.effects.speed.duration"),
|
||||
"Speed"
|
||||
));
|
||||
}
|
||||
|
||||
if (Config.CONFIG.getBoolean("modes.gravity-shift.effects.slowness.enabled")) {
|
||||
effects.add(new EffectData(
|
||||
PotionEffectType.SLOW,
|
||||
Config.CONFIG.getInt("modes.gravity-shift.effects.slowness.amplifier"),
|
||||
Config.CONFIG.getInt("modes.gravity-shift.effects.slowness.duration"),
|
||||
"Slowness"
|
||||
));
|
||||
}
|
||||
|
||||
if (Config.CONFIG.getBoolean("modes.gravity-shift.effects.levitation.enabled")) {
|
||||
effects.add(new EffectData(
|
||||
PotionEffectType.LEVITATION,
|
||||
Config.CONFIG.getInt("modes.gravity-shift.effects.levitation.amplifier"),
|
||||
Config.CONFIG.getInt("modes.gravity-shift.effects.levitation.duration"),
|
||||
"Levitation"
|
||||
));
|
||||
}
|
||||
|
||||
if (effects.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
EffectData selected = effects.get(new Random().nextInt(effects.size()));
|
||||
currentEffect = new PotionEffect(
|
||||
selected.type,
|
||||
selected.duration * 20,
|
||||
selected.amplifier - 1,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
player.player.addPotionEffect(currentEffect);
|
||||
|
||||
// Visual feedback
|
||||
player.player.sendTitle("§6⚡", "§e" + selected.name, 5, 20, 10);
|
||||
player.player.playSound(player.player.getLocation(), org.bukkit.Sound.ENTITY_ENDERMAN_TELEPORT, 1.0f, 1.5f);
|
||||
player.player.spawnParticle(org.bukkit.Particle.PORTAL, player.player.getLocation().add(0, 1, 0), 50, 0.5, 0.5, 0.5, 0.5);
|
||||
|
||||
player.sendTranslated("modes.gravity-shift.effect-applied", selected.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset(boolean regenerate) {
|
||||
if (currentEffect != null && player != null && player.player != null) {
|
||||
player.player.removePotionEffect(currentEffect.getType());
|
||||
}
|
||||
jumpsUntilShift = shiftInterval;
|
||||
super.reset(regenerate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mode getMode() {
|
||||
return Modes.GRAVITY_SHIFT;
|
||||
}
|
||||
}
|
||||
|
||||
private static class EffectData {
|
||||
final PotionEffectType type;
|
||||
final int amplifier;
|
||||
final int duration;
|
||||
final String name;
|
||||
|
||||
EffectData(PotionEffectType type, int amplifier, int duration, String name) {
|
||||
this.type = type;
|
||||
this.amplifier = amplifier;
|
||||
this.duration = duration;
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
package dev.loki.loparkour.mode;
|
||||
|
||||
import dev.loki.loparkour.util.Item;
|
||||
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.session.Session;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class HardcoreMode implements Mode {
|
||||
|
||||
private final Leaderboard leaderboard = new Leaderboard(getName(), Leaderboard.Sort.SCORE);
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public String getName() {
|
||||
return "hardcore";
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public dev.loki.loparkour.util.Item getItem(String locale) {
|
||||
return Locales.getItem(locale, "play.single.hardcore");
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Leaderboard getLeaderboard() {
|
||||
return leaderboard;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void create(Player player) {
|
||||
if (!Config.CONFIG.getBoolean("joining")) {
|
||||
player.sendMessage("Joining is currently disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
ParkourPlayer pp = ParkourPlayer.getPlayer(player);
|
||||
if (pp != null && pp.session.generator != null && pp.session.generator.getMode() instanceof HardcoreMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
player.closeInventory();
|
||||
|
||||
Session.create(session -> new HardcoreGenerator(session), null, null, player);
|
||||
}
|
||||
|
||||
private static class HardcoreGenerator extends ParkourGenerator {
|
||||
|
||||
public HardcoreGenerator(@NotNull Session session) {
|
||||
super(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void fall() {
|
||||
boolean resetRewards = Config.CONFIG.getBoolean("modes.hardcore.reset-rewards");
|
||||
|
||||
if (resetRewards) {
|
||||
player.collectedRewards.clear();
|
||||
player.sendTranslated("modes.hardcore.rewards-lost");
|
||||
}
|
||||
|
||||
player.sendTranslated("modes.hardcore.full-reset");
|
||||
|
||||
super.fall();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mode getMode() {
|
||||
return Modes.HARDCORE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,8 +7,6 @@ public class Modes {
|
|||
public static DefaultMode DEFAULT;
|
||||
public static SpectatorMode SPECTATOR;
|
||||
public static SpeedrunMode SPEEDRUN;
|
||||
public static GravityShiftMode GRAVITY_SHIFT;
|
||||
public static HardcoreMode HARDCORE;
|
||||
public static ElytraMode ELYTRA;
|
||||
public static RaceMode RACE;
|
||||
public static CoopMode COOP;
|
||||
|
|
@ -17,8 +15,6 @@ public class Modes {
|
|||
DEFAULT = (DefaultMode) Registry.getMode("default");
|
||||
SPECTATOR = (SpectatorMode) Registry.getMode("spectator");
|
||||
SPEEDRUN = (SpeedrunMode) Registry.getMode("speedrun");
|
||||
GRAVITY_SHIFT = (GravityShiftMode) Registry.getMode("gravity-shift");
|
||||
HARDCORE = (HardcoreMode) Registry.getMode("hardcore");
|
||||
ELYTRA = (ElytraMode) Registry.getMode("elytra");
|
||||
RACE = (RaceMode) Registry.getMode("race");
|
||||
COOP = (CoopMode) Registry.getMode("coop");
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ public class RaceMode implements Mode {
|
|||
@Override
|
||||
public void create(Player player) {
|
||||
if (!Config.CONFIG.getBoolean("joining")) {
|
||||
player.sendMessage("§cJoining is currently disabled.");
|
||||
player.sendMessage(Locales.getString(player, "other.joining_disabled"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -59,6 +59,12 @@ public class RaceMode implements Mode {
|
|||
public RaceGenerator(@NotNull Session session) {
|
||||
super(session);
|
||||
this.targetScore = Config.CONFIG.getInt("modes.race.target-score");
|
||||
|
||||
// Notify players
|
||||
for (ParkourPlayer pp : session.getPlayers()) {
|
||||
pp.player.sendTitle("§6§lRACE MODE", "§7First to " + targetScore + " points wins!", 10, 70, 20);
|
||||
pp.player.sendMessage("§6§lRace Mode §7activated! Target: §e" + targetScore + " §7points.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -63,12 +63,18 @@ public class SpeedrunMode implements Mode {
|
|||
Session.create(session -> new SpeedrunGenerator(session), null, null, player);
|
||||
}
|
||||
|
||||
private static class SpeedrunGenerator extends ParkourGenerator {
|
||||
public static class SpeedrunGenerator extends ParkourGenerator {
|
||||
private final Map<Block, Long> blockTimestamps = new HashMap<>();
|
||||
private final Map<Block, ScheduledTask> scheduledTasks = new HashMap<>();
|
||||
|
||||
public SpeedrunGenerator(@NotNull Session session) {
|
||||
super(session);
|
||||
|
||||
// Notify player
|
||||
for (ParkourPlayer pp : session.getPlayers()) {
|
||||
pp.player.sendTitle("§c§lSPEEDRUN MODE", "§7Blocks disappear after you step on them!", 10, 70, 20);
|
||||
pp.player.sendMessage("§c§lSpeedrun Mode §7activated! Don't look back!");
|
||||
}
|
||||
}
|
||||
|
||||
public void onBlockTouch(@NotNull Block block) {
|
||||
|
|
|
|||
|
|
@ -85,7 +85,9 @@ public class ParkourPlayer extends ParkourUser {
|
|||
* @return player as a {@link ParkourPlayer}, null if not found.
|
||||
*/
|
||||
public static @Nullable ParkourPlayer getPlayer(@NotNull Player player) {
|
||||
return getPlayers().stream()
|
||||
// Optimized: search directly in sessions without creating intermediate list
|
||||
return Divider.sections.keySet().stream()
|
||||
.flatMap(session -> session.getPlayers().stream())
|
||||
.filter(other -> other.getUUID().equals(player.getUniqueId()))
|
||||
.findAny()
|
||||
.orElse(null);
|
||||
|
|
@ -166,10 +168,7 @@ public class ParkourPlayer extends ParkourUser {
|
|||
player.getInventory().clear();
|
||||
|
||||
// Load hotbar slots from config
|
||||
if (ParkourOption.PLAY.mayPerform(player)) {
|
||||
int slot = Config.CONFIG.getInt("options.hotbar-slots.play");
|
||||
player.getInventory().setItem(slot, Locales.getItem(locale, "play.item").build());
|
||||
}
|
||||
// Don't show Play button when already in parkour
|
||||
if (ParkourOption.COMMUNITY.mayPerform(player)) {
|
||||
int slot = Config.CONFIG.getInt("options.hotbar-slots.community");
|
||||
player.getInventory().setItem(slot, Locales.getItem(locale, "community.item").build());
|
||||
|
|
@ -192,6 +191,33 @@ public class ParkourPlayer extends ParkourUser {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates hotbar items with current language
|
||||
*/
|
||||
public void updateHotbar() {
|
||||
if (!Config.CONFIG.getBoolean("options.inventory-handling")) return;
|
||||
|
||||
player.getInventory().clear();
|
||||
|
||||
// Don't show Play button when already in parkour
|
||||
if (ParkourOption.COMMUNITY.mayPerform(player)) {
|
||||
int slot = Config.CONFIG.getInt("options.hotbar-slots.community");
|
||||
player.getInventory().setItem(slot, Locales.getItem(locale, "community.item").build());
|
||||
}
|
||||
if (ParkourOption.SETTINGS.mayPerform(player)) {
|
||||
int slot = Config.CONFIG.getInt("options.hotbar-slots.settings");
|
||||
player.getInventory().setItem(slot, Locales.getItem(locale, "settings.item").build());
|
||||
}
|
||||
if (ParkourOption.LOBBY.mayPerform(player)) {
|
||||
int slot = Config.CONFIG.getInt("options.hotbar-slots.lobby");
|
||||
player.getInventory().setItem(slot, Locales.getItem(locale, "lobby.item").build());
|
||||
}
|
||||
if (ParkourOption.QUIT.mayPerform(player)) {
|
||||
int slot = Config.CONFIG.getInt("options.hotbar-slots.quit");
|
||||
player.getInventory().setItem(slot, Locales.getItem(locale, "other.quit").build());
|
||||
}
|
||||
}
|
||||
|
||||
public record OptionContainer(ParkourOption option, BiConsumer<ParkourPlayer, String> consumer) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,16 +85,30 @@ class StorageSQL {
|
|||
}
|
||||
|
||||
public static void writeScores(@NotNull String mode, @NotNull Map<UUID, Score> scores) {
|
||||
new HashMap<>(scores).forEach((uuid, score) -> queryExecutor.executeUpdate("""
|
||||
String sql = """
|
||||
INSERT INTO `%s`
|
||||
(uuid, name, time, difficulty, score)
|
||||
VALUES ('%s', '%s', '%s', '%s', %d)
|
||||
ON DUPLICATE KEY UPDATE name = '%s',
|
||||
time = '%s',
|
||||
difficulty = '%s',
|
||||
score = %d;
|
||||
""".formatted(getTableName(mode), uuid, score.name(), score.time(), score.difficulty(), score.score(),
|
||||
score.name(), score.time(), score.difficulty(), score.score())));
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE name = VALUES(name),
|
||||
time = VALUES(time),
|
||||
difficulty = VALUES(difficulty),
|
||||
score = VALUES(score);
|
||||
""".formatted(getTableName(mode));
|
||||
|
||||
new HashMap<>(scores).forEach((uuid, score) -> {
|
||||
try (PreparedStatement stmt = queryExecutor.prepareStatement(sql)) {
|
||||
if (stmt == null) return;
|
||||
stmt.setString(1, uuid.toString());
|
||||
stmt.setString(2, score.name());
|
||||
stmt.setString(3, score.time());
|
||||
stmt.setString(4, score.difficulty());
|
||||
stmt.setInt(5, score.score());
|
||||
stmt.executeUpdate();
|
||||
} catch (SQLException ex) {
|
||||
LoParkour.getPlugin().getLogger().severe(
|
||||
"Error writing score for %s in mode %s - %s".formatted(uuid, mode, ex.getMessage()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static String getTableName(String mode) {
|
||||
|
|
@ -135,31 +149,43 @@ class StorageSQL {
|
|||
DecimalFormat df = new DecimalFormat("#.######", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
|
||||
String schematicDifficulty = df.format(player.schematicDifficulty);
|
||||
|
||||
queryExecutor.executeUpdate("""
|
||||
String sql = """
|
||||
INSERT INTO `%soptions`
|
||||
(uuid, style, blockLead, useParticles, useSpecial, showFallMsg, showScoreboard,
|
||||
selectedTime, collectedRewards, locale, schematicDifficulty, sound)
|
||||
VALUES ('%s', '%s', %d, %b, %b, %b, %b, %d, '%s', '%s', %s, %b)
|
||||
ON DUPLICATE KEY UPDATE style = '%s',
|
||||
blockLead = %d,
|
||||
useParticles = %b,
|
||||
useSpecial = %b,
|
||||
showFallMsg = %b,
|
||||
showScoreboard = %b,
|
||||
selectedTime = %d,
|
||||
collectedRewards = '%s',
|
||||
locale = '%s',
|
||||
schematicDifficulty = %s,
|
||||
sound = %b;
|
||||
""".formatted(Option.SQL_PREFIX,
|
||||
player.getUUID(), player.style, player.blockLead,
|
||||
player.particles, player.useSpecialBlocks, player.showFallMessage,
|
||||
player.showScoreboard, player.selectedTime, String.join(",", player.collectedRewards),
|
||||
player.locale, schematicDifficulty, player.sound,
|
||||
player.style, player.blockLead,
|
||||
player.particles, player.useSpecialBlocks, player.showFallMessage,
|
||||
player.showScoreboard, player.selectedTime, String.join(",", player.collectedRewards),
|
||||
player.locale, schematicDifficulty, player.sound));
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE style = VALUES(style),
|
||||
blockLead = VALUES(blockLead),
|
||||
useParticles = VALUES(useParticles),
|
||||
useSpecial = VALUES(useSpecial),
|
||||
showFallMsg = VALUES(showFallMsg),
|
||||
showScoreboard = VALUES(showScoreboard),
|
||||
selectedTime = VALUES(selectedTime),
|
||||
collectedRewards = VALUES(collectedRewards),
|
||||
locale = VALUES(locale),
|
||||
schematicDifficulty = VALUES(schematicDifficulty),
|
||||
sound = VALUES(sound);
|
||||
""".formatted(Option.SQL_PREFIX);
|
||||
|
||||
try (PreparedStatement stmt = queryExecutor.prepareStatement(sql)) {
|
||||
if (stmt == null) return;
|
||||
stmt.setString(1, player.getUUID().toString());
|
||||
stmt.setString(2, player.style);
|
||||
stmt.setInt(3, player.blockLead);
|
||||
stmt.setBoolean(4, player.particles);
|
||||
stmt.setBoolean(5, player.useSpecialBlocks);
|
||||
stmt.setBoolean(6, player.showFallMessage);
|
||||
stmt.setBoolean(7, player.showScoreboard);
|
||||
stmt.setInt(8, player.selectedTime);
|
||||
stmt.setString(9, String.join(",", player.collectedRewards));
|
||||
stmt.setString(10, player.locale);
|
||||
stmt.setString(11, schematicDifficulty);
|
||||
stmt.setBoolean(12, player.sound);
|
||||
stmt.executeUpdate();
|
||||
} catch (SQLException ex) {
|
||||
LoParkour.getPlugin().getLogger().severe(
|
||||
"Error writing player data for %s - %s".formatted(player.getName(), ex.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
private static void connect() {
|
||||
|
|
|
|||
|
|
@ -12,10 +12,11 @@ import java.util.regex.Pattern;
|
|||
public class ColorUtil {
|
||||
|
||||
private static final Pattern HEX_PATTERN = Pattern.compile("&#([A-Fa-f0-9]{6})");
|
||||
private static final Pattern MINIMESSAGE_HEX_PATTERN = Pattern.compile("<#([A-Fa-f0-9]{6})>");
|
||||
|
||||
/**
|
||||
* Converts color codes to Minecraft format
|
||||
* Supports both legacy (&) and hex (&#RRGGBB) colors
|
||||
* Supports legacy (&), hex (&#RRGGBB), and MiniMessage (<#RRGGBB>) colors
|
||||
*
|
||||
* @param text The text to color
|
||||
* @return Colored text
|
||||
|
|
@ -25,7 +26,41 @@ public class ColorUtil {
|
|||
return "";
|
||||
}
|
||||
|
||||
// Convert hex colors &#RRGGBB to <#RRGGBB>
|
||||
// Convert MiniMessage hex colors <#RRGGBB> to ChatColor
|
||||
Matcher miniMatcher = MINIMESSAGE_HEX_PATTERN.matcher(text);
|
||||
StringBuffer miniBuffer = new StringBuffer();
|
||||
while (miniMatcher.find()) {
|
||||
String hexCode = miniMatcher.group(1);
|
||||
miniMatcher.appendReplacement(miniBuffer, ChatColor.of("#" + hexCode).toString());
|
||||
}
|
||||
miniMatcher.appendTail(miniBuffer);
|
||||
text = miniBuffer.toString();
|
||||
|
||||
// Convert MiniMessage formatting tags to legacy codes
|
||||
text = text.replace("<bold>", "&l")
|
||||
.replace("<italic>", "&o")
|
||||
.replace("<underlined>", "&n")
|
||||
.replace("<strikethrough>", "&m")
|
||||
.replace("<obfuscated>", "&k")
|
||||
.replace("<reset>", "&r")
|
||||
.replace("<black>", "&0")
|
||||
.replace("<dark_blue>", "&1")
|
||||
.replace("<dark_green>", "&2")
|
||||
.replace("<dark_aqua>", "&3")
|
||||
.replace("<dark_red>", "&4")
|
||||
.replace("<dark_purple>", "&5")
|
||||
.replace("<gold>", "&6")
|
||||
.replace("<gray>", "&7")
|
||||
.replace("<dark_gray>", "&8")
|
||||
.replace("<blue>", "&9")
|
||||
.replace("<green>", "&a")
|
||||
.replace("<aqua>", "&b")
|
||||
.replace("<red>", "&c")
|
||||
.replace("<light_purple>", "&d")
|
||||
.replace("<yellow>", "&e")
|
||||
.replace("<white>", "&f");
|
||||
|
||||
// Convert hex colors &#RRGGBB to ChatColor
|
||||
Matcher matcher = HEX_PATTERN.matcher(text);
|
||||
StringBuffer buffer = new StringBuffer();
|
||||
|
||||
|
|
|
|||
|
|
@ -318,8 +318,13 @@ ghost-mode:
|
|||
# -= Game modes options =-
|
||||
modes:
|
||||
|
||||
# -= Default mode =-
|
||||
default:
|
||||
enabled: true
|
||||
|
||||
# -= Speedrun mode =-
|
||||
speedrun:
|
||||
enabled: true
|
||||
# Block lifetime in seconds before disappearing
|
||||
block-lifetime: 1.5
|
||||
# Warning time in seconds before block disappears
|
||||
|
|
@ -327,6 +332,7 @@ modes:
|
|||
|
||||
# -= Gravity shift mode =-
|
||||
gravity-shift:
|
||||
enabled: true
|
||||
# Interval in jumps before effect changes
|
||||
interval: 10
|
||||
|
||||
|
|
@ -350,13 +356,14 @@ modes:
|
|||
|
||||
# -= Hardcore mode =-
|
||||
hardcore:
|
||||
enabled: false
|
||||
# Reset all rewards on fall
|
||||
reset-rewards: true
|
||||
|
||||
# -= Elytra mode =-
|
||||
elytra:
|
||||
# Enable elytra mode
|
||||
enabled: true
|
||||
enabled: false
|
||||
# Distance between rings
|
||||
ring-distance-min: 20
|
||||
ring-distance-max: 40
|
||||
|
|
@ -379,7 +386,7 @@ modes:
|
|||
# -= Co-op mode =-
|
||||
coop:
|
||||
# Enable co-op mode
|
||||
enabled: true
|
||||
enabled: false
|
||||
# Maximum players in co-op session
|
||||
max-players: 4
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,17 @@ generation:
|
|||
pane: 10
|
||||
fence: 10
|
||||
|
||||
# -= Jump type variations =-
|
||||
# Chances for different jump types within normal blocks
|
||||
# These add variety to the parkour generation
|
||||
jump-types:
|
||||
normal: 70
|
||||
neo: 5
|
||||
head-hitter: 5
|
||||
fence: 10
|
||||
trapdoor: 5
|
||||
ladder: 5
|
||||
|
||||
# -= Jump settings =-
|
||||
settings:
|
||||
|
||||
|
|
|
|||
|
|
@ -158,21 +158,20 @@ settings:
|
|||
lore: "�D4FFCurrently: &#A0A0A0%s||&#A0A0A0Your current visual time."
|
||||
|
||||
schematics:
|
||||
material: ""
|
||||
material: "paper"
|
||||
name: "�D4FF&lSchematic difficulty"
|
||||
lore: "�D4FFCurrently: &#A0A0A0%s||&#A0A0A0The difficulty of schematics"
|
||||
values: ["&#FF4444off", "�D4FFeasy", "&#FFFF00medium", "�D4FFhard", "B0000very hard"]
|
||||
|
||||
scoreboard:
|
||||
material: ""
|
||||
material: "oak_sign"
|
||||
name: "Show scoreboard"
|
||||
lore: "&#A0A0A0Currently: %s||&#A0A0A0If enabled shows the scoreboard."
|
||||
|
||||
fall_message:
|
||||
item:
|
||||
material: ""
|
||||
name: "Show fall message"
|
||||
lore: "&#A0A0A0Currently: %s||&#A0A0A0If enabled shows a message when you fall||&#A0A0A0with extra info."
|
||||
material: "feather"
|
||||
name: "Show fall message"
|
||||
lore: "&#A0A0A0Currently: %s||&#A0A0A0If enabled shows a message when you fall||&#A0A0A0with extra info."
|
||||
|
||||
divider: "񢩈&m-----------------------------------"
|
||||
score: "�D4FF&lScore &r&#FFFFFF%s"
|
||||
|
|
@ -185,22 +184,22 @@ settings:
|
|||
tied: "&#A0A0A0You tied your high score!"
|
||||
|
||||
particles:
|
||||
material: ""
|
||||
material: "blaze_powder"
|
||||
name: "Show particles"
|
||||
lore: "&#A0A0A0Currently: %s||&#A0A0A0If enabled, every generated block||&#A0A0A0will show particles."
|
||||
|
||||
sound:
|
||||
material: ""
|
||||
material: "note_block"
|
||||
name: "Play sounds"
|
||||
lore: "&#A0A0A0Currently: %s||&#A0A0A0If enabled, every generated block||&#A0A0A0will play a sound."
|
||||
|
||||
special_blocks:
|
||||
material: ""
|
||||
material: "ice"
|
||||
name: "Use special blocks"
|
||||
lore: "&#A0A0A0Currently: %s||&#A0A0A0If enabled, uses special blocks like ice and slabs."
|
||||
|
||||
score_difficulty:
|
||||
material: ""
|
||||
material: "experience_bottle"
|
||||
name: "Score difficulty"
|
||||
lore: "&#A0A0A0Currently: %s||&#A0A0A0When enabled, the higher score you have,||&#A0A0A0the more difficult it gets."
|
||||
|
||||
|
|
@ -258,6 +257,8 @@ other:
|
|||
no_do: "&#A0A0A0You can't do this right now."
|
||||
no_teleporting: "&#FFFFFFYou can't enter the parkour world by teleporting!"
|
||||
customize: "&#A0A0A0Customize your parkour experience by using \"/parkour\""
|
||||
joining_disabled: "&#FF6464Joining is currently disabled."
|
||||
no_menu_items: "&#FF6464No menu items available. Check your permissions and locale files."
|
||||
|
||||
quit:
|
||||
material: "barrier"
|
||||
|
|
@ -277,8 +278,8 @@ other:
|
|||
modes:
|
||||
elytra:
|
||||
material: "elytra"
|
||||
name: "�D4FF&lElytra"
|
||||
lore: "&#A0A0A0Fly through rings!||&#A0A0A0Use fireworks to boost"
|
||||
name: "�D4FF&lElytra &#FF6464&l[BETA]"
|
||||
lore: "&#A0A0A0Fly through rings!||&#A0A0A0Use fireworks to boost||&#FF6464Mode in development"
|
||||
|
||||
race:
|
||||
material: "golden_boots"
|
||||
|
|
|
|||
|
|
@ -157,21 +157,20 @@ settings:
|
|||
lore: "�D4FFТекущее: &#A0A0A0%s||&#A0A0A0Ваше текущее визуальное время."
|
||||
|
||||
schematics:
|
||||
material: ""
|
||||
material: "paper"
|
||||
name: "�D4FF&lСложность схематик"
|
||||
lore: "�D4FFТекущая: &#A0A0A0%s||&#A0A0A0Сложность схематик"
|
||||
values: ["&#FF4444выкл", "�D4FFлёгкая", "&#FFFF00средняя", "�D4FFсложная", "B0000очень сложная"]
|
||||
|
||||
scoreboard:
|
||||
material: ""
|
||||
material: "oak_sign"
|
||||
name: "Показывать табло"
|
||||
lore: "&#A0A0A0Текущее: %s||&#A0A0A0Если включено, показывает табло."
|
||||
|
||||
fall_message:
|
||||
item:
|
||||
material: ""
|
||||
name: "Показывать сообщение о падении"
|
||||
lore: "&#A0A0A0Текущее: %s||&#A0A0A0Если включено, показывает сообщение при падении||&#A0A0A0с дополнительной информацией."
|
||||
material: "feather"
|
||||
name: "Показывать сообщение о падении"
|
||||
lore: "&#A0A0A0Текущее: %s||&#A0A0A0Если включено, показывает сообщение при падении||&#A0A0A0с дополнительной информацией."
|
||||
|
||||
divider: "񢩈&m-----------------------------------"
|
||||
score: "�D4FF&lСчёт &r&#FFFFFF%s"
|
||||
|
|
@ -184,22 +183,22 @@ settings:
|
|||
tied: "&#A0A0A0Вы повторили свой рекорд!"
|
||||
|
||||
particles:
|
||||
material: ""
|
||||
material: "blaze_powder"
|
||||
name: "Показывать частицы"
|
||||
lore: "&#A0A0A0Текущее: %s||&#A0A0A0Если включено, каждый сгенерированный блок||&#A0A0A0будет показывать частицы."
|
||||
|
||||
sound:
|
||||
material: ""
|
||||
material: "note_block"
|
||||
name: "Воспроизводить звуки"
|
||||
lore: "&#A0A0A0Текущее: %s||&#A0A0A0Если включено, каждый сгенерированный блок||&#A0A0A0будет воспроизводить звук."
|
||||
|
||||
special_blocks:
|
||||
material: ""
|
||||
material: "ice"
|
||||
name: "Использовать специальные блоки"
|
||||
lore: "&#A0A0A0Текущее: %s||&#A0A0A0Если включено, использует специальные блоки||&#A0A0A0такие как лёд и плиты."
|
||||
|
||||
score_difficulty:
|
||||
material: ""
|
||||
material: "experience_bottle"
|
||||
name: "Сложность от счёта"
|
||||
lore: "&#A0A0A0Текущее: %s||&#A0A0A0Если включено, чем выше ваш счёт,||&#A0A0A0тем сложнее становится."
|
||||
|
||||
|
|
@ -257,6 +256,8 @@ other:
|
|||
no_do: "&#A0A0A0Вы не можете сделать это сейчас."
|
||||
no_teleporting: "&#FFFFFFВы не можете войти в мир паркура телепортацией!"
|
||||
customize: "&#A0A0A0Настройте свой паркур используя \"/parkour\""
|
||||
joining_disabled: "&#FF6464Вход в паркур временно отключён."
|
||||
no_menu_items: "&#FF6464Нет доступных пунктов меню. Проверьте права доступа и файлы локализации."
|
||||
|
||||
quit:
|
||||
material: "barrier"
|
||||
|
|
@ -276,8 +277,8 @@ other:
|
|||
modes:
|
||||
elytra:
|
||||
material: "elytra"
|
||||
name: "�D4FF&lЭлитры"
|
||||
lore: "&#A0A0A0Летайте через кольца!||&#A0A0A0Используйте фейерверки для ускорения"
|
||||
name: "�D4FF&lЭлитры &#FF6464&l[BETA]"
|
||||
lore: "&#A0A0A0Летайте через кольца!||&#A0A0A0Используйте фейерверки для ускорения||&#FF6464Режим в разработке"
|
||||
|
||||
race:
|
||||
material: "golden_boots"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,140 @@
|
|||
package dev.loki.loparkour.generator.jump;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* Unit tests for JumpValidator.
|
||||
* Tests jump possibility calculations for parkour generation.
|
||||
*/
|
||||
class JumpValidatorTest {
|
||||
|
||||
private JumpValidator validator;
|
||||
|
||||
@Mock
|
||||
private World world;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
validator = new JumpValidator();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCanJump_SameLevel_Distance1() {
|
||||
// 1 block forward, same height - always possible
|
||||
Location from = new Location(world, 0, 100, 0);
|
||||
Location to = new Location(world, 1, 100, 0);
|
||||
|
||||
assertTrue(validator.canJump(from, to), "1 block jump at same level should be possible");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCanJump_SameLevel_Distance4() {
|
||||
// 4 blocks forward, same height - possible
|
||||
Location from = new Location(world, 0, 100, 0);
|
||||
Location to = new Location(world, 4, 100, 0);
|
||||
|
||||
assertTrue(validator.canJump(from, to), "4 block jump at same level should be possible");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCanJump_SameLevel_Distance5() {
|
||||
// 5 blocks forward, same height - impossible without speed
|
||||
Location from = new Location(world, 0, 100, 0);
|
||||
Location to = new Location(world, 5, 100, 0);
|
||||
|
||||
assertFalse(validator.canJump(from, to), "5 block jump at same level should be impossible");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCanJump_Up1_Distance3() {
|
||||
// 3 blocks forward, 1 block up - possible
|
||||
Location from = new Location(world, 0, 100, 0);
|
||||
Location to = new Location(world, 3, 101, 0);
|
||||
|
||||
assertTrue(validator.canJump(from, to), "3 block jump with 1 up should be possible");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCanJump_Up1_Distance4() {
|
||||
// 4 blocks forward, 1 block up - impossible
|
||||
Location from = new Location(world, 0, 100, 0);
|
||||
Location to = new Location(world, 4, 101, 0);
|
||||
|
||||
assertFalse(validator.canJump(from, to), "4 block jump with 1 up should be impossible");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCanJump_Down1_Distance4() {
|
||||
// 4 blocks forward, 1 block down - possible
|
||||
Location from = new Location(world, 0, 100, 0);
|
||||
Location to = new Location(world, 4, 99, 0);
|
||||
|
||||
assertTrue(validator.canJump(from, to), "4 block jump with 1 down should be possible");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCanJump_Down2_Distance4() {
|
||||
// 4 blocks forward, 2 blocks down - possible
|
||||
Location from = new Location(world, 0, 100, 0);
|
||||
Location to = new Location(world, 4, 98, 0);
|
||||
|
||||
assertTrue(validator.canJump(from, to), "4 block jump with 2 down should be possible");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCanJump_Up2_Distance1() {
|
||||
// 1 block forward, 2 blocks up - impossible (max jump height is 1.25)
|
||||
Location from = new Location(world, 0, 100, 0);
|
||||
Location to = new Location(world, 1, 102, 0);
|
||||
|
||||
assertFalse(validator.canJump(from, to), "Jump 2 blocks up should be impossible");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCanJump_Diagonal() {
|
||||
// Diagonal jump - 3 blocks X, 2 blocks Z, same height
|
||||
Location from = new Location(world, 0, 100, 0);
|
||||
Location to = new Location(world, 3, 100, 2);
|
||||
|
||||
double distance = Math.sqrt(3*3 + 2*2); // ~3.6 blocks
|
||||
assertTrue(validator.canJump(from, to), "Diagonal jump of ~3.6 blocks should be possible");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCanJump_NullLocations() {
|
||||
// Test null safety
|
||||
Location validLoc = new Location(world, 0, 100, 0);
|
||||
assertFalse(validator.canJump((Location)null, validLoc),
|
||||
"Should return false for null from location");
|
||||
assertFalse(validator.canJump(validLoc, (Location)null),
|
||||
"Should return false for null to location");
|
||||
assertFalse(validator.canJump((Location)null, (Location)null),
|
||||
"Should return false for both null locations");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCanJump_SameLocation() {
|
||||
// Jump to same location - should be false
|
||||
Location loc = new Location(world, 0, 100, 0);
|
||||
|
||||
assertFalse(validator.canJump(loc, loc), "Jump to same location should be impossible");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCanJump_VeryLongDistance() {
|
||||
// 10 blocks forward - impossible
|
||||
Location from = new Location(world, 0, 100, 0);
|
||||
Location to = new Location(world, 10, 100, 0);
|
||||
|
||||
assertFalse(validator.canJump(from, to), "10 block jump should be impossible");
|
||||
}
|
||||
}
|
||||
180
src/test/java/dev/loki/loparkour/ghost/GhostDataTest.java
Normal file
180
src/test/java/dev/loki/loparkour/ghost/GhostDataTest.java
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
package dev.loki.loparkour.ghost;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for GhostData serialization and deserialization.
|
||||
* Tests file I/O operations for ghost recordings.
|
||||
*/
|
||||
class GhostDataTest {
|
||||
|
||||
@Test
|
||||
void testConstructor() {
|
||||
// Test basic construction
|
||||
List<GhostFrame> frames = new ArrayList<>();
|
||||
frames.add(new GhostFrame(0, 0, 100, 0, 0, 0));
|
||||
frames.add(new GhostFrame(1000, 1, 100, 0, 90, 0));
|
||||
|
||||
GhostData data = new GhostData("TestPlayer", "uuid-123", 42, frames);
|
||||
|
||||
assertEquals("TestPlayer", data.getPlayerName());
|
||||
assertEquals("uuid-123", data.getPlayerUUID());
|
||||
assertEquals(42, data.getScore());
|
||||
assertEquals(2, data.getFrames().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSaveAndLoad(@TempDir Path tempDir) throws IOException {
|
||||
// Create test data
|
||||
List<GhostFrame> frames = new ArrayList<>();
|
||||
frames.add(new GhostFrame(0, 0, 100, 0, 0, 0));
|
||||
frames.add(new GhostFrame(500, 1, 100, 0, 45, 0));
|
||||
frames.add(new GhostFrame(1000, 2, 100, 0, 90, 0));
|
||||
|
||||
GhostData original = new GhostData("Player1", "uuid-456", 100, frames);
|
||||
|
||||
// Save to file
|
||||
File file = tempDir.resolve("test.ghost").toFile();
|
||||
original.saveToFile(file);
|
||||
|
||||
assertTrue(file.exists(), "Ghost file should be created");
|
||||
assertTrue(file.length() > 0, "Ghost file should not be empty");
|
||||
|
||||
// Load from file
|
||||
GhostData loaded = GhostData.loadFromFile(file);
|
||||
|
||||
// Verify data
|
||||
assertEquals(original.getPlayerName(), loaded.getPlayerName());
|
||||
assertEquals(original.getPlayerUUID(), loaded.getPlayerUUID());
|
||||
assertEquals(original.getScore(), loaded.getScore());
|
||||
assertEquals(original.getFrames().size(), loaded.getFrames().size());
|
||||
|
||||
// Verify frames
|
||||
for (int i = 0; i < original.getFrames().size(); i++) {
|
||||
GhostFrame origFrame = original.getFrames().get(i);
|
||||
GhostFrame loadedFrame = loaded.getFrames().get(i);
|
||||
|
||||
assertEquals(origFrame.getTimestamp(), loadedFrame.getTimestamp());
|
||||
assertEquals(origFrame.getX(), loadedFrame.getX(), 0.001);
|
||||
assertEquals(origFrame.getY(), loadedFrame.getY(), 0.001);
|
||||
assertEquals(origFrame.getZ(), loadedFrame.getZ(), 0.001);
|
||||
assertEquals(origFrame.getYaw(), loadedFrame.getYaw(), 0.001);
|
||||
assertEquals(origFrame.getPitch(), loadedFrame.getPitch(), 0.001);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmptyFrames(@TempDir Path tempDir) throws IOException {
|
||||
// Test with no frames
|
||||
GhostData data = new GhostData("EmptyPlayer", "uuid-789", 0, new ArrayList<>());
|
||||
|
||||
File file = tempDir.resolve("empty.ghost").toFile();
|
||||
data.saveToFile(file);
|
||||
|
||||
GhostData loaded = GhostData.loadFromFile(file);
|
||||
|
||||
assertEquals(0, loaded.getFrames().size());
|
||||
assertEquals("EmptyPlayer", loaded.getPlayerName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLargeDataset(@TempDir Path tempDir) throws IOException {
|
||||
// Test with many frames (simulate 60 seconds at 20 TPS)
|
||||
List<GhostFrame> frames = new ArrayList<>();
|
||||
for (int i = 0; i < 1200; i++) {
|
||||
frames.add(new GhostFrame(
|
||||
i * 50L, // 50ms per frame
|
||||
i * 0.1, // X position
|
||||
100.0, // Y position
|
||||
i * 0.05, // Z position
|
||||
(i % 360), // Yaw
|
||||
0 // Pitch
|
||||
));
|
||||
}
|
||||
|
||||
GhostData data = new GhostData("LongRunner", "uuid-long", 1200, frames);
|
||||
|
||||
File file = tempDir.resolve("large.ghost").toFile();
|
||||
data.saveToFile(file);
|
||||
|
||||
GhostData loaded = GhostData.loadFromFile(file);
|
||||
|
||||
assertEquals(1200, loaded.getFrames().size());
|
||||
assertEquals(1200, loaded.getScore());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSpecialCharactersInName(@TempDir Path tempDir) throws IOException {
|
||||
// Test with special characters in player name
|
||||
GhostData data = new GhostData(
|
||||
"Player_123-ABC",
|
||||
"uuid-special",
|
||||
50,
|
||||
List.of(new GhostFrame(0, 0, 100, 0, 0, 0))
|
||||
);
|
||||
|
||||
File file = tempDir.resolve("special.ghost").toFile();
|
||||
data.saveToFile(file);
|
||||
|
||||
GhostData loaded = GhostData.loadFromFile(file);
|
||||
|
||||
assertEquals("Player_123-ABC", loaded.getPlayerName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNegativeCoordinates(@TempDir Path tempDir) throws IOException {
|
||||
// Test with negative coordinates
|
||||
List<GhostFrame> frames = new ArrayList<>();
|
||||
frames.add(new GhostFrame(0, -100, 50, -200, -90, -45));
|
||||
frames.add(new GhostFrame(1000, -99, 51, -199, -45, 0));
|
||||
|
||||
GhostData data = new GhostData("NegativePlayer", "uuid-neg", 10, frames);
|
||||
|
||||
File file = tempDir.resolve("negative.ghost").toFile();
|
||||
data.saveToFile(file);
|
||||
|
||||
GhostData loaded = GhostData.loadFromFile(file);
|
||||
|
||||
GhostFrame frame = loaded.getFrames().get(0);
|
||||
assertEquals(-100, frame.getX(), 0.001);
|
||||
assertEquals(50, frame.getY(), 0.001);
|
||||
assertEquals(-200, frame.getZ(), 0.001);
|
||||
assertEquals(-90, frame.getYaw(), 0.001);
|
||||
assertEquals(-45, frame.getPitch(), 0.001);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoadNonExistentFile() {
|
||||
// Test loading from non-existent file
|
||||
File file = new File("nonexistent.ghost");
|
||||
|
||||
assertThrows(IOException.class, () -> {
|
||||
GhostData.loadFromFile(file);
|
||||
}, "Should throw IOException for non-existent file");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFramesImmutability() {
|
||||
// Test that frames list is copied (defensive copy)
|
||||
List<GhostFrame> originalFrames = new ArrayList<>();
|
||||
originalFrames.add(new GhostFrame(0, 0, 100, 0, 0, 0));
|
||||
|
||||
GhostData data = new GhostData("Player", "uuid", 10, originalFrames);
|
||||
|
||||
// Modify original list
|
||||
originalFrames.add(new GhostFrame(1000, 1, 100, 0, 0, 0));
|
||||
|
||||
// GhostData should still have only 1 frame
|
||||
assertEquals(1, data.getFrames().size(),
|
||||
"GhostData should create defensive copy of frames list");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue