refactor: reorganize packages and replace LPSchematic with WorldEdit
Restructure core modules into subpackages (generator, schematic, config, session, etc.); add .schem/.nbt schematic pipeline, commands, adaptive stats skeleton, and bootstrap wiring.
This commit is contained in:
parent
6ba6e5137d
commit
fd0ba2cc0e
203 changed files with 5652 additions and 2367 deletions
|
|
@ -1,34 +1,19 @@
|
|||
package dev.loki.loparkour;
|
||||
|
||||
import dev.loki.loparkour.api.Registry;
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.config.Option;
|
||||
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.Modes;
|
||||
import dev.loki.loparkour.mode.SpectatorMode;
|
||||
import dev.loki.loparkour.mode.SpeedrunMode;
|
||||
import dev.loki.loparkour.mode.RaceMode;
|
||||
import dev.loki.loparkour.mode.CoopMode;
|
||||
import dev.loki.loparkour.mode.GravityShiftMode;
|
||||
import dev.loki.loparkour.mode.HardcoreMode;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.loki.loparkour.player.UserRegistry;
|
||||
import dev.loki.loparkour.reward.Rewards;
|
||||
import dev.loki.loparkour.schematic.lpschem.LPSchematicManager;
|
||||
import dev.loki.loparkour.bootstrap.PluginBootstrap;
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.hook.papi.PAPIHook;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
import dev.loki.loparkour.player.core.ParkourUser;
|
||||
import dev.loki.loparkour.reward.core.Rewards;
|
||||
import dev.loki.loparkour.schematic.core.SchematicManager;
|
||||
import dev.loki.loparkour.storage.Storage;
|
||||
import dev.loki.loparkour.world.Divider;
|
||||
import dev.loki.loparkour.world.World;
|
||||
import dev.loki.loparkour.world.core.Divider;
|
||||
import dev.loki.loparkour.world.core.World;
|
||||
import dev.lolib.core.LoPlugin;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import org.bstats.bukkit.Metrics;
|
||||
import org.bstats.charts.SimplePie;
|
||||
import org.bstats.charts.SingleLineChart;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
|
|
@ -41,45 +26,34 @@ import java.util.ArrayList;
|
|||
*/
|
||||
public final class LoParkour extends LoPlugin {
|
||||
|
||||
public static final String NAME = "&#FF6464<bold>LoParkour<reset>";
|
||||
public static final String NAME = "&#FF6464&lLoParkour&r";
|
||||
public static final String PREFIX = NAME + " 񢩈» &#A0A0A0";
|
||||
|
||||
private static final Gson gson = new GsonBuilder()
|
||||
.setPrettyPrinting()
|
||||
.excludeFieldsWithoutExposeAnnotation()
|
||||
.create();
|
||||
|
||||
public static Gson getGson() {
|
||||
return gson;
|
||||
}
|
||||
|
||||
private static LoParkour instance;
|
||||
private static LPSchematicManager schematicManager;
|
||||
private static SchematicManager schematicManager;
|
||||
|
||||
@Nullable
|
||||
private static PAPIHook placeholderHook;
|
||||
|
||||
public static Gson getGson() {
|
||||
return gson;
|
||||
}
|
||||
|
||||
public static void log(String message) {
|
||||
if (Config.CONFIG.getBoolean("debug")) {
|
||||
LoParkour.getPlugin().getLogger().info("[Debug] " + message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param child The file name.
|
||||
* @return A file from within the plugin folder.
|
||||
*/
|
||||
public static File getInFolder(String child) {
|
||||
return new File(instance.getDataFolder(), child);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return This plugin's {@link Logging} instance.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @return The plugin instance.
|
||||
*/
|
||||
public static LoParkour getPlugin() {
|
||||
return instance;
|
||||
}
|
||||
|
|
@ -89,104 +63,32 @@ public final class LoParkour extends LoPlugin {
|
|||
return placeholderHook;
|
||||
}
|
||||
|
||||
public static LPSchematicManager getSchematicManager() {
|
||||
public static SchematicManager getSchematicManager() {
|
||||
return schematicManager;
|
||||
}
|
||||
|
||||
|
||||
public void setSchematicManager(@NotNull SchematicManager manager) {
|
||||
schematicManager = manager;
|
||||
}
|
||||
|
||||
public void setPlaceholderHook(@Nullable PAPIHook hook) {
|
||||
placeholderHook = hook;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable() {
|
||||
instance = this; // must be first — Config enum accesses instance inside reload()
|
||||
loadConfigs();
|
||||
loadSchematics();
|
||||
registerModes();
|
||||
registerHooks();
|
||||
setupWorld();
|
||||
registerEventsAndCommands();
|
||||
setupMetrics();
|
||||
}
|
||||
|
||||
private void loadConfigs() {
|
||||
Config.reload(true);
|
||||
}
|
||||
|
||||
private void loadSchematics() {
|
||||
schematicManager = new LPSchematicManager();
|
||||
schematicManager.loadAll();
|
||||
}
|
||||
|
||||
private void registerModes() {
|
||||
Registry.register(new DefaultMode());
|
||||
Registry.register(new SpectatorMode());
|
||||
Registry.register(new SpeedrunMode());
|
||||
Registry.register(new ElytraMode());
|
||||
Registry.register(new RaceMode());
|
||||
Registry.register(new CoopMode());
|
||||
Registry.register(new GravityShiftMode());
|
||||
Registry.register(new HardcoreMode());
|
||||
Modes.init();
|
||||
}
|
||||
|
||||
private void registerHooks() {
|
||||
if (getServer().getPluginManager().isPluginEnabled("HolographicDisplays")) {
|
||||
getLogger().info("Registered Holographic Displays hook");
|
||||
HoloHook.init();
|
||||
}
|
||||
if (getServer().getPluginManager().isPluginEnabled("PlaceholderAPI")) {
|
||||
getLogger().info("Registered PlaceholderAPI hook");
|
||||
placeholderHook = new PAPIHook();
|
||||
placeholderHook.register();
|
||||
}
|
||||
if (Config.CONFIG.getBoolean("bungeecord.enabled")) {
|
||||
getServer().getMessenger().registerOutgoingPluginChannel(this, "BungeeCord");
|
||||
getLogger().info("Registered BungeeCord hook");
|
||||
}
|
||||
}
|
||||
|
||||
private void setupWorld() {
|
||||
if (!Config.CONFIG.getBoolean("joining")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// World creation MUST happen on the main thread (Bukkit API requirement)
|
||||
try {
|
||||
loLogger().info("Creating parkour world...");
|
||||
World.create();
|
||||
loLogger().info("Parkour world created successfully!");
|
||||
} catch (Exception ex) {
|
||||
loLogger().error("Failed to create parkour world", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void registerEventsAndCommands() {
|
||||
dev.lolib.gui.GUIManager.init(this); // required for LoLib GUI click handling
|
||||
|
||||
var pm = getServer().getPluginManager();
|
||||
pm.registerEvents(new dev.loki.loparkour.listener.PlayerConnectionListener(), this);
|
||||
pm.registerEvents(new dev.loki.loparkour.listener.ParkourRestrictionListener(), this);
|
||||
pm.registerEvents(new dev.loki.loparkour.listener.SchematicWandListener(), this);
|
||||
|
||||
LoParkourCommand cmd = new LoParkourCommand();
|
||||
getCommand("LoParkour").setExecutor(cmd);
|
||||
getCommand("LoParkour").setTabCompleter(cmd);
|
||||
}
|
||||
|
||||
private void setupMetrics() {
|
||||
Metrics metrics = new Metrics(this, 29754);
|
||||
metrics.addCustomChart(new SimplePie("using_sql", () -> Boolean.toString(Option.SQL)));
|
||||
metrics.addCustomChart(new SimplePie("using_rewards", () -> Boolean.toString(Rewards.REWARDS_ENABLED)));
|
||||
metrics.addCustomChart(new SimplePie("locale_count", () -> Integer.toString(Locales.getLocaleCount())));
|
||||
metrics.addCustomChart(new SingleLineChart("player_joins", () -> {
|
||||
int joins = UserRegistry.getJoinCount();
|
||||
return joins;
|
||||
}));
|
||||
instance = this;
|
||||
PluginBootstrap.enable(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disable() {
|
||||
PluginBootstrap.disable(this);
|
||||
}
|
||||
|
||||
/** Called from {@link PluginBootstrap} on shutdown. */
|
||||
public void runShutdownSequence() {
|
||||
try {
|
||||
// Clear all sessions BEFORE leaving players to prevent memory leaks
|
||||
new ArrayList<>(Divider.sections.keySet()).forEach(session -> {
|
||||
try {
|
||||
session.onAllPlayersLeft();
|
||||
|
|
@ -194,23 +96,19 @@ public final class LoParkour extends LoPlugin {
|
|||
getLogger().warning("Error cleaning up session: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
// Remove all sessions from Divider
|
||||
|
||||
new ArrayList<>(Divider.sections.keySet()).forEach(Divider::remove);
|
||||
|
||||
// Now leave all players
|
||||
|
||||
for (ParkourUser user : ParkourUser.getUsers()) {
|
||||
ParkourUser.leave(user);
|
||||
}
|
||||
|
||||
// write all LoParkour gamemodes (may be null if enable failed)
|
||||
if (Modes.DEFAULT != null && Modes.DEFAULT.getLeaderboard() != null) {
|
||||
Modes.DEFAULT.getLeaderboard().write(false);
|
||||
}
|
||||
|
||||
// Clear rewards to prevent memory leaks
|
||||
dev.loki.loparkour.reward.Rewards.clear();
|
||||
|
||||
Rewards.clear();
|
||||
dev.loki.loparkour.adaptive.bootstrap.AdaptiveServices.shutdown();
|
||||
Storage.close();
|
||||
World.delete();
|
||||
} catch (Throwable t) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,172 @@
|
|||
package dev.loki.loparkour.adaptive.bootstrap;
|
||||
|
||||
import dev.loki.loparkour.adaptive.core.AdaptiveDifficulty;
|
||||
import dev.loki.loparkour.adaptive.core.MetricsCollector;
|
||||
import dev.loki.loparkour.adaptive.model.AdaptiveConfig;
|
||||
import dev.loki.loparkour.adaptive.model.SkillRating;
|
||||
import dev.loki.loparkour.adaptive.storage.FileStatsStorage;
|
||||
import dev.loki.loparkour.adaptive.storage.SQLStatsStorage;
|
||||
import dev.loki.loparkour.adaptive.storage.StatsRepository;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
import dev.loki.loparkour.generator.core.model.Profile;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import dev.loki.loparkour.storage.Storage;
|
||||
import dev.loki.loparkour.storage.sql.StorageSQL;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Bootstrap for the adaptive difficulty subsystem.
|
||||
*/
|
||||
public final class AdaptiveServices {
|
||||
|
||||
@Nullable
|
||||
private static Plugin plugin;
|
||||
@Nullable
|
||||
private static StatsRepository repository;
|
||||
@Nullable
|
||||
private static MetricsCollector metricsCollector;
|
||||
|
||||
private AdaptiveServices() {
|
||||
}
|
||||
|
||||
public static void init(@NotNull Plugin pluginInstance) {
|
||||
plugin = pluginInstance;
|
||||
AdaptiveConfig.init();
|
||||
if (!AdaptiveConfig.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Runnable setup = () -> startCollector(createRepository());
|
||||
if (Option.SQL) {
|
||||
Storage.runWhenReady(setup);
|
||||
} else {
|
||||
setup.run();
|
||||
}
|
||||
}
|
||||
|
||||
public static void reload() {
|
||||
shutdownCollector();
|
||||
AdaptiveConfig.reload();
|
||||
if (plugin != null && AdaptiveConfig.isEnabled()) {
|
||||
Runnable setup = () -> startCollector(createRepository());
|
||||
if (Option.SQL) {
|
||||
Storage.runWhenReady(setup);
|
||||
} else {
|
||||
setup.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void attachToGenerator(@NotNull ParkourGenerator generator) {
|
||||
if (metricsCollector != null) {
|
||||
generator.registerEventListener(metricsCollector);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies adaptive difficulty weights to a profile being built for a generator.
|
||||
*/
|
||||
public static void applyAdaptiveProfile(@NotNull ParkourGenerator generator, @NotNull Profile profile) {
|
||||
if (!AdaptiveConfig.isEnabled() || metricsCollector == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
double target = AdaptiveDifficulty.resolveTargetDifficulty(generator, AdaptiveServices::resolveSkillRating);
|
||||
AdaptiveDifficulty.applyToProfile(profile, target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates stored skill rating from current session metrics after a fall.
|
||||
*/
|
||||
public static void persistRatingAfterFall(@NotNull ParkourPlayer player) {
|
||||
if (!AdaptiveConfig.isEnabled() || repository == null || metricsCollector == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
UUID uuid = player.getUUID();
|
||||
SkillRating session = AdaptiveDifficulty.analyzeSession(player, metricsCollector);
|
||||
SkillRating stored = repository.loadSkillRating(uuid);
|
||||
|
||||
if (stored != null) {
|
||||
stored.updateRating(session.getRating(), AdaptiveConfig.getRatingWeight());
|
||||
repository.saveSkillRating(stored);
|
||||
} else {
|
||||
repository.saveSkillRating(session);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuilds generator profile/chances from latest ratings (call once per fall event).
|
||||
*/
|
||||
public static void refreshGeneratorDifficulty(@NotNull ParkourGenerator generator) {
|
||||
if (!AdaptiveConfig.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
generator.profileManager.recalculateProfile();
|
||||
}
|
||||
|
||||
public static void unloadPlayer(@NotNull UUID playerUuid) {
|
||||
if (metricsCollector != null) {
|
||||
metricsCollector.unloadPlayer(playerUuid);
|
||||
}
|
||||
}
|
||||
|
||||
public static void shutdown() {
|
||||
shutdownCollector();
|
||||
repository = null;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static SkillRating resolveSkillRating(@NotNull ParkourPlayer player) {
|
||||
UUID uuid = player.getUUID();
|
||||
SkillRating stored = repository != null ? repository.loadSkillRating(uuid) : null;
|
||||
|
||||
if (metricsCollector == null) {
|
||||
return stored != null ? stored : new SkillRating(uuid);
|
||||
}
|
||||
|
||||
var metrics = metricsCollector.getMetrics(uuid);
|
||||
if (metrics.getTotalJumps() < AdaptiveConfig.getMinSessions()) {
|
||||
if (stored != null && stored.isReliable()) {
|
||||
return stored;
|
||||
}
|
||||
return stored != null ? stored : new SkillRating(uuid);
|
||||
}
|
||||
|
||||
SkillRating session = AdaptiveDifficulty.analyzeSession(player, metricsCollector);
|
||||
if (stored == null) {
|
||||
return session;
|
||||
}
|
||||
return AdaptiveDifficulty.mergeSessionRating(stored, session);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static StatsRepository createRepository() {
|
||||
if (Option.SQL && StorageSQL.isConnected()) {
|
||||
return new SQLStatsStorage(StorageSQL.getConnectionManager());
|
||||
}
|
||||
return new FileStatsStorage();
|
||||
}
|
||||
|
||||
private static void startCollector(@NotNull StatsRepository repo) {
|
||||
if (plugin == null) {
|
||||
return;
|
||||
}
|
||||
repository = repo;
|
||||
metricsCollector = new MetricsCollector(plugin, repo);
|
||||
metricsCollector.startAutoSave();
|
||||
}
|
||||
|
||||
private static void shutdownCollector() {
|
||||
if (metricsCollector != null) {
|
||||
metricsCollector.stopAutoSave();
|
||||
metricsCollector.saveAllMetrics();
|
||||
metricsCollector = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
package dev.loki.loparkour.adaptive.core;
|
||||
|
||||
import dev.loki.loparkour.adaptive.model.AdaptiveConfig;
|
||||
import dev.loki.loparkour.adaptive.model.SkillRating;
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
import dev.loki.loparkour.generator.core.model.Profile;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Applies adaptive difficulty to a generator {@link Profile}.
|
||||
*/
|
||||
public final class AdaptiveDifficulty {
|
||||
|
||||
private static final DifficultyCalculator CALCULATOR = new DifficultyCalculator();
|
||||
private static final DifficultyAdjuster ADJUSTER = new DifficultyAdjuster();
|
||||
private static final SkillAnalyzer ANALYZER = new SkillAnalyzer();
|
||||
|
||||
private static final Set<String> DIFFICULTY_PREFIXES = Set.of(
|
||||
"distance.", "height.", "type."
|
||||
);
|
||||
|
||||
private AdaptiveDifficulty() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges difficulty-related keys from {@code adjusted} into {@code target}.
|
||||
*/
|
||||
public static void mergeDifficultySettings(@NotNull Profile adjusted, @NotNull Profile target) {
|
||||
for (String key : adjusted.settings.keySet()) {
|
||||
if (isDifficultyKey(key)) {
|
||||
target.set(key, adjusted.get(key).value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static double resolveTargetDifficulty(
|
||||
@NotNull ParkourGenerator generator,
|
||||
@NotNull SkillRatingResolver resolver) {
|
||||
var players = generator.getPlayers();
|
||||
if (players.isEmpty()) {
|
||||
return 0.5;
|
||||
}
|
||||
|
||||
double weightedSum = 0.0;
|
||||
double weightTotal = 0.0;
|
||||
|
||||
for (ParkourPlayer player : players) {
|
||||
SkillRating rating = resolver.resolve(player);
|
||||
double target = CALCULATOR.calculateTargetDifficulty(rating);
|
||||
double weight = Math.max(0.1, rating.getConfidence());
|
||||
weightedSum += target * weight;
|
||||
weightTotal += weight;
|
||||
}
|
||||
|
||||
return weightTotal > 0 ? weightedSum / weightTotal : 0.5;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static SkillRating mergeSessionRating(@NotNull SkillRating stored, @NotNull SkillRating sessionSample) {
|
||||
double weight = AdaptiveConfig.getRatingWeight();
|
||||
double merged = (stored.getRating() * (1.0 - weight)) + (sessionSample.getRating() * weight);
|
||||
double confidence = Math.max(stored.getConfidence(), sessionSample.getConfidence());
|
||||
return new SkillRating(stored.getPlayerUuid(), merged, confidence, stored.getSessionsCount() + 1);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static SkillRating analyzeSession(@NotNull ParkourPlayer player, @NotNull MetricsCollector collector) {
|
||||
return ANALYZER.analyzeSkill(collector.getMetrics(player.getUUID()));
|
||||
}
|
||||
|
||||
public static void applyToProfile(
|
||||
@NotNull Profile baseProfile,
|
||||
double targetDifficulty) {
|
||||
Profile adjusted = ADJUSTER.adjustProfile(baseProfile, targetDifficulty);
|
||||
mergeDifficultySettings(adjusted, baseProfile);
|
||||
}
|
||||
|
||||
private static boolean isDifficultyKey(@NotNull String key) {
|
||||
return DIFFICULTY_PREFIXES.stream().anyMatch(key::startsWith);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface SkillRatingResolver {
|
||||
@NotNull
|
||||
SkillRating resolve(@NotNull ParkourPlayer player);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
package dev.loki.loparkour.adaptive.core;
|
||||
|
||||
import dev.loki.loparkour.generator.core.model.Profile;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Adjusts difficulty profile parameters based on target difficulty.
|
||||
* Modifies distance, height, and special block chances to match player skill.
|
||||
*
|
||||
* Difficulty scale (0.0-1.0):
|
||||
* - 0.0-0.3: Easy (short jumps, minimal height variation)
|
||||
* - 0.3-0.6: Medium (balanced jumps, moderate height)
|
||||
* - 0.6-0.8: Hard (longer jumps, more height variation)
|
||||
* - 0.8-1.0: Expert (maximum distance, high variation)
|
||||
*
|
||||
* Thread-safe: Stateless, all methods are pure functions.
|
||||
*/
|
||||
public class DifficultyAdjuster {
|
||||
|
||||
// Distance bounds (blocks)
|
||||
private static final int MIN_DISTANCE = 1;
|
||||
private static final int MAX_DISTANCE = 4;
|
||||
|
||||
// Height bounds (blocks)
|
||||
private static final int MIN_HEIGHT = -1;
|
||||
private static final int MAX_HEIGHT = 2;
|
||||
|
||||
/**
|
||||
* Adjusts a profile to match the target difficulty.
|
||||
* Creates a new profile with modified parameters.
|
||||
*
|
||||
* @param baseProfile The base profile to adjust
|
||||
* @param targetDifficulty Target difficulty (0.0-1.0)
|
||||
* @return New profile with adjusted parameters
|
||||
*/
|
||||
@NotNull
|
||||
public Profile adjustProfile(@NotNull Profile baseProfile, double targetDifficulty) {
|
||||
Profile adjusted = new Profile();
|
||||
|
||||
// Copy base settings
|
||||
copyBaseSettings(baseProfile, adjusted);
|
||||
|
||||
// Adjust difficulty parameters
|
||||
adjustDistanceChances(adjusted, targetDifficulty);
|
||||
adjustHeightChances(adjusted, targetDifficulty);
|
||||
adjustSpecialFrequency(adjusted, targetDifficulty);
|
||||
|
||||
return adjusted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies non-difficulty settings from base profile.
|
||||
*/
|
||||
private void copyBaseSettings(@NotNull Profile source, @NotNull Profile target) {
|
||||
String[] settingsKeys = {"min-y", "max-y", "heading", "first-block-material"};
|
||||
|
||||
for (String key : settingsKeys) {
|
||||
if (source.settings.containsKey(key)) {
|
||||
target.set(key, source.get(key).value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts distance chances based on difficulty.
|
||||
*/
|
||||
private void adjustDistanceChances(@NotNull Profile profile, double difficulty) {
|
||||
double[] weights = DifficultyWeights.calculateDistanceWeights(difficulty);
|
||||
|
||||
profile.set("distance.min", String.valueOf(MIN_DISTANCE));
|
||||
profile.set("distance.max", String.valueOf(MAX_DISTANCE));
|
||||
|
||||
for (int distance = MIN_DISTANCE; distance <= MAX_DISTANCE; distance++) {
|
||||
int index = distance - MIN_DISTANCE;
|
||||
int chance = (int) Math.round(weights[index] * 100);
|
||||
profile.set("distance.chances." + distance, String.valueOf(chance));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts height variation based on difficulty.
|
||||
*/
|
||||
private void adjustHeightChances(@NotNull Profile profile, double difficulty) {
|
||||
double[] weights = DifficultyWeights.calculateHeightWeights(difficulty);
|
||||
|
||||
profile.set("height.min", String.valueOf(MIN_HEIGHT));
|
||||
profile.set("height.max", String.valueOf(MAX_HEIGHT));
|
||||
|
||||
int index = 0;
|
||||
for (int height = MIN_HEIGHT; height <= MAX_HEIGHT; height++) {
|
||||
int chance = (int) Math.round(weights[index] * 100);
|
||||
profile.set("height.chances." + height, String.valueOf(chance));
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts special block frequency based on difficulty.
|
||||
*/
|
||||
private void adjustSpecialFrequency(@NotNull Profile profile, double difficulty) {
|
||||
double specialPercent = DifficultyWeights.calculateSpecialFrequency(difficulty);
|
||||
|
||||
int defaultPercent = (int) Math.round(100.0 - specialPercent);
|
||||
int specialChance = (int) Math.round(specialPercent * 0.75);
|
||||
int schematicChance = (int) Math.round(specialPercent * 0.25);
|
||||
|
||||
profile.set("type.default", String.valueOf(defaultPercent));
|
||||
profile.set("type.special", String.valueOf(specialChance));
|
||||
profile.set("type.schematic", String.valueOf(schematicChance));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the effective difficulty of a profile.
|
||||
* Inverse operation of adjustProfile.
|
||||
*
|
||||
* @param profile The profile to analyze
|
||||
* @return Estimated difficulty (0.0-1.0)
|
||||
*/
|
||||
public double estimateDifficulty(@NotNull Profile profile) {
|
||||
double distanceDifficulty = estimateDistanceDifficulty(profile);
|
||||
double heightDifficulty = estimateHeightDifficulty(profile);
|
||||
double specialDifficulty = estimateSpecialDifficulty(profile);
|
||||
|
||||
// Weighted average
|
||||
return (distanceDifficulty * 0.5) + (heightDifficulty * 0.3) + (specialDifficulty * 0.2);
|
||||
}
|
||||
|
||||
private double estimateDistanceDifficulty(@NotNull Profile profile) {
|
||||
double avgDistance = 0.0;
|
||||
double totalWeight = 0.0;
|
||||
|
||||
for (int distance = MIN_DISTANCE; distance <= MAX_DISTANCE; distance++) {
|
||||
String key = "distance.chances." + distance;
|
||||
if (profile.settings.containsKey(key)) {
|
||||
double weight = profile.get(key).asDouble();
|
||||
avgDistance += distance * weight;
|
||||
totalWeight += weight;
|
||||
}
|
||||
}
|
||||
|
||||
if (totalWeight > 0) {
|
||||
avgDistance /= totalWeight;
|
||||
}
|
||||
|
||||
// Map average distance to difficulty (1.0 = easy, 4.0 = hard)
|
||||
return (avgDistance - 1.0) / 3.0;
|
||||
}
|
||||
|
||||
private double estimateHeightDifficulty(@NotNull Profile profile) {
|
||||
double variation = 0.0;
|
||||
double totalWeight = 0.0;
|
||||
|
||||
for (int height = MIN_HEIGHT; height <= MAX_HEIGHT; height++) {
|
||||
String key = "height.chances." + height;
|
||||
if (profile.settings.containsKey(key)) {
|
||||
double weight = profile.get(key).asDouble();
|
||||
variation += Math.abs(height) * weight;
|
||||
totalWeight += weight;
|
||||
}
|
||||
}
|
||||
|
||||
if (totalWeight > 0) {
|
||||
variation /= totalWeight;
|
||||
}
|
||||
|
||||
// Map variation to difficulty (0.0 = flat, 1.5 = varied)
|
||||
return Math.min(1.0, variation / 1.5);
|
||||
}
|
||||
|
||||
private double estimateSpecialDifficulty(@NotNull Profile profile) {
|
||||
if (!profile.settings.containsKey("type.special")) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
double specialPercent = profile.get("type.special").asDouble();
|
||||
double minFrequency = 5.0;
|
||||
double maxFrequency = 30.0;
|
||||
|
||||
// Map special percentage to difficulty
|
||||
return (specialPercent - minFrequency) / (maxFrequency - minFrequency);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
package dev.loki.loparkour.adaptive.core;
|
||||
|
||||
import dev.loki.loparkour.adaptive.model.SkillRating;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Calculates target difficulty based on player skill rating.
|
||||
* Converts skill ratings into normalized difficulty values (0.0-1.0).
|
||||
*
|
||||
* Formula: difficulty = min(1.0, rating / 2.0)
|
||||
* - Rating 0.0 = 0.0 difficulty (easiest)
|
||||
* - Rating 1.0 = 0.5 difficulty (medium)
|
||||
* - Rating 2.0+ = 1.0 difficulty (hardest)
|
||||
*
|
||||
* Thread-safe: Stateless, all methods are pure functions.
|
||||
*/
|
||||
public class DifficultyCalculator {
|
||||
|
||||
private static final double MAX_RATING_FOR_FULL_DIFFICULTY = 2.0;
|
||||
private static final double MIN_DIFFICULTY = 0.0;
|
||||
private static final double MAX_DIFFICULTY = 1.0;
|
||||
|
||||
/**
|
||||
* Calculates the target difficulty for a player based on their skill rating.
|
||||
*
|
||||
* @param skillRating The player's skill rating
|
||||
* @return Target difficulty (0.0-1.0)
|
||||
*/
|
||||
public double calculateTargetDifficulty(@NotNull SkillRating skillRating) {
|
||||
double rating = skillRating.getRating();
|
||||
double confidence = skillRating.getConfidence();
|
||||
|
||||
// Calculate base difficulty from rating
|
||||
double baseDifficulty = calculateBaseDifficulty(rating);
|
||||
|
||||
// Apply confidence adjustment (lower confidence = more conservative difficulty)
|
||||
double adjustedDifficulty = applyConfidenceAdjustment(baseDifficulty, confidence);
|
||||
|
||||
return clampDifficulty(adjustedDifficulty);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates base difficulty from skill rating.
|
||||
* Linear mapping: rating 2.0 = difficulty 1.0
|
||||
*
|
||||
* @param rating The skill rating
|
||||
* @return Base difficulty (0.0-1.0+)
|
||||
*/
|
||||
private double calculateBaseDifficulty(double rating) {
|
||||
return rating / MAX_RATING_FOR_FULL_DIFFICULTY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies confidence adjustment to difficulty.
|
||||
* Low confidence ratings result in more conservative difficulty.
|
||||
*
|
||||
* @param baseDifficulty The base difficulty
|
||||
* @param confidence The confidence level (0.0-1.0)
|
||||
* @return Adjusted difficulty
|
||||
*/
|
||||
private double applyConfidenceAdjustment(double baseDifficulty, double confidence) {
|
||||
// If confidence is low, reduce difficulty to be safe
|
||||
// confidence 0.0 = 50% of base difficulty
|
||||
// confidence 1.0 = 100% of base difficulty
|
||||
double confidenceMultiplier = 0.5 + (confidence * 0.5);
|
||||
return baseDifficulty * confidenceMultiplier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamps difficulty to valid range [0.0, 1.0].
|
||||
*
|
||||
* @param difficulty The difficulty value
|
||||
* @return Clamped difficulty
|
||||
*/
|
||||
private double clampDifficulty(double difficulty) {
|
||||
return Math.max(MIN_DIFFICULTY, Math.min(MAX_DIFFICULTY, difficulty));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the recommended rating for a target difficulty.
|
||||
* Inverse of calculateTargetDifficulty (without confidence adjustment).
|
||||
*
|
||||
* @param targetDifficulty The desired difficulty (0.0-1.0)
|
||||
* @return Recommended skill rating
|
||||
*/
|
||||
public double calculateRecommendedRating(double targetDifficulty) {
|
||||
double clampedDifficulty = clampDifficulty(targetDifficulty);
|
||||
return clampedDifficulty * MAX_RATING_FOR_FULL_DIFFICULTY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a player is ready for increased difficulty.
|
||||
* Checks if rating is significantly above current difficulty level.
|
||||
*
|
||||
* @param skillRating The player's skill rating
|
||||
* @param currentDifficulty The current difficulty (0.0-1.0)
|
||||
* @return true if player is ready for harder content
|
||||
*/
|
||||
public boolean isReadyForIncrease(@NotNull SkillRating skillRating, double currentDifficulty) {
|
||||
double targetDifficulty = calculateTargetDifficulty(skillRating);
|
||||
double threshold = 0.1; // 10% increase threshold
|
||||
|
||||
return targetDifficulty > currentDifficulty + threshold
|
||||
&& skillRating.getConfidence() >= 0.5; // Require reasonable confidence
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if difficulty should be decreased for a player.
|
||||
* Checks if rating is significantly below current difficulty level.
|
||||
*
|
||||
* @param skillRating The player's skill rating
|
||||
* @param currentDifficulty The current difficulty (0.0-1.0)
|
||||
* @return true if difficulty should be reduced
|
||||
*/
|
||||
public boolean shouldDecrease(@NotNull SkillRating skillRating, double currentDifficulty) {
|
||||
double targetDifficulty = calculateTargetDifficulty(skillRating);
|
||||
double threshold = 0.15; // 15% decrease threshold (more aggressive than increase)
|
||||
|
||||
return targetDifficulty < currentDifficulty - threshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates a smooth difficulty transition.
|
||||
* Prevents sudden difficulty spikes by limiting change rate.
|
||||
*
|
||||
* @param currentDifficulty The current difficulty
|
||||
* @param targetDifficulty The target difficulty
|
||||
* @param maxChange Maximum allowed change per adjustment
|
||||
* @return Smoothed difficulty
|
||||
*/
|
||||
public double smoothTransition(double currentDifficulty, double targetDifficulty, double maxChange) {
|
||||
double difference = targetDifficulty - currentDifficulty;
|
||||
|
||||
if (Math.abs(difference) <= maxChange) {
|
||||
return targetDifficulty;
|
||||
}
|
||||
|
||||
// Move towards target by maxChange
|
||||
return currentDifficulty + Math.signum(difference) * maxChange;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
package dev.loki.loparkour.adaptive.core;
|
||||
|
||||
/**
|
||||
* Calculates weight distributions for different difficulty levels.
|
||||
* Contains formulas for distance, height, and special block distributions.
|
||||
*
|
||||
* Thread-safe: Stateless, all methods are pure functions.
|
||||
*/
|
||||
class DifficultyWeights {
|
||||
|
||||
/**
|
||||
* Calculates distance weight distribution for given difficulty.
|
||||
*
|
||||
* @param difficulty Target difficulty (0.0-1.0)
|
||||
* @return Array of weights [dist1, dist2, dist3, dist4]
|
||||
*/
|
||||
static double[] calculateDistanceWeights(double difficulty) {
|
||||
double[] weights = new double[4];
|
||||
|
||||
if (difficulty < 0.3) {
|
||||
// Easy: favor short jumps (1-2 blocks)
|
||||
weights[0] = 0.40; // 1 block
|
||||
weights[1] = 0.40; // 2 blocks
|
||||
weights[2] = 0.15; // 3 blocks
|
||||
weights[3] = 0.05; // 4 blocks
|
||||
} else if (difficulty < 0.6) {
|
||||
// Medium: balanced distribution
|
||||
weights[0] = 0.20; // 1 block
|
||||
weights[1] = 0.35; // 2 blocks
|
||||
weights[2] = 0.30; // 3 blocks
|
||||
weights[3] = 0.15; // 4 blocks
|
||||
} else if (difficulty < 0.8) {
|
||||
// Hard: favor longer jumps
|
||||
weights[0] = 0.10; // 1 block
|
||||
weights[1] = 0.25; // 2 blocks
|
||||
weights[2] = 0.40; // 3 blocks
|
||||
weights[3] = 0.25; // 4 blocks
|
||||
} else {
|
||||
// Expert: maximum challenge
|
||||
weights[0] = 0.05; // 1 block
|
||||
weights[1] = 0.15; // 2 blocks
|
||||
weights[2] = 0.35; // 3 blocks
|
||||
weights[3] = 0.45; // 4 blocks
|
||||
}
|
||||
|
||||
return weights;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates height weight distribution for given difficulty.
|
||||
*
|
||||
* @param difficulty Target difficulty (0.0-1.0)
|
||||
* @return Array of weights [-1, 0, 1, 2]
|
||||
*/
|
||||
static double[] calculateHeightWeights(double difficulty) {
|
||||
double[] weights = new double[4];
|
||||
|
||||
if (difficulty < 0.3) {
|
||||
// Easy: mostly flat, minimal drops
|
||||
weights[0] = 0.10; // -1 (down)
|
||||
weights[1] = 0.60; // 0 (flat)
|
||||
weights[2] = 0.25; // +1 (up)
|
||||
weights[3] = 0.05; // +2 (high up)
|
||||
} else if (difficulty < 0.6) {
|
||||
// Medium: balanced variation
|
||||
weights[0] = 0.20; // -1
|
||||
weights[1] = 0.40; // 0
|
||||
weights[2] = 0.30; // +1
|
||||
weights[3] = 0.10; // +2
|
||||
} else if (difficulty < 0.8) {
|
||||
// Hard: more variation
|
||||
weights[0] = 0.25; // -1
|
||||
weights[1] = 0.30; // 0
|
||||
weights[2] = 0.30; // +1
|
||||
weights[3] = 0.15; // +2
|
||||
} else {
|
||||
// Expert: maximum variation
|
||||
weights[0] = 0.30; // -1
|
||||
weights[1] = 0.20; // 0
|
||||
weights[2] = 0.30; // +1
|
||||
weights[3] = 0.20; // +2
|
||||
}
|
||||
|
||||
return weights;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates special block frequency for given difficulty.
|
||||
*
|
||||
* @param difficulty Target difficulty (0.0-1.0)
|
||||
* @return Special block percentage (5.0-30.0)
|
||||
*/
|
||||
static double calculateSpecialFrequency(double difficulty) {
|
||||
double minFrequency = 5.0;
|
||||
double maxFrequency = 30.0;
|
||||
return minFrequency + (maxFrequency - minFrequency) * difficulty;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,214 @@
|
|||
package dev.loki.loparkour.adaptive.core;
|
||||
|
||||
import dev.loki.loparkour.adaptive.model.PlayerMetrics;
|
||||
import dev.loki.loparkour.adaptive.storage.StatsRepository;
|
||||
import dev.loki.loparkour.generator.core.coordinator.GeneratorEventListener;
|
||||
import dev.loki.loparkour.generator.jump.calculation.JumpType;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Collects and manages player performance metrics.
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Track player jumps, falls, and scoring events
|
||||
* - Detect near-miss jumps (distance > 3.5 blocks)
|
||||
* - Calculate average time per block
|
||||
* - Maintain jump type statistics
|
||||
* - Auto-save metrics every 5 minutes
|
||||
*
|
||||
* Thread-safe: Uses ConcurrentHashMap for player metrics cache.
|
||||
*/
|
||||
public class MetricsCollector implements GeneratorEventListener {
|
||||
|
||||
private static final double NEAR_MISS_THRESHOLD = 3.5;
|
||||
private static final long AUTO_SAVE_INTERVAL_TICKS = 20 * 60 * 5; // 5 minutes
|
||||
|
||||
private final Plugin plugin;
|
||||
private final StatsRepository repository;
|
||||
private final Map<UUID, PlayerMetrics> metricsCache;
|
||||
private final Map<UUID, Block> lastBlockCache;
|
||||
private final Map<UUID, Long> lastBlockTimeCache;
|
||||
|
||||
private BukkitRunnable autoSaveTask;
|
||||
|
||||
/**
|
||||
* Create a new MetricsCollector.
|
||||
*
|
||||
* @param plugin The plugin instance for scheduling tasks
|
||||
* @param repository The repository for persisting metrics
|
||||
*/
|
||||
public MetricsCollector(@NotNull Plugin plugin, @NotNull StatsRepository repository) {
|
||||
this.plugin = plugin;
|
||||
this.repository = repository;
|
||||
this.metricsCache = new ConcurrentHashMap<>();
|
||||
this.lastBlockCache = new ConcurrentHashMap<>();
|
||||
this.lastBlockTimeCache = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the auto-save task.
|
||||
* Should be called when the collector is initialized.
|
||||
*/
|
||||
public void startAutoSave() {
|
||||
if (autoSaveTask != null) {
|
||||
autoSaveTask.cancel();
|
||||
}
|
||||
|
||||
autoSaveTask = new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
saveAllMetrics();
|
||||
}
|
||||
};
|
||||
|
||||
autoSaveTask.runTaskTimerAsynchronously(plugin, AUTO_SAVE_INTERVAL_TICKS, AUTO_SAVE_INTERVAL_TICKS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the auto-save task.
|
||||
* Should be called when the plugin is disabled.
|
||||
*/
|
||||
public void stopAutoSave() {
|
||||
if (autoSaveTask != null) {
|
||||
autoSaveTask.cancel();
|
||||
autoSaveTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create metrics for a player.
|
||||
*
|
||||
* @param playerUuid The player's UUID
|
||||
* @return The player's metrics
|
||||
*/
|
||||
@NotNull
|
||||
public PlayerMetrics getMetrics(@NotNull UUID playerUuid) {
|
||||
return metricsCache.computeIfAbsent(playerUuid, uuid -> {
|
||||
// Try to load from repository
|
||||
PlayerMetrics loaded = repository.loadMetrics(uuid);
|
||||
if (loaded != null) {
|
||||
return loaded;
|
||||
}
|
||||
// Create new metrics if not found
|
||||
return new PlayerMetrics(uuid);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a player lands on a block.
|
||||
* Detects near-miss jumps and updates timing metrics.
|
||||
*
|
||||
* @param player The player who landed
|
||||
* @param block The block the player landed on
|
||||
* @param distance The distance of the jump
|
||||
*/
|
||||
public void onBlockLand(@NotNull Player player, @NotNull Block block, double distance) {
|
||||
UUID uuid = player.getUniqueId();
|
||||
PlayerMetrics metrics = getMetrics(uuid);
|
||||
|
||||
// Calculate time since last block
|
||||
long currentTime = System.currentTimeMillis();
|
||||
Long lastTime = lastBlockTimeCache.get(uuid);
|
||||
if (lastTime != null) {
|
||||
long timeDiff = currentTime - lastTime;
|
||||
// Record jump with timing (use "normal" as default type)
|
||||
metrics.recordJump("normal", timeDiff);
|
||||
}
|
||||
lastBlockTimeCache.put(uuid, currentTime);
|
||||
|
||||
// Detect near-miss (long jump that succeeded)
|
||||
if (distance > NEAR_MISS_THRESHOLD) {
|
||||
metrics.recordNearMiss();
|
||||
}
|
||||
|
||||
// Update last block
|
||||
lastBlockCache.put(uuid, block);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlockGenerated(@NotNull Player player, @NotNull Block block, @NotNull JumpType jumpType, double distance) {
|
||||
// Not used for metrics collection, but required by interface
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerScore(@NotNull Player player, @NotNull Block block, double distance) {
|
||||
onBlockLand(player, block, distance);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerFall(@NotNull Player player, @Nullable Block lastBlock) {
|
||||
UUID uuid = player.getUniqueId();
|
||||
PlayerMetrics metrics = getMetrics(uuid);
|
||||
|
||||
// Save metrics on fall (important event)
|
||||
repository.saveMetrics(metrics);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onJump(@NotNull Player player, @NotNull JumpType jumpType) {
|
||||
UUID uuid = player.getUniqueId();
|
||||
PlayerMetrics metrics = getMetrics(uuid);
|
||||
|
||||
// Calculate time since last block
|
||||
long currentTime = System.currentTimeMillis();
|
||||
Long lastTime = lastBlockTimeCache.get(uuid);
|
||||
long timeDiff = (lastTime != null) ? (currentTime - lastTime) : 0;
|
||||
|
||||
// Record jump with type and timing
|
||||
metrics.recordJump(jumpType.name().toLowerCase(), timeDiff);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save metrics for a specific player.
|
||||
*
|
||||
* @param playerUuid The player's UUID
|
||||
*/
|
||||
public void saveMetrics(@NotNull UUID playerUuid) {
|
||||
PlayerMetrics metrics = metricsCache.get(playerUuid);
|
||||
if (metrics != null) {
|
||||
repository.saveMetrics(metrics);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save all cached metrics.
|
||||
* Called by auto-save task and on plugin disable.
|
||||
*/
|
||||
public void saveAllMetrics() {
|
||||
for (PlayerMetrics metrics : metricsCache.values()) {
|
||||
repository.saveMetrics(metrics);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a player from the cache.
|
||||
* Should be called when a player leaves the server.
|
||||
*
|
||||
* @param playerUuid The player's UUID
|
||||
*/
|
||||
public void unloadPlayer(@NotNull UUID playerUuid) {
|
||||
// Save before removing
|
||||
saveMetrics(playerUuid);
|
||||
metricsCache.remove(playerUuid);
|
||||
lastBlockCache.remove(playerUuid);
|
||||
lastBlockTimeCache.remove(playerUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of cached players.
|
||||
*
|
||||
* @return The cache size
|
||||
*/
|
||||
public int getCacheSize() {
|
||||
return metricsCache.size();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
package dev.loki.loparkour.adaptive.core;
|
||||
|
||||
import dev.loki.loparkour.adaptive.model.PlayerMetrics;
|
||||
import dev.loki.loparkour.adaptive.model.SkillRating;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Analyzes player metrics and calculates skill ratings.
|
||||
* Uses a formula that considers success rate, near misses, and consistency.
|
||||
*
|
||||
* Formula: rating = baseRating * (successRate^2) * (1 + nearMissBonus) * streakMultiplier
|
||||
* - baseRating: 1.0 (baseline skill level)
|
||||
* - successRate: percentage of successful jumps (0.0-1.0)
|
||||
* - nearMissBonus: 0.1 per near miss (rewards precision)
|
||||
* - streakMultiplier: 1 + (successfulJumps / 100) (rewards consistency)
|
||||
*
|
||||
* Thread-safe: Stateless, all methods are pure functions.
|
||||
*/
|
||||
public class SkillAnalyzer {
|
||||
|
||||
private static final double BASE_RATING = 1.0;
|
||||
private static final double NEAR_MISS_BONUS = 0.1;
|
||||
private static final double STREAK_DIVISOR = 100.0;
|
||||
private static final int MIN_JUMPS_FOR_CONFIDENCE = 10;
|
||||
private static final int MAX_JUMPS_FOR_CONFIDENCE = 100;
|
||||
|
||||
/**
|
||||
* Analyzes player metrics and calculates a skill rating.
|
||||
*
|
||||
* @param metrics The player's performance metrics
|
||||
* @return A skill rating with confidence level
|
||||
*/
|
||||
@NotNull
|
||||
public SkillRating analyzeSkill(@NotNull PlayerMetrics metrics) {
|
||||
double successRate = metrics.getSuccessRate();
|
||||
int nearMisses = metrics.getNearMissCount();
|
||||
int successfulJumps = metrics.getSuccessfulJumps();
|
||||
int totalJumps = metrics.getTotalJumps();
|
||||
|
||||
// Calculate rating components
|
||||
double successComponent = calculateSuccessComponent(successRate);
|
||||
double nearMissBonus = calculateNearMissBonus(nearMisses);
|
||||
double streakMultiplier = calculateStreakMultiplier(successfulJumps);
|
||||
|
||||
// Combine components
|
||||
double rating = BASE_RATING * successComponent * nearMissBonus * streakMultiplier;
|
||||
|
||||
// Calculate confidence based on sample size
|
||||
double confidence = calculateConfidence(totalJumps);
|
||||
|
||||
// Sessions count is 1 for current session analysis
|
||||
int sessionsCount = 1;
|
||||
|
||||
return new SkillRating(metrics.getPlayerUuid(), rating, confidence, sessionsCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the success rate component of the rating.
|
||||
* Uses squared success rate to emphasize consistency.
|
||||
*
|
||||
* @param successRate The player's success rate (0.0-1.0)
|
||||
* @return Success component (0.0-1.0)
|
||||
*/
|
||||
private double calculateSuccessComponent(double successRate) {
|
||||
return Math.pow(successRate, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the near miss bonus multiplier.
|
||||
* Rewards players who land close to block edges (precision).
|
||||
*
|
||||
* @param nearMisses Number of near miss landings
|
||||
* @return Near miss multiplier (1.0+)
|
||||
*/
|
||||
private double calculateNearMissBonus(int nearMisses) {
|
||||
return 1.0 + (nearMisses * NEAR_MISS_BONUS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the streak multiplier based on successful jumps.
|
||||
* Rewards consistent performance over time.
|
||||
*
|
||||
* @param successfulJumps Number of successful jumps
|
||||
* @return Streak multiplier (1.0+)
|
||||
*/
|
||||
private double calculateStreakMultiplier(int successfulJumps) {
|
||||
return 1.0 + (successfulJumps / STREAK_DIVISOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates confidence level based on sample size.
|
||||
* More jumps = higher confidence in the rating.
|
||||
*
|
||||
* @param totalJumps Total number of jumps attempted
|
||||
* @return Confidence level (0.0-1.0)
|
||||
*/
|
||||
private double calculateConfidence(int totalJumps) {
|
||||
if (totalJumps < MIN_JUMPS_FOR_CONFIDENCE) {
|
||||
// Low confidence for small samples
|
||||
return (double) totalJumps / MIN_JUMPS_FOR_CONFIDENCE;
|
||||
} else if (totalJumps >= MAX_JUMPS_FOR_CONFIDENCE) {
|
||||
// Maximum confidence reached
|
||||
return 1.0;
|
||||
} else {
|
||||
// Linear interpolation between min and max
|
||||
double range = MAX_JUMPS_FOR_CONFIDENCE - MIN_JUMPS_FOR_CONFIDENCE;
|
||||
double progress = totalJumps - MIN_JUMPS_FOR_CONFIDENCE;
|
||||
return progress / range;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimates the minimum rating for a given success rate.
|
||||
* Useful for UI display and difficulty selection.
|
||||
*
|
||||
* @param successRate The success rate (0.0-1.0)
|
||||
* @return Estimated minimum rating
|
||||
*/
|
||||
public double estimateMinRating(double successRate) {
|
||||
return BASE_RATING * calculateSuccessComponent(successRate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimates the maximum rating for a given success rate.
|
||||
* Assumes high near miss count and long streak.
|
||||
*
|
||||
* @param successRate The success rate (0.0-1.0)
|
||||
* @return Estimated maximum rating
|
||||
*/
|
||||
public double estimateMaxRating(double successRate) {
|
||||
double successComponent = calculateSuccessComponent(successRate);
|
||||
double maxNearMissBonus = calculateNearMissBonus(50); // Assume 50 near misses
|
||||
double maxStreakMultiplier = calculateStreakMultiplier(MAX_JUMPS_FOR_CONFIDENCE);
|
||||
|
||||
return BASE_RATING * successComponent * maxNearMissBonus * maxStreakMultiplier;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
package dev.loki.loparkour.adaptive.model;
|
||||
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Cached configuration for the adaptive difficulty system.
|
||||
* Loads settings from config.yml and caches them for performance.
|
||||
*/
|
||||
public class AdaptiveConfig {
|
||||
|
||||
private static final ConcurrentHashMap<String, Object> cache = new ConcurrentHashMap<>();
|
||||
private static volatile boolean initialized = false;
|
||||
|
||||
// Configuration keys
|
||||
private static final String KEY_ENABLED = "adaptive.enabled";
|
||||
private static final String KEY_MIN_SESSIONS = "adaptive.min-sessions";
|
||||
private static final String KEY_UPDATE_INTERVAL = "adaptive.update-interval";
|
||||
private static final String KEY_RATING_WEIGHT = "adaptive.rating-weight";
|
||||
private static final String KEY_CONFIDENCE_THRESHOLD = "adaptive.confidence-threshold";
|
||||
private static final String KEY_NEAR_MISS_DISTANCE = "adaptive.near-miss-distance";
|
||||
|
||||
// Default values
|
||||
private static final boolean DEFAULT_ENABLED = true;
|
||||
private static final int DEFAULT_MIN_SESSIONS = 3;
|
||||
private static final int DEFAULT_UPDATE_INTERVAL = 300; // 5 minutes in seconds
|
||||
private static final double DEFAULT_RATING_WEIGHT = 0.15;
|
||||
private static final double DEFAULT_CONFIDENCE_THRESHOLD = 0.3;
|
||||
private static final double DEFAULT_NEAR_MISS_DISTANCE = 0.3;
|
||||
|
||||
/**
|
||||
* Initializes the configuration cache from config.yml.
|
||||
* Should be called during plugin startup.
|
||||
*/
|
||||
public static void init() {
|
||||
if (initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
cache.put(KEY_ENABLED, Config.CONFIG.getBoolean(KEY_ENABLED, DEFAULT_ENABLED));
|
||||
cache.put(KEY_MIN_SESSIONS, Config.CONFIG.getInt(KEY_MIN_SESSIONS, DEFAULT_MIN_SESSIONS));
|
||||
cache.put(KEY_UPDATE_INTERVAL, Config.CONFIG.getInt(KEY_UPDATE_INTERVAL, DEFAULT_UPDATE_INTERVAL));
|
||||
cache.put(KEY_RATING_WEIGHT, configDouble(KEY_RATING_WEIGHT, DEFAULT_RATING_WEIGHT));
|
||||
cache.put(KEY_CONFIDENCE_THRESHOLD, configDouble(KEY_CONFIDENCE_THRESHOLD, DEFAULT_CONFIDENCE_THRESHOLD));
|
||||
cache.put(KEY_NEAR_MISS_DISTANCE, configDouble(KEY_NEAR_MISS_DISTANCE, DEFAULT_NEAR_MISS_DISTANCE));
|
||||
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads configuration from config.yml.
|
||||
* Call this after config reload.
|
||||
*/
|
||||
public static void reload() {
|
||||
initialized = false;
|
||||
cache.clear();
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if adaptive difficulty system is enabled.
|
||||
*
|
||||
* @return True if enabled
|
||||
*/
|
||||
public static boolean isEnabled() {
|
||||
ensureInitialized();
|
||||
return (boolean) cache.getOrDefault(KEY_ENABLED, DEFAULT_ENABLED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets minimum number of sessions before adaptive difficulty activates.
|
||||
*
|
||||
* @return Minimum sessions count
|
||||
*/
|
||||
public static int getMinSessions() {
|
||||
ensureInitialized();
|
||||
return (int) cache.getOrDefault(KEY_MIN_SESSIONS, DEFAULT_MIN_SESSIONS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the interval (in seconds) between automatic stats updates.
|
||||
*
|
||||
* @return Update interval in seconds
|
||||
*/
|
||||
public static int getUpdateInterval() {
|
||||
ensureInitialized();
|
||||
return (int) cache.getOrDefault(KEY_UPDATE_INTERVAL, DEFAULT_UPDATE_INTERVAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the weight for rating updates (how much new performance affects rating).
|
||||
*
|
||||
* @return Rating weight (0.0 to 1.0)
|
||||
*/
|
||||
public static double getRatingWeight() {
|
||||
ensureInitialized();
|
||||
Object value = cache.get(KEY_RATING_WEIGHT);
|
||||
if (value instanceof Double) {
|
||||
return (double) value;
|
||||
}
|
||||
return DEFAULT_RATING_WEIGHT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the confidence threshold for reliable ratings.
|
||||
*
|
||||
* @return Confidence threshold (0.0 to 1.0)
|
||||
*/
|
||||
public static double getConfidenceThreshold() {
|
||||
ensureInitialized();
|
||||
Object value = cache.get(KEY_CONFIDENCE_THRESHOLD);
|
||||
if (value instanceof Double) {
|
||||
return (double) value;
|
||||
}
|
||||
return DEFAULT_CONFIDENCE_THRESHOLD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the distance threshold for near-miss detection.
|
||||
*
|
||||
* @return Near-miss distance in blocks
|
||||
*/
|
||||
public static double getNearMissDistance() {
|
||||
ensureInitialized();
|
||||
Object value = cache.get(KEY_NEAR_MISS_DISTANCE);
|
||||
if (value instanceof Double) {
|
||||
return (double) value;
|
||||
}
|
||||
return DEFAULT_NEAR_MISS_DISTANCE;
|
||||
}
|
||||
|
||||
private static void ensureInitialized() {
|
||||
if (!initialized) {
|
||||
init();
|
||||
}
|
||||
}
|
||||
|
||||
private static double configDouble(@NotNull String key, double defaultValue) {
|
||||
return Config.CONFIG.isPath(key) ? Config.CONFIG.getDouble(key) : defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a custom config value with caching.
|
||||
*
|
||||
* @param key The config key
|
||||
* @param defaultValue Default value if key not found
|
||||
* @return The config value
|
||||
*/
|
||||
@NotNull
|
||||
public static <T> T get(@NotNull String key, @NotNull T defaultValue) {
|
||||
ensureInitialized();
|
||||
@SuppressWarnings("unchecked")
|
||||
T value = (T) cache.get(key);
|
||||
return value != null ? value : defaultValue;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
package dev.loki.loparkour.adaptive.model;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Stores detailed player performance metrics for adaptive difficulty calculation.
|
||||
* Tracks timing, jump patterns, and near-miss statistics.
|
||||
*/
|
||||
public class PlayerMetrics {
|
||||
|
||||
private final UUID playerUuid;
|
||||
private double avgTimePerBlock;
|
||||
private final Map<String, Integer> jumpTypeStats;
|
||||
private int nearMissCount;
|
||||
private int totalJumps;
|
||||
private long lastUpdated;
|
||||
|
||||
public PlayerMetrics(@NotNull UUID playerUuid) {
|
||||
this.playerUuid = playerUuid;
|
||||
this.avgTimePerBlock = 0.0;
|
||||
this.jumpTypeStats = new HashMap<>();
|
||||
this.nearMissCount = 0;
|
||||
this.totalJumps = 0;
|
||||
this.lastUpdated = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a successful jump with timing data.
|
||||
*
|
||||
* @param jumpType The type of jump (e.g., "normal", "diagonal", "momentum")
|
||||
* @param timeMs Time taken for this jump in milliseconds
|
||||
*/
|
||||
public void recordJump(@NotNull String jumpType, long timeMs) {
|
||||
jumpTypeStats.merge(jumpType, 1, Integer::sum);
|
||||
totalJumps++;
|
||||
|
||||
// Update rolling average
|
||||
if (avgTimePerBlock == 0.0) {
|
||||
avgTimePerBlock = timeMs;
|
||||
} else {
|
||||
avgTimePerBlock = (avgTimePerBlock * 0.9) + (timeMs * 0.1);
|
||||
}
|
||||
|
||||
lastUpdated = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a near-miss event (landed close to block edge).
|
||||
*/
|
||||
public void recordNearMiss() {
|
||||
nearMissCount++;
|
||||
lastUpdated = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the frequency of a specific jump type.
|
||||
*
|
||||
* @param jumpType The jump type to query
|
||||
* @return Percentage (0.0 to 1.0) of total jumps
|
||||
*/
|
||||
public double getJumpTypeFrequency(@NotNull String jumpType) {
|
||||
if (totalJumps == 0) {
|
||||
return 0.0;
|
||||
}
|
||||
return jumpTypeStats.getOrDefault(jumpType, 0) / (double) totalJumps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets near-miss rate as percentage of total jumps.
|
||||
*
|
||||
* @return Near-miss rate (0.0 to 1.0)
|
||||
*/
|
||||
public double getNearMissRate() {
|
||||
if (totalJumps == 0) {
|
||||
return 0.0;
|
||||
}
|
||||
return nearMissCount / (double) totalJumps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates success rate based on total jumps.
|
||||
* Assumes all recorded jumps are successful (falls are tracked separately).
|
||||
*
|
||||
* @return Success rate (0.0 to 1.0)
|
||||
*/
|
||||
public double getSuccessRate() {
|
||||
// Since we only record successful jumps, success rate is based on
|
||||
// the assumption that player has some failures not tracked here
|
||||
// For now, return 1.0 if we have jumps, or use near-miss rate as proxy
|
||||
if (totalJumps == 0) {
|
||||
return 0.0;
|
||||
}
|
||||
// High near-miss rate indicates good performance
|
||||
return Math.min(1.0, 0.7 + (getNearMissRate() * 0.3));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of successful jumps (same as total jumps for now).
|
||||
*
|
||||
* @return Number of successful jumps
|
||||
*/
|
||||
public int getSuccessfulJumps() {
|
||||
return totalJumps;
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
@NotNull
|
||||
public UUID getPlayerUuid() {
|
||||
return playerUuid;
|
||||
}
|
||||
|
||||
public double getAvgTimePerBlock() {
|
||||
return avgTimePerBlock;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Map<String, Integer> getJumpTypeStats() {
|
||||
return new HashMap<>(jumpTypeStats);
|
||||
}
|
||||
|
||||
public int getNearMissCount() {
|
||||
return nearMissCount;
|
||||
}
|
||||
|
||||
public int getTotalJumps() {
|
||||
return totalJumps;
|
||||
}
|
||||
|
||||
public long getLastUpdated() {
|
||||
return lastUpdated;
|
||||
}
|
||||
|
||||
// Setters for deserialization
|
||||
|
||||
public void setAvgTimePerBlock(double avgTimePerBlock) {
|
||||
this.avgTimePerBlock = avgTimePerBlock;
|
||||
}
|
||||
|
||||
public void setNearMissCount(int nearMissCount) {
|
||||
this.nearMissCount = nearMissCount;
|
||||
}
|
||||
|
||||
public void setTotalJumps(int totalJumps) {
|
||||
this.totalJumps = totalJumps;
|
||||
}
|
||||
|
||||
public void setLastUpdated(long lastUpdated) {
|
||||
this.lastUpdated = lastUpdated;
|
||||
}
|
||||
|
||||
public void putJumpTypeStat(@NotNull String jumpType, int count) {
|
||||
jumpTypeStats.put(jumpType, count);
|
||||
}
|
||||
}
|
||||
133
src/main/java/dev/loki/loparkour/adaptive/model/SkillRating.java
Normal file
133
src/main/java/dev/loki/loparkour/adaptive/model/SkillRating.java
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
package dev.loki.loparkour.adaptive.model;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Represents a player's skill rating with confidence level.
|
||||
* Used for adaptive difficulty calculation.
|
||||
*/
|
||||
public class SkillRating {
|
||||
|
||||
private final UUID playerUuid;
|
||||
private double rating;
|
||||
private double confidence;
|
||||
private int sessionsCount;
|
||||
private long lastUpdated;
|
||||
|
||||
/**
|
||||
* Creates a new skill rating with default values.
|
||||
*
|
||||
* @param playerUuid The player's UUID
|
||||
*/
|
||||
public SkillRating(@NotNull UUID playerUuid) {
|
||||
this.playerUuid = playerUuid;
|
||||
this.rating = 1.0;
|
||||
this.confidence = 0.0;
|
||||
this.sessionsCount = 0;
|
||||
this.lastUpdated = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a skill rating with specific values.
|
||||
*
|
||||
* @param playerUuid The player's UUID
|
||||
* @param rating The skill rating (typically 0.5 to 2.0)
|
||||
* @param confidence The confidence level (0.0 to 1.0)
|
||||
* @param sessionsCount Number of sessions used to calculate this rating
|
||||
*/
|
||||
public SkillRating(@NotNull UUID playerUuid, double rating, double confidence, int sessionsCount) {
|
||||
this.playerUuid = playerUuid;
|
||||
this.rating = clampRating(rating);
|
||||
this.confidence = clampConfidence(confidence);
|
||||
this.sessionsCount = sessionsCount;
|
||||
this.lastUpdated = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the skill rating based on performance.
|
||||
*
|
||||
* @param performanceScore Performance score from recent session (0.0 to 2.0)
|
||||
* @param weight Weight of this update (0.0 to 1.0)
|
||||
*/
|
||||
public void updateRating(double performanceScore, double weight) {
|
||||
rating = (rating * (1.0 - weight)) + (performanceScore * weight);
|
||||
rating = clampRating(rating);
|
||||
|
||||
sessionsCount++;
|
||||
confidence = Math.min(1.0, confidence + 0.05);
|
||||
|
||||
lastUpdated = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the effective rating adjusted by confidence.
|
||||
* Low confidence ratings are pulled toward 1.0 (neutral).
|
||||
*
|
||||
* @return Confidence-adjusted rating
|
||||
*/
|
||||
public double getEffectiveRating() {
|
||||
return (rating * confidence) + (1.0 * (1.0 - confidence));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this rating is reliable enough for adaptive difficulty.
|
||||
*
|
||||
* @return True if confidence is above threshold
|
||||
*/
|
||||
public boolean isReliable() {
|
||||
return confidence >= 0.3 && sessionsCount >= 3;
|
||||
}
|
||||
|
||||
private double clampRating(double value) {
|
||||
return Math.max(0.5, Math.min(2.0, value));
|
||||
}
|
||||
|
||||
private double clampConfidence(double value) {
|
||||
return Math.max(0.0, Math.min(1.0, value));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public UUID getPlayerUuid() {
|
||||
return playerUuid;
|
||||
}
|
||||
|
||||
public double getRating() {
|
||||
return rating;
|
||||
}
|
||||
|
||||
public double getConfidence() {
|
||||
return confidence;
|
||||
}
|
||||
|
||||
public int getSessionsCount() {
|
||||
return sessionsCount;
|
||||
}
|
||||
|
||||
public long getLastUpdated() {
|
||||
return lastUpdated;
|
||||
}
|
||||
|
||||
public void setRating(double rating) {
|
||||
this.rating = clampRating(rating);
|
||||
}
|
||||
|
||||
public void setConfidence(double confidence) {
|
||||
this.confidence = clampConfidence(confidence);
|
||||
}
|
||||
|
||||
public void setSessionsCount(int sessionsCount) {
|
||||
this.sessionsCount = sessionsCount;
|
||||
}
|
||||
|
||||
public void setLastUpdated(long lastUpdated) {
|
||||
this.lastUpdated = lastUpdated;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("SkillRating{rating=%.2f, confidence=%.2f, sessions=%d}",
|
||||
rating, confidence, sessionsCount);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
package dev.loki.loparkour.adaptive.storage;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonObject;
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.adaptive.model.PlayerMetrics;
|
||||
import dev.loki.loparkour.adaptive.model.SkillRating;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* JSON file-based implementation of StatsRepository.
|
||||
* Stores data in playerdata/<uuid>.json files.
|
||||
* Saves only when player leaves to minimize I/O.
|
||||
*/
|
||||
public class FileStatsStorage implements StatsRepository {
|
||||
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
private final File dataFolder;
|
||||
|
||||
public FileStatsStorage() {
|
||||
this.dataFolder = new File(LoParkour.getPlugin().getDataFolder(), "playerdata");
|
||||
if (!dataFolder.exists()) {
|
||||
dataFolder.mkdirs();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public SkillRating loadSkillRating(@NotNull UUID playerUuid) {
|
||||
JsonObject data = loadPlayerData(playerUuid);
|
||||
if (data == null || !data.has("skillRating")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
JsonObject ratingObj = data.getAsJsonObject("skillRating");
|
||||
double rating = ratingObj.get("rating").getAsDouble();
|
||||
double confidence = ratingObj.get("confidence").getAsDouble();
|
||||
int sessions = ratingObj.get("sessionsCount").getAsInt();
|
||||
|
||||
return new SkillRating(playerUuid, rating, confidence, sessions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveSkillRating(@NotNull SkillRating rating) {
|
||||
JsonObject data = loadPlayerData(rating.getPlayerUuid());
|
||||
if (data == null) {
|
||||
data = new JsonObject();
|
||||
}
|
||||
|
||||
JsonObject ratingObj = new JsonObject();
|
||||
ratingObj.addProperty("rating", rating.getRating());
|
||||
ratingObj.addProperty("confidence", rating.getConfidence());
|
||||
ratingObj.addProperty("sessionsCount", rating.getSessionsCount());
|
||||
ratingObj.addProperty("lastUpdated", rating.getLastUpdated());
|
||||
|
||||
data.add("skillRating", ratingObj);
|
||||
savePlayerData(rating.getPlayerUuid(), data);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public PlayerMetrics loadMetrics(@NotNull UUID playerUuid) {
|
||||
JsonObject data = loadPlayerData(playerUuid);
|
||||
if (data == null || !data.has("metrics")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
JsonObject metricsObj = data.getAsJsonObject("metrics");
|
||||
PlayerMetrics metrics = new PlayerMetrics(playerUuid);
|
||||
|
||||
metrics.setAvgTimePerBlock(metricsObj.get("avgTimePerBlock").getAsDouble());
|
||||
metrics.setNearMissCount(metricsObj.get("nearMissCount").getAsInt());
|
||||
metrics.setTotalJumps(metricsObj.get("totalJumps").getAsInt());
|
||||
metrics.setLastUpdated(metricsObj.get("lastUpdated").getAsLong());
|
||||
|
||||
JsonObject jumpStats = metricsObj.getAsJsonObject("jumpTypeStats");
|
||||
for (Map.Entry<String, com.google.gson.JsonElement> entry : jumpStats.entrySet()) {
|
||||
metrics.putJumpTypeStat(entry.getKey(), entry.getValue().getAsInt());
|
||||
}
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveMetrics(@NotNull PlayerMetrics metrics) {
|
||||
JsonObject data = loadPlayerData(metrics.getPlayerUuid());
|
||||
if (data == null) {
|
||||
data = new JsonObject();
|
||||
}
|
||||
|
||||
JsonObject metricsObj = new JsonObject();
|
||||
metricsObj.addProperty("avgTimePerBlock", metrics.getAvgTimePerBlock());
|
||||
metricsObj.addProperty("nearMissCount", metrics.getNearMissCount());
|
||||
metricsObj.addProperty("totalJumps", metrics.getTotalJumps());
|
||||
metricsObj.addProperty("lastUpdated", metrics.getLastUpdated());
|
||||
|
||||
JsonObject jumpStats = new JsonObject();
|
||||
for (Map.Entry<String, Integer> entry : metrics.getJumpTypeStats().entrySet()) {
|
||||
jumpStats.addProperty(entry.getKey(), entry.getValue());
|
||||
}
|
||||
metricsObj.add("jumpTypeStats", jumpStats);
|
||||
|
||||
data.add("metrics", metricsObj);
|
||||
savePlayerData(metrics.getPlayerUuid(), data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void incrementJumps(@NotNull UUID playerUuid, int amount) {
|
||||
JsonObject data = loadPlayerData(playerUuid);
|
||||
if (data == null) {
|
||||
data = new JsonObject();
|
||||
}
|
||||
|
||||
int current = data.has("totalJumps") ? data.get("totalJumps").getAsInt() : 0;
|
||||
data.addProperty("totalJumps", current + amount);
|
||||
savePlayerData(playerUuid, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void incrementFalls(@NotNull UUID playerUuid, int amount) {
|
||||
JsonObject data = loadPlayerData(playerUuid);
|
||||
if (data == null) {
|
||||
data = new JsonObject();
|
||||
}
|
||||
|
||||
int current = data.has("totalFalls") ? data.get("totalFalls").getAsInt() : 0;
|
||||
data.addProperty("totalFalls", current + amount);
|
||||
savePlayerData(playerUuid, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateLongestStreak(@NotNull UUID playerUuid, int streak) {
|
||||
JsonObject data = loadPlayerData(playerUuid);
|
||||
if (data == null) {
|
||||
data = new JsonObject();
|
||||
}
|
||||
|
||||
int current = data.has("longestStreak") ? data.get("longestStreak").getAsInt() : 0;
|
||||
if (streak > current) {
|
||||
data.addProperty("longestStreak", streak);
|
||||
savePlayerData(playerUuid, data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// No resources to close for file storage
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private JsonObject loadPlayerData(@NotNull UUID playerUuid) {
|
||||
File file = new File(dataFolder, playerUuid.toString() + ".json");
|
||||
if (!file.exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try (FileReader reader = new FileReader(file)) {
|
||||
return GSON.fromJson(reader, JsonObject.class);
|
||||
} catch (IOException e) {
|
||||
LoParkour.getPlugin().getLogger().warning(
|
||||
"Failed to load player data for " + playerUuid + ": " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void savePlayerData(@NotNull UUID playerUuid, @NotNull JsonObject data) {
|
||||
File file = new File(dataFolder, playerUuid.toString() + ".json");
|
||||
|
||||
try (FileWriter writer = new FileWriter(file)) {
|
||||
GSON.toJson(data, writer);
|
||||
} catch (IOException e) {
|
||||
LoParkour.getPlugin().getLogger().warning(
|
||||
"Failed to save player data for " + playerUuid + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package dev.loki.loparkour.adaptive.storage;
|
||||
|
||||
import dev.loki.loparkour.adaptive.model.PlayerMetrics;
|
||||
import dev.loki.loparkour.adaptive.model.SkillRating;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* No-op implementation of StatsRepository.
|
||||
* Used as a placeholder until the full storage implementation is ready.
|
||||
*
|
||||
* This allows MetricsCollector to function without throwing errors,
|
||||
* but metrics will not be persisted between server restarts.
|
||||
*/
|
||||
public class NoOpStatsRepository implements StatsRepository {
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public SkillRating loadSkillRating(@NotNull UUID playerUuid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveSkillRating(@NotNull SkillRating rating) {
|
||||
// No-op
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public PlayerMetrics loadMetrics(@NotNull UUID playerUuid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveMetrics(@NotNull PlayerMetrics metrics) {
|
||||
// No-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void incrementJumps(@NotNull UUID playerUuid, int amount) {
|
||||
// No-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void incrementFalls(@NotNull UUID playerUuid, int amount) {
|
||||
// No-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateLongestStreak(@NotNull UUID playerUuid, int streak) {
|
||||
// No-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// No-op
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
package dev.loki.loparkour.adaptive.storage;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.adaptive.model.PlayerMetrics;
|
||||
import dev.loki.loparkour.adaptive.model.SkillRating;
|
||||
import dev.loki.loparkour.storage.sql.SQLConnectionManager;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* SQL-based implementation of StatsRepository using loparkour_player_stats table.
|
||||
* Uses batch updates for optimization.
|
||||
*/
|
||||
public class SQLStatsStorage implements StatsRepository {
|
||||
|
||||
private final SQLConnectionManager connectionManager;
|
||||
private final ConcurrentHashMap<UUID, PendingUpdate> pendingUpdates;
|
||||
private static final int BATCH_SIZE = 50;
|
||||
|
||||
public SQLStatsStorage(@NotNull SQLConnectionManager connectionManager) {
|
||||
this.connectionManager = connectionManager;
|
||||
this.pendingUpdates = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public SkillRating loadSkillRating(@NotNull UUID playerUuid) {
|
||||
String query = "SELECT skill_rating, sessions_count FROM loparkour_player_stats WHERE player_uuid = ?";
|
||||
|
||||
try (Connection conn = connectionManager.getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(query)) {
|
||||
|
||||
stmt.setString(1, playerUuid.toString());
|
||||
ResultSet rs = stmt.executeQuery();
|
||||
|
||||
if (rs.next()) {
|
||||
double rating = rs.getDouble("skill_rating");
|
||||
int sessions = rs.getInt("sessions_count");
|
||||
double confidence = Math.min(1.0, sessions * 0.05);
|
||||
return new SkillRating(playerUuid, rating, confidence, sessions);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LoParkour.getPlugin().getLogger().warning("Failed to load skill rating: " + e.getMessage());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveSkillRating(@NotNull SkillRating rating) {
|
||||
String query = "INSERT INTO loparkour_player_stats (player_uuid, skill_rating, sessions_count) " +
|
||||
"VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE skill_rating = ?, sessions_count = ?";
|
||||
|
||||
try (Connection conn = connectionManager.getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(query)) {
|
||||
|
||||
stmt.setString(1, rating.getPlayerUuid().toString());
|
||||
stmt.setDouble(2, rating.getRating());
|
||||
stmt.setInt(3, rating.getSessionsCount());
|
||||
stmt.setDouble(4, rating.getRating());
|
||||
stmt.setInt(5, rating.getSessionsCount());
|
||||
|
||||
stmt.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
LoParkour.getPlugin().getLogger().warning("Failed to save skill rating: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public PlayerMetrics loadMetrics(@NotNull UUID playerUuid) {
|
||||
// Metrics are stored in JSON files, not SQL
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveMetrics(@NotNull PlayerMetrics metrics) {
|
||||
// Metrics are stored in JSON files, not SQL
|
||||
}
|
||||
|
||||
@Override
|
||||
public void incrementJumps(@NotNull UUID playerUuid, int amount) {
|
||||
PendingUpdate update = pendingUpdates.computeIfAbsent(playerUuid, k -> new PendingUpdate());
|
||||
update.jumps += amount;
|
||||
|
||||
if (pendingUpdates.size() >= BATCH_SIZE) {
|
||||
flushBatch();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void incrementFalls(@NotNull UUID playerUuid, int amount) {
|
||||
PendingUpdate update = pendingUpdates.computeIfAbsent(playerUuid, k -> new PendingUpdate());
|
||||
update.falls += amount;
|
||||
|
||||
if (pendingUpdates.size() >= BATCH_SIZE) {
|
||||
flushBatch();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateLongestStreak(@NotNull UUID playerUuid, int streak) {
|
||||
PendingUpdate update = pendingUpdates.computeIfAbsent(playerUuid, k -> new PendingUpdate());
|
||||
update.longestStreak = Math.max(update.longestStreak, streak);
|
||||
|
||||
if (pendingUpdates.size() >= BATCH_SIZE) {
|
||||
flushBatch();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes all pending updates to database using batch operations.
|
||||
*/
|
||||
public void flushBatch() {
|
||||
if (pendingUpdates.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String query = "INSERT INTO loparkour_player_stats (player_uuid, total_jumps, total_falls, longest_streak) " +
|
||||
"VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE " +
|
||||
"total_jumps = total_jumps + VALUES(total_jumps), " +
|
||||
"total_falls = total_falls + VALUES(total_falls), " +
|
||||
"longest_streak = GREATEST(longest_streak, VALUES(longest_streak))";
|
||||
|
||||
try (Connection conn = connectionManager.getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(query)) {
|
||||
|
||||
List<UUID> processed = new ArrayList<>();
|
||||
|
||||
for (var entry : pendingUpdates.entrySet()) {
|
||||
UUID uuid = entry.getKey();
|
||||
PendingUpdate update = entry.getValue();
|
||||
|
||||
stmt.setString(1, uuid.toString());
|
||||
stmt.setInt(2, update.jumps);
|
||||
stmt.setInt(3, update.falls);
|
||||
stmt.setInt(4, update.longestStreak);
|
||||
stmt.addBatch();
|
||||
|
||||
processed.add(uuid);
|
||||
}
|
||||
|
||||
stmt.executeBatch();
|
||||
|
||||
for (UUID uuid : processed) {
|
||||
pendingUpdates.remove(uuid);
|
||||
}
|
||||
|
||||
} catch (SQLException e) {
|
||||
LoParkour.getPlugin().getLogger().warning("Failed to flush batch updates: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
flushBatch();
|
||||
}
|
||||
|
||||
private static class PendingUpdate {
|
||||
int jumps = 0;
|
||||
int falls = 0;
|
||||
int longestStreak = 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package dev.loki.loparkour.adaptive.storage;
|
||||
|
||||
import dev.loki.loparkour.adaptive.model.PlayerMetrics;
|
||||
import dev.loki.loparkour.adaptive.model.SkillRating;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Repository interface for storing and retrieving player statistics.
|
||||
* Implementations can use SQL or file-based storage.
|
||||
*/
|
||||
public interface StatsRepository {
|
||||
|
||||
/**
|
||||
* Loads skill rating for a player.
|
||||
*
|
||||
* @param playerUuid The player's UUID
|
||||
* @return SkillRating or null if not found
|
||||
*/
|
||||
@Nullable
|
||||
SkillRating loadSkillRating(@NotNull UUID playerUuid);
|
||||
|
||||
/**
|
||||
* Saves skill rating for a player.
|
||||
*
|
||||
* @param rating The skill rating to save
|
||||
*/
|
||||
void saveSkillRating(@NotNull SkillRating rating);
|
||||
|
||||
/**
|
||||
* Loads detailed metrics for a player.
|
||||
*
|
||||
* @param playerUuid The player's UUID
|
||||
* @return PlayerMetrics or null if not found
|
||||
*/
|
||||
@Nullable
|
||||
PlayerMetrics loadMetrics(@NotNull UUID playerUuid);
|
||||
|
||||
/**
|
||||
* Saves detailed metrics for a player.
|
||||
*
|
||||
* @param metrics The metrics to save
|
||||
*/
|
||||
void saveMetrics(@NotNull PlayerMetrics metrics);
|
||||
|
||||
/**
|
||||
* Increments total jumps counter for a player.
|
||||
*
|
||||
* @param playerUuid The player's UUID
|
||||
* @param amount Amount to increment
|
||||
*/
|
||||
void incrementJumps(@NotNull UUID playerUuid, int amount);
|
||||
|
||||
/**
|
||||
* Increments total falls counter for a player.
|
||||
*
|
||||
* @param playerUuid The player's UUID
|
||||
* @param amount Amount to increment
|
||||
*/
|
||||
void incrementFalls(@NotNull UUID playerUuid, int amount);
|
||||
|
||||
/**
|
||||
* Updates longest streak for a player if new streak is longer.
|
||||
*
|
||||
* @param playerUuid The player's UUID
|
||||
* @param streak The new streak value
|
||||
*/
|
||||
void updateLongestStreak(@NotNull UUID playerUuid, int streak);
|
||||
|
||||
/**
|
||||
* Closes the repository and releases resources.
|
||||
*/
|
||||
void close();
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package dev.loki.loparkour.api;
|
||||
package dev.loki.loparkour.api.core;
|
||||
|
||||
import dev.loki.loparkour.api.event.ParkourBlockGenerateEvent;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.core.ParkourUser;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
|
|
@ -14,7 +14,7 @@ import org.jetbrains.annotations.Nullable;
|
|||
* <li>For player spectating, please view {@link dev.efnilite.LoParkour.player.ParkourSpectator}.</li>
|
||||
* <li>For events, please view {@link ParkourBlockGenerateEvent} and others in the events package.</li>
|
||||
* <li>For Sessions, please view {@link dev.efnilite.LoParkour.session.Session}.</li>
|
||||
* <li>For Schematics, please view {@link dev.efnilite.LoParkour.schematic.LPSchematic}</li>
|
||||
* <li>For jump structures, see {@link dev.loki.loparkour.schematic.core.ParkourSchematic}</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class ParkourAPI {
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package dev.loki.loparkour.api;
|
||||
package dev.loki.loparkour.api.core;
|
||||
|
||||
import dev.loki.loparkour.mode.Mode;
|
||||
import dev.loki.loparkour.style.Style;
|
||||
import dev.loki.loparkour.mode.base.Mode;
|
||||
import dev.loki.loparkour.style.core.Style;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package dev.loki.loparkour.api.event;
|
||||
|
||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.HandlerList;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package dev.loki.loparkour.api.event;
|
||||
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.HandlerList;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package dev.loki.loparkour.api.event;
|
||||
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.loki.loparkour.player.core.ParkourUser;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.HandlerList;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package dev.loki.loparkour.api.event;
|
||||
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.loki.loparkour.player.core.ParkourUser;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.HandlerList;
|
||||
|
|
|
|||
|
|
@ -1,27 +1,24 @@
|
|||
package dev.loki.loparkour.api.event;
|
||||
|
||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.schematic.lpschem.LPSchematic;
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import dev.loki.loparkour.schematic.core.ParkourSchematic;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.HandlerList;
|
||||
|
||||
/**
|
||||
* Gets called when a new jump is generated. Read-only.
|
||||
*
|
||||
* @author loki
|
||||
* @since 5.0.0
|
||||
* Gets called when a schematic jump is pasted. Read-only.
|
||||
*/
|
||||
public class ParkourSchematicGenerateEvent extends Event {
|
||||
|
||||
private static final HandlerList HANDLERS = new HandlerList();
|
||||
|
||||
public final LPSchematic schematic;
|
||||
public final ParkourSchematic schematic;
|
||||
public final ParkourGenerator generator;
|
||||
public final ParkourPlayer player;
|
||||
|
||||
public ParkourSchematicGenerateEvent(LPSchematic schematic, ParkourGenerator generator, ParkourPlayer player) {
|
||||
public ParkourSchematicGenerateEvent(ParkourSchematic schematic, ParkourGenerator generator, ParkourPlayer player) {
|
||||
this.schematic = schematic;
|
||||
this.generator = generator;
|
||||
this.player = player;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package dev.loki.loparkour.api.event;
|
||||
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.HandlerList;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package dev.loki.loparkour.api.event;
|
||||
|
||||
import dev.loki.loparkour.player.ParkourSpectator;
|
||||
import dev.loki.loparkour.player.spectator.ParkourSpectator;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.HandlerList;
|
||||
|
|
|
|||
133
src/main/java/dev/loki/loparkour/bootstrap/PluginBootstrap.java
Normal file
133
src/main/java/dev/loki/loparkour/bootstrap/PluginBootstrap.java
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
package dev.loki.loparkour.bootstrap;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.adaptive.bootstrap.AdaptiveServices;
|
||||
import dev.loki.loparkour.api.core.Registry;
|
||||
import dev.loki.loparkour.command.LoParkourCommand;
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.hook.holo.HoloHook;
|
||||
import dev.loki.loparkour.hook.papi.PAPIHook;
|
||||
import dev.loki.loparkour.listener.gameplay.ParkourRestrictionListener;
|
||||
import dev.loki.loparkour.listener.schematic.SchematicWandListener;
|
||||
import dev.loki.loparkour.listener.player.PlayerConnectionListener;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
import dev.loki.loparkour.mode.impl.CoopMode;
|
||||
import dev.loki.loparkour.mode.impl.DefaultMode;
|
||||
import dev.loki.loparkour.mode.impl.ElytraMode;
|
||||
import dev.loki.loparkour.mode.impl.GravityShiftMode;
|
||||
import dev.loki.loparkour.mode.impl.HardcoreMode;
|
||||
import dev.loki.loparkour.mode.impl.RaceMode;
|
||||
import dev.loki.loparkour.mode.impl.SpectatorMode;
|
||||
import dev.loki.loparkour.mode.impl.SpeedrunMode;
|
||||
import dev.loki.loparkour.reward.core.Rewards;
|
||||
import dev.loki.loparkour.schematic.core.SchematicManager;
|
||||
import dev.loki.loparkour.world.core.World;
|
||||
import org.bstats.bukkit.Metrics;
|
||||
import org.bstats.charts.SimplePie;
|
||||
import org.bstats.charts.SingleLineChart;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Wires plugin subsystems during {@link LoParkour#enable()} and {@link LoParkour#disable()}.
|
||||
*/
|
||||
public final class PluginBootstrap {
|
||||
|
||||
private PluginBootstrap() {
|
||||
}
|
||||
|
||||
public static void enable(@NotNull LoParkour plugin) {
|
||||
Config.reload(true);
|
||||
AdaptiveServices.init(plugin);
|
||||
loadSchematics(plugin);
|
||||
registerModes();
|
||||
registerHooks(plugin);
|
||||
setupWorld(plugin);
|
||||
registerEventsAndCommands(plugin);
|
||||
setupMetrics(plugin);
|
||||
}
|
||||
|
||||
public static void disable(@NotNull LoParkour plugin) {
|
||||
plugin.runShutdownSequence();
|
||||
}
|
||||
|
||||
private static void loadSchematics(@NotNull LoParkour plugin) {
|
||||
extractSchematicDocs(plugin);
|
||||
SchematicManager manager = new SchematicManager();
|
||||
manager.loadAll();
|
||||
plugin.setSchematicManager(manager);
|
||||
}
|
||||
|
||||
private static void extractSchematicDocs(@NotNull LoParkour plugin) {
|
||||
java.io.File doc = LoParkour.getInFolder("docs/SCHEMATICS.md");
|
||||
if (!doc.exists()) {
|
||||
doc.getParentFile().mkdirs();
|
||||
plugin.saveResource("docs/SCHEMATICS.md", false);
|
||||
}
|
||||
}
|
||||
|
||||
private static void registerModes() {
|
||||
Registry.register(new DefaultMode());
|
||||
Registry.register(new SpectatorMode());
|
||||
Registry.register(new SpeedrunMode());
|
||||
Registry.register(new ElytraMode());
|
||||
Registry.register(new RaceMode());
|
||||
Registry.register(new CoopMode());
|
||||
Registry.register(new GravityShiftMode());
|
||||
Registry.register(new HardcoreMode());
|
||||
Modes.init();
|
||||
}
|
||||
|
||||
private static void registerHooks(@NotNull LoParkour plugin) {
|
||||
if (plugin.getServer().getPluginManager().isPluginEnabled("HolographicDisplays")) {
|
||||
plugin.getLogger().info("Registered Holographic Displays hook");
|
||||
HoloHook.init();
|
||||
}
|
||||
if (plugin.getServer().getPluginManager().isPluginEnabled("PlaceholderAPI")) {
|
||||
plugin.getLogger().info("Registered PlaceholderAPI hook");
|
||||
PAPIHook hook = new PAPIHook();
|
||||
hook.register();
|
||||
plugin.setPlaceholderHook(hook);
|
||||
}
|
||||
if (Config.CONFIG.getBoolean("bungeecord.enabled")) {
|
||||
plugin.getServer().getMessenger().registerOutgoingPluginChannel(plugin, "BungeeCord");
|
||||
plugin.getLogger().info("Registered BungeeCord hook");
|
||||
}
|
||||
}
|
||||
|
||||
private static void setupWorld(@NotNull LoParkour plugin) {
|
||||
if (!Config.CONFIG.getBoolean("joining")) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
plugin.loLogger().info("Creating parkour world...");
|
||||
World.create();
|
||||
plugin.loLogger().info("Parkour world created successfully!");
|
||||
} catch (Exception ex) {
|
||||
plugin.loLogger().error("Failed to create parkour world", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void registerEventsAndCommands(@NotNull LoParkour plugin) {
|
||||
dev.lolib.gui.GUIManager.init(plugin);
|
||||
|
||||
var pm = plugin.getServer().getPluginManager();
|
||||
pm.registerEvents(new PlayerConnectionListener(), plugin);
|
||||
pm.registerEvents(new ParkourRestrictionListener(), plugin);
|
||||
pm.registerEvents(new SchematicWandListener(), plugin);
|
||||
LoParkourCommand cmd = new LoParkourCommand();
|
||||
plugin.getCommand("LoParkour").setExecutor(cmd);
|
||||
plugin.getCommand("LoParkour").setTabCompleter(cmd);
|
||||
}
|
||||
|
||||
private static void setupMetrics(@NotNull LoParkour plugin) {
|
||||
Metrics metrics = new Metrics(plugin, 29754);
|
||||
metrics.addCustomChart(new SimplePie("using_sql", () -> Boolean.toString(Option.SQL)));
|
||||
metrics.addCustomChart(new SimplePie("using_rewards", () -> Boolean.toString(Rewards.REWARDS_ENABLED)));
|
||||
metrics.addCustomChart(new SimplePie("locale_count", () -> Integer.toString(Locales.getLocaleCount())));
|
||||
metrics.addCustomChart(new SingleLineChart("player_joins", () ->
|
||||
dev.loki.loparkour.player.service.UserRegistry.getJoinCount()));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +1,23 @@
|
|||
package dev.loki.loparkour;
|
||||
package dev.loki.loparkour.command;
|
||||
|
||||
import dev.loki.loparkour.api.Registry;
|
||||
import dev.loki.loparkour.command.AdminCommandHandler;
|
||||
import dev.loki.loparkour.command.PlayerCommandHandler;
|
||||
import dev.loki.loparkour.command.SchematicCommandHandler;
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||
import dev.loki.loparkour.menu.Menus;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.loki.loparkour.mode.Mode;
|
||||
import dev.loki.loparkour.mode.Modes;
|
||||
import dev.loki.loparkour.mode.MultiMode;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.loki.loparkour.session.Session;
|
||||
import dev.loki.loparkour.util.ColorUtil;
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.api.core.Registry;
|
||||
import dev.loki.loparkour.command.admin.AdminCommandHandler;
|
||||
import dev.loki.loparkour.command.player.PlayerCommandHandler;
|
||||
import dev.loki.loparkour.command.schematic.SchematicCommandHandler;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.leaderboard.core.Leaderboard;
|
||||
import dev.loki.loparkour.menu.core.Menus;
|
||||
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||
import dev.loki.loparkour.mode.base.Mode;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
import dev.loki.loparkour.mode.base.MultiMode;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.core.ParkourUser;
|
||||
import dev.loki.loparkour.schematic.core.SchematicManager;
|
||||
import dev.loki.loparkour.session.core.Session;
|
||||
import dev.loki.loparkour.util.text.ColorUtil;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
|
@ -38,6 +39,11 @@ import java.util.logging.Level;
|
|||
*/
|
||||
public class LoParkourCommand implements CommandExecutor, TabCompleter {
|
||||
|
||||
private static final List<String> SCHEMATIC_DIFFICULTIES = List.of("0.0", "0.25", "0.5", "0.75", "1.0");
|
||||
private static final List<String> SCHEMATIC_SUBCOMMANDS = List.of(
|
||||
"wand", "pos1", "pos2", "create", "convert", "paste", "list", "reload"
|
||||
);
|
||||
|
||||
private final PlayerCommandHandler player = new PlayerCommandHandler();
|
||||
private final AdminCommandHandler admin = new AdminCommandHandler(player);
|
||||
|
||||
|
|
@ -52,13 +58,14 @@ public class LoParkourCommand implements CommandExecutor, TabCompleter {
|
|||
case 1 -> player.handle(args[0], sender, p);
|
||||
case 2 -> handle2(args[0], args[1], sender, p);
|
||||
case 3 -> handle3(args[0], args[1], args[2], sender, p);
|
||||
default -> sender.sendMessage(ChatColor.GRAY + "Too many arguments. Try /parkour help");
|
||||
case 4 -> handle4(args[0], args[1], args[2], args[3], sender, p);
|
||||
default -> Locales.send(sender, "commands.too_many_args");
|
||||
}
|
||||
return true;
|
||||
} catch (Throwable t) {
|
||||
LoParkour.getPlugin().getLogger().log(Level.SEVERE,
|
||||
"LoParkour command failed (" + label + " " + String.join(" ", args) + ")", t);
|
||||
sender.sendMessage(ChatColor.RED + "LoParkour: command error — see server console.");
|
||||
Locales.send(sender, "commands.error");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -76,6 +83,11 @@ public class LoParkourCommand implements CommandExecutor, TabCompleter {
|
|||
|
||||
if (p == null) return;
|
||||
|
||||
if (a1.equalsIgnoreCase("create") && p.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
SchematicCommandHandler.handleCreateDifficultyOnly(a2, sender, p, player);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (a1.toLowerCase()) {
|
||||
case "join" -> handleJoin(a2, sender, p);
|
||||
case "leaderboard" -> handleLeaderboard(a2, sender, p);
|
||||
|
|
@ -93,9 +105,22 @@ public class LoParkourCommand implements CommandExecutor, TabCompleter {
|
|||
|
||||
private void handle3(String a1, String a2, String a3, CommandSender sender, @Nullable Player p) {
|
||||
if (p == null) return;
|
||||
if (!a1.equalsIgnoreCase("schematic")) return;
|
||||
if (!p.hasPermission(ParkourOption.ADMIN.permission)) return;
|
||||
SchematicCommandHandler.handleSubcommandWithName(a2, a3, sender, p, player);
|
||||
|
||||
if (a1.equalsIgnoreCase("schematic")) {
|
||||
if (a2.equalsIgnoreCase("create")) {
|
||||
SchematicCommandHandler.handleCreateDifficultyOnly(a3, sender, p, player);
|
||||
return;
|
||||
}
|
||||
SchematicCommandHandler.handleSubcommandWithName(a2, a3, sender, p);
|
||||
}
|
||||
}
|
||||
|
||||
private void handle4(String a1, String a2, String a3, String a4, CommandSender sender, @Nullable Player p) {
|
||||
if (p == null) return;
|
||||
if (!p.hasPermission(ParkourOption.ADMIN.permission)) return;
|
||||
if (!a1.equalsIgnoreCase("schematic") || !a2.equalsIgnoreCase("create")) return;
|
||||
SchematicCommandHandler.handleCreateWithDifficulty(a3, a4, sender, p, player);
|
||||
}
|
||||
|
||||
// ── join logic ─────────────────────────────────────────────────────────────
|
||||
|
|
@ -107,10 +132,10 @@ public class LoParkourCommand implements CommandExecutor, TabCompleter {
|
|||
if (mode != null) { mode.create(p); return; }
|
||||
|
||||
Player other = Bukkit.getPlayer(arg);
|
||||
if (other == null) { send(sender, LoParkour.PREFIX + "Unknown player."); return; }
|
||||
if (other == null) { send(sender, LoParkour.PREFIX + Locales.getString(sender, "commands.unknown_player")); return; }
|
||||
|
||||
ParkourPlayer pp = ParkourPlayer.getPlayer(other);
|
||||
if (pp == null) { send(sender, LoParkour.PREFIX + "That player isn't playing."); return; }
|
||||
if (pp == null) { send(sender, LoParkour.PREFIX + Locales.getString(sender, "commands.not_playing")); return; }
|
||||
|
||||
ParkourUser user = ParkourUser.getUser(p);
|
||||
Session session = pp.session;
|
||||
|
|
@ -156,7 +181,7 @@ public class LoParkourCommand implements CommandExecutor, TabCompleter {
|
|||
if (ParkourOption.PLAY.mayPerform(sender)) completions.add("play");
|
||||
if (ParkourOption.LEADERBOARDS.mayPerform(sender)) completions.add("leaderboard");
|
||||
if (sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
completions.addAll(List.of("schematic", "reload", "forcejoin", "forceleave", "reset", "recoverinventory"));
|
||||
completions.addAll(List.of("schematic", "create", "reload", "forcejoin", "forceleave", "reset", "recoverinventory"));
|
||||
}
|
||||
return filter(args[0], completions);
|
||||
}
|
||||
|
|
@ -172,7 +197,9 @@ public class LoParkourCommand implements CommandExecutor, TabCompleter {
|
|||
} else if (a1.equals("leaderboard") && ParkourOption.LEADERBOARDS.mayPerform(sender)) {
|
||||
Registry.getModes().forEach(m -> completions.add(m.getName()));
|
||||
} else if (a1.equals("schematic") && sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
completions.addAll(Arrays.asList("wand", "pos1", "pos2", "save", "paste", "list", "reload"));
|
||||
completions.addAll(SCHEMATIC_SUBCOMMANDS);
|
||||
} else if (a1.equals("create") && sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
completions.addAll(SCHEMATIC_DIFFICULTIES);
|
||||
} else if ((a1.equals("forcejoin") || a1.equals("forceleave")) && sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
completions.add("everyone");
|
||||
if (a1.equals("forcejoin")) completions.add("nearest");
|
||||
|
|
@ -183,9 +210,45 @@ public class LoParkourCommand implements CommandExecutor, TabCompleter {
|
|||
return filter(args[1], completions);
|
||||
}
|
||||
|
||||
if (args.length == 3 && sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
String a1 = args[0].toLowerCase();
|
||||
String a2 = args[1].toLowerCase();
|
||||
if (a1.equals("schematic")) {
|
||||
if (a2.equals("paste")) {
|
||||
addLoadedSchematicIds(completions);
|
||||
} else if (a2.equals("create")) {
|
||||
completions.addAll(SCHEMATIC_DIFFICULTIES);
|
||||
}
|
||||
}
|
||||
return filter(args[2], completions);
|
||||
}
|
||||
|
||||
if (args.length == 4 && sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
String a1 = args[0].toLowerCase();
|
||||
String a2 = args[1].toLowerCase();
|
||||
if (a1.equals("schematic") && a2.equals("create")) {
|
||||
completions.addAll(SCHEMATIC_DIFFICULTIES);
|
||||
}
|
||||
return filter(args[3], completions);
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private static void addLoadedSchematicIds(@NotNull List<String> completions) {
|
||||
SchematicManager manager = LoParkour.getSchematicManager();
|
||||
if (manager == null) {
|
||||
return;
|
||||
}
|
||||
for (String id : manager.getAll().keySet()) {
|
||||
completions.add(id);
|
||||
String shortId = SchematicManager.configKey(id);
|
||||
if (!shortId.equalsIgnoreCase(id)) {
|
||||
completions.add(shortId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── util ───────────────────────────────────────────────────────────────────
|
||||
|
||||
private List<String> filter(String input, List<String> options) {
|
||||
|
|
@ -1,172 +0,0 @@
|
|||
package dev.loki.loparkour.command;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.schematic.lpschem.LPSchematic;
|
||||
import dev.loki.loparkour.schematic.lpschem.LPSchematicBuilder;
|
||||
import dev.loki.loparkour.util.ColorUtil;
|
||||
import dev.loki.loparkour.util.Locations;
|
||||
import dev.loki.loparkour.util.ParticleUtil;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.util.BoundingBox;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Handles /LoParkour schematic sub-commands (wand, pos1, pos2, save, paste, list, reload).
|
||||
*/
|
||||
public class SchematicCommandHandler {
|
||||
|
||||
public static final Map<Player, Location[]> selections = new HashMap<>();
|
||||
|
||||
private static final ItemStack WAND = buildWand();
|
||||
|
||||
private static ItemStack buildWand() {
|
||||
ItemStack item = new ItemStack(Material.GOLDEN_AXE);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setDisplayName(ColorUtil.color("<red><bold>LPSchematic Wand"));
|
||||
meta.setLore(List.of(
|
||||
ColorUtil.color("<gray>Left click: first position"),
|
||||
ColorUtil.color("<gray>Right click: second position")
|
||||
));
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
public static ItemStack getWand() {
|
||||
return WAND.clone();
|
||||
}
|
||||
|
||||
public static void sendHelp(Player player) {
|
||||
send(player, "");
|
||||
send(player, "<dark_red><bold>Schematics <reset><dark_gray>-----------");
|
||||
send(player, "<red>/LoParkour schematic wand <dark_gray>- <gray>Get the wand");
|
||||
send(player, "<red>/LoParkour schematic pos1 <dark_gray>- <gray>Set position 1");
|
||||
send(player, "<red>/LoParkour schematic pos2 <dark_gray>- <gray>Set position 2");
|
||||
send(player, "<red>/LoParkour schematic save <n> <dark_gray>- <gray>Save selection");
|
||||
send(player, "<red>/LoParkour schematic paste <n> <dark_gray>- <gray>Paste schematic");
|
||||
send(player, "<red>/LoParkour schematic list <dark_gray>- <gray>List loaded schematics");
|
||||
send(player, "<red>/LoParkour schematic reload <dark_gray>- <gray>Reload schematics");
|
||||
send(player, "");
|
||||
}
|
||||
|
||||
/** Called for 2-arg schematic commands (wand/pos1/pos2/list/reload) */
|
||||
public static void handleSubcommand(String sub, CommandSender sender, Player player, PlayerCommandHandler cooldowns) {
|
||||
Location playerLoc = player.getLocation();
|
||||
Location[] sel = selections.get(player);
|
||||
|
||||
switch (sub.toLowerCase()) {
|
||||
case "wand" -> {
|
||||
player.getInventory().addItem(WAND.clone());
|
||||
send(player, "<dark_red><bold>Schematics <reset><gray>Wand given. Left/Right click to set positions.");
|
||||
}
|
||||
case "pos1" -> setPos(player, sel, playerLoc, 0);
|
||||
case "pos2" -> setPos(player, sel, playerLoc, 1);
|
||||
case "list" -> {
|
||||
send(player, "<dark_red><bold>Schematics <reset><gray>Loaded schematics:");
|
||||
LoParkour.getSchematicManager().getAllSchematics().forEach((name, s) ->
|
||||
send(player, "<gray>- <red>" + name + " <dark_gray>(" + s.getMetadata().getDifficulty() + ")"));
|
||||
}
|
||||
case "reload" -> {
|
||||
if (!cooldowns.cooldown(sender, "schematic-reload", 2500)) return;
|
||||
LoParkour.getSchematicManager().reload();
|
||||
send(player, "<dark_red><bold>Schematics <reset><gray>Reloaded.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Called for 3-arg schematic commands (save <n>, paste <n>) */
|
||||
public static void handleSubcommandWithName(String sub, String name, CommandSender sender,
|
||||
Player player, PlayerCommandHandler cooldowns) {
|
||||
switch (sub.toLowerCase()) {
|
||||
case "save" -> handleSave(name, sender, player, cooldowns);
|
||||
case "paste" -> handlePaste(name, sender, player);
|
||||
}
|
||||
}
|
||||
|
||||
// ── internal ───────────────────────────────────────────────────────────────
|
||||
|
||||
private static void setPos(Player player, Location[] existing, Location loc, int index) {
|
||||
String label = index == 0 ? "1" : "2";
|
||||
send(player, LoParkour.PREFIX + "Position " + label + " set to " + Locations.toString(loc, true));
|
||||
|
||||
Location[] updated = existing != null ? existing.clone() : new Location[]{null, null};
|
||||
updated[index] = loc;
|
||||
selections.put(player, updated);
|
||||
|
||||
if (updated[0] != null && updated[1] != null) {
|
||||
ParticleUtil.box(BoundingBox.of(updated[0], updated[1]), player.getWorld(), Particle.END_ROD, player, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
private static void handleSave(String name, CommandSender sender, Player player, PlayerCommandHandler cooldowns) {
|
||||
if (!cooldowns.cooldown(sender, "schematic-save", 2500)) return;
|
||||
|
||||
// Validate schematic name to prevent path traversal
|
||||
if (!isValidSchematicName(name)) {
|
||||
send(player, "<dark_red><bold>Schematics <reset><red>Invalid name. Use only letters, numbers, hyphens and underscores.");
|
||||
return;
|
||||
}
|
||||
|
||||
Location[] sel = selections.get(player);
|
||||
if (sel == null || sel[0] == null || sel[1] == null) {
|
||||
send(player, "<dark_red><bold>Schematics <reset><red>Selection incomplete. Set both positions first.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
int w = Math.abs(sel[1].getBlockX() - sel[0].getBlockX()) + 1;
|
||||
int h = Math.abs(sel[1].getBlockY() - sel[0].getBlockY()) + 1;
|
||||
int l = Math.abs(sel[1].getBlockZ() - sel[0].getBlockZ()) + 1;
|
||||
|
||||
LPSchematic schematic = new LPSchematicBuilder(name, player.getName(), 0.5, w, h, l)
|
||||
.fromWorld(player.getWorld(), sel[0], sel[1])
|
||||
.build();
|
||||
|
||||
LoParkour.getSchematicManager().saveSchematic(schematic);
|
||||
send(player, "<dark_red><bold>Schematics <reset><gray>Saved as <red>" + name + "<gray>!");
|
||||
} catch (Exception e) {
|
||||
send(player, "<dark_red><bold>Schematics <reset><red>Failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates schematic name to prevent path traversal and filesystem issues.
|
||||
* Only allows alphanumeric characters, hyphens, and underscores.
|
||||
*/
|
||||
private static boolean isValidSchematicName(String name) {
|
||||
if (name == null || name.isEmpty() || name.length() > 64) {
|
||||
return false;
|
||||
}
|
||||
return name.matches("^[a-zA-Z0-9_-]+$");
|
||||
}
|
||||
|
||||
private static void handlePaste(String name, CommandSender sender, Player player) {
|
||||
// Validate schematic name to prevent path traversal
|
||||
if (!isValidSchematicName(name)) {
|
||||
send(player, "<dark_red><bold>Schematics <reset><red>Invalid name. Use only letters, numbers, hyphens and underscores.");
|
||||
return;
|
||||
}
|
||||
|
||||
LPSchematic s = LoParkour.getSchematicManager().getSchematic(name);
|
||||
if (s == null) {
|
||||
send(sender, LoParkour.PREFIX + "Schematic '" + name + "' not found.");
|
||||
return;
|
||||
}
|
||||
s.paste(player.getLocation(), player.getWorld());
|
||||
send(sender, LoParkour.PREFIX + "Pasted " + name + ".");
|
||||
}
|
||||
|
||||
private static void send(CommandSender sender, String msg) {
|
||||
sender.sendMessage(ColorUtil.color(msg));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,17 @@
|
|||
package dev.loki.loparkour.command;
|
||||
package dev.loki.loparkour.command.admin;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.api.Registry;
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.loki.loparkour.mode.Mode;
|
||||
import dev.loki.loparkour.mode.Modes;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.loki.loparkour.api.core.Registry;
|
||||
import dev.loki.loparkour.command.player.PlayerCommandHandler;
|
||||
import dev.loki.loparkour.command.schematic.SchematicCommandHandler;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||
import dev.loki.loparkour.mode.base.Mode;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.core.ParkourUser;
|
||||
import dev.loki.loparkour.player.data.InventoryData;
|
||||
import dev.loki.loparkour.util.ColorUtil;
|
||||
import dev.loki.loparkour.util.text.ColorUtil;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
|
|
@ -27,8 +29,6 @@ import java.util.UUID;
|
|||
*/
|
||||
public class AdminCommandHandler {
|
||||
|
||||
private static final String COOLDOWN_HINT = "<gray>Please wait before using this again.";
|
||||
|
||||
private final PlayerCommandHandler base;
|
||||
|
||||
public AdminCommandHandler(PlayerCommandHandler base) {
|
||||
|
|
@ -61,18 +61,18 @@ public class AdminCommandHandler {
|
|||
|
||||
if (target.equalsIgnoreCase("everyone")) {
|
||||
Bukkit.getOnlinePlayers().forEach(p -> Modes.DEFAULT.create(p));
|
||||
send(sender, LoParkour.PREFIX + "Force joined everyone!");
|
||||
send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.force_join_everyone"));
|
||||
return;
|
||||
}
|
||||
if (target.equalsIgnoreCase("nearest")) {
|
||||
Player closest = findNearest(sender);
|
||||
if (closest == null) return;
|
||||
Modes.DEFAULT.create(closest);
|
||||
send(sender, LoParkour.PREFIX + "Force joined " + closest.getName() + "!");
|
||||
send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.force_join_player").formatted(closest.getName()));
|
||||
return;
|
||||
}
|
||||
Player other = Bukkit.getPlayer(target);
|
||||
if (other == null) { send(sender, LoParkour.PREFIX + "Player not online!"); return; }
|
||||
if (other == null) { send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.player_not_online")); return; }
|
||||
Modes.DEFAULT.create(other);
|
||||
}
|
||||
|
||||
|
|
@ -83,27 +83,27 @@ public class AdminCommandHandler {
|
|||
|
||||
if (target.equalsIgnoreCase("everyone")) {
|
||||
ParkourPlayer.getPlayers().forEach(ParkourUser::leave);
|
||||
send(sender, LoParkour.PREFIX + "Force kicked everyone!");
|
||||
send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.force_leave_everyone"));
|
||||
return;
|
||||
}
|
||||
Player other = Bukkit.getPlayer(target);
|
||||
if (other == null) { send(sender, LoParkour.PREFIX + "Player not online!"); return; }
|
||||
if (other == null) { send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.player_not_online")); return; }
|
||||
ParkourUser user = ParkourUser.getUser(other);
|
||||
if (user == null) { send(sender, LoParkour.PREFIX + "Player isn't playing!"); return; }
|
||||
if (user == null) { send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.player_not_playing")); return; }
|
||||
ParkourUser.leave(user);
|
||||
}
|
||||
|
||||
// ── reset ──────────────────────────────────────────────────────────────────
|
||||
|
||||
private void handleReset(String target, CommandSender sender) {
|
||||
if (!base.cooldown(sender, "reset", 2500, COOLDOWN_HINT)) return;
|
||||
if (!base.cooldown(sender, "reset", 2500, Locales.getString(sender, "admin.cooldown"))) return;
|
||||
|
||||
if (target.equalsIgnoreCase("everyone")) {
|
||||
Registry.getModes().stream()
|
||||
.map(Mode::getLeaderboard)
|
||||
.filter(lb -> lb != null)
|
||||
.forEach(lb -> { lb.resetAll(); lb.write(true); });
|
||||
send(sender, LoParkour.PREFIX + "Reset all high scores.");
|
||||
send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.reset_all"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -115,7 +115,7 @@ public class AdminCommandHandler {
|
|||
.filter(lb -> lb != null)
|
||||
.forEach(lb -> { lb.remove(uuid); lb.write(true); });
|
||||
|
||||
send(sender, LoParkour.PREFIX + "Reset high score of " + name + ".");
|
||||
send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.reset_player").formatted(name));
|
||||
}
|
||||
|
||||
// ── recoverinventory ───────────────────────────────────────────────────────
|
||||
|
|
@ -123,13 +123,13 @@ public class AdminCommandHandler {
|
|||
private void handleRecoverInventory(String target, CommandSender sender) {
|
||||
if (!base.cooldown(sender, "recoverinventory", 2500, Locales.getString(sender, "admin.cooldown"))) return;
|
||||
Player other = Bukkit.getPlayer(target);
|
||||
if (other == null) { send(sender, LoParkour.PREFIX + "Player not online!"); return; }
|
||||
if (other == null) { send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.player_not_online")); return; }
|
||||
|
||||
new InventoryData(other).load(result -> {
|
||||
if (result != null) {
|
||||
send(sender, LoParkour.PREFIX + "Recovered inventory of " + other.getName() + ".");
|
||||
send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.recover_inventory").formatted(other.getName()));
|
||||
} else {
|
||||
send(sender, LoParkour.PREFIX + "<red>No saved inventory found for " + other.getName() + ".");
|
||||
send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.recover_inventory_missing").formatted(other.getName()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -1,13 +1,14 @@
|
|||
package dev.loki.loparkour.command;
|
||||
package dev.loki.loparkour.command.player;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.menu.Menus;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.loki.loparkour.mode.Modes;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.loki.loparkour.util.ColorUtil;
|
||||
import dev.loki.loparkour.command.schematic.SchematicCommandHandler;
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.menu.core.Menus;
|
||||
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
import dev.loki.loparkour.player.core.ParkourUser;
|
||||
import dev.loki.loparkour.util.text.ColorUtil;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
|
@ -80,34 +81,38 @@ public class PlayerCommandHandler {
|
|||
private void handleReload(@NotNull CommandSender sender, @Nullable Player player) {
|
||||
if (!cooldown(sender, "reload", 2500)) return;
|
||||
if (!sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
send(sender, Locales.getString(player, "other.no_do"));
|
||||
send(sender, Locales.getString(sender, "other.no_do"));
|
||||
return;
|
||||
}
|
||||
Config.reload(false);
|
||||
send(sender, LoParkour.PREFIX + "Reloading config files... (locales are loading asynchronously)");
|
||||
dev.loki.loparkour.adaptive.bootstrap.AdaptiveServices.reload();
|
||||
send(sender, LoParkour.PREFIX + Locales.getString(sender, "commands.reload"));
|
||||
}
|
||||
|
||||
public void sendHelp(@NotNull CommandSender sender) {
|
||||
send(sender, "");
|
||||
send(sender, "<dark_gray><strikethrough>---------------<reset> " + LoParkour.NAME + " <dark_gray><strikethrough>---------------<reset>");
|
||||
send(sender, Locales.getString(sender, "commands.help.header"));
|
||||
send(sender, "");
|
||||
send(sender, "<gray>/parkour <dark_gray>- Main command");
|
||||
send(sender, Locales.getString(sender, "commands.help.line_parkour"));
|
||||
if (sender.hasPermission(ParkourOption.JOIN.permission)) {
|
||||
send(sender, "<gray>/parkour join [mode/player] <dark_gray>- Join a mode");
|
||||
send(sender, "<gray>/parkour leave <dark_gray>- Leave parkour");
|
||||
send(sender, Locales.getString(sender, "commands.help.line_join"));
|
||||
send(sender, Locales.getString(sender, "commands.help.line_leave"));
|
||||
}
|
||||
if (sender.hasPermission(ParkourOption.MAIN.permission)) {
|
||||
send(sender, Locales.getString(sender, "commands.help.line_menu"));
|
||||
}
|
||||
if (sender.hasPermission(ParkourOption.PLAY.permission)) {
|
||||
send(sender, Locales.getString(sender, "commands.help.line_play"));
|
||||
}
|
||||
if (sender.hasPermission(ParkourOption.LEADERBOARDS.permission)) {
|
||||
send(sender, Locales.getString(sender, "commands.help.line_leaderboard"));
|
||||
}
|
||||
if (sender.hasPermission(ParkourOption.MAIN.permission))
|
||||
send(sender, "<gray>/parkour menu <dark_gray>- Open the menu");
|
||||
if (sender.hasPermission(ParkourOption.PLAY.permission))
|
||||
send(sender, "<gray>/parkour play <dark_gray>- Mode selection menu");
|
||||
if (sender.hasPermission(ParkourOption.LEADERBOARDS.permission))
|
||||
send(sender, "<gray>/parkour leaderboard [type] <dark_gray>- Leaderboard");
|
||||
if (sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
send(sender, "<gray>/LoParkour schematic <dark_gray>- Schematic tools");
|
||||
send(sender, "<gray>/LoParkour reload <dark_gray>- Reload configs");
|
||||
send(sender, "<gray>/LoParkour reset <everyone/player> <dark_gray>- Reset scores");
|
||||
send(sender, "<gray>/LoParkour forcejoin/forceleave <target> <dark_gray>- Force join/leave");
|
||||
send(sender, "<gray>/LoParkour recoverinventory <player> <dark_gray>- Recover inventory");
|
||||
send(sender, Locales.getString(sender, "commands.help.line_schematic"));
|
||||
send(sender, Locales.getString(sender, "commands.help.line_reload"));
|
||||
send(sender, Locales.getString(sender, "commands.help.line_reset"));
|
||||
send(sender, Locales.getString(sender, "commands.help.line_force"));
|
||||
send(sender, Locales.getString(sender, "commands.help.line_recover"));
|
||||
}
|
||||
send(sender, "");
|
||||
}
|
||||
|
|
@ -117,7 +122,7 @@ public class PlayerCommandHandler {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param whenBlocked message sent (MiniMessage color) if still on cooldown; may be null for silent block
|
||||
* @param whenBlocked message sent ({@code &#RRGGBB} / {@code &} colors) if still on cooldown; may be null for silent block
|
||||
*/
|
||||
public boolean cooldown(CommandSender sender, String key, long millis, @Nullable String whenBlocked) {
|
||||
String fullKey = sender.getName() + ":" + key;
|
||||
|
|
@ -0,0 +1,271 @@
|
|||
package dev.loki.loparkour.command.schematic;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.command.player.PlayerCommandHandler;
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||
import dev.loki.loparkour.schematic.convert.LpschemConverter;
|
||||
import dev.loki.loparkour.schematic.core.ParkourSchematic;
|
||||
import dev.loki.loparkour.schematic.create.SchematicCreator;
|
||||
import dev.loki.loparkour.util.particle.ParticleUtil;
|
||||
import dev.loki.loparkour.util.text.ColorUtil;
|
||||
import dev.loki.loparkour.util.world.Locations;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.bukkit.util.BoundingBox;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Admin schematic tools: wand, create, convert, list, paste, reload.
|
||||
*/
|
||||
public class SchematicCommandHandler {
|
||||
|
||||
public static final Map<Player, Location[]> selections = new HashMap<>();
|
||||
public static final NamespacedKey WAND_KEY = new NamespacedKey(LoParkour.getPlugin(), "schematic_wand");
|
||||
|
||||
private static ItemStack cachedWand;
|
||||
|
||||
@NotNull
|
||||
public static ItemStack getWand() {
|
||||
return buildWand().clone();
|
||||
}
|
||||
|
||||
public static boolean isWand(@Nullable ItemStack item) {
|
||||
if (item == null || !item.hasItemMeta()) {
|
||||
return false;
|
||||
}
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
return meta != null
|
||||
&& meta.getPersistentDataContainer().has(WAND_KEY, PersistentDataType.BYTE);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static Location[] getSelection(@NotNull Player player) {
|
||||
Location[] sel = selections.get(player);
|
||||
if (sel == null) {
|
||||
sel = new Location[]{null, null};
|
||||
selections.put(player, sel);
|
||||
}
|
||||
return sel;
|
||||
}
|
||||
|
||||
public static void setPos(@NotNull Player player, @NotNull Location loc, int index) {
|
||||
Location[] updated = getSelection(player).clone();
|
||||
updated[index] = loc.clone();
|
||||
selections.put(player, updated);
|
||||
}
|
||||
|
||||
public static void clearWandCache() {
|
||||
cachedWand = null;
|
||||
}
|
||||
|
||||
public static void sendHelp(@NotNull Player player) {
|
||||
send(player, "");
|
||||
send(player, Locales.getString(player, "schematic.help.header"));
|
||||
send(player, "");
|
||||
send(player, Locales.getString(player, "schematic.help.line_wand"));
|
||||
send(player, Locales.getString(player, "schematic.help.line_pos1"));
|
||||
send(player, Locales.getString(player, "schematic.help.line_pos2"));
|
||||
send(player, Locales.getString(player, "schematic.help.line_create"));
|
||||
send(player, Locales.getString(player, "schematic.help.line_create_named"));
|
||||
send(player, Locales.getString(player, "schematic.help.line_convert"));
|
||||
send(player, Locales.getString(player, "schematic.help.line_paste"));
|
||||
send(player, Locales.getString(player, "schematic.help.line_list"));
|
||||
send(player, Locales.getString(player, "schematic.help.line_reload"));
|
||||
send(player, Locales.getString(player, "schematic.help.line_folder"));
|
||||
send(player, Locales.getString(player, "schematic.help.line_docs"));
|
||||
send(player, "");
|
||||
}
|
||||
|
||||
public static void handleSubcommand(String sub, CommandSender sender, Player player, PlayerCommandHandler cooldowns) {
|
||||
Location playerLoc = player.getLocation();
|
||||
Location[] sel = getSelection(player);
|
||||
|
||||
switch (sub.toLowerCase()) {
|
||||
case "wand" -> {
|
||||
player.getInventory().addItem(getWand());
|
||||
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.wand_given"));
|
||||
}
|
||||
case "pos1" -> setPosCommand(player, sel, playerLoc, 0);
|
||||
case "pos2" -> setPosCommand(player, sel, playerLoc, 1);
|
||||
case "list" -> {
|
||||
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.list_header"));
|
||||
LoParkour.getSchematicManager().getAll().forEach((name, s) ->
|
||||
send(player, Locales.getString(player, "schematic.list_entry")
|
||||
.formatted(name, s.getDifficulty(), s.getFormat().name().toLowerCase())));
|
||||
}
|
||||
case "reload" -> {
|
||||
if (!cooldowns.cooldown(sender, "schematic-reload", 2500)) {
|
||||
return;
|
||||
}
|
||||
Config.reload(false);
|
||||
LoParkour.getSchematicManager().reload();
|
||||
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.reloaded"));
|
||||
}
|
||||
case "convert" -> handleConvert(sender, player, cooldowns);
|
||||
default -> sendHelp(player);
|
||||
}
|
||||
}
|
||||
|
||||
public static void handleSubcommandWithName(String sub, String name, CommandSender sender, Player player) {
|
||||
switch (sub.toLowerCase()) {
|
||||
case "paste" -> handlePaste(name, sender, player);
|
||||
case "create" -> handleCreate(name, null, sender, player);
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** {@code schematic create <name> <difficulty>} */
|
||||
public static void handleCreateWithDifficulty(
|
||||
String name,
|
||||
String difficultyRaw,
|
||||
CommandSender sender,
|
||||
Player player,
|
||||
PlayerCommandHandler cooldowns
|
||||
) {
|
||||
if (!cooldowns.cooldown(sender, "schematic-create", 2500)) {
|
||||
return;
|
||||
}
|
||||
handleCreate(name, difficultyRaw, sender, player);
|
||||
}
|
||||
|
||||
/** {@code create <difficulty>} or {@code schematic create <difficulty>} */
|
||||
public static void handleCreateDifficultyOnly(
|
||||
String difficultyRaw,
|
||||
CommandSender sender,
|
||||
Player player,
|
||||
PlayerCommandHandler cooldowns
|
||||
) {
|
||||
if (!cooldowns.cooldown(sender, "schematic-create", 2500)) {
|
||||
return;
|
||||
}
|
||||
handleCreate(null, difficultyRaw, sender, player);
|
||||
}
|
||||
|
||||
private static void handleCreate(
|
||||
@Nullable String optionalName,
|
||||
@NotNull String difficultyRaw,
|
||||
CommandSender sender,
|
||||
Player player
|
||||
) {
|
||||
Location[] sel = selections.get(player);
|
||||
if (sel == null || sel[0] == null || sel[1] == null) {
|
||||
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.selection_incomplete"));
|
||||
return;
|
||||
}
|
||||
|
||||
double difficulty;
|
||||
try {
|
||||
difficulty = SchematicCreator.parseDifficulty(difficultyRaw);
|
||||
} catch (NumberFormatException e) {
|
||||
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.invalid_difficulty"));
|
||||
return;
|
||||
} catch (IllegalArgumentException e) {
|
||||
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.invalid_difficulty"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SchematicCreator.CreateResult result = SchematicCreator.create(sel[0], sel[1], optionalName, difficulty);
|
||||
LoParkour.getSchematicManager().reload();
|
||||
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.created")
|
||||
.formatted(result.stem(), result.difficulty(), result.file().getName()));
|
||||
} catch (IllegalArgumentException e) {
|
||||
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.invalid_name"));
|
||||
} catch (Exception e) {
|
||||
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.create_failed").formatted(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
private static void handleConvert(CommandSender sender, Player player, PlayerCommandHandler cooldowns) {
|
||||
if (!cooldowns.cooldown(sender, "schematic-convert", 5000)) {
|
||||
return;
|
||||
}
|
||||
LpschemConverter.ConvertResult result = LpschemConverter.convertAll();
|
||||
if (result.converted() > 0) {
|
||||
LoParkour.getSchematicManager().reload();
|
||||
}
|
||||
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.convert_done")
|
||||
.formatted(result.converted(), result.failed()));
|
||||
for (String line : result.messages()) {
|
||||
if (result.messages().size() <= 12) {
|
||||
send(player, "&#A0A0A0 " + line);
|
||||
}
|
||||
}
|
||||
if (result.messages().size() > 12) {
|
||||
send(player, Locales.getString(player, "schematic.convert_truncated").formatted(result.messages().size() - 12));
|
||||
}
|
||||
}
|
||||
|
||||
private static void setPosCommand(Player player, Location[] existing, Location loc, int index) {
|
||||
setPos(player, loc, index);
|
||||
String label = index == 0 ? "1" : "2";
|
||||
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.pos_set")
|
||||
.formatted(label, Locations.toString(loc, true)));
|
||||
|
||||
Location[] updated = getSelection(player);
|
||||
if (updated[0] != null && updated[1] != null) {
|
||||
ParticleUtil.box(BoundingBox.of(updated[0], updated[1]), player.getWorld(), Particle.END_ROD, player, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
private static void handlePaste(String name, CommandSender sender, Player player) {
|
||||
if (!isValidSchematicName(name)) {
|
||||
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.invalid_name"));
|
||||
return;
|
||||
}
|
||||
|
||||
ParkourSchematic schematic = LoParkour.getSchematicManager().get(name);
|
||||
if (schematic == null) {
|
||||
send(sender, LoParkour.PREFIX + Locales.getString(sender, "schematic.not_found").formatted(name));
|
||||
return;
|
||||
}
|
||||
schematic.paste(player.getLocation(), player.getWorld());
|
||||
send(sender, LoParkour.PREFIX + Locales.getString(sender, "schematic.pasted").formatted(name));
|
||||
}
|
||||
|
||||
static boolean isValidSchematicName(String name) {
|
||||
if (name == null || name.isEmpty() || name.length() > 64) {
|
||||
return false;
|
||||
}
|
||||
return name.matches("^[a-zA-Z0-9_-]+$");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static ItemStack buildWand() {
|
||||
if (cachedWand != null) {
|
||||
return cachedWand;
|
||||
}
|
||||
|
||||
String locale = Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG);
|
||||
if (locale == null || locale.isBlank()) {
|
||||
locale = "en";
|
||||
}
|
||||
|
||||
ItemStack item = Locales.getItem(locale, "schematic.wand").build();
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.getPersistentDataContainer().set(WAND_KEY, PersistentDataType.BYTE, (byte) 1);
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
cachedWand = item;
|
||||
return item;
|
||||
}
|
||||
|
||||
private static void send(CommandSender sender, String msg) {
|
||||
sender.sendMessage(ColorUtil.color(msg));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.loparkour.config;
|
||||
package dev.loki.loparkour.config.core;
|
||||
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
|
@ -116,4 +116,4 @@ public enum Config {
|
|||
public List<String> getChildren(@NotNull String path, boolean... deep) {
|
||||
return accessor.getChildren(path, deep);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.loparkour.config;
|
||||
package dev.loki.loparkour.config.core;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
|
|
@ -144,4 +144,4 @@ public class ConfigAccessor {
|
|||
"Missing config path '" + path + "' in " + fileName + ". Using a safe default value.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
package dev.loki.loparkour.config;
|
||||
package dev.loki.loparkour.config.core;
|
||||
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
|
|
@ -55,9 +57,9 @@ public class ConfigLoader {
|
|||
}
|
||||
|
||||
public static void initializeDependentSystems(boolean firstLoad) {
|
||||
dev.loki.loparkour.reward.Rewards.init();
|
||||
dev.loki.loparkour.reward.core.Rewards.init();
|
||||
Locales.init();
|
||||
dev.loki.loparkour.schematic.Schematics.init();
|
||||
dev.loki.loparkour.schematic.registry.Schematics.init();
|
||||
Option.init(firstLoad);
|
||||
}
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.loparkour.config;
|
||||
package dev.loki.loparkour.config.core;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.loparkour.config;
|
||||
package dev.loki.loparkour.config.locale;
|
||||
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.loparkour.config;
|
||||
package dev.loki.loparkour.config.locale;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
package dev.loki.loparkour.config;
|
||||
package dev.loki.loparkour.config.locale;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.loki.loparkour.util.Item;
|
||||
import dev.loki.loparkour.util.Materials;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||
import dev.loki.loparkour.player.core.ParkourUser;
|
||||
import dev.loki.loparkour.util.text.ColorUtil;
|
||||
import dev.loki.loparkour.util.item.Item;
|
||||
import dev.loki.loparkour.util.item.Materials;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
|
|
@ -29,6 +31,7 @@ public class Locales {
|
|||
public static void init() {
|
||||
Map<String, FileConfiguration> loaded = LocaleLoader.loadAll();
|
||||
cache.setLocales(loaded);
|
||||
dev.loki.loparkour.command.schematic.SchematicCommandHandler.clearWandCache();
|
||||
LoParkour.log("Locales loaded successfully (" + loaded.size() + " locales)");
|
||||
}
|
||||
|
||||
|
|
@ -61,6 +64,24 @@ public class Locales {
|
|||
return cache.cachedValue(locale, c -> c.getString(path), "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a colored message from locale. Safe for console ({@link #getString(CommandSender, String)}).
|
||||
*/
|
||||
public static void send(@NotNull CommandSender sender, @NotNull String path, Object... format) {
|
||||
String message = getString(sender, path);
|
||||
if (format.length > 0) {
|
||||
message = message.formatted(format);
|
||||
}
|
||||
sender.sendMessage(ColorUtil.color(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a colored message using the player's locale.
|
||||
*/
|
||||
public static void send(@NotNull Player player, @NotNull String path, Object... format) {
|
||||
send((CommandSender) player, path, format);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static List<String> getStringList(@NotNull String locale, @NotNull String path) {
|
||||
return cache.cachedValue(locale, c -> c.getStringList(path), Collections.emptyList());
|
||||
|
|
@ -107,11 +128,9 @@ public class Locales {
|
|||
String name = applyReplacements(config.getString(namePath, ""), replace);
|
||||
String lore = applyReplacements(config.getString(lorePath, ""), replace);
|
||||
|
||||
// Debug logging
|
||||
if ("STONE".equals(material)) {
|
||||
LoParkour.getPlugin().getLogger().warning("Material defaulted to STONE for path: " + path);
|
||||
LoParkour.getPlugin().getLogger().warning(" Tried: " + materialPath + " = " + config.getString(materialPath));
|
||||
LoParkour.getPlugin().getLogger().warning(" Available keys: " + config.getKeys(true));
|
||||
if ("STONE".equals(material) && !config.contains(materialPath)) {
|
||||
LoParkour.getPlugin().getLogger().warning(
|
||||
"Missing locale key '%s' (material defaults to STONE)".formatted(materialPath));
|
||||
}
|
||||
|
||||
Material mat = Materials.parse(material);
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
package dev.loki.loparkour.config;
|
||||
package dev.loki.loparkour.config.options;
|
||||
|
||||
import dev.loki.loparkour.api.Registry;
|
||||
import dev.loki.loparkour.api.core.Registry;
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.config.options.*;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.loki.loparkour.style.RandomStyle;
|
||||
import dev.loki.loparkour.style.Style;
|
||||
import dev.loki.loparkour.util.ParticleData;
|
||||
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||
import dev.loki.loparkour.style.core.RandomStyle;
|
||||
import dev.loki.loparkour.style.core.Style;
|
||||
import dev.loki.loparkour.util.particle.ParticleData;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Particle;
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package dev.loki.loparkour.config.options;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package dev.loki.loparkour.config.options;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
package dev.loki.loparkour.config.options;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.Option;
|
||||
import dev.loki.loparkour.util.ParticleData;
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.util.particle.ParticleData;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.Sound;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package dev.loki.loparkour.config.options;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
|
||||
/**
|
||||
* SQL database configuration options.
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
package dev.loki.loparkour.config.options;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.ConfigAccessor;
|
||||
import dev.loki.loparkour.style.Style;
|
||||
import dev.loki.loparkour.util.Materials;
|
||||
import dev.loki.loparkour.config.core.ConfigAccessor;
|
||||
import dev.loki.loparkour.style.core.Style;
|
||||
import dev.loki.loparkour.util.item.Materials;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
package dev.loki.loparkour.generator.core.coordinator;
|
||||
|
||||
import dev.loki.loparkour.generator.jump.calculation.JumpType;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Interface for listening to generator events.
|
||||
* Allows adaptive system to integrate without creating direct dependencies.
|
||||
*
|
||||
* This decouples the generator from the adaptive system:
|
||||
* - Generator fires events through this interface
|
||||
* - Adaptive system implements this interface to collect metrics
|
||||
* - No circular dependencies between generator and adaptive packages
|
||||
*/
|
||||
public interface GeneratorEventListener {
|
||||
|
||||
/**
|
||||
* Called when a block is generated.
|
||||
*
|
||||
* @param player The player for whom the block was generated
|
||||
* @param block The generated block
|
||||
* @param jumpType The type of jump required to reach this block
|
||||
* @param distance The distance from the previous block
|
||||
*/
|
||||
void onBlockGenerated(@NotNull Player player, @NotNull Block block, @NotNull JumpType jumpType, double distance);
|
||||
|
||||
/**
|
||||
* Called when a player successfully lands on a block and scores.
|
||||
*
|
||||
* @param player The player who scored
|
||||
* @param block The block the player landed on
|
||||
* @param distance The distance of the jump
|
||||
*/
|
||||
void onPlayerScore(@NotNull Player player, @NotNull Block block, double distance);
|
||||
|
||||
/**
|
||||
* Called when a player falls off the parkour.
|
||||
*
|
||||
* @param player The player who fell
|
||||
* @param lastBlock The last block the player was on (may be null)
|
||||
*/
|
||||
void onPlayerFall(@NotNull Player player, Block lastBlock);
|
||||
|
||||
/**
|
||||
* Called when a player performs a jump.
|
||||
*
|
||||
* @param player The player who jumped
|
||||
* @param jumpType The type of jump performed
|
||||
*/
|
||||
void onJump(@NotNull Player player, @NotNull JumpType jumpType);
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
package dev.loki.loparkour.generator;
|
||||
package dev.loki.loparkour.generator.core.coordinator;
|
||||
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.adaptive.bootstrap.AdaptiveServices;
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.generator.core.model.Profile;
|
||||
import dev.loki.loparkour.generator.profile.GenerationProfileLoader;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
|
|
@ -10,22 +13,31 @@ import java.util.Map;
|
|||
* Manages generator profiles and difficulty calculations.
|
||||
*/
|
||||
public class GeneratorProfileManager {
|
||||
|
||||
|
||||
private final ParkourGenerator generator;
|
||||
|
||||
|
||||
public GeneratorProfileManager(@NotNull ParkourGenerator generator) {
|
||||
this.generator = generator;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Override profile settings based on generator options.
|
||||
* Rebuilds profile from config, player settings, and adaptive difficulty when enabled.
|
||||
*/
|
||||
public void overrideProfile() {
|
||||
// Generator options are for disabling features, not difficulty
|
||||
// Difficulty is controlled by profile settings
|
||||
// This method can be used to apply custom profile overrides if needed
|
||||
Profile rebuilt = GenerationProfileLoader.loadFromConfig();
|
||||
GenerationProfileLoader.preservePlayerSettings(generator.profile, rebuilt);
|
||||
AdaptiveServices.applyAdaptiveProfile(generator, rebuilt);
|
||||
GenerationProfileLoader.copyInto(generator.profile, rebuilt);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Recalculates chances after a fall or settings change.
|
||||
*/
|
||||
public void recalculateProfile() {
|
||||
overrideProfile();
|
||||
calculateChances();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate generation chances based on current profile.
|
||||
*/
|
||||
|
|
@ -35,112 +47,113 @@ public class GeneratorProfileManager {
|
|||
calculateDefaultChances();
|
||||
calculateSpecialChances();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculate difficulty score based on current settings.
|
||||
*/
|
||||
public double getDifficultyScore() {
|
||||
double score = 0.0;
|
||||
|
||||
// Distance difficulty
|
||||
|
||||
for (Map.Entry<Integer, Double> entry : generator.state.distanceChances.entrySet()) {
|
||||
score += entry.getKey() * entry.getValue() * 0.3;
|
||||
}
|
||||
|
||||
// Height difficulty
|
||||
|
||||
for (Map.Entry<Integer, Double> entry : generator.state.heightChances.entrySet()) {
|
||||
score += Math.abs(entry.getKey()) * entry.getValue() * 0.2;
|
||||
}
|
||||
|
||||
// Special blocks difficulty
|
||||
|
||||
for (Map.Entry<BlockData, Double> entry : generator.state.specialChances.entrySet()) {
|
||||
score += entry.getValue() * 0.5;
|
||||
}
|
||||
|
||||
|
||||
return Math.round(score * 100.0) / 100.0;
|
||||
}
|
||||
|
||||
|
||||
private void calculateDistanceChances() {
|
||||
generator.state.distanceChances.clear();
|
||||
|
||||
// Get distance range from config
|
||||
int minDistance = Config.GENERATION.getInt("advanced.distance.min", 1);
|
||||
int maxDistance = Config.GENERATION.getInt("advanced.distance.max", 4);
|
||||
|
||||
|
||||
int minDistance = profileInt("distance.min", "advanced.distance.min", 1);
|
||||
int maxDistance = profileInt("distance.max", "advanced.distance.max", 4);
|
||||
|
||||
for (int distance = minDistance; distance <= maxDistance; distance++) {
|
||||
int chance = Config.GENERATION.getInt("advanced.distance.chances." + distance, 1);
|
||||
int chance = profileInt(
|
||||
"distance.chances." + distance,
|
||||
"advanced.distance.chances." + distance,
|
||||
1);
|
||||
generator.state.distanceChances.put(distance, (double) chance);
|
||||
}
|
||||
|
||||
// Normalize chances to sum to 1.0
|
||||
|
||||
normalizeMap(generator.state.distanceChances);
|
||||
}
|
||||
|
||||
|
||||
private void calculateHeightChances() {
|
||||
generator.state.heightChances.clear();
|
||||
|
||||
// Get height range from config
|
||||
int minHeight = Config.GENERATION.getInt("advanced.height.min", -1);
|
||||
int maxHeight = Config.GENERATION.getInt("advanced.height.max", 2);
|
||||
|
||||
|
||||
int minHeight = profileInt("height.min", "advanced.height.min", -1);
|
||||
int maxHeight = profileInt("height.max", "advanced.height.max", 2);
|
||||
|
||||
for (int height = minHeight; height <= maxHeight; height++) {
|
||||
int chance = Config.GENERATION.getInt("advanced.height.chances." + height, 1);
|
||||
int chance = profileInt(
|
||||
"height.chances." + height,
|
||||
"advanced.height.chances." + height,
|
||||
1);
|
||||
generator.state.heightChances.put(height, (double) chance);
|
||||
}
|
||||
|
||||
// Normalize chances to sum to 1.0
|
||||
|
||||
normalizeMap(generator.state.heightChances);
|
||||
}
|
||||
|
||||
|
||||
private void calculateDefaultChances() {
|
||||
generator.state.defaultChances.clear();
|
||||
|
||||
int defaultChance = Config.GENERATION.getInt("advanced.type.default", 80);
|
||||
int specialChance = Config.GENERATION.getInt("advanced.type.special", 15);
|
||||
int schematicChance = Config.GENERATION.getInt("advanced.type.schematic", 5);
|
||||
|
||||
|
||||
int defaultChance = profileInt("type.default", "advanced.type.default", 80);
|
||||
int specialChance = profileInt("type.special", "advanced.type.special", 15);
|
||||
int schematicChance = profileInt("type.schematic", "advanced.type.schematic", 5);
|
||||
|
||||
generator.state.defaultChances.put(ParkourGenerator.BlockGenerationType.DEFAULT, (double) defaultChance);
|
||||
generator.state.defaultChances.put(ParkourGenerator.BlockGenerationType.SPECIAL, (double) specialChance);
|
||||
generator.state.defaultChances.put(ParkourGenerator.BlockGenerationType.SCHEMATIC, (double) schematicChance);
|
||||
|
||||
// Normalize chances to sum to 1.0
|
||||
|
||||
normalizeMap(generator.state.defaultChances);
|
||||
}
|
||||
|
||||
|
||||
private void calculateSpecialChances() {
|
||||
generator.state.specialChances.clear();
|
||||
|
||||
|
||||
try {
|
||||
for (String specialType : Config.GENERATION.getChildren("advanced.special")) {
|
||||
String materialPath = "advanced.special." + specialType + ".material";
|
||||
String chancePath = "advanced.special." + specialType + ".chance";
|
||||
|
||||
|
||||
String materialName = Config.GENERATION.getString(materialPath);
|
||||
int chance = Config.GENERATION.getInt(chancePath, 1);
|
||||
|
||||
|
||||
BlockData blockData = org.bukkit.Bukkit.createBlockData(materialName);
|
||||
generator.state.specialChances.put(blockData, (double) chance);
|
||||
}
|
||||
|
||||
// Normalize chances to sum to 1.0
|
||||
|
||||
normalizeMap(generator.state.specialChances);
|
||||
} catch (Exception e) {
|
||||
// If special blocks config is missing, just skip
|
||||
} catch (Exception ignored) {
|
||||
// special blocks config optional
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a map so all values sum to 1.0
|
||||
* If sum is 0, distributes values equally.
|
||||
*/
|
||||
|
||||
private int profileInt(@NotNull String profileKey, @NotNull String configKey, int defaultValue) {
|
||||
if (generator.profile.settings.containsKey(profileKey)) {
|
||||
return generator.profile.get(profileKey).asInt();
|
||||
}
|
||||
return Config.GENERATION.getInt(configKey, defaultValue);
|
||||
}
|
||||
|
||||
private <K> void normalizeMap(Map<K, Double> map) {
|
||||
if (map.isEmpty()) return;
|
||||
if (map.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
double sum = map.values().stream().mapToDouble(Double::doubleValue).sum();
|
||||
if (sum > 0) {
|
||||
map.replaceAll((k, v) -> v / sum);
|
||||
} else {
|
||||
// If all values are 0, distribute equally
|
||||
double equalValue = 1.0 / map.size();
|
||||
map.replaceAll((k, v) -> equalValue);
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
package dev.loki.loparkour.generator;
|
||||
package dev.loki.loparkour.generator.core.coordinator;
|
||||
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||
import dev.loki.loparkour.leaderboard.Score;
|
||||
import dev.loki.loparkour.mode.Mode;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.leaderboard.core.Leaderboard;
|
||||
import dev.loki.loparkour.leaderboard.model.Score;
|
||||
import dev.loki.loparkour.mode.base.Mode;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
|
|
@ -94,4 +94,4 @@ public class GeneratorStatistics {
|
|||
|
||||
return formatter.format(date);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +1,23 @@
|
|||
package dev.loki.loparkour.generator;
|
||||
package dev.loki.loparkour.generator.core.coordinator;
|
||||
|
||||
import dev.loki.loparkour.generator.jump.BlockPlacer;
|
||||
import dev.loki.loparkour.generator.lifecycle.GeneratorLifecycle;
|
||||
import dev.loki.loparkour.menu.Menus;
|
||||
import dev.loki.loparkour.mode.Mode;
|
||||
import dev.loki.loparkour.mode.Modes;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.ParkourSpectator;
|
||||
import dev.loki.loparkour.schematic.lpschem.LPSchematic;
|
||||
import dev.loki.loparkour.session.Session;
|
||||
import dev.loki.loparkour.generator.core.model.GeneratorOption;
|
||||
import dev.loki.loparkour.generator.core.model.GeneratorState;
|
||||
import dev.loki.loparkour.generator.core.model.Island;
|
||||
import dev.loki.loparkour.generator.core.model.Profile;
|
||||
import dev.loki.loparkour.generator.jump.placement.BlockPlacer;
|
||||
import dev.loki.loparkour.generator.lifecycle.loop.GeneratorLifecycle;
|
||||
import dev.loki.loparkour.menu.core.Menus;
|
||||
import dev.loki.loparkour.mode.base.Mode;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.spectator.ParkourSpectator;
|
||||
import dev.loki.loparkour.session.core.Session;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.block.Block;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
|
|
@ -32,37 +36,38 @@ public class ParkourGenerator {
|
|||
protected final GeneratorLifecycle lifecycle;
|
||||
public final GeneratorProfileManager profileManager;
|
||||
private final GeneratorStatistics statistics;
|
||||
|
||||
// Event listeners for adaptive system integration
|
||||
private final List<GeneratorEventListener> eventListeners;
|
||||
|
||||
public ParkourGenerator(@NotNull Session session, @Nullable LPSchematic schematic, GeneratorOption... generatorOptions) {
|
||||
public ParkourGenerator(@NotNull Session session, GeneratorOption... generatorOptions) {
|
||||
this.session = session;
|
||||
this.generatorOptions = generatorOptions;
|
||||
this.island = new Island(session, schematic);
|
||||
this.island = new Island(session);
|
||||
this.profile = new Profile();
|
||||
this.state = new GeneratorState();
|
||||
|
||||
this.eventListeners = new ArrayList<>();
|
||||
|
||||
// Initialize zone from session
|
||||
if (session != null) {
|
||||
Location[] selection = dev.loki.loparkour.world.Divider.toSelection(session);
|
||||
Location[] selection = dev.loki.loparkour.world.core.Divider.toSelection(session);
|
||||
if (selection == null || selection.length < 2) {
|
||||
throw new IllegalArgumentException("Invalid session zone: selection must contain at least 2 locations");
|
||||
}
|
||||
this.state.zone = selection;
|
||||
}
|
||||
|
||||
|
||||
// Initialize components
|
||||
this.placer = new BlockPlacer(this);
|
||||
this.lifecycle = new GeneratorLifecycle(this);
|
||||
this.profileManager = new GeneratorProfileManager(this);
|
||||
this.statistics = new GeneratorStatistics(this);
|
||||
|
||||
|
||||
// Setup initial state
|
||||
profileManager.overrideProfile();
|
||||
profileManager.calculateChances();
|
||||
}
|
||||
|
||||
public ParkourGenerator(@NotNull Session session, GeneratorOption... generatorOptions) {
|
||||
this(session, null, generatorOptions);
|
||||
}
|
||||
|
||||
// Core generation methods
|
||||
public void generate() { placer.generate(); }
|
||||
|
|
@ -125,8 +130,41 @@ public class ParkourGenerator {
|
|||
if (state.history.isEmpty()) return null;
|
||||
return state.history.get(state.history.size() - 1);
|
||||
}
|
||||
|
||||
|
||||
// Event listener management
|
||||
|
||||
/**
|
||||
* Register an event listener for generator events.
|
||||
* Used by adaptive system to collect metrics without creating direct dependencies.
|
||||
*
|
||||
* @param listener The listener to register
|
||||
*/
|
||||
public void registerEventListener(@NotNull GeneratorEventListener listener) {
|
||||
if (!eventListeners.contains(listener)) {
|
||||
eventListeners.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister an event listener.
|
||||
*
|
||||
* @param listener The listener to unregister
|
||||
*/
|
||||
public void unregisterEventListener(@NotNull GeneratorEventListener listener) {
|
||||
eventListeners.remove(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered event listeners.
|
||||
*
|
||||
* @return Immutable list of event listeners
|
||||
*/
|
||||
@NotNull
|
||||
public List<GeneratorEventListener> getEventListeners() {
|
||||
return new ArrayList<>(eventListeners);
|
||||
}
|
||||
|
||||
public enum BlockGenerationType {
|
||||
DEFAULT, SPECIAL, SCHEMATIC
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.loparkour.generator;
|
||||
package dev.loki.loparkour.generator.core.model;
|
||||
|
||||
/**
|
||||
* Options for {@link ParkourGenerator}s.
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package dev.loki.loparkour.generator;
|
||||
package dev.loki.loparkour.generator.core.model;
|
||||
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
package dev.loki.loparkour.generator;
|
||||
package dev.loki.loparkour.generator.core.model;
|
||||
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.Option;
|
||||
import dev.loki.loparkour.session.Session;
|
||||
import dev.loki.loparkour.util.Materials;
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.session.core.Session;
|
||||
import dev.loki.loparkour.util.item.Materials;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
|
|
@ -12,8 +12,6 @@ import org.bukkit.block.Block;
|
|||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.util.Vector;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
|
@ -26,16 +24,11 @@ public final class Island {
|
|||
private static final int PLATFORM_RADIUS = 4;
|
||||
|
||||
public final Session session;
|
||||
/**
|
||||
* Unused for spawn; kept for API / {@link dev.loki.loparkour.generator.jump.BlockPlacer} schematic branch.
|
||||
*/
|
||||
public final dev.loki.loparkour.schematic.lpschem.LPSchematic schematic;
|
||||
|
||||
public List<Block> blocks;
|
||||
|
||||
public Island(@NotNull Session session, @Nullable dev.loki.loparkour.schematic.lpschem.LPSchematic schematic) {
|
||||
public Island(@NotNull Session session) {
|
||||
this.session = session;
|
||||
this.schematic = schematic;
|
||||
}
|
||||
|
||||
public void build(Location location) {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.loparkour.generator;
|
||||
package dev.loki.loparkour.generator.core.model;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
package dev.loki.loparkour.generator.effect;
|
||||
|
||||
import dev.loki.loparkour.config.Option;
|
||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.util.ParticleUtil;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import dev.loki.loparkour.util.particle.ParticleUtil;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.block.Block;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
package dev.loki.loparkour.generator.jump;
|
||||
package dev.loki.loparkour.generator.jump.calculation;
|
||||
|
||||
import dev.loki.loparkour.config.Option;
|
||||
import dev.loki.loparkour.generator.GeneratorOption;
|
||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.generator.core.model.GeneratorOption;
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
import dev.loki.loparkour.generator.jump.placement.BlockSelector;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
|
|
@ -162,4 +163,4 @@ public class JumpCalculator {
|
|||
this.maxDistance = maxDistance;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.loparkour.generator.jump;
|
||||
package dev.loki.loparkour.generator.jump.calculation;
|
||||
|
||||
import org.bukkit.util.BoundingBox;
|
||||
import org.bukkit.util.Vector;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package dev.loki.loparkour.generator.jump;
|
||||
package dev.loki.loparkour.generator.jump.calculation;
|
||||
|
||||
import dev.loki.loparkour.util.Probs;
|
||||
import dev.loki.loparkour.util.misc.Probs;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.loparkour.generator.jump;
|
||||
package dev.loki.loparkour.generator.jump.calculation;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.loparkour.generator.jump;
|
||||
package dev.loki.loparkour.generator.jump.calculation;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.util.Vector;
|
||||
|
|
@ -1,9 +1,13 @@
|
|||
package dev.loki.loparkour.generator.jump;
|
||||
package dev.loki.loparkour.generator.jump.placement;
|
||||
|
||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.schematic.lpschem.LPSchematic;
|
||||
import dev.loki.loparkour.util.Probs;
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.api.event.ParkourSchematicGenerateEvent;
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
import dev.loki.loparkour.generator.jump.calculation.JumpCalculator;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import dev.loki.loparkour.schematic.core.ParkourSchematic;
|
||||
import dev.loki.loparkour.schematic.core.SchematicManager;
|
||||
import dev.loki.loparkour.util.misc.Probs;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.Sound;
|
||||
|
|
@ -154,31 +158,38 @@ public class BlockPlacer {
|
|||
}
|
||||
|
||||
private boolean tryGenerateSchematic() {
|
||||
// Simplified schematic generation
|
||||
LPSchematic schematic = generator.island.schematic; // Получаем схематик из island
|
||||
if (schematic == null) return false;
|
||||
|
||||
List<Block> blocks = selectBlocks();
|
||||
if (blocks.isEmpty()) return false;
|
||||
|
||||
Block targetBlock = blocks.get(0);
|
||||
Location pasteLocation = targetBlock.getLocation();
|
||||
|
||||
// Paste schematic (simplified)
|
||||
List<Block> schematicBlocks = pasteSchematic(schematic, pasteLocation);
|
||||
|
||||
if (!schematicBlocks.isEmpty()) {
|
||||
generator.state.history.addAll(schematicBlocks);
|
||||
generator.state.schematicBlocks = schematicBlocks;
|
||||
return true;
|
||||
SchematicManager manager = LoParkour.getSchematicManager();
|
||||
if (manager == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
double maxDifficulty = generator.getPlayers().stream()
|
||||
.mapToDouble(p -> p.schematicDifficulty)
|
||||
.min()
|
||||
.orElse(1.0);
|
||||
|
||||
ParkourSchematic schematic = manager.pick(maxDifficulty);
|
||||
if (schematic == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
List<Block> blocks = selectBlocks();
|
||||
if (blocks.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Location pasteLocation = blocks.get(0).getLocation();
|
||||
List<Block> schematicBlocks = schematic.paste(pasteLocation, pasteLocation.getWorld());
|
||||
if (schematicBlocks.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
generator.state.history.addAll(schematicBlocks);
|
||||
generator.state.schematicBlocks = schematicBlocks;
|
||||
|
||||
for (ParkourPlayer player : generator.getPlayers()) {
|
||||
new ParkourSchematicGenerateEvent(schematic, generator, player).call();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private List<Block> pasteSchematic(@NotNull LPSchematic schematic, @NotNull Location location) {
|
||||
// Use the schematic's built-in paste method
|
||||
return schematic.paste(location, location.getWorld());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
package dev.loki.loparkour.generator.jump;
|
||||
package dev.loki.loparkour.generator.jump.placement;
|
||||
|
||||
import dev.loki.loparkour.api.Registry;
|
||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||
import dev.loki.loparkour.style.Style;
|
||||
import dev.loki.loparkour.api.core.Registry;
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
import dev.loki.loparkour.generator.jump.calculation.JumpType;
|
||||
import dev.loki.loparkour.style.core.Style;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
|
@ -138,4 +139,4 @@ public class BlockSelector {
|
|||
material == Material.WARPED_TRAPDOOR ||
|
||||
material == Material.IRON_TRAPDOOR;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
package dev.loki.loparkour.generator.lifecycle;
|
||||
package dev.loki.loparkour.generator.lifecycle.loop;
|
||||
|
||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
import dev.loki.loparkour.generator.lifecycle.player.GeneratorCleanup;
|
||||
import dev.loki.loparkour.generator.lifecycle.player.PlayerInteractionHandler;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
package dev.loki.loparkour.generator.lifecycle;
|
||||
package dev.loki.loparkour.generator.lifecycle.loop;
|
||||
|
||||
import dev.loki.loparkour.api.event.ParkourFallEvent;
|
||||
import dev.loki.loparkour.api.event.ParkourScoreEvent;
|
||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||
import dev.loki.loparkour.mode.Mode;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.reward.Reward;
|
||||
import dev.loki.loparkour.reward.Rewards;
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
import dev.loki.loparkour.mode.base.Mode;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import dev.loki.loparkour.reward.core.Reward;
|
||||
import dev.loki.loparkour.reward.core.Rewards;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
|
|
@ -104,4 +104,4 @@ public class LifecycleEventManager {
|
|||
// Reset generator state
|
||||
generator.reset(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
package dev.loki.loparkour.generator.lifecycle;
|
||||
package dev.loki.loparkour.generator.lifecycle.loop;
|
||||
|
||||
import dev.loki.loparkour.config.Option;
|
||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
import dev.loki.loparkour.generator.lifecycle.player.GeneratorCleanup;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package dev.loki.loparkour.generator.lifecycle;
|
||||
package dev.loki.loparkour.generator.lifecycle.loop;
|
||||
|
||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package dev.loki.loparkour.generator.lifecycle;
|
||||
package dev.loki.loparkour.generator.lifecycle.player;
|
||||
|
||||
import dev.loki.loparkour.config.Option;
|
||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
|
|
@ -115,7 +115,7 @@ public class GeneratorCleanup {
|
|||
Location blockSpawn = spawn.clone();
|
||||
|
||||
// Move block forward based on heading (6 blocks forward, same height)
|
||||
switch (dev.loki.loparkour.config.Option.HEADING) {
|
||||
switch (dev.loki.loparkour.config.options.Option.HEADING) {
|
||||
case NORTH -> blockSpawn.add(0, 0, -6);
|
||||
case SOUTH -> blockSpawn.add(0, 0, 6);
|
||||
case WEST -> blockSpawn.add(-6, 0, 0);
|
||||
|
|
@ -127,4 +127,4 @@ public class GeneratorCleanup {
|
|||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
package dev.loki.loparkour.generator.lifecycle;
|
||||
package dev.loki.loparkour.generator.lifecycle.player;
|
||||
|
||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.schematic.lpschem.LPSchematic;
|
||||
import dev.loki.loparkour.adaptive.bootstrap.AdaptiveServices;
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
import dev.loki.loparkour.generator.core.coordinator.GeneratorEventListener;
|
||||
import dev.loki.loparkour.generator.lifecycle.loop.LifecycleEventManager;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
|
|
@ -47,7 +49,18 @@ public class PlayerInteractionHandler {
|
|||
public void handleScore() {
|
||||
generator.state.score++;
|
||||
eventManager.handleScore();
|
||||
|
||||
|
||||
// Fire event listeners for adaptive system
|
||||
Block latestBlock = generator.getLatest();
|
||||
if (latestBlock != null) {
|
||||
for (ParkourPlayer player : generator.getPlayers()) {
|
||||
double distance = calculateDistance(player, latestBlock);
|
||||
for (GeneratorEventListener listener : generator.getEventListeners()) {
|
||||
listener.onPlayerScore(player.player, latestBlock, distance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate more blocks after scoring
|
||||
generator.generate(1);
|
||||
}
|
||||
|
|
@ -56,7 +69,20 @@ public class PlayerInteractionHandler {
|
|||
* Handle player falling.
|
||||
*/
|
||||
public void handleFall() {
|
||||
// Fire event listeners for adaptive system
|
||||
Block latestBlock = generator.getLatest();
|
||||
for (ParkourPlayer player : generator.getPlayers()) {
|
||||
for (GeneratorEventListener listener : generator.getEventListeners()) {
|
||||
listener.onPlayerFall(player.player, latestBlock);
|
||||
}
|
||||
}
|
||||
|
||||
eventManager.handleFall();
|
||||
|
||||
for (ParkourPlayer player : generator.getPlayers()) {
|
||||
AdaptiveServices.persistRatingAfterFall(player);
|
||||
}
|
||||
AdaptiveServices.refreshGeneratorDifficulty(generator);
|
||||
}
|
||||
|
||||
private void handleBlockInteraction(@NotNull ParkourPlayer player, @NotNull Block block) {
|
||||
|
|
@ -84,12 +110,8 @@ public class PlayerInteractionHandler {
|
|||
}
|
||||
|
||||
private boolean isSchematicEndBlock(@NotNull Block block) {
|
||||
LPSchematic schematic = generator.island.schematic;
|
||||
if (schematic == null) return false;
|
||||
|
||||
// Simplified check - would need full schematic logic
|
||||
return generator.state.schematicBlocks != null &&
|
||||
generator.state.schematicBlocks.contains(block);
|
||||
return generator.state.schematicBlocks != null
|
||||
&& generator.state.schematicBlocks.contains(block);
|
||||
}
|
||||
|
||||
/** Block directly under feet; {@code null} in air — scoring runs when landed. */
|
||||
|
|
@ -100,4 +122,13 @@ public class PlayerInteractionHandler {
|
|||
|
||||
return below.getType() != Material.AIR ? below : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate distance between player and block.
|
||||
*/
|
||||
private double calculateDistance(@NotNull ParkourPlayer player, @NotNull Block block) {
|
||||
Location playerLoc = player.player.getLocation();
|
||||
Location blockLoc = block.getLocation().add(0.5, 0, 0.5);
|
||||
return playerLoc.distance(blockLoc);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package dev.loki.loparkour.generator.profile;
|
||||
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.generator.core.model.Profile;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Loads default generation weights from generation.yml into a {@link Profile}.
|
||||
*/
|
||||
public final class GenerationProfileLoader {
|
||||
|
||||
private static final Set<String> PLAYER_KEYS = Set.of(
|
||||
"schematicDifficulty", "blockLead", "particles", "sound",
|
||||
"useSpecialBlocks", "showFallMessage", "showScoreboard",
|
||||
"selectedTime", "style"
|
||||
);
|
||||
|
||||
private GenerationProfileLoader() {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static Profile loadFromConfig() {
|
||||
Profile profile = new Profile();
|
||||
|
||||
int minDistance = Config.GENERATION.getInt("advanced.distance.min", 1);
|
||||
int maxDistance = Config.GENERATION.getInt("advanced.distance.max", 4);
|
||||
profile.set("distance.min", String.valueOf(minDistance));
|
||||
profile.set("distance.max", String.valueOf(maxDistance));
|
||||
for (int distance = minDistance; distance <= maxDistance; distance++) {
|
||||
int chance = Config.GENERATION.getInt("advanced.distance.chances." + distance, 1);
|
||||
profile.set("distance.chances." + distance, String.valueOf(chance));
|
||||
}
|
||||
|
||||
int minHeight = Config.GENERATION.getInt("advanced.height.min", -1);
|
||||
int maxHeight = Config.GENERATION.getInt("advanced.height.max", 2);
|
||||
profile.set("height.min", String.valueOf(minHeight));
|
||||
profile.set("height.max", String.valueOf(maxHeight));
|
||||
for (int height = minHeight; height <= maxHeight; height++) {
|
||||
int chance = Config.GENERATION.getInt("advanced.height.chances." + height, 1);
|
||||
profile.set("height.chances." + height, String.valueOf(chance));
|
||||
}
|
||||
|
||||
profile.set("type.default", String.valueOf(Config.GENERATION.getInt("advanced.type.default", 80)));
|
||||
profile.set("type.special", String.valueOf(Config.GENERATION.getInt("advanced.type.special", 15)));
|
||||
profile.set("type.schematic", String.valueOf(Config.GENERATION.getInt("advanced.type.schematic", 5)));
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
public static void preservePlayerSettings(@NotNull Profile from, @NotNull Profile to) {
|
||||
for (String key : PLAYER_KEYS) {
|
||||
if (from.settings.containsKey(key)) {
|
||||
to.set(key, from.get(key).value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Replaces all settings on {@code target} with a snapshot from {@code source}. */
|
||||
public static void copyInto(@NotNull Profile target, @NotNull Profile source) {
|
||||
target.settings.clear();
|
||||
for (Map.Entry<String, Profile.ProfileValue> entry : source.settings.entrySet()) {
|
||||
target.set(entry.getKey(), entry.getValue().value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
package dev.loki.loparkour.ghost;
|
||||
package dev.loki.loparkour.ghost.core;
|
||||
import dev.loki.loparkour.ghost.core.GhostPlayer;
|
||||
import dev.loki.loparkour.ghost.model.GhostData;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
package dev.loki.loparkour.ghost;
|
||||
package dev.loki.loparkour.ghost.core;
|
||||
import dev.loki.loparkour.ghost.model.GhostFrame;
|
||||
import dev.loki.loparkour.ghost.model.GhostData;
|
||||
|
||||
import dev.lolib.scheduler.Scheduler;
|
||||
import dev.lolib.scheduler.ScheduledTask;
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
package dev.loki.loparkour.ghost;
|
||||
package dev.loki.loparkour.ghost.core;
|
||||
import dev.loki.loparkour.ghost.model.GhostFrame;
|
||||
import dev.loki.loparkour.ghost.model.GhostData;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.loparkour.ghost;
|
||||
package dev.loki.loparkour.ghost.model;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.loparkour.ghost;
|
||||
package dev.loki.loparkour.ghost.model;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.loparkour.hook;
|
||||
package dev.loki.loparkour.hook.floodgate;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
import org.geysermc.floodgate.api.FloodgateApi;
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
package dev.loki.loparkour.hook;
|
||||
package dev.loki.loparkour.hook.holo;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.api.Registry;
|
||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||
import dev.loki.loparkour.leaderboard.Score;
|
||||
import dev.loki.loparkour.mode.Mode;
|
||||
import dev.loki.loparkour.api.core.Registry;
|
||||
import dev.loki.loparkour.leaderboard.core.Leaderboard;
|
||||
import dev.loki.loparkour.leaderboard.model.Score;
|
||||
import dev.loki.loparkour.mode.base.Mode;
|
||||
import me.filoghost.holographicdisplays.api.HolographicDisplaysAPI;
|
||||
|
||||
public class HoloHook {
|
||||
|
|
@ -94,4 +94,4 @@ public class HoloHook {
|
|||
}
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
package dev.loki.loparkour.hook;
|
||||
package dev.loki.loparkour.hook.papi;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.api.Registry;
|
||||
import dev.loki.loparkour.generator.ParkourGenerator;
|
||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||
import dev.loki.loparkour.leaderboard.Score;
|
||||
import dev.loki.loparkour.mode.Mode;
|
||||
import dev.loki.loparkour.mode.Modes;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.ParkourSpectator;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.loki.loparkour.api.core.Registry;
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
import dev.loki.loparkour.leaderboard.core.Leaderboard;
|
||||
import dev.loki.loparkour.leaderboard.model.Score;
|
||||
import dev.loki.loparkour.mode.base.Mode;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.spectator.ParkourSpectator;
|
||||
import dev.loki.loparkour.player.core.ParkourUser;
|
||||
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.loparkour.hook;
|
||||
package dev.loki.loparkour.hook.vault;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import net.milkbowl.vault.economy.Economy;
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
package dev.loki.loparkour.leaderboard;
|
||||
package dev.loki.loparkour.leaderboard.core;
|
||||
import dev.loki.loparkour.leaderboard.persistence.LeaderboardStorage;
|
||||
import dev.loki.loparkour.leaderboard.model.Score;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
package dev.loki.loparkour.leaderboard;
|
||||
package dev.loki.loparkour.leaderboard.core;
|
||||
import dev.loki.loparkour.leaderboard.model.Score;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.loparkour.leaderboard;
|
||||
package dev.loki.loparkour.leaderboard.model;
|
||||
|
||||
/**
|
||||
* Represents a record, used to keep track of the score a player may achieve.
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
package dev.loki.loparkour.leaderboard;
|
||||
package dev.loki.loparkour.leaderboard.persistence;
|
||||
import dev.loki.loparkour.leaderboard.model.Score;
|
||||
import dev.loki.loparkour.leaderboard.core.LeaderboardSorter;
|
||||
import dev.loki.loparkour.leaderboard.core.Leaderboard;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.storage.Storage;
|
||||
import dev.lolib.scheduler.Scheduler;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
package dev.loki.loparkour.listener;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.command.SchematicCommandHandler;
|
||||
import dev.loki.loparkour.util.ColorUtil;
|
||||
import dev.loki.loparkour.util.Locations;
|
||||
import dev.loki.loparkour.util.ParticleUtil;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.Action;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.inventory.EquipmentSlot;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.util.BoundingBox;
|
||||
|
||||
/**
|
||||
* Handles LPSchematic wand clicks (left/right click on blocks).
|
||||
*/
|
||||
public class SchematicWandListener implements Listener {
|
||||
|
||||
@EventHandler
|
||||
public void onWandInteract(PlayerInteractEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
ItemStack item = player.getInventory().getItemInMainHand();
|
||||
|
||||
if (!player.hasPermission("LoParkour.admin")) return;
|
||||
if (item.getItemMeta() == null || !item.getItemMeta().getDisplayName().contains("LPSchematic Wand")) return;
|
||||
if (event.getClickedBlock() == null || event.getHand() != EquipmentSlot.HAND) return;
|
||||
|
||||
Location loc = event.getClickedBlock().getLocation();
|
||||
Location[] sel = SchematicCommandHandler.selections.get(player);
|
||||
event.setCancelled(true);
|
||||
|
||||
if (event.getAction() == Action.LEFT_CLICK_BLOCK) {
|
||||
Location[] updated = new Location[]{loc, sel != null ? sel[1] : null};
|
||||
SchematicCommandHandler.selections.put(player, updated);
|
||||
send(player, LoParkour.PREFIX + "Position 1 set to " + Locations.toString(loc, true));
|
||||
if (updated[1] != null)
|
||||
ParticleUtil.box(BoundingBox.of(loc, updated[1]), player.getWorld(), Particle.END_ROD, player, 0.2);
|
||||
|
||||
} else if (event.getAction() == Action.RIGHT_CLICK_BLOCK) {
|
||||
Location[] updated = new Location[]{sel != null ? sel[0] : null, loc};
|
||||
SchematicCommandHandler.selections.put(player, updated);
|
||||
send(player, LoParkour.PREFIX + "Position 2 set to " + Locations.toString(loc, true));
|
||||
if (updated[0] != null)
|
||||
ParticleUtil.box(BoundingBox.of(updated[0], loc), player.getWorld(), Particle.END_ROD, player, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
private void send(CommandSender sender, String msg) {
|
||||
sender.sendMessage(ColorUtil.color(msg));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
package dev.loki.loparkour.listener;
|
||||
package dev.loki.loparkour.listener.gameplay;
|
||||
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.config.Option;
|
||||
import dev.loki.loparkour.menu.Menus;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.loki.loparkour.session.Session;
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.menu.core.Menus;
|
||||
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.core.ParkourUser;
|
||||
import dev.loki.loparkour.session.core.Session;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Cancellable;
|
||||
import org.bukkit.event.EventHandler;
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
package dev.loki.loparkour.listener;
|
||||
package dev.loki.loparkour.listener.player;
|
||||
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.loki.loparkour.mode.Modes;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.loki.loparkour.world.World;
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
import dev.loki.loparkour.player.core.ParkourUser;
|
||||
import dev.loki.loparkour.world.core.World;
|
||||
import io.papermc.lib.PaperLib;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package dev.loki.loparkour.listener.schematic;
|
||||
|
||||
import dev.loki.loparkour.command.schematic.SchematicCommandHandler;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.util.particle.ParticleUtil;
|
||||
import dev.loki.loparkour.util.text.ColorUtil;
|
||||
import dev.loki.loparkour.util.world.Locations;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.Action;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.inventory.EquipmentSlot;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Wand left/right click sets schematic selection corners.
|
||||
*/
|
||||
public class SchematicWandListener implements Listener {
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onInteract(@NotNull PlayerInteractEvent event) {
|
||||
if (event.getHand() != EquipmentSlot.HAND) {
|
||||
return;
|
||||
}
|
||||
if (!SchematicCommandHandler.isWand(event.getItem())) {
|
||||
return;
|
||||
}
|
||||
|
||||
Action action = event.getAction();
|
||||
if (action != Action.LEFT_CLICK_BLOCK && action != Action.RIGHT_CLICK_BLOCK) {
|
||||
return;
|
||||
}
|
||||
if (event.getClickedBlock() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.setCancelled(true);
|
||||
var player = event.getPlayer();
|
||||
Location loc = event.getClickedBlock().getLocation();
|
||||
int index = action == Action.LEFT_CLICK_BLOCK ? 0 : 1;
|
||||
|
||||
SchematicCommandHandler.setPos(player, loc, index);
|
||||
String label = index == 0 ? "1" : "2";
|
||||
player.sendMessage(ColorUtil.color(
|
||||
Locales.getString(player, "schematic.pos_set").formatted(label, Locations.toString(loc, true))));
|
||||
|
||||
Location[] sel = SchematicCommandHandler.getSelection(player);
|
||||
if (sel[0] != null && sel[1] != null) {
|
||||
ParticleUtil.box(
|
||||
org.bukkit.util.BoundingBox.of(sel[0], sel[1]),
|
||||
player.getWorld(),
|
||||
Particle.END_ROD,
|
||||
player,
|
||||
0.2
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
package dev.loki.loparkour.menu.community;
|
||||
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.menu.LPMenu;
|
||||
import dev.loki.loparkour.menu.Menus;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.menu.core.LPMenu;
|
||||
import dev.loki.loparkour.menu.core.Menus;
|
||||
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
package dev.loki.loparkour.menu.community;
|
||||
|
||||
import dev.loki.loparkour.api.Registry;
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||
import dev.loki.loparkour.menu.LPMenu;
|
||||
import dev.loki.loparkour.menu.Menus;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.loki.loparkour.mode.Mode;
|
||||
import dev.loki.loparkour.api.core.Registry;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.leaderboard.core.Leaderboard;
|
||||
import dev.loki.loparkour.menu.core.LPMenu;
|
||||
import dev.loki.loparkour.menu.core.Menus;
|
||||
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||
import dev.loki.loparkour.mode.base.Mode;
|
||||
import dev.lolib.gui.InventoryGUI;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
|
@ -64,12 +64,12 @@ public class LeaderboardsMenu extends LPMenu {
|
|||
// Add navigation buttons
|
||||
if (currentPage > 0) {
|
||||
// Previous page button
|
||||
gui = gui.setItem(0, backItem(), e -> open(player, currentPage - 1));
|
||||
gui = gui.setItem(0, backItem(player), e -> open(player, currentPage - 1));
|
||||
}
|
||||
|
||||
if (currentPage < totalPages - 1) {
|
||||
// Next page button
|
||||
gui = gui.setItem(8, nextItem(), e -> open(player, currentPage + 1));
|
||||
gui = gui.setItem(8, nextItem(player), e -> open(player, currentPage + 1));
|
||||
}
|
||||
|
||||
gui.open(player);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
package dev.loki.loparkour.menu.community;
|
||||
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.leaderboard.Leaderboard;
|
||||
import dev.loki.loparkour.leaderboard.Score;
|
||||
import dev.loki.loparkour.menu.LPMenu;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.loki.loparkour.mode.Mode;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.leaderboard.core.Leaderboard;
|
||||
import dev.loki.loparkour.leaderboard.model.Score;
|
||||
import dev.loki.loparkour.menu.core.LPMenu;
|
||||
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||
import dev.loki.loparkour.mode.base.Mode;
|
||||
import dev.loki.loparkour.player.core.ParkourUser;
|
||||
import dev.lolib.gui.InventoryGUI;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
|
|
@ -89,12 +89,12 @@ public class SingleLeaderboardMenu extends LPMenu {
|
|||
// Add navigation buttons
|
||||
if (currentPage > 0) {
|
||||
// Previous page button
|
||||
gui = gui.setItem(0, backItem(), e -> open(player, mode, sort, currentPage - 1));
|
||||
gui = gui.setItem(0, backItem(player), e -> open(player, mode, sort, currentPage - 1));
|
||||
}
|
||||
|
||||
if (currentPage < totalPages - 1) {
|
||||
// Next page button
|
||||
gui = gui.setItem(8, nextItem(), e -> open(player, mode, sort, currentPage + 1));
|
||||
gui = gui.setItem(8, nextItem(player), e -> open(player, mode, sort, currentPage + 1));
|
||||
}
|
||||
|
||||
gui.open(player);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package dev.loki.loparkour.menu;
|
||||
package dev.loki.loparkour.menu.core;
|
||||
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.loki.loparkour.player.core.ParkourUser;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
|
@ -29,7 +29,7 @@ public abstract class DynamicMenu {
|
|||
*/
|
||||
// Stub: migrate to LoLib GUI when dynamic rows are wired up.
|
||||
/*
|
||||
public void registerMainItem(int row, int id, BiFunction<@NotNull Player, @Nullable ParkourUser, dev.loki.loparkour.util.MenuItem> item, Predicate<Player> shouldDisplay) {
|
||||
public void registerMainItem(int row, int id, BiFunction<@NotNull Player, @Nullable ParkourUser, dev.loki.loparkour.util.gui.MenuItem> item, Predicate<Player> shouldDisplay) {
|
||||
if (id < 0 || row < 0 || row > 4) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -75,6 +75,6 @@ public abstract class DynamicMenu {
|
|||
/**
|
||||
* Data class for registered items
|
||||
*/
|
||||
protected record ItemContainer(int id, BiFunction<@NotNull Player, @Nullable ParkourUser, dev.loki.loparkour.util.MenuItem> item,
|
||||
protected record ItemContainer(int id, BiFunction<@NotNull Player, @Nullable ParkourUser, dev.loki.loparkour.util.gui.MenuItem> item,
|
||||
Predicate<Player> predicate) {}
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package dev.loki.loparkour.menu;
|
||||
package dev.loki.loparkour.menu.core;
|
||||
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.config.Option;
|
||||
import dev.loki.loparkour.util.ColorUtil;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.util.text.ColorUtil;
|
||||
import dev.lolib.gui.InventoryGUI;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
|
|
@ -25,7 +25,7 @@ public abstract class LPMenu {
|
|||
public abstract void open(@NotNull Player player);
|
||||
|
||||
protected String locale(@NotNull Player player) {
|
||||
var user = dev.loki.loparkour.player.ParkourUser.getUser(player);
|
||||
var user = dev.loki.loparkour.player.core.ParkourUser.getUser(player);
|
||||
String loc = user != null ? user.locale : Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG);
|
||||
return (loc != null && !loc.isBlank()) ? loc : "en";
|
||||
}
|
||||
|
|
@ -40,24 +40,12 @@ public abstract class LPMenu {
|
|||
return item;
|
||||
}
|
||||
|
||||
protected ItemStack backItem() {
|
||||
ItemStack item = new ItemStack(Material.RED_DYE);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setDisplayName(ColorUtil.color("<#DE1F1F><bold>Назад"));
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
return item;
|
||||
protected ItemStack backItem(@NotNull Player player) {
|
||||
return localeItem(player, "other.back");
|
||||
}
|
||||
|
||||
protected ItemStack nextItem() {
|
||||
ItemStack item = new ItemStack(Material.LIME_DYE);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setDisplayName(ColorUtil.color("<#0DCB07><bold>Далее"));
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
return item;
|
||||
protected ItemStack nextItem(@NotNull Player player) {
|
||||
return localeItem(player, "other.next");
|
||||
}
|
||||
|
||||
protected ItemStack closeButton(@NotNull Player player) {
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package dev.loki.loparkour.menu;
|
||||
package dev.loki.loparkour.menu.core;
|
||||
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.player.core.ParkourUser;
|
||||
import dev.lolib.gui.InventoryGUI;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
|
@ -59,7 +59,7 @@ public class MainMenu extends LPMenu {
|
|||
|
||||
int count = visibleItems.size();
|
||||
if (count == 0) {
|
||||
player.sendMessage(Locales.getString(player, "other.no_menu_items"));
|
||||
Locales.send(player, "other.no_menu_items");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.loparkour.menu;
|
||||
package dev.loki.loparkour.menu.core;
|
||||
|
||||
/**
|
||||
* Placeholder during menu migration; real flows use {@link Menus} / LoLib GUI.
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.loparkour.menu;
|
||||
package dev.loki.loparkour.menu.core;
|
||||
|
||||
import dev.loki.loparkour.menu.community.CommunityMenu;
|
||||
import dev.loki.loparkour.menu.community.LeaderboardsMenu;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package dev.loki.loparkour.menu;
|
||||
package dev.loki.loparkour.menu.core;
|
||||
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.Option;
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import org.bukkit.permissions.Permissible;
|
||||
|
||||
/**
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
package dev.loki.loparkour.menu.lobby;
|
||||
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.menu.LPMenu;
|
||||
import dev.loki.loparkour.menu.Menus;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.loki.loparkour.session.Session;
|
||||
import dev.loki.loparkour.util.ColorUtil;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.menu.core.LPMenu;
|
||||
import dev.loki.loparkour.menu.core.Menus;
|
||||
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||
import dev.loki.loparkour.player.core.ParkourUser;
|
||||
import dev.loki.loparkour.session.core.Session;
|
||||
import dev.loki.loparkour.util.text.ColorUtil;
|
||||
import dev.lolib.gui.InventoryGUI;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
package dev.loki.loparkour.menu.lobby;
|
||||
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.menu.LPMenu;
|
||||
import dev.loki.loparkour.menu.Menus;
|
||||
import dev.loki.loparkour.mode.Modes;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.loki.loparkour.util.ColorUtil;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.menu.core.LPMenu;
|
||||
import dev.loki.loparkour.menu.core.Menus;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.core.ParkourUser;
|
||||
import dev.loki.loparkour.util.text.ColorUtil;
|
||||
import dev.lolib.gui.InventoryGUI;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
|
|
@ -51,10 +51,18 @@ public class PlayerManagementMenu extends LPMenu {
|
|||
boolean muted = session.isMuted(other);
|
||||
ItemMeta displayMeta = skull.getItemMeta();
|
||||
if (displayMeta != null) {
|
||||
displayMeta.setDisplayName(ColorUtil.color("<yellow>" + other.getName() + (muted ? " §c[muted]" : "")));
|
||||
String muteSuffix = muted
|
||||
? Locales.getString(locale, "lobby.player_management.head.muted_suffix")
|
||||
: "";
|
||||
displayMeta.setDisplayName(ColorUtil.color(
|
||||
Locales.getString(locale, "lobby.player_management.head.name")
|
||||
.formatted(other.getName(), muteSuffix)));
|
||||
String muteLoreKey = muted
|
||||
? "lobby.player_management.head.lore_unmute"
|
||||
: "lobby.player_management.head.lore_mute";
|
||||
displayMeta.setLore(List.of(
|
||||
ColorUtil.color("§7Left-click §fto kick"),
|
||||
ColorUtil.color("§7Right-click §f" + (muted ? "to unmute" : "to mute"))
|
||||
ColorUtil.color(Locales.getString(locale, "lobby.player_management.head.lore_kick")),
|
||||
ColorUtil.color(Locales.getString(locale, muteLoreKey))
|
||||
));
|
||||
skull.setItemMeta(displayMeta);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
package dev.loki.loparkour.menu.play;
|
||||
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.menu.LPMenu;
|
||||
import dev.loki.loparkour.menu.Menus;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.menu.core.LPMenu;
|
||||
import dev.loki.loparkour.menu.core.Menus;
|
||||
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
package dev.loki.loparkour.menu.play;
|
||||
|
||||
import dev.loki.loparkour.api.Registry;
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.menu.LPMenu;
|
||||
import dev.loki.loparkour.mode.Mode;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.loki.loparkour.api.core.Registry;
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.menu.core.LPMenu;
|
||||
import dev.loki.loparkour.mode.base.Mode;
|
||||
import dev.loki.loparkour.player.core.ParkourUser;
|
||||
import dev.lolib.gui.InventoryGUI;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
|
@ -81,12 +81,12 @@ public class SingleMenu extends LPMenu {
|
|||
// Add navigation buttons
|
||||
if (currentPage > 0) {
|
||||
// Previous page button
|
||||
gui = gui.setItem(0, backItem(), e -> open(player, currentPage - 1));
|
||||
gui = gui.setItem(0, backItem(player), e -> open(player, currentPage - 1));
|
||||
}
|
||||
|
||||
if (currentPage < totalPages - 1) {
|
||||
// Next page button
|
||||
gui = gui.setItem(8, nextItem(), e -> open(player, currentPage + 1));
|
||||
gui = gui.setItem(8, nextItem(player), e -> open(player, currentPage + 1));
|
||||
}
|
||||
|
||||
gui.open(player);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
package dev.loki.loparkour.menu.play;
|
||||
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.menu.LPMenu;
|
||||
import dev.loki.loparkour.mode.Modes;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.loki.loparkour.session.Session;
|
||||
import dev.loki.loparkour.world.Divider;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.menu.core.LPMenu;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
import dev.loki.loparkour.player.core.ParkourUser;
|
||||
import dev.loki.loparkour.session.core.Session;
|
||||
import dev.loki.loparkour.world.core.Divider;
|
||||
import dev.lolib.gui.InventoryGUI;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
|
|
@ -68,12 +68,12 @@ public class SpectatorMenu extends LPMenu {
|
|||
// Add navigation buttons
|
||||
if (currentPage > 0) {
|
||||
// Previous page button
|
||||
gui = gui.setItem(0, backItem(), e -> open(player, currentPage - 1));
|
||||
gui = gui.setItem(0, backItem(player), e -> open(player, currentPage - 1));
|
||||
}
|
||||
|
||||
if (currentPage < totalPages - 1) {
|
||||
// Next page button
|
||||
gui = gui.setItem(8, nextItem(), e -> open(player, currentPage + 1));
|
||||
gui = gui.setItem(8, nextItem(player), e -> open(player, currentPage + 1));
|
||||
}
|
||||
|
||||
gui.open(player);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package dev.loki.loparkour.menu.settings;
|
||||
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.menu.LPMenu;
|
||||
import dev.loki.loparkour.menu.Menus;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.util.ColorUtil;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.menu.core.LPMenu;
|
||||
import dev.loki.loparkour.menu.core.Menus;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import dev.loki.loparkour.util.text.ColorUtil;
|
||||
import dev.lolib.gui.InventoryGUI;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
package dev.loki.loparkour.menu.settings;
|
||||
|
||||
import dev.loki.loparkour.api.Registry;
|
||||
import dev.loki.loparkour.config.Config;
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.menu.LPMenu;
|
||||
import dev.loki.loparkour.menu.Menus;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.style.Style;
|
||||
import dev.loki.loparkour.util.ColorUtil;
|
||||
import dev.loki.loparkour.api.core.Registry;
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.menu.core.LPMenu;
|
||||
import dev.loki.loparkour.menu.core.Menus;
|
||||
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import dev.loki.loparkour.style.core.Style;
|
||||
import dev.loki.loparkour.util.text.ColorUtil;
|
||||
import dev.lolib.gui.InventoryGUI;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
package dev.loki.loparkour.menu.settings;
|
||||
|
||||
import dev.loki.loparkour.config.Locales;
|
||||
import dev.loki.loparkour.menu.LPMenu;
|
||||
import dev.loki.loparkour.menu.Menus;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.loki.loparkour.player.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.menu.core.LPMenu;
|
||||
import dev.loki.loparkour.menu.core.Menus;
|
||||
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.core.ParkourUser;
|
||||
import dev.lolib.gui.InventoryGUI;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue