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]