diff --git a/src/main/java/com/Lino/battlePass/gui/LeaderboardGui.java b/src/main/java/com/Lino/battlePass/gui/LeaderboardGui.java index a06e2f7..f3695b7 100644 --- a/src/main/java/com/Lino/battlePass/gui/LeaderboardGui.java +++ b/src/main/java/com/Lino/battlePass/gui/LeaderboardGui.java @@ -29,6 +29,7 @@ public void open() { setupTitleItem(gui); setupLeaderboard(gui); + setupPlayerRank(gui); if (plugin.getConfigManager().isShopEnabled()) { setupCoinsInfo(gui); @@ -61,15 +62,16 @@ private void setupTitleItem(Inventory gui) { } private void setupLeaderboard(Inventory gui) { + int leaderboardSize = plugin.getConfigManager().getLeaderboardSize(); + int xpPerLevel = plugin.getConfigManager().getXpPerLevel(); + plugin.getDatabaseManager().getTop10Players().thenAccept(topPlayers -> { Bukkit.getScheduler().runTask(plugin, () -> { - int[] slots = {19, 20, 21, 22, 23, 24, 25, 28, 29, 30}; - - for (int i = 0; i < topPlayers.size() && i < 10; i++) { + // Row 2 (slots 9-17), Row 3 (slots 18-26), Row 4 (slots 27+) + for (int i = 0; i < topPlayers.size() && i < leaderboardSize; i++) { PlayerData topPlayer = topPlayers.get(i); OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(topPlayer.uuid); - // FIX: Controllo se il nome è null per evitare il crash String playerName = offlinePlayer.getName(); if (playerName == null) { playerName = "Unknown"; @@ -92,12 +94,16 @@ private void setupLeaderboard(Inventory gui) { plugin.getMessageManager().getMessage("items.leaderboard-status.you") : plugin.getMessageManager().getMessage("items.leaderboard-status.other"); + String progressBar = createProgressBar(topPlayer.xp, xpPerLevel); + List lore = new ArrayList<>(); for (String line : plugin.getMessageManager().getMessagesConfig().getStringList("items.leaderboard-player.lore")) { String processedLine = line .replace("%level%", String.valueOf(topPlayer.level)) .replace("%total_levels%", String.valueOf(topPlayer.totalLevels)) .replace("%xp%", String.valueOf(topPlayer.xp)) + .replace("%xp_needed%", String.valueOf(xpPerLevel)) + .replace("%progress_bar%", progressBar) .replace("%coins%", String.valueOf(topPlayer.battleCoins)) .replace("%status%", status); lore.add(GradientColorParser.parse(processedLine)); @@ -105,9 +111,53 @@ private void setupLeaderboard(Inventory gui) { skullMeta.setLore(lore); skull.setItemMeta(skullMeta); - gui.setItem(slots[i], skull); + // Slots: 9-17 (row 2), 18-26 (row 3), 27-35 (row 4) + gui.setItem(9 + i, skull); + } + + if (player.getOpenInventory().getTitle().equals(title)) { + player.updateInventory(); + } + }); + }); + } + + private void setupPlayerRank(Inventory gui) { + int xpPerLevel = plugin.getConfigManager().getXpPerLevel(); + int minLevel = plugin.getConfigManager().getLeaderboardMinLevel(); + + plugin.getDatabaseManager().getPlayerRankInfo(player.getUniqueId()).thenAccept(rankInfo -> { + Bukkit.getScheduler().runTask(plugin, () -> { + int rank = rankInfo[0]; + int total = rankInfo[1]; + + PlayerData data = plugin.getPlayerDataManager().getPlayerData(player.getUniqueId()); + if (data == null) return; + + ItemStack item = new ItemStack(Material.RECOVERY_COMPASS); + ItemMeta meta = item.getItemMeta(); + meta.setDisplayName(plugin.getMessageManager().getMessage("items.player-rank.name")); + + String progressBar = createProgressBar(data.xp, xpPerLevel); + String lorePath = rank > 0 ? "items.player-rank.lore-ranked" : "items.player-rank.lore-unranked"; + + List lore = new ArrayList<>(); + for (String line : plugin.getMessageManager().getMessagesConfig().getStringList(lorePath)) { + String processedLine = line + .replace("%rank%", rank > 0 ? String.valueOf(rank) : "-") + .replace("%total%", String.valueOf(total)) + .replace("%level%", String.valueOf(data.level)) + .replace("%xp%", String.valueOf(data.xp)) + .replace("%xp_needed%", String.valueOf(xpPerLevel)) + .replace("%progress_bar%", progressBar) + .replace("%min_level%", String.valueOf(minLevel)); + lore.add(GradientColorParser.parse(processedLine)); } + meta.setLore(lore); + item.setItemMeta(meta); + gui.setItem(31, item); + if (player.getOpenInventory().getTitle().equals(title)) { player.updateInventory(); } @@ -131,6 +181,20 @@ private void setupCoinsInfo(Inventory gui) { meta.setLore(lore); coinsInfo.setItemMeta(meta); - gui.setItem(40, coinsInfo); + gui.setItem(34, coinsInfo); + } + + private String createProgressBar(int current, int max) { + int totalBars = 10; + int filled; + if (max <= 0) { + filled = 0; + } else { + filled = Math.min(totalBars, (int) Math.round((double) current / max * totalBars)); + } + StringBuilder bar = new StringBuilder(); + for (int i = 0; i < filled; i++) bar.append("&a█"); + for (int i = filled; i < totalBars; i++) bar.append("&8░"); + return bar.toString(); } -} \ No newline at end of file +} diff --git a/src/main/java/com/Lino/battlePass/managers/ConfigManager.java b/src/main/java/com/Lino/battlePass/managers/ConfigManager.java index 1adbad9..d8c104c 100644 --- a/src/main/java/com/Lino/battlePass/managers/ConfigManager.java +++ b/src/main/java/com/Lino/battlePass/managers/ConfigManager.java @@ -29,6 +29,11 @@ public class ConfigManager { private int coinsDistributionHours = 24; private int missionResetHours = 24; private boolean customItemSoundsEnabled = true; + private String leaderboardSortBy = "level"; + private String leaderboardTiebreaker = "xp"; + private int leaderboardMinLevel = 2; + private int leaderboardSize = 20; + private boolean resetTotalLevelsOnSeasonEnd = true; private Material guiFreeLockedMaterial = Material.GRAY_STAINED_GLASS; private Material guiPremiumLockedMaterial = Material.GRAY_STAINED_GLASS; @@ -65,6 +70,11 @@ public void reload() { coinsDistributionHours = config.getInt("battle-coins.distribution-hours", 24); missionResetHours = config.getInt("missions.reset-hours", 24); customItemSoundsEnabled = config.getBoolean("custom-items.sounds-enabled", true); + leaderboardSortBy = config.getString("leaderboard.sort-by", "level").toLowerCase(); + leaderboardTiebreaker = config.getString("leaderboard.tiebreaker", "xp").toLowerCase(); + leaderboardMinLevel = config.getInt("leaderboard.min-level", 2); + leaderboardSize = Math.min(config.getInt("leaderboard.size", 20), 27); + resetTotalLevelsOnSeasonEnd = config.getBoolean("leaderboard.reset-total-levels-on-season-end", true); databaseType = config.getString("database.type", "SQLITE"); dbHost = config.getString("database.host", "localhost"); @@ -268,4 +278,24 @@ public String getDbPrefix() { public int getDbPoolSize() { return dbPoolSize; } + + public String getLeaderboardSortBy() { + return leaderboardSortBy; + } + + public String getLeaderboardTiebreaker() { + return leaderboardTiebreaker; + } + + public int getLeaderboardMinLevel() { + return leaderboardMinLevel; + } + + public int getLeaderboardSize() { + return leaderboardSize; + } + + public boolean isResetTotalLevelsOnSeasonEnd() { + return resetTotalLevelsOnSeasonEnd; + } } diff --git a/src/main/java/com/Lino/battlePass/managers/DatabaseManager.java b/src/main/java/com/Lino/battlePass/managers/DatabaseManager.java index 318231b..c17f4b6 100644 --- a/src/main/java/com/Lino/battlePass/managers/DatabaseManager.java +++ b/src/main/java/com/Lino/battlePass/managers/DatabaseManager.java @@ -371,15 +371,34 @@ public CompletableFuture updatePlayerCoins(UUID uuid, int amount) { }, databaseExecutor); } + private String mapSortColumn(String configValue) { + switch (configValue) { + case "level": return "level"; + case "total_levels": return "total_levels"; + case "xp": return "xp"; + case "battle_coins": return "battle_coins"; + default: return "level"; + } + } + public CompletableFuture> getTop10Players() { return CompletableFuture.supplyAsync(() -> { List allPlayers = new ArrayList<>(); Connection conn = null; boolean shouldClose = isMySQL; + String sortBy = mapSortColumn(plugin.getConfigManager().getLeaderboardSortBy()); + String tiebreaker = mapSortColumn(plugin.getConfigManager().getLeaderboardTiebreaker()); + int minLevel = plugin.getConfigManager().getLeaderboardMinLevel(); + int limit = plugin.getConfigManager().getLeaderboardSize(); + + String query = "SELECT * FROM " + prefix + "players WHERE exclude_from_top = 0 AND level >= ? ORDER BY " + sortBy + " DESC, " + tiebreaker + " DESC LIMIT ?"; + try { conn = getConnection(); - try (PreparedStatement ps = conn.prepareStatement("SELECT * FROM " + prefix + "players WHERE exclude_from_top = 0 ORDER BY total_levels DESC, level DESC, xp DESC LIMIT 10")) { + try (PreparedStatement ps = conn.prepareStatement(query)) { + ps.setInt(1, minLevel); + ps.setInt(2, limit); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { PlayerData data = new PlayerData(UUID.fromString(rs.getString("uuid"))); @@ -403,6 +422,76 @@ public CompletableFuture> getTop10Players() { }, databaseExecutor); } + /** + * Returns [rank, totalEligiblePlayers]. rank=-1 if player is not eligible. + */ + public CompletableFuture getPlayerRankInfo(UUID uuid) { + return CompletableFuture.supplyAsync(() -> { + int[] result = new int[]{-1, 0}; + Connection conn = null; + boolean shouldClose = isMySQL; + + String sortBy = mapSortColumn(plugin.getConfigManager().getLeaderboardSortBy()); + String tiebreaker = mapSortColumn(plugin.getConfigManager().getLeaderboardTiebreaker()); + int minLevel = plugin.getConfigManager().getLeaderboardMinLevel(); + + try { + conn = getConnection(); + + // Get player's own values + int playerLevel = 0, playerSortVal = 0, playerTieVal = 0; + boolean excluded = false; + boolean found = false; + try (PreparedStatement ps = conn.prepareStatement( + "SELECT level, xp, total_levels, battle_coins, exclude_from_top FROM " + prefix + "players WHERE uuid = ?")) { + ps.setString(1, uuid.toString()); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + found = true; + playerLevel = rs.getInt("level"); + playerSortVal = rs.getInt(sortBy); + playerTieVal = rs.getInt(tiebreaker); + excluded = rs.getInt("exclude_from_top") == 1; + } + } + } + + // Get total eligible players + try (PreparedStatement ps = conn.prepareStatement( + "SELECT COUNT(*) FROM " + prefix + "players WHERE exclude_from_top = 0 AND level >= ?")) { + ps.setInt(1, minLevel); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) result[1] = rs.getInt(1); + } + } + + if (!found || excluded || playerLevel < minLevel) { + return result; + } + + // Count players ranked higher + String rankQuery = "SELECT COUNT(*) FROM " + prefix + "players WHERE exclude_from_top = 0 AND level >= ? AND (" + + sortBy + " > ? OR (" + sortBy + " = ? AND " + tiebreaker + " > ?))"; + try (PreparedStatement ps = conn.prepareStatement(rankQuery)) { + ps.setInt(1, minLevel); + ps.setInt(2, playerSortVal); + ps.setInt(3, playerSortVal); + ps.setInt(4, playerTieVal); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) result[0] = rs.getInt(1) + 1; + } + } + } catch (SQLException e) { + e.printStackTrace(); + } finally { + if (shouldClose && conn != null) { + try { conn.close(); } catch (SQLException e) {} + } + } + return result; + }, databaseExecutor); + } + public CompletableFuture saveSeasonData(LocalDateTime endDate, LocalDateTime missionResetTime, String currentMissionDate) { return CompletableFuture.runAsync(() -> { Connection conn = null; @@ -670,12 +759,17 @@ public CompletableFuture resetSeason() { conn = getConnection(); try (Statement stmt = conn.createStatement()) { boolean resetCoins = plugin.getConfigManager().isResetCoinsOnSeasonEnd(); + boolean resetTotalLevels = plugin.getConfigManager().isResetTotalLevelsOnSeasonEnd(); + StringBuilder sql = new StringBuilder("UPDATE " + prefix + "players SET xp = 0, level = 1, claimed_free = '', claimed_premium = '', has_premium = 0, last_daily_reward = 0"); if (resetCoins) { - stmt.executeUpdate("UPDATE " + prefix + "players SET xp = 0, level = 1, claimed_free = '', claimed_premium = '', has_premium = 0, last_daily_reward = 0, battle_coins = 0"); - } else { - stmt.executeUpdate("UPDATE " + prefix + "players SET xp = 0, level = 1, claimed_free = '', claimed_premium = '', has_premium = 0, last_daily_reward = 0"); + sql.append(", battle_coins = 0"); } + if (resetTotalLevels) { + sql.append(", total_levels = 0"); + } + stmt.executeUpdate(sql.toString()); + stmt.executeUpdate("DELETE FROM " + prefix + "missions"); stmt.executeUpdate("DELETE FROM " + prefix + "daily_missions"); } diff --git a/src/main/java/com/Lino/battlePass/placeholders/BattlePassExpansion.java b/src/main/java/com/Lino/battlePass/placeholders/BattlePassExpansion.java index a3a8c75..f763a1c 100644 --- a/src/main/java/com/Lino/battlePass/placeholders/BattlePassExpansion.java +++ b/src/main/java/com/Lino/battlePass/placeholders/BattlePassExpansion.java @@ -160,15 +160,10 @@ public String onRequest(OfflinePlayer player, String identifier) { } private String getPlayerRank(UUID uuid) { - CompletableFuture> future = plugin.getDatabaseManager().getTop10Players(); - List topPlayers = future.join(); - - for (int i = 0; i < topPlayers.size(); i++) { - if (topPlayers.get(i).uuid.equals(uuid)) { - return String.valueOf(i + 1); - } + int[] rankInfo = plugin.getDatabaseManager().getPlayerRankInfo(uuid).join(); + if (rankInfo[0] > 0) { + return String.valueOf(rankInfo[0]); } - return "Unranked"; } @@ -207,7 +202,8 @@ private String getTopPlayerPlaceholder(String identifier) { String[] parts = identifier.split("_"); int position = Integer.parseInt(parts[0]); - if (position < 1 || position > 10) { + int maxPosition = plugin.getConfigManager().getLeaderboardSize(); + if (position < 1 || position > maxPosition) { return ""; } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 1b177b4..a7f2566 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -57,6 +57,14 @@ battle-coins: 9: 2 10: 1 +# Leaderboard configuration +leaderboard: + sort-by: level + tiebreaker: xp + min-level: 2 + size: 20 + reset-total-levels-on-season-end: true + # Shop configuration shop: enabled: true