From 954420ffd720b1394d053a0f06f9589f74777f71 Mon Sep 17 00:00:00 2001 From: Thatsaphorn Atchariyaphap Date: Mon, 1 Sep 2025 21:19:38 +0200 Subject: [PATCH] Death Counter --- .gitignore | 113 ++++++++ pom.xml | 105 +++++++ .../java/dev/tatsi/reloadmc/smp/ReloadMC.java | 39 +++ .../smp/command/DeathStatsCommand.java | 95 +++++++ .../smp/listener/PlayerDeathListener.java | 51 ++++ .../smp/manager/DeathCounterManager.java | 95 +++++++ .../tatsi/reloadmc/smp/model/DeathRecord.java | 53 ++++ .../reloadmc/smp/model/PlayerDeathData.java | 51 ++++ src/main/resources/plugin.yml | 14 + .../smp/manager/DeathCounterManagerTest.java | 257 ++++++++++++++++++ 10 files changed, 873 insertions(+) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/dev/tatsi/reloadmc/smp/ReloadMC.java create mode 100644 src/main/java/dev/tatsi/reloadmc/smp/command/DeathStatsCommand.java create mode 100644 src/main/java/dev/tatsi/reloadmc/smp/listener/PlayerDeathListener.java create mode 100644 src/main/java/dev/tatsi/reloadmc/smp/manager/DeathCounterManager.java create mode 100644 src/main/java/dev/tatsi/reloadmc/smp/model/DeathRecord.java create mode 100644 src/main/java/dev/tatsi/reloadmc/smp/model/PlayerDeathData.java create mode 100644 src/main/resources/plugin.yml create mode 100644 src/test/java/dev/tatsi/reloadmc/smp/manager/DeathCounterManagerTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4788b4b --- /dev/null +++ b/.gitignore @@ -0,0 +1,113 @@ +# User-specific stuff +.idea/ + +*.iml +*.ipr +*.iws + +# IntelliJ +out/ + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +target/ + +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next + +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar +.flattened-pom.xml + +# Common working directory +run/ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..a7ac1da --- /dev/null +++ b/pom.xml @@ -0,0 +1,105 @@ + + + 4.0.0 + + dev.tatsi.reloadmc.smp + reloadmc_smp + 1.0.0 + jar + + reloadmc_smp + + + 21 + UTF-8 + + + + clean package + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + ${java.version} + ${java.version} + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.3 + + + package + + shade + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + + false + + + + + + src/main/resources + true + + + + + + + papermc-repo + https://repo.papermc.io/repository/maven-public/ + + + jitpack.io + https://jitpack.io + + + + + + io.papermc.paper + paper-api + 1.21.8-R0.1-SNAPSHOT + provided + + + com.google.code.gson + gson + 2.10.1 + + + + + org.junit.jupiter + junit-jupiter + 5.10.0 + test + + + org.mockito + mockito-core + 5.5.0 + test + + + org.mockito + mockito-junit-jupiter + 5.5.0 + test + + + diff --git a/src/main/java/dev/tatsi/reloadmc/smp/ReloadMC.java b/src/main/java/dev/tatsi/reloadmc/smp/ReloadMC.java new file mode 100644 index 0000000..2601dba --- /dev/null +++ b/src/main/java/dev/tatsi/reloadmc/smp/ReloadMC.java @@ -0,0 +1,39 @@ +package dev.tatsi.reloadmc.smp; + +import dev.tatsi.reloadmc.smp.command.DeathStatsCommand; +import dev.tatsi.reloadmc.smp.listener.PlayerDeathListener; +import dev.tatsi.reloadmc.smp.manager.DeathCounterManager; +import org.bukkit.plugin.java.JavaPlugin; + +public final class ReloadMC extends JavaPlugin { + private DeathCounterManager deathCounterManager; + + @Override + public void onEnable() { + // Initialize death counter manager + deathCounterManager = new DeathCounterManager(this); + + // Register event listeners + getServer().getPluginManager().registerEvents(new PlayerDeathListener(deathCounterManager), this); + + // Register commands + getCommand("deathstats").setExecutor(new DeathStatsCommand(deathCounterManager)); + + getLogger().info("ReloadMC SMP Plugin has been enabled!"); + getLogger().info("Death counter system is now active."); + } + + @Override + public void onDisable() { + // Save death counter data on shutdown + if (deathCounterManager != null) { + deathCounterManager.shutdown(); + } + + getLogger().info("ReloadMC SMP Plugin has been disabled!"); + } + + public DeathCounterManager getDeathCounterManager() { + return deathCounterManager; + } +} diff --git a/src/main/java/dev/tatsi/reloadmc/smp/command/DeathStatsCommand.java b/src/main/java/dev/tatsi/reloadmc/smp/command/DeathStatsCommand.java new file mode 100644 index 0000000..9ac170a --- /dev/null +++ b/src/main/java/dev/tatsi/reloadmc/smp/command/DeathStatsCommand.java @@ -0,0 +1,95 @@ +package dev.tatsi.reloadmc.smp.command; + +import dev.tatsi.reloadmc.smp.manager.DeathCounterManager; +import dev.tatsi.reloadmc.smp.model.DeathRecord; +import dev.tatsi.reloadmc.smp.model.PlayerDeathData; +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.List; +import java.util.UUID; + +public class DeathStatsCommand implements CommandExecutor { + private final DeathCounterManager deathCounterManager; + + public DeathStatsCommand(DeathCounterManager deathCounterManager) { + this.deathCounterManager = deathCounterManager; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (args.length == 0) { + // Show own stats if player, or usage if console + if (sender instanceof Player) { + showPlayerStats(sender, (Player) sender); + } else { + sender.sendMessage("§cUsage: /deathstats "); + } + return true; + } + + // Show stats for specified player + String targetPlayerName = args[0]; + Player targetPlayer = Bukkit.getPlayer(targetPlayerName); + + if (targetPlayer == null) { + // Try to find offline player data + UUID targetUuid = null; + for (PlayerDeathData data : deathCounterManager.getAllPlayerData().values()) { + if (data.getPlayerName().equalsIgnoreCase(targetPlayerName)) { + targetUuid = data.getPlayerUuid(); + break; + } + } + + if (targetUuid == null) { + sender.sendMessage("§cPlayer '" + targetPlayerName + "' not found or has no death records."); + return true; + } + + showPlayerStatsByUuid(sender, targetUuid, targetPlayerName); + } else { + showPlayerStats(sender, targetPlayer); + } + + return true; + } + + private void showPlayerStats(CommandSender sender, Player player) { + showPlayerStatsByUuid(sender, player.getUniqueId(), player.getName()); + } + + private void showPlayerStatsByUuid(CommandSender sender, UUID playerUuid, String playerName) { + PlayerDeathData deathData = deathCounterManager.getPlayerDeathData(playerUuid); + + if (deathData == null || deathData.getDeathCount() == 0) { + sender.sendMessage("§a" + playerName + " has no recorded deaths. Lucky!"); + return; + } + + sender.sendMessage("§6=== Death Statistics for " + playerName + " ==="); + sender.sendMessage("§eTotalDeaths: §c" + deathData.getDeathCount()); + + List deaths = deathData.getDeaths(); + if (deaths.size() > 0) { + sender.sendMessage("§eRecent Deaths:"); + int showCount = Math.min(5, deaths.size()); // Show last 5 deaths + for (int i = deaths.size() - showCount; i < deaths.size(); i++) { + DeathRecord death = deaths.get(i); + sender.sendMessage(String.format("§7- §f%s §7at §e%.1f, %.1f, %.1f §7in §b%s §7(%s)", + death.getDeathReason(), + death.getX(), death.getY(), death.getZ(), + death.getWorld(), + death.getTimestamp().replace("T", " ") + )); + } + + if (deaths.size() > 5) { + sender.sendMessage("§7... and " + (deaths.size() - 5) + " more deaths."); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/tatsi/reloadmc/smp/listener/PlayerDeathListener.java b/src/main/java/dev/tatsi/reloadmc/smp/listener/PlayerDeathListener.java new file mode 100644 index 0000000..8c0e807 --- /dev/null +++ b/src/main/java/dev/tatsi/reloadmc/smp/listener/PlayerDeathListener.java @@ -0,0 +1,51 @@ +package dev.tatsi.reloadmc.smp.listener; + +import dev.tatsi.reloadmc.smp.manager.DeathCounterManager; +import dev.tatsi.reloadmc.smp.model.DeathRecord; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.PlayerDeathEvent; + +public class PlayerDeathListener implements Listener { + private final DeathCounterManager deathCounterManager; + + public PlayerDeathListener(DeathCounterManager deathCounterManager) { + this.deathCounterManager = deathCounterManager; + } + + @EventHandler + public void onPlayerDeath(PlayerDeathEvent event) { + Player player = event.getEntity(); + Location deathLocation = player.getLocation(); + + // Get death reason from the death message, or use a default if null + String deathReason = event.getDeathMessage(); + if (deathReason == null || deathReason.trim().isEmpty()) { + deathReason = "Unknown cause"; + } + + // Create death record with location and reason + DeathRecord deathRecord = new DeathRecord( + deathLocation.getX(), + deathLocation.getY(), + deathLocation.getZ(), + deathLocation.getWorld().getName(), + deathReason + ); + + // Record the death + deathCounterManager.addDeath( + player.getUniqueId(), + player.getName(), + deathRecord + ); + + // Optional: Send a message to the player about their death count + int deathCount = deathCounterManager.getDeathCount(player.getUniqueId()); + player.sendMessage(String.format("§cYou have died %d time(s). Death recorded at %.1f, %.1f, %.1f in %s", + deathCount, deathLocation.getX(), deathLocation.getY(), deathLocation.getZ(), + deathLocation.getWorld().getName())); + } +} \ No newline at end of file diff --git a/src/main/java/dev/tatsi/reloadmc/smp/manager/DeathCounterManager.java b/src/main/java/dev/tatsi/reloadmc/smp/manager/DeathCounterManager.java new file mode 100644 index 0000000..87740dc --- /dev/null +++ b/src/main/java/dev/tatsi/reloadmc/smp/manager/DeathCounterManager.java @@ -0,0 +1,95 @@ +package dev.tatsi.reloadmc.smp.manager; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import dev.tatsi.reloadmc.smp.model.DeathRecord; +import dev.tatsi.reloadmc.smp.model.PlayerDeathData; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.logging.Level; + +public class DeathCounterManager { + private final JavaPlugin plugin; + private final File dataFile; + private final Gson gson; + private final Map playerDeathData; + + public DeathCounterManager(JavaPlugin plugin) { + this.plugin = plugin; + this.dataFile = new File(plugin.getDataFolder(), "death_counter.json"); + this.gson = new GsonBuilder().setPrettyPrinting().create(); + this.playerDeathData = new HashMap<>(); + + // Create plugin data folder if it doesn't exist + if (!plugin.getDataFolder().exists()) { + plugin.getDataFolder().mkdirs(); + } + + loadData(); + } + + public void addDeath(UUID playerUuid, String playerName, DeathRecord deathRecord) { + PlayerDeathData data = playerDeathData.computeIfAbsent(playerUuid, + uuid -> new PlayerDeathData(uuid, playerName)); + + data.addDeath(deathRecord); + saveData(); + + plugin.getLogger().info(String.format("Recorded death for %s: %s", playerName, deathRecord)); + } + + public PlayerDeathData getPlayerDeathData(UUID playerUuid) { + return playerDeathData.get(playerUuid); + } + + public int getDeathCount(UUID playerUuid) { + PlayerDeathData data = playerDeathData.get(playerUuid); + return data != null ? data.getDeathCount() : 0; + } + + public Map getAllPlayerData() { + return new HashMap<>(playerDeathData); + } + + private void loadData() { + if (!dataFile.exists()) { + plugin.getLogger().info("Death counter data file not found, starting with empty data."); + return; + } + + try (FileReader reader = new FileReader(dataFile)) { + Type type = new TypeToken>() { + }.getType(); + Map loadedData = gson.fromJson(reader, type); + + if (loadedData != null) { + playerDeathData.putAll(loadedData); + plugin.getLogger().info(String.format("Loaded death data for %d players.", loadedData.size())); + } + } catch (IOException e) { + plugin.getLogger().log(Level.SEVERE, "Failed to load death counter data", e); + } + } + + private void saveData() { + try (FileWriter writer = new FileWriter(dataFile)) { + gson.toJson(playerDeathData, writer); + } catch (IOException e) { + plugin.getLogger().log(Level.SEVERE, "Failed to save death counter data", e); + } + } + + public void shutdown() { + saveData(); + plugin.getLogger().info("Death counter data saved on shutdown."); + } +} \ No newline at end of file diff --git a/src/main/java/dev/tatsi/reloadmc/smp/model/DeathRecord.java b/src/main/java/dev/tatsi/reloadmc/smp/model/DeathRecord.java new file mode 100644 index 0000000..e62810c --- /dev/null +++ b/src/main/java/dev/tatsi/reloadmc/smp/model/DeathRecord.java @@ -0,0 +1,53 @@ +package dev.tatsi.reloadmc.smp.model; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class DeathRecord { + private final double x; + private final double y; + private final double z; + private final String world; + private final String deathReason; + private final String timestamp; + + public DeathRecord(double x, double y, double z, String world, String deathReason) { + this.x = x; + this.y = y; + this.z = z; + this.world = world; + this.deathReason = deathReason; + this.timestamp = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); + } + + // Getters + public double getX() { + return x; + } + + public double getY() { + return y; + } + + public double getZ() { + return z; + } + + public String getWorld() { + return world; + } + + public String getDeathReason() { + return deathReason; + } + + public String getTimestamp() { + return timestamp; + } + + @Override + public String toString() { + return String.format("DeathRecord{x=%.2f, y=%.2f, z=%.2f, world='%s', reason='%s', time='%s'}", + x, y, z, world, deathReason, timestamp); + } +} \ No newline at end of file diff --git a/src/main/java/dev/tatsi/reloadmc/smp/model/PlayerDeathData.java b/src/main/java/dev/tatsi/reloadmc/smp/model/PlayerDeathData.java new file mode 100644 index 0000000..b2b7e8d --- /dev/null +++ b/src/main/java/dev/tatsi/reloadmc/smp/model/PlayerDeathData.java @@ -0,0 +1,51 @@ +package dev.tatsi.reloadmc.smp.model; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class PlayerDeathData { + private final UUID playerUuid; + private final String playerName; + private final List deaths; + + public PlayerDeathData(UUID playerUuid, String playerName) { + this.playerUuid = playerUuid; + this.playerName = playerName; + this.deaths = new ArrayList<>(); + } + + // Constructor for JSON deserialization + public PlayerDeathData(UUID playerUuid, String playerName, List deaths) { + this.playerUuid = playerUuid; + this.playerName = playerName; + this.deaths = deaths != null ? new ArrayList<>(deaths) : new ArrayList<>(); + } + + public void addDeath(DeathRecord deathRecord) { + deaths.add(deathRecord); + } + + public int getDeathCount() { + return deaths.size(); + } + + // Getters + public UUID getPlayerUuid() { + return playerUuid; + } + + public String getPlayerName() { + return playerName; + } + + public List getDeaths() { + return new ArrayList<>(deaths); // Return copy to prevent external modification + } + + @Override + public String toString() { + return String.format("PlayerDeathData{uuid=%s, name='%s', deathCount=%d}", + playerUuid, playerName, deaths.size()); + } +} \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..fdbaa9b --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,14 @@ +name: reloadmc_smp +version: '1.0.0' +main: dev.tatsi.reloadmc.smp.ReloadMC +api-version: '1.21' +prefix: CorePlugin +authors: [ Bummsa / Thatsaphorn Atchariyaphap ] +description: Custom Core Plugin for mini OGSquad +website: https://tatsi.dev + +commands: + deathstats: + description: View death statistics for yourself or another player + usage: /deathstats [player] + aliases: [deaths, deathcount] diff --git a/src/test/java/dev/tatsi/reloadmc/smp/manager/DeathCounterManagerTest.java b/src/test/java/dev/tatsi/reloadmc/smp/manager/DeathCounterManagerTest.java new file mode 100644 index 0000000..4efe279 --- /dev/null +++ b/src/test/java/dev/tatsi/reloadmc/smp/manager/DeathCounterManagerTest.java @@ -0,0 +1,257 @@ +package dev.tatsi.reloadmc.smp.manager; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import dev.tatsi.reloadmc.smp.model.DeathRecord; +import dev.tatsi.reloadmc.smp.model.PlayerDeathData; +import org.bukkit.plugin.java.JavaPlugin; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class DeathCounterManagerTest { + + @Mock + private JavaPlugin mockPlugin; + + @Mock + private Logger mockLogger; + + @TempDir + Path tempDir; + + private DeathCounterManager deathCounterManager; + private File dataFolder; + private UUID testPlayerUuid; + private String testPlayerName; + + @BeforeEach + void setUp() { + // Setup mock plugin + dataFolder = tempDir.toFile(); + when(mockPlugin.getDataFolder()).thenReturn(dataFolder); + when(mockPlugin.getLogger()).thenReturn(mockLogger); + + // Test data + testPlayerUuid = UUID.randomUUID(); + testPlayerName = "TestPlayer"; + + // Create DeathCounterManager instance + deathCounterManager = new DeathCounterManager(mockPlugin); + } + + @Test + void testConstructor_CreatesDataFolderIfNotExists() { + // Given: A new temp directory that doesn't exist + File newDataFolder = new File(tempDir.toFile(), "newFolder"); + when(mockPlugin.getDataFolder()).thenReturn(newDataFolder); + + // When: Creating a new DeathCounterManager + new DeathCounterManager(mockPlugin); + + // Then: The data folder should be created + assertTrue(newDataFolder.exists()); + } + + @Test + void testAddDeath_NewPlayer() { + // Given: A new death record + DeathRecord deathRecord = new DeathRecord(100.0, 64.0, 200.0, "world", "Fell from a high place"); + + // When: Adding a death for a new player + deathCounterManager.addDeath(testPlayerUuid, testPlayerName, deathRecord); + + // Then: Player data should be created and death should be recorded + PlayerDeathData playerData = deathCounterManager.getPlayerDeathData(testPlayerUuid); + assertNotNull(playerData); + assertEquals(testPlayerUuid, playerData.getPlayerUuid()); + assertEquals(testPlayerName, playerData.getPlayerName()); + assertEquals(1, playerData.getDeathCount()); + assertEquals(1, playerData.getDeaths().size()); + assertEquals(deathRecord.getDeathReason(), playerData.getDeaths().get(0).getDeathReason()); + } + + @Test + void testAddDeath_ExistingPlayer() { + // Given: A player with existing death data + DeathRecord firstDeath = new DeathRecord(100.0, 64.0, 200.0, "world", "Fell from a high place"); + DeathRecord secondDeath = new DeathRecord(150.0, 70.0, 250.0, "nether", "Burned to death"); + + // When: Adding multiple deaths for the same player + deathCounterManager.addDeath(testPlayerUuid, testPlayerName, firstDeath); + deathCounterManager.addDeath(testPlayerUuid, testPlayerName, secondDeath); + + // Then: Both deaths should be recorded + PlayerDeathData playerData = deathCounterManager.getPlayerDeathData(testPlayerUuid); + assertNotNull(playerData); + assertEquals(2, playerData.getDeathCount()); + assertEquals(2, playerData.getDeaths().size()); + } + + @Test + void testGetPlayerDeathData_NonExistentPlayer() { + // Given: A UUID that doesn't exist in the data + UUID nonExistentUuid = UUID.randomUUID(); + + // When: Getting player data for non-existent player + PlayerDeathData result = deathCounterManager.getPlayerDeathData(nonExistentUuid); + + // Then: Should return null + assertNull(result); + } + + @Test + void testGetDeathCount_ExistingPlayer() { + // Given: A player with death data + DeathRecord deathRecord = new DeathRecord(100.0, 64.0, 200.0, "world", "Fell from a high place"); + deathCounterManager.addDeath(testPlayerUuid, testPlayerName, deathRecord); + + // When: Getting death count + int deathCount = deathCounterManager.getDeathCount(testPlayerUuid); + + // Then: Should return correct count + assertEquals(1, deathCount); + } + + @Test + void testGetDeathCount_NonExistentPlayer() { + // Given: A UUID that doesn't exist in the data + UUID nonExistentUuid = UUID.randomUUID(); + + // When: Getting death count for non-existent player + int deathCount = deathCounterManager.getDeathCount(nonExistentUuid); + + // Then: Should return 0 + assertEquals(0, deathCount); + } + + @Test + void testGetAllPlayerData() { + // Given: Multiple players with death data + UUID player1Uuid = UUID.randomUUID(); + UUID player2Uuid = UUID.randomUUID(); + DeathRecord death1 = new DeathRecord(100.0, 64.0, 200.0, "world", "Fell from a high place"); + DeathRecord death2 = new DeathRecord(150.0, 70.0, 250.0, "nether", "Burned to death"); + + deathCounterManager.addDeath(player1Uuid, "Player1", death1); + deathCounterManager.addDeath(player2Uuid, "Player2", death2); + + // When: Getting all player data + Map allData = deathCounterManager.getAllPlayerData(); + + // Then: Should return copy of all data + assertEquals(2, allData.size()); + assertTrue(allData.containsKey(player1Uuid)); + assertTrue(allData.containsKey(player2Uuid)); + + // Verify it's a copy (modifying returned map shouldn't affect internal data) + allData.clear(); + assertEquals(2, deathCounterManager.getAllPlayerData().size()); + } + + @Test + void testDataPersistence_SaveAndLoad() throws IOException { + // Given: Some death data + DeathRecord deathRecord = new DeathRecord(100.0, 64.0, 200.0, "world", "Fell from a high place"); + deathCounterManager.addDeath(testPlayerUuid, testPlayerName, deathRecord); + + // When: Creating a new DeathCounterManager (which should load existing data) + DeathCounterManager newManager = new DeathCounterManager(mockPlugin); + + // Then: Data should be loaded correctly + PlayerDeathData loadedData = newManager.getPlayerDeathData(testPlayerUuid); + assertNotNull(loadedData); + assertEquals(testPlayerUuid, loadedData.getPlayerUuid()); + assertEquals(testPlayerName, loadedData.getPlayerName()); + assertEquals(1, loadedData.getDeathCount()); + } + + @Test + void testLoadData_EmptyFile() throws IOException { + // Given: An empty data file + File dataFile = new File(dataFolder, "death_counter.json"); + // Create an empty file + dataFile.createNewFile(); + assertTrue(dataFile.exists()); + + // When: Creating a new DeathCounterManager + DeathCounterManager newManager = new DeathCounterManager(mockPlugin); + + // Then: Should handle empty data gracefully + assertEquals(0, newManager.getAllPlayerData().size()); + } + + @Test + void testShutdown_SavesData() { + // Given: Some death data + DeathRecord deathRecord = new DeathRecord(100.0, 64.0, 200.0, "world", "Fell from a high place"); + deathCounterManager.addDeath(testPlayerUuid, testPlayerName, deathRecord); + + // When: Calling shutdown + deathCounterManager.shutdown(); + + // Then: Should log shutdown message + verify(mockLogger).info("Death counter data saved on shutdown."); + } + + @Test + void testDeathRecordDetails() { + // Given: A death record with specific details + double x = 123.45; + double y = 67.89; + double z = 234.56; + String world = "test_world"; + String reason = "Killed by Zombie"; + + DeathRecord deathRecord = new DeathRecord(x, y, z, world, reason); + deathCounterManager.addDeath(testPlayerUuid, testPlayerName, deathRecord); + + // When: Retrieving the death record + PlayerDeathData playerData = deathCounterManager.getPlayerDeathData(testPlayerUuid); + DeathRecord retrievedRecord = playerData.getDeaths().get(0); + + // Then: All details should be preserved + assertEquals(x, retrievedRecord.getX(), 0.001); + assertEquals(y, retrievedRecord.getY(), 0.001); + assertEquals(z, retrievedRecord.getZ(), 0.001); + assertEquals(world, retrievedRecord.getWorld()); + assertEquals(reason, retrievedRecord.getDeathReason()); + assertNotNull(retrievedRecord.getTimestamp()); + } + + @Test + void testMultipleDeathsPreserveOrder() { + // Given: Multiple deaths added in sequence + DeathRecord death1 = new DeathRecord(100.0, 64.0, 200.0, "world", "First death"); + DeathRecord death2 = new DeathRecord(150.0, 70.0, 250.0, "nether", "Second death"); + DeathRecord death3 = new DeathRecord(200.0, 80.0, 300.0, "end", "Third death"); + + // When: Adding deaths in sequence + deathCounterManager.addDeath(testPlayerUuid, testPlayerName, death1); + deathCounterManager.addDeath(testPlayerUuid, testPlayerName, death2); + deathCounterManager.addDeath(testPlayerUuid, testPlayerName, death3); + + // Then: Deaths should be preserved in order + PlayerDeathData playerData = deathCounterManager.getPlayerDeathData(testPlayerUuid); + assertEquals(3, playerData.getDeathCount()); + assertEquals("First death", playerData.getDeaths().get(0).getDeathReason()); + assertEquals("Second death", playerData.getDeaths().get(1).getDeathReason()); + assertEquals("Third death", playerData.getDeaths().get(2).getDeathReason()); + } +}