diff --git a/pom.xml b/pom.xml index 2181259..abe2c80 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.retromc arenafighter - 1.0.8 + 1.0.9 jar diff --git a/src/main/java/org/garsooon/arenafighter/Commands/FightCommand.java b/src/main/java/org/garsooon/arenafighter/Commands/FightCommand.java index 5e120c4..f69ce13 100644 --- a/src/main/java/org/garsooon/arenafighter/Commands/FightCommand.java +++ b/src/main/java/org/garsooon/arenafighter/Commands/FightCommand.java @@ -7,6 +7,7 @@ import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; +import org.garsooon.arenafighter.Data.Bet; import org.garsooon.arenafighter.Data.Challenge; import org.garsooon.arenafighter.Economy.Method; import org.garsooon.arenafighter.Economy.Methods; @@ -14,6 +15,7 @@ import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Map; public class FightCommand implements CommandExecutor { @@ -65,6 +67,20 @@ public boolean onCommand(CommandSender sender, Command command, String label, St } return handleCancel(player); + case "leaderboard": + if (!player.hasPermission("arenafighter.fight")) { + player.sendMessage(ChatColor.RED + "You do not have permission to see the leaderboard."); + return true; + } + return handleLeaderboard(player); + + case "stats": + if (!player.hasPermission("arenafighter.fight")) { + player.sendMessage(ChatColor.RED + "You do not have permission to see stats."); + return true; + } + return handleStats(player, args); + case "help": default: sendHelp(player); @@ -124,6 +140,7 @@ private boolean handleChallenge(Player challenger, String[] args) { if (args.length >= 3) { try { wagerAmount = Double.parseDouble(args[2]); + wagerAmount = Bet.roundDownTwoDecimals(wagerAmount); // <-- round down here if (wagerAmount < 0) { challenger.sendMessage(ChatColor.RED + "Wager amount cannot be negative."); return true; @@ -223,6 +240,7 @@ private boolean handleAccept(Player accepter, String[] args) { } double wager = challenge.getWagerAmount(); + double truncatedWager = Bet.roundDownTwoDecimals(wager); // Balance check for wagers if (wager > 0) { @@ -239,7 +257,7 @@ private boolean handleAccept(Player accepter, String[] args) { // Broadcast fight accept with wager message when there is a wager String line1 = ChatColor.GOLD + accepter.getName() + ChatColor.YELLOW + " has accepted " + ChatColor.GOLD + challenger.getName() + ChatColor.YELLOW + "'s duel request"; - String line2 = (wager > 0 ? ChatColor.YELLOW + " with a wager of " + ChatColor.GREEN + wager : "") + ChatColor.YELLOW + "!"; + String line2 = (truncatedWager > 0 ? ChatColor.YELLOW + " with a wager of " + ChatColor.GREEN + truncatedWager : "") + ChatColor.YELLOW + "!"; String line3 = ChatColor.YELLOW + "The fight will begin in 30 seconds..."; Bukkit.broadcastMessage(line1); @@ -264,6 +282,42 @@ private boolean handleAccept(Player accepter, String[] args) { return true; } + private boolean handleLeaderboard(Player player) { + Map topPlayers = fightManager.getTopPlayersByWins(); + + if (topPlayers.isEmpty()) { + player.sendMessage(ChatColor.RED + "No leaderboard data found."); + return true; + } + + player.sendMessage(ChatColor.GOLD + "=== ArenaFighter Leaderboard ==="); + int rank = 1; + for (Map.Entry entry : topPlayers.entrySet()) { + player.sendMessage(ChatColor.YELLOW + "#" + rank + " " + ChatColor.AQUA + entry.getKey() + + ChatColor.WHITE + " - " + ChatColor.GREEN + entry.getValue() + " wins"); + rank++; + } + + return true; + } + + private boolean handleStats(Player player, String[] args) { + String targetName; + + if (args.length == 1) { + // Show own stats + targetName = player.getName(); + } else { + targetName = args[1]; + } + + List statsLines = fightManager.getPlayerStatsByName(targetName); + for (String line : statsLines) { + player.sendMessage(line); + } + return true; + } + private boolean handleCancel(Player player) { if (!fightManager.isInFight(player)) { player.sendMessage(ChatColor.RED + "You're not in a fight."); @@ -280,6 +334,8 @@ private void sendHelp(Player player) { player.sendMessage(ChatColor.YELLOW + "/fight challenge " + ChatColor.WHITE + " - Challenge a player"); player.sendMessage(ChatColor.YELLOW + "/fight accept " + ChatColor.WHITE + " - Accept a challenge"); player.sendMessage(ChatColor.YELLOW + "/fight cancel" + ChatColor.WHITE + " - Cancel current fight"); + player.sendMessage(ChatColor.YELLOW + "/fight leaderboard" + ChatColor.WHITE + " - Show top 10 fighters"); + player.sendMessage(ChatColor.YELLOW + "/fight stats " + ChatColor.WHITE + " - Show player stats"); player.sendMessage(ChatColor.YELLOW + "/fight help" + ChatColor.WHITE + " - Show help"); player.sendMessage(ChatColor.YELLOW + "/spectate " + ChatColor.WHITE + " - Start spectating"); } diff --git a/src/main/java/org/garsooon/arenafighter/Commands/SpectateBetCommand.java b/src/main/java/org/garsooon/arenafighter/Commands/SpectateBetCommand.java index 5092945..b867dc1 100644 --- a/src/main/java/org/garsooon/arenafighter/Commands/SpectateBetCommand.java +++ b/src/main/java/org/garsooon/arenafighter/Commands/SpectateBetCommand.java @@ -9,6 +9,7 @@ import org.garsooon.arenafighter.Fight.Fight; import org.garsooon.arenafighter.Economy.Methods; import org.garsooon.arenafighter.Economy.Method; +import org.garsooon.arenafighter.Data.Bet; public class SpectateBetCommand implements CommandExecutor { @@ -29,7 +30,7 @@ public boolean onCommand(CommandSender sender, Command command, String label, St player.sendMessage(ChatColor.RED + "Economy plugin not detected. Betting is disabled."); return true; } - + if (args.length != 2) { player.sendMessage(ChatColor.RED + "Usage: /bet "); return true; @@ -40,6 +41,11 @@ public boolean onCommand(CommandSender sender, Command command, String label, St try { amount = Double.parseDouble(args[1]); + amount = Bet.roundDownTwoDecimals(amount); + if (amount <= 0) { + player.sendMessage(ChatColor.RED + "Bet amount must be greater than 0."); + return true; + } } catch (NumberFormatException e) { player.sendMessage(ChatColor.RED + "Invalid amount."); return true; @@ -58,7 +64,6 @@ public boolean onCommand(CommandSender sender, Command command, String label, St return true; } - //FUTURE USE if (fight.hasStarted()) { player.sendMessage(ChatColor.RED + "You cannot bet, the fight has already started."); return true; @@ -69,13 +74,13 @@ public boolean onCommand(CommandSender sender, Command command, String label, St return true; } - if (!fight.placeBet(player.getName(), fighterName, amount)) { - player.sendMessage(ChatColor.RED + "You have already placed a bet."); + if (fight.containsPlayer(player)) { + player.sendMessage(ChatColor.RED + "You cannot bet on a fight you are participating in."); return true; } - if (fight.containsPlayer(player)) { - player.sendMessage(ChatColor.RED + "You cannot bet on a fight you are participating in."); + if (!fight.placeBet(player.getName(), fighterName, amount)) { + player.sendMessage(ChatColor.RED + "You have already placed a bet."); return true; } @@ -83,8 +88,13 @@ public boolean onCommand(CommandSender sender, Command command, String label, St player.sendMessage(ChatColor.RED + "Failed to withdraw funds."); return true; } + String msg = ChatColor.AQUA + player.getName() + + ChatColor.YELLOW + " placed a bet of " + + ChatColor.GOLD + amount + + ChatColor.YELLOW + " on " + + ChatColor.GREEN + fighterName + ChatColor.YELLOW + "!"; + player.getServer().broadcastMessage(msg); - player.sendMessage(ChatColor.GREEN + "You placed a bet of " + amount + " on " + fighterName + "."); return true; } } diff --git a/src/main/java/org/garsooon/arenafighter/Data/Bet.java b/src/main/java/org/garsooon/arenafighter/Data/Bet.java index fa000eb..30f8d10 100644 --- a/src/main/java/org/garsooon/arenafighter/Data/Bet.java +++ b/src/main/java/org/garsooon/arenafighter/Data/Bet.java @@ -1,5 +1,8 @@ package org.garsooon.arenafighter.Data; +import java.math.BigDecimal; +import java.math.RoundingMode; + public class Bet { private final String spectator; private final String fighter; @@ -23,4 +26,10 @@ public String getFighter() { public double getAmount() { return amount; } + + public static double roundDownTwoDecimals(double value) { + BigDecimal bd = BigDecimal.valueOf(value); + bd = bd.setScale(2, RoundingMode.DOWN); // truncate to 2 decimal places, no rounding up + return bd.doubleValue(); + } } diff --git a/src/main/java/org/garsooon/arenafighter/Data/PlayerDataManager.java b/src/main/java/org/garsooon/arenafighter/Data/PlayerDataManager.java new file mode 100644 index 0000000..b07a2a5 --- /dev/null +++ b/src/main/java/org/garsooon/arenafighter/Data/PlayerDataManager.java @@ -0,0 +1,43 @@ +package org.garsooon.arenafighter.Data; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +// Utility class to try and stop the feature bloat in FightManager +public class PlayerDataManager { + private static final Map uuidToUsername = new HashMap<>(); + private static final Map usernameToUUID = new HashMap<>(); + + public static void setPlayer(UUID uuid, String username) { + if (uuid == null || username == null) return; + + uuidToUsername.put(uuid, username); + usernameToUUID.put(username.toLowerCase(), uuid); + } + + public static String getUsername(UUID uuid) { + return uuidToUsername.get(uuid); + } + + public static UUID getUUID(String username) { + return usernameToUUID.get(username.toLowerCase()); + } + + public static boolean hasPlayer(UUID uuid) { + return uuidToUsername.containsKey(uuid); + } + + public static boolean hasUsername(String name) { + return usernameToUUID.containsKey(name.toLowerCase()); + } + + public static Map getAllUUIDToUsername() { + return new HashMap<>(uuidToUsername); + } + + public static void clear() { + uuidToUsername.clear(); + usernameToUUID.clear(); + } +} diff --git a/src/main/java/org/garsooon/arenafighter/Fight/FightManager.java b/src/main/java/org/garsooon/arenafighter/Fight/FightManager.java index f5ed853..2238354 100644 --- a/src/main/java/org/garsooon/arenafighter/Fight/FightManager.java +++ b/src/main/java/org/garsooon/arenafighter/Fight/FightManager.java @@ -2,6 +2,7 @@ import net.minecraft.server.EntityHuman; import net.minecraft.server.EntityPlayer; +import com.legacyminecraft.poseidon.util.*; import org.bukkit.craftbukkit.inventory.CraftItemStack; import org.bukkit.inventory.ItemStack; import org.bukkit.Bukkit; @@ -12,14 +13,18 @@ import org.garsooon.arenafighter.Arena.Arena; import org.garsooon.arenafighter.Arena.ArenaFighter; import org.garsooon.arenafighter.Arena.ArenaManager; +import org.garsooon.arenafighter.Data.Bet; import org.garsooon.arenafighter.Data.Challenge; +import org.garsooon.arenafighter.Data.PlayerDataManager; import org.garsooon.arenafighter.Economy.Method; import org.garsooon.arenafighter.Economy.Methods; import org.yaml.snakeyaml.Yaml; import java.io.File; import java.io.FileInputStream; +import java.io.FileWriter; import java.util.*; +import java.util.stream.Collectors; public class FightManager { @@ -35,6 +40,9 @@ public class FightManager { private final Map punishments = new HashMap<>(); private final long punishmentDurationMillis; private final Method economy; + private final File statsFile; + private Map stats; // Key: UUID.toString() + private final PlayerDataManager playerDataManager = new PlayerDataManager(); public FightManager(ArenaFighter plugin, ArenaManager arenaManager, Method economy) { this.plugin = plugin; @@ -45,6 +53,8 @@ public FightManager(ArenaFighter plugin, ArenaManager arenaManager, Method econo this.pendingChallenges = new HashMap<>(); this.spectatorOriginalLocations = new HashMap<>(); this.punishmentDurationMillis = loadPunishmentDuration(plugin.getDataFolder()); + this.statsFile = new File(plugin.getDataFolder(), "stats.yml"); + loadStats(); } private long loadPunishmentDuration(File dataFolder) { @@ -156,6 +166,8 @@ private void ejectPlayer(Player player) { } public boolean startFight(Player player1, Player player2, double wager) { + double truncatedWager = Bet.roundDownTwoDecimals(wager); + if (isInFight(player1) || isInFight(player2)) return false; // Final balance check before fight begins @@ -241,7 +253,7 @@ public boolean startFight(Player player1, Player player2, double wager) { ChatColor.GREEN + arena.getName(); if (wager > 0) { - message += ChatColor.YELLOW + " with a wager of " + ChatColor.GOLD + wager; + message += ChatColor.YELLOW + " with a wager of " + ChatColor.GOLD + truncatedWager; } fight.clearBets(); @@ -302,12 +314,16 @@ public void endFight(Player winner, Player loser) { double wager = fight.getWager(); if (wager > 0) { + double truncatedWager = Bet.roundDownTwoDecimals(wager); deposit(winner, wager * 2); winner.sendMessage(ChatColor.GREEN + "You have won " + (wager * 2) + " from the wager!"); } fight.resolveBets(winner.getName()); + incrementStat(winner.getUniqueId(), "wins", winner.getName()); + incrementStat(loser.getUniqueId(), "losses", loser.getName()); + String message = ChatColor.GOLD + winner.getName() + ChatColor.YELLOW + " has defeated " + ChatColor.RED + loser.getName() + @@ -315,7 +331,8 @@ public void endFight(Player winner, Player loser) { ChatColor.GREEN + fight.getArena().getName(); if (wager > 0) { - message += ChatColor.YELLOW + " and won a wager of " + ChatColor.GOLD + (wager * 2); + double truncatedWager = Bet.roundDownTwoDecimals(wager * 2); + message += ChatColor.YELLOW + " and won a wager of " + ChatColor.GOLD + truncatedWager; } message += ChatColor.YELLOW + "!"; @@ -773,7 +790,7 @@ public boolean isSpectating(Player player) { return spectatorOriginalLocations.containsKey(player.getUniqueId()); } - //ECO Wager per Challenge data + //ECO Wager per Challenge data start private static class FightChallenge { private final UUID challengerId; private final UUID targetId; @@ -797,4 +814,170 @@ public double getWager() { return wager; } } + //ECO Wager per Challenge data end + + //Stat and leaderboard functions start + public static int getInt(Object obj) { + if (obj == null) return 0; + if (obj instanceof Integer) return (Integer) obj; + if (obj instanceof Number) return ((Number) obj).intValue(); + try { + return Integer.parseInt(obj.toString()); + } catch (NumberFormatException e) { + return 0; + } + } + + @SuppressWarnings("unchecked") + private void loadStats() { + stats = new HashMap<>(); + if (!statsFile.exists()) return; + + try (FileInputStream fis = new FileInputStream(statsFile)) { + Yaml yaml = new Yaml(); + Object data = yaml.load(fis); + if (data instanceof Map) { + stats = (Map) data; + } + } catch (Exception e) { + plugin.getServer().getLogger().warning("Failed to load stats.yml: " + e.getMessage()); + } + } + + private void saveStats() { + try (FileWriter writer = new FileWriter(statsFile)) { + Yaml yaml = new Yaml(); + yaml.dump(stats, writer); + } catch (Exception e) { + plugin.getServer().getLogger().warning("Failed to save stats.yml: " + e.getMessage()); + } + } + + //I LOVE SUPPRESS WARNINGS I LOVE SUPPRESS WARNINGS I LOVE SUPPRESS WARNINGS, GOD I LOVE SUPPRESS WARNINGS + @SuppressWarnings("unchecked") + public void incrementStat(UUID uuid, String key, String username) { + String uuidKey = uuid.toString(); + + Map playerStats = (Map) stats.get(uuidKey); + if (playerStats == null) { + playerStats = new HashMap<>(); + stats.put(uuidKey, playerStats); + } + + playerStats.put("username", username); + + PlayerDataManager.setPlayer(uuid, username); + + int current = 0; + Object val = playerStats.get(key); + if (val instanceof Number) { + current = ((Number) val).intValue(); + } + + playerStats.put(key, current + 1); + saveStats(); + } + + @SuppressWarnings("unchecked") + public Map getTopPlayersByWins() { + Map winsMap = new HashMap<>(); + + for (Map.Entry entry : stats.entrySet()) { + String uuidStr = entry.getKey(); + Object value = entry.getValue(); + + if (value instanceof Map) { + Map playerStats = (Map) value; + Object winsObj = playerStats.get("wins"); + String storedName = (String) playerStats.get("username"); + + if (winsObj instanceof Number && storedName != null) { + winsMap.put(storedName, ((Number) winsObj).intValue()); + } + } + } + + return winsMap.entrySet().stream() + .sorted(Map.Entry.comparingByValue().reversed()) + .limit(10) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (e1, e2) -> e1, + LinkedHashMap::new + )); + } + + @SuppressWarnings("unchecked") + public List getPlayerStats(UUID uuid) { + Object raw = stats.get(uuid.toString()); + + if (!(raw instanceof Map)) { + return Collections.singletonList(ChatColor.RED + "No stats found for " + uuid + "."); + } + + // Checked type, safe to cast. In other words I LOVE SUPPRESS WARNING!!!11! + Map playerStats = (Map) raw; + + int wins = getInt(playerStats.get("wins")); + int losses = getInt(playerStats.get("losses")); + + // Resolve username from PlayerDataManager or fallback to a UUID string + String displayName = PlayerDataManager.getUsername(uuid); + if (displayName == null) { + displayName = uuid.toString(); + } + + List lines = new ArrayList<>(); + lines.add(ChatColor.GOLD + "=== Stats for " + ChatColor.AQUA + displayName + ChatColor.GOLD + " ==="); + lines.add(ChatColor.YELLOW + "Wins: " + ChatColor.GREEN + wins); + lines.add(ChatColor.YELLOW + "Losses: " + ChatColor.RED + losses); + + return lines; + } + + // For /fight stats + @SuppressWarnings("unchecked") + public List getPlayerStatsByName(String name) { + UUID uuid = null; + + // Search for UUID based on stored username in the raw YAML map + for (String key : stats.keySet()) { + Object raw = stats.get(key); + if (raw instanceof Map) { + Map entry = (Map) raw; + String storedName = (String) entry.get("username"); + if (storedName != null && storedName.equalsIgnoreCase(name)) { + try { + uuid = UUID.fromString(key); + break; + } catch (IllegalArgumentException ignored) {} + } + } + } + + if (uuid == null) { + return Collections.singletonList(ChatColor.RED + "No stats found for " + name + "."); + } + + Object raw = stats.get(uuid.toString()); + if (!(raw instanceof Map)) { + return Collections.singletonList(ChatColor.RED + "No stats found for " + name + "."); + } + + Map playerStats = (Map) raw; + int wins = getInt(playerStats.get("wins")); + int losses = getInt(playerStats.get("losses")); + String displayName = (String) playerStats.get("username"); + if (displayName == null) displayName = uuid.toString().substring(0, 8); + + List lines = new ArrayList<>(); + lines.add(ChatColor.GOLD + "=== Stats for " + ChatColor.AQUA + displayName + ChatColor.GOLD + " ==="); + lines.add(ChatColor.YELLOW + "Wins: " + ChatColor.GREEN + wins); + lines.add(ChatColor.YELLOW + "Losses: " + ChatColor.RED + losses); + + return lines; + } + // Stat and leaderboard functions end + } diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 246002e..511775a 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: ArenaFighter -version: 1.0.8 +version: 1.0.9 description: A plugin that allows players to fight in arenas and teleports them back after fights conclude author: Garsooon main: org.garsooon.arenafighter.Arena.ArenaFighter @@ -10,7 +10,7 @@ softdepend: [Essentials, Fundamentals, ZCore, MultiCurrency, iCo4, iCo5, iCo6, B commands: fight: description: Challenge other players to arena fights - usage: /fight [player] [wager] + usage: /fight [player] [wager] arena: description: Manage arena configurations usage: /arena [args]