diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..c15b9fc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,69 @@ +name: "๐Ÿ› Bug Report" +description: Report a bug or unexpected behavior +labels: ["bug"] +body: + - type: input + id: summary + attributes: + label: Summary + description: One-line summary of the bug + validations: + required: true + - type: textarea + id: steps + attributes: + label: Steps to Reproduce + description: Step-by-step instructions to reproduce + placeholder: | + 1. Start server with plugin version X + 2. Run command /... + 3. Observe error + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected Behavior + description: What should happen? + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual Behavior + description: What actually happens? + validations: + required: true + - type: input + id: plugin_version + attributes: + label: Plugin Version + placeholder: "e.g. v1.2.10" + validations: + required: true + - type: input + id: server_version + attributes: + label: Server Version + description: Output of /version + placeholder: "e.g. Paper 1.21.4-123" + validations: + required: true + - type: input + id: java_version + attributes: + label: Java Version + placeholder: "e.g. Java 21.0.2" + validations: + required: true + - type: textarea + id: logs + attributes: + label: Logs / Stacktrace + description: Relevant console output (use code block) + render: shell + - type: textarea + id: extra + attributes: + label: Additional Context + description: Screenshots, configs, other plugins installed, etc. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..532c583 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: "๐Ÿ’ฌ Discord Support" + url: https://discord.gg/RYFamQzkcB + about: Get help or chat with the community + - name: "๐Ÿ“– Documentation" + url: https://chafficplugins.github.io + about: Check the docs before filing an issue diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..515ff20 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,33 @@ +name: "โœจ Feature Request" +description: Suggest a new feature or improvement +labels: ["feature"] +body: + - type: textarea + id: problem + attributes: + label: Problem / Motivation + description: What problem does this solve? Why is it needed? + validations: + required: true + - type: textarea + id: solution + attributes: + label: Proposed Solution + description: How should this work? + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives Considered + description: Other approaches you've thought about + - type: textarea + id: acceptance + attributes: + label: Acceptance Criteria + description: How do we know this is done? + placeholder: | + - [ ] Criterion 1 + - [ ] Criterion 2 + validations: + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..5505fee --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,25 @@ +## What + + +## Why + + +Closes # + +## How + + +## Testing + + +- [ ] Unit tests added/updated +- [ ] `mvn verify` passes locally +- [ ] Tested manually on a test server (if applicable) + +## Checklist + +- [ ] Issue linked above +- [ ] Tests pass +- [ ] No new warnings +- [ ] Docs updated (if user-facing change) +- [ ] Changelog entry added (if user-facing change) diff --git a/docs/installation.md b/docs/installation.md index 7997bf4..48d19b1 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -29,7 +29,11 @@ After first run, MiningLevels creates the following files: ``` plugins/MiningLevels/ โ”œโ”€โ”€ config.yml # Main configuration -โ””โ”€โ”€ config/ - โ”œโ”€โ”€ levels.json # Level definitions - โ””โ”€โ”€ blocks.json # Mining block definitions +โ”œโ”€โ”€ config/ +โ”‚ โ”œโ”€โ”€ levels.json # Level definitions +โ”‚ โ””โ”€โ”€ blocks.json # Mining block definitions +โ””โ”€โ”€ data/ + โ””โ”€โ”€ players.json # Player progress data ``` + +Default `levels.json` and `blocks.json` are bundled inside the plugin JAR and copied automatically on first run. No internet connection is required for setup. diff --git a/src/main/java/de/chafficplugins/mininglevels/MiningLevels.java b/src/main/java/de/chafficplugins/mininglevels/MiningLevels.java index e65b8d4..40742c4 100644 --- a/src/main/java/de/chafficplugins/mininglevels/MiningLevels.java +++ b/src/main/java/de/chafficplugins/mininglevels/MiningLevels.java @@ -86,11 +86,13 @@ public void onEnable() { @Override public void onDisable() { - // Plugin shutdown logic - try { - MiningPlayer.save(); - } catch (IOException e) { - error("Failed to save files."); + // Only save if startup completed successfully (fileManager was initialized) + if (fileManager != null) { + try { + MiningPlayer.save(); + } catch (IOException e) { + error("Failed to save files."); + } } Bukkit.getScheduler().cancelTasks(this); log(ChatColor.DARK_GREEN + getDescription().getName() + " is now disabled (Version: " + getDescription().getVersion() + ")"); diff --git a/src/main/java/de/chafficplugins/mininglevels/io/FileManager.java b/src/main/java/de/chafficplugins/mininglevels/io/FileManager.java index eccf692..bcc480e 100644 --- a/src/main/java/de/chafficplugins/mininglevels/io/FileManager.java +++ b/src/main/java/de/chafficplugins/mininglevels/io/FileManager.java @@ -4,19 +4,28 @@ import io.github.chafficui.CrucialLib.io.Json; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; -import java.net.URL; -import java.nio.channels.Channels; -import java.nio.channels.ReadableByteChannel; - -import static de.chafficplugins.mininglevels.utils.ConfigStrings.*; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; public class FileManager { - private final static MiningLevels plugin = MiningLevels.getPlugin(MiningLevels.class); - public final static String PLAYERS = plugin.getDataFolder() + "/data/players.json"; - public final static String BLOCKS = plugin.getDataFolder() + "/config/blocks.json"; - public final static String LEVELS = plugin.getDataFolder() + "/config/levels.json"; + private static MiningLevels plugin; + private static MiningLevels getPlugin() { + if (plugin == null) plugin = MiningLevels.getPlugin(MiningLevels.class); + return plugin; + } + + public static String PLAYERS; + public static String BLOCKS; + public static String LEVELS; + + static { + MiningLevels p = getPlugin(); + PLAYERS = p.getDataFolder() + "/data/players.json"; + BLOCKS = p.getDataFolder() + "/config/blocks.json"; + LEVELS = p.getDataFolder() + "/config/levels.json"; + } public FileManager() throws IOException { setupFiles(); @@ -39,27 +48,24 @@ private void setupFiles() throws IOException { } File blocksFile = new File(BLOCKS); if (!blocksFile.exists()) { - downloadFile(blocksFile, MINING_BLOCKS_JSON); + copyDefault("defaults/blocks.json", blocksFile); } File levelsFile = new File(LEVELS); if (!levelsFile.exists()) { - downloadFile(levelsFile, MINING_LEVELS_JSON); + copyDefault("defaults/levels.json", levelsFile); } } - private void downloadFile(File file, String downloadURL) throws IOException { - File dir = file.getParentFile(); + private void copyDefault(String resourcePath, File destination) throws IOException { + File dir = destination.getParentFile(); if (!dir.exists()) { dir.mkdirs(); } - try { - URL url = new URL(DOWNLOAD_URL + downloadURL); - ReadableByteChannel rbc = Channels.newChannel(url.openStream()); - FileOutputStream fos = new FileOutputStream(file); - fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); - } catch (IOException e) { - e.printStackTrace(); - throw new IOException("Could not download " + file.getAbsolutePath() + "."); + try (InputStream in = getPlugin().getResource(resourcePath)) { + if (in == null) { + throw new IOException("Bundled default resource not found: " + resourcePath); + } + Files.copy(in, destination.toPath(), StandardCopyOption.REPLACE_EXISTING); } } diff --git a/src/main/java/de/chafficplugins/mininglevels/utils/ConfigStrings.java b/src/main/java/de/chafficplugins/mininglevels/utils/ConfigStrings.java index 85623ba..d096c3a 100644 --- a/src/main/java/de/chafficplugins/mininglevels/utils/ConfigStrings.java +++ b/src/main/java/de/chafficplugins/mininglevels/utils/ConfigStrings.java @@ -9,10 +9,6 @@ public class ConfigStrings { public static String PREFIX = "ยง8[ยง6MLยง8] ยงr"; - public final static String DOWNLOAD_URL = "https://drive.google.com/uc?export=download&id="; - public final static String MINING_LEVELS_JSON = "145W6qtWM2-PA3vyDlOpa3nnYrn0bUVyb"; - public final static String MINING_BLOCKS_JSON = "1W1EN0NIJKH69cokExNKVglOw6YPbEBYT"; - //Permissions public final static String PERMISSION_ADMIN = "mininglevels.*"; public final static String PERMISSION_SET_LEVEL = "mininglevels.setlevel"; diff --git a/src/main/resources/defaults/blocks.json b/src/main/resources/defaults/blocks.json new file mode 100644 index 0000000..681db4b --- /dev/null +++ b/src/main/resources/defaults/blocks.json @@ -0,0 +1,73 @@ +[ + { + "materials": [ + "STONE" + ], + "xp": 1, + "minLevel": 0 + }, + { + "materials": [ + "DEEPSLATE_REDSTONE_ORE", + "REDSTONE_ORE" + ], + "xp": 1, + "minLevel": 0 + }, + { + "materials": [ + "DEEPSLATE_LAPIS_ORE", + "LAPIS_ORE" + ], + "xp": 1, + "minLevel": 3 + }, + { + "materials": [ + "DEEPSLATE_COAL_ORE", + "COAL_ORE" + ], + "xp": 10, + "minLevel": 0 + }, + { + "materials": [ + "DEEPSLATE_GOLD_ORE", + "GOLD_ORE" + ], + "xp": 100, + "minLevel": 2 + }, + { + "materials": [ + "DEEPSLATE_EMERALD_ORE", + "EMERALD_ORE" + ], + "xp": 10, + "minLevel": 3 + }, + { + "materials": [ + "DEEPSLATE_COPPER_ORE", + "COPPER_ORE" + ], + "xp": 5, + "minLevel": 2 + }, + { + "materials": [ + "DEEPSLATE_IRON_ORE", + "IRON_ORE" + ], + "xp": 50, + "minLevel": 1 + }, + { + "materials": [ + "DEEPSLATE_DIAMOND_ORE", + "DIAMOND_ORE" + ], + "xp": 1000, + "minLevel": 5 + } +] diff --git a/src/main/resources/defaults/levels.json b/src/main/resources/defaults/levels.json new file mode 100644 index 0000000..1a7278b --- /dev/null +++ b/src/main/resources/defaults/levels.json @@ -0,0 +1,112 @@ +[ + { + "name": "1", + "nextLevelXP": 100, + "ordinal": 0, + "instantBreakProbability": 0.0, + "extraOreProbability": 0.0, + "maxExtraOre": 0, + "hasteLevel": 0, + "commands": [], + "rewards": [] + }, + { + "name": "2", + "nextLevelXP": 300, + "ordinal": 1, + "instantBreakProbability": 5.0, + "extraOreProbability": 10.0, + "maxExtraOre": 0, + "hasteLevel": 0, + "commands": [], + "rewards": [] + }, + { + "name": "3", + "nextLevelXP": 600, + "ordinal": 2, + "instantBreakProbability": 10.0, + "extraOreProbability": 10.0, + "maxExtraOre": 2, + "hasteLevel": 0, + "commands": [], + "rewards": [] + }, + { + "name": "4", + "nextLevelXP": 1000, + "ordinal": 3, + "instantBreakProbability": 15.0, + "extraOreProbability": 20.0, + "maxExtraOre": 2, + "hasteLevel": 0, + "commands": [], + "rewards": [] + }, + { + "name": "5", + "nextLevelXP": 2000, + "ordinal": 4, + "instantBreakProbability": 20.0, + "extraOreProbability": 20.0, + "maxExtraOre": 4, + "hasteLevel": 1, + "commands": [], + "rewards": [] + }, + { + "name": "6", + "nextLevelXP": 6000, + "ordinal": 5, + "instantBreakProbability": 25.0, + "extraOreProbability": 30.0, + "maxExtraOre": 4, + "hasteLevel": 1, + "commands": [], + "rewards": [] + }, + { + "name": "7", + "nextLevelXP": 14000, + "ordinal": 6, + "instantBreakProbability": 28.0, + "extraOreProbability": 30.0, + "maxExtraOre": 6, + "hasteLevel": 1, + "commands": [], + "rewards": [] + }, + { + "name": "8", + "nextLevelXP": 32000, + "ordinal": 7, + "instantBreakProbability": 30.0, + "extraOreProbability": 40.0, + "maxExtraOre": 6, + "hasteLevel": 1, + "commands": [], + "rewards": [] + }, + { + "name": "9", + "nextLevelXP": 69000, + "ordinal": 8, + "instantBreakProbability": 32.0, + "extraOreProbability": 40.0, + "maxExtraOre": 8, + "hasteLevel": 2, + "commands": [], + "rewards": [] + }, + { + "name": "10", + "nextLevelXP": 150000, + "ordinal": 9, + "instantBreakProbability": 35.0, + "extraOreProbability": 50.0, + "maxExtraOre": 10, + "hasteLevel": 2, + "commands": [], + "rewards": [] + } +] diff --git a/src/test/java/de/chafficplugins/mininglevels/api/DefaultConfigTest.java b/src/test/java/de/chafficplugins/mininglevels/api/DefaultConfigTest.java new file mode 100644 index 0000000..951d7f9 --- /dev/null +++ b/src/test/java/de/chafficplugins/mininglevels/api/DefaultConfigTest.java @@ -0,0 +1,205 @@ +package de.chafficplugins.mininglevels.api; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.bukkit.Material; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.MockBukkit; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.HashSet; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Validates the bundled default configuration files (levels.json and blocks.json) + * to catch issues like invalid values, missing fields, or broken cross-references. + *

+ * These tests load the JSON directly from the classpath without CrucialLib, + * ensuring the defaults ship in a valid, consistent state. + */ +class DefaultConfigTest { + + private static JsonArray levels; + private static JsonArray blocks; + + @BeforeAll + static void setUp() { + MockBukkit.mock(); + levels = loadJsonArray("/defaults/levels.json"); + blocks = loadJsonArray("/defaults/blocks.json"); + } + + @AfterAll + static void tearDown() { + MockBukkit.unmock(); + } + + private static JsonArray loadJsonArray(String path) { + try (InputStream is = DefaultConfigTest.class.getResourceAsStream(path)) { + assertNotNull(is, "Resource not found: " + path); + try (Reader reader = new InputStreamReader(is)) { + JsonElement element = JsonParser.parseReader(reader); + assertTrue(element.isJsonArray(), path + " should be a JSON array"); + return element.getAsJsonArray(); + } + } catch (IOException e) { + return fail("Failed to read " + path + ": " + e.getMessage()); + } + } + + // ---- levels.json ---- + + @Test + void levelsJson_shouldNotBeEmpty() { + assertFalse(levels.isEmpty(), "levels.json should contain at least one level"); + } + + @Test + void levelsJson_shouldHaveRequiredFields() { + for (JsonElement el : levels) { + JsonObject level = el.getAsJsonObject(); + assertTrue(level.has("name"), "Level missing 'name' field"); + assertTrue(level.has("nextLevelXP"), "Level missing 'nextLevelXP' field"); + assertTrue(level.has("ordinal"), "Level missing 'ordinal' field"); + } + } + + @Test + void levelsJson_ordinalsShouldBeSequential() { + for (int i = 0; i < levels.size(); i++) { + int ordinal = levels.get(i).getAsJsonObject().get("ordinal").getAsInt(); + assertEquals(i, ordinal, "Level at index " + i + " should have ordinal " + i); + } + } + + @Test + void levelsJson_xpThresholdsShouldBePositive() { + for (JsonElement el : levels) { + JsonObject level = el.getAsJsonObject(); + int xp = level.get("nextLevelXP").getAsInt(); + assertTrue(xp > 0, "Level '" + level.get("name").getAsString() + + "' should have positive XP threshold, got " + xp); + } + } + + @Test + void levelsJson_xpThresholdsShouldIncrease() { + int previousXp = 0; + for (JsonElement el : levels) { + JsonObject level = el.getAsJsonObject(); + int xp = level.get("nextLevelXP").getAsInt(); + assertTrue(xp > previousXp, "Level '" + level.get("name").getAsString() + + "' XP (" + xp + ") should be greater than previous (" + previousXp + ")"); + previousXp = xp; + } + } + + @Test + void levelsJson_probabilitiesShouldBeInRange() { + for (JsonElement el : levels) { + JsonObject level = el.getAsJsonObject(); + String name = level.get("name").getAsString(); + if (level.has("instantBreakProbability")) { + float p = level.get("instantBreakProbability").getAsFloat(); + assertTrue(p >= 0 && p <= 100, + "Level '" + name + "' instantBreakProbability " + p + " out of range [0,100]"); + } + if (level.has("extraOreProbability")) { + float p = level.get("extraOreProbability").getAsFloat(); + assertTrue(p >= 0 && p <= 100, + "Level '" + name + "' extraOreProbability " + p + " out of range [0,100]"); + } + } + } + + @Test + void levelsJson_hasteAndExtraOreShouldBeNonNegative() { + for (JsonElement el : levels) { + JsonObject level = el.getAsJsonObject(); + String name = level.get("name").getAsString(); + if (level.has("maxExtraOre")) { + int v = level.get("maxExtraOre").getAsInt(); + assertTrue(v >= 0, "Level '" + name + "' maxExtraOre should be >= 0, got " + v); + } + if (level.has("hasteLevel")) { + int v = level.get("hasteLevel").getAsInt(); + assertTrue(v >= 0, "Level '" + name + "' hasteLevel should be >= 0, got " + v); + } + } + } + + // ---- blocks.json ---- + + @Test + void blocksJson_shouldNotBeEmpty() { + assertFalse(blocks.isEmpty(), "blocks.json should contain at least one block"); + } + + @Test + void blocksJson_shouldHaveRequiredFields() { + for (int i = 0; i < blocks.size(); i++) { + JsonObject block = blocks.get(i).getAsJsonObject(); + assertTrue(block.has("materials"), "Block " + i + " missing 'materials' field"); + assertTrue(block.has("xp"), "Block " + i + " missing 'xp' field"); + assertTrue(block.has("minLevel"), "Block " + i + " missing 'minLevel' field"); + } + } + + @Test + void blocksJson_materialsShouldBeValidBukkitMaterials() { + for (int i = 0; i < blocks.size(); i++) { + JsonObject block = blocks.get(i).getAsJsonObject(); + JsonArray materials = block.getAsJsonArray("materials"); + assertFalse(materials.isEmpty(), "Block " + i + " should have at least one material"); + for (JsonElement mat : materials) { + String name = mat.getAsString(); + assertDoesNotThrow(() -> Material.valueOf(name), + "Block " + i + " has invalid material: " + name); + assertTrue(Material.valueOf(name).isBlock(), + "Material " + name + " in block " + i + " should be a block type"); + } + } + } + + @Test + void blocksJson_xpShouldBePositive() { + for (int i = 0; i < blocks.size(); i++) { + JsonObject block = blocks.get(i).getAsJsonObject(); + int xp = block.get("xp").getAsInt(); + assertTrue(xp > 0, "Block " + i + " should have positive XP, got " + xp); + } + } + + @Test + void blocksJson_minLevelsShouldReferenceExistingLevels() { + int maxOrdinal = levels.size() - 1; + for (int i = 0; i < blocks.size(); i++) { + JsonObject block = blocks.get(i).getAsJsonObject(); + int minLevel = block.get("minLevel").getAsInt(); + assertTrue(minLevel >= 0 && minLevel <= maxOrdinal, + "Block " + i + " minLevel " + minLevel + " out of range [0," + maxOrdinal + "]"); + } + } + + @Test + void blocksJson_shouldNotHaveDuplicateMaterials() { + Set allMaterials = new HashSet<>(); + for (int i = 0; i < blocks.size(); i++) { + JsonArray materials = blocks.get(i).getAsJsonObject().getAsJsonArray("materials"); + for (JsonElement mat : materials) { + String name = mat.getAsString(); + assertTrue(allMaterials.add(name), + "Duplicate material '" + name + "' found in block " + i); + } + } + } +} diff --git a/src/test/java/de/chafficplugins/mininglevels/api/MiningBlockTest.java b/src/test/java/de/chafficplugins/mininglevels/api/MiningBlockTest.java index 0fbec93..f08b35e 100644 --- a/src/test/java/de/chafficplugins/mininglevels/api/MiningBlockTest.java +++ b/src/test/java/de/chafficplugins/mininglevels/api/MiningBlockTest.java @@ -1,12 +1,15 @@ package de.chafficplugins.mininglevels.api; import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockbukkit.mockbukkit.MockBukkit; +import java.util.ArrayList; + import static org.junit.jupiter.api.Assertions.*; class MiningBlockTest { @@ -30,6 +33,8 @@ void setUp() { MiningLevel.miningLevels.add(new MiningLevel("Expert", 500, 2)); } + // --- single-material constructor --- + @Test void constructor_singleMaterial_shouldCreateBlock() { MiningBlock block = new MiningBlock(Material.COAL_ORE, 1, 0); @@ -39,6 +44,23 @@ void constructor_singleMaterial_shouldCreateBlock() { assertTrue(block.getMaterials().contains(Material.COAL_ORE)); } + @Test + void constructor_singleMaterial_shouldHaveOneMaterial() { + MiningBlock block = new MiningBlock(Material.IRON_ORE, 3, 1); + assertEquals(1, block.getMaterials().size()); + } + + @Test + void constructor_differentBlockTypes_shouldWork() { + assertDoesNotThrow(() -> new MiningBlock(Material.STONE, 1, 0)); + assertDoesNotThrow(() -> new MiningBlock(Material.DIAMOND_ORE, 5, 0)); + assertDoesNotThrow(() -> new MiningBlock(Material.DEEPSLATE_GOLD_ORE, 4, 0)); + assertDoesNotThrow(() -> new MiningBlock(Material.NETHERRACK, 1, 0)); + assertDoesNotThrow(() -> new MiningBlock(Material.OBSIDIAN, 2, 0)); + } + + // --- multi-material constructor --- + @Test void constructor_multipleMaterials_shouldCreateBlock() { MiningBlock block = new MiningBlock( @@ -50,12 +72,61 @@ void constructor_multipleMaterials_shouldCreateBlock() { assertTrue(block.getMaterials().contains(Material.DEEPSLATE_REDSTONE_ORE)); } + @Test + void constructor_threeMaterials_shouldCreateBlock() { + MiningBlock block = new MiningBlock( + new Material[]{Material.COAL_ORE, Material.DEEPSLATE_COAL_ORE, Material.STONE}, + 1, 0 + ); + assertEquals(3, block.getMaterials().size()); + } + + @Test + void constructor_singleMaterialArray_shouldWork() { + MiningBlock block = new MiningBlock( + new Material[]{Material.DIAMOND_ORE}, 5, 2 + ); + assertEquals(1, block.getMaterials().size()); + assertTrue(block.getMaterials().contains(Material.DIAMOND_ORE)); + } + + // --- non-block material rejection --- + @Test void constructor_nonBlockMaterial_shouldThrow() { assertThrows(IllegalArgumentException.class, () -> new MiningBlock(Material.DIAMOND, 1, 0)); } + @Test + void constructor_nonBlockMaterials_shouldThrow() { + assertThrows(IllegalArgumentException.class, () -> + new MiningBlock(new Material[]{Material.DIAMOND_SWORD}, 1, 0)); + } + + @Test + void constructor_mixedBlockAndNonBlock_shouldThrow() { + assertThrows(IllegalArgumentException.class, () -> + new MiningBlock( + new Material[]{Material.COAL_ORE, Material.DIAMOND}, + 1, 0 + )); + } + + @Test + void constructor_itemMaterial_shouldThrow() { + assertThrows(IllegalArgumentException.class, () -> + new MiningBlock(Material.IRON_INGOT, 1, 0)); + } + + @Test + void constructor_toolMaterial_shouldThrow() { + assertThrows(IllegalArgumentException.class, () -> + new MiningBlock(Material.DIAMOND_PICKAXE, 1, 0)); + } + + // --- getMiningBlock --- + @Test void getMiningBlock_existingMaterial_shouldReturnBlock() { MiningBlock block = new MiningBlock(Material.DIAMOND_ORE, 5, 2); @@ -87,6 +158,28 @@ void getMiningBlock_fromMultiMaterialBlock_shouldReturnBlock() { assertEquals(block, MiningBlock.getMiningBlock(Material.IRON_ORE)); } + @Test + void getMiningBlock_emptyList_shouldReturnNull() { + assertNull(MiningBlock.getMiningBlock(Material.COAL_ORE)); + } + + @Test + void getMiningBlock_multipleBlocks_shouldFindCorrectOne() { + MiningBlock coal = new MiningBlock(Material.COAL_ORE, 1, 0); + MiningBlock iron = new MiningBlock(Material.IRON_ORE, 3, 1); + MiningBlock diamond = new MiningBlock(Material.DIAMOND_ORE, 5, 2); + MiningBlock.miningBlocks.add(coal); + MiningBlock.miningBlocks.add(iron); + MiningBlock.miningBlocks.add(diamond); + + MiningBlock found = MiningBlock.getMiningBlock(Material.IRON_ORE); + assertNotNull(found); + assertEquals(3, found.getXp()); + assertEquals(1, found.getMinLevel()); + } + + // --- setXp --- + @Test void setXp_shouldUpdateValue() { MiningBlock block = new MiningBlock(Material.COAL_ORE, 1, 0); @@ -94,6 +187,33 @@ void setXp_shouldUpdateValue() { assertEquals(10, block.getXp()); } + @Test + void setXp_toZero_shouldWork() { + MiningBlock block = new MiningBlock(Material.COAL_ORE, 5, 0); + block.setXp(0); + assertEquals(0, block.getXp()); + } + + @Test + void setXp_toLargeValue_shouldWork() { + MiningBlock block = new MiningBlock(Material.COAL_ORE, 1, 0); + block.setXp(99999); + assertEquals(99999, block.getXp()); + } + + @Test + void setXp_multipleTimes_shouldOverwrite() { + MiningBlock block = new MiningBlock(Material.COAL_ORE, 1, 0); + block.setXp(5); + assertEquals(5, block.getXp()); + block.setXp(15); + assertEquals(15, block.getXp()); + block.setXp(1); + assertEquals(1, block.getXp()); + } + + // --- setMinLevel --- + @Test void setMinLevel_validRange_shouldUpdateValue() { MiningBlock block = new MiningBlock(Material.COAL_ORE, 1, 0); @@ -101,6 +221,14 @@ void setMinLevel_validRange_shouldUpdateValue() { assertEquals(1, block.getMinLevel()); } + @Test + void setMinLevel_maxValidOrdinal_shouldWork() { + // miningLevels.size() is 3, so max valid is 2 (< 3 and > 0) + MiningBlock block = new MiningBlock(Material.COAL_ORE, 1, 1); + block.setMinLevel(2); + assertEquals(2, block.getMinLevel()); + } + @Test void setMinLevel_outOfRange_shouldNotChange() { MiningBlock block = new MiningBlock(Material.COAL_ORE, 1, 1); @@ -112,6 +240,62 @@ void setMinLevel_outOfRange_shouldNotChange() { assertEquals(1, block.getMinLevel()); } + @Test + void setMinLevel_exactlySize_shouldNotChange() { + MiningBlock block = new MiningBlock(Material.COAL_ORE, 1, 1); + block.setMinLevel(3); // equals size, condition is < size + assertEquals(1, block.getMinLevel()); + } + + @Test + void setMinLevel_negative_shouldNotChange() { + MiningBlock block = new MiningBlock(Material.COAL_ORE, 1, 1); + block.setMinLevel(-1); + assertEquals(1, block.getMinLevel()); + } + + // --- setMaterials --- + + @Test + void setMaterials_shouldReplaceMaterials() { + MiningBlock block = new MiningBlock(Material.COAL_ORE, 1, 0); + assertEquals(1, block.getMaterials().size()); + assertTrue(block.getMaterials().contains(Material.COAL_ORE)); + + ArrayList newMaterials = new ArrayList<>(); + newMaterials.add(new ItemStack(Material.IRON_ORE)); + newMaterials.add(new ItemStack(Material.GOLD_ORE)); + block.setMaterials(newMaterials); + + assertEquals(2, block.getMaterials().size()); + assertTrue(block.getMaterials().contains(Material.IRON_ORE)); + assertTrue(block.getMaterials().contains(Material.GOLD_ORE)); + assertFalse(block.getMaterials().contains(Material.COAL_ORE)); + } + + @Test + void setMaterials_emptyList_shouldClearMaterials() { + MiningBlock block = new MiningBlock(Material.COAL_ORE, 1, 0); + block.setMaterials(new ArrayList<>()); + assertEquals(0, block.getMaterials().size()); + } + + @Test + void setMaterials_singleItem_shouldWork() { + MiningBlock block = new MiningBlock( + new Material[]{Material.COAL_ORE, Material.DEEPSLATE_COAL_ORE}, + 1, 0 + ); + ArrayList newMaterials = new ArrayList<>(); + newMaterials.add(new ItemStack(Material.DIAMOND_ORE)); + block.setMaterials(newMaterials); + + assertEquals(1, block.getMaterials().size()); + assertTrue(block.getMaterials().contains(Material.DIAMOND_ORE)); + } + + // --- multiple blocks in registry --- + @Test void multipleMiningBlocks_shouldTrackSeparately() { MiningBlock coal = new MiningBlock(Material.COAL_ORE, 1, 0); @@ -127,4 +311,57 @@ void multipleMiningBlocks_shouldTrackSeparately() { assertEquals(iron, MiningBlock.getMiningBlock(Material.IRON_ORE)); assertEquals(diamond, MiningBlock.getMiningBlock(Material.DIAMOND_ORE)); } + + @Test + void multipleMultiMaterialBlocks_shouldTrackSeparately() { + MiningBlock redstone = new MiningBlock( + new Material[]{Material.REDSTONE_ORE, Material.DEEPSLATE_REDSTONE_ORE}, 2, 0 + ); + MiningBlock lapis = new MiningBlock( + new Material[]{Material.LAPIS_ORE, Material.DEEPSLATE_LAPIS_ORE}, 3, 1 + ); + MiningBlock.miningBlocks.add(redstone); + MiningBlock.miningBlocks.add(lapis); + + assertEquals(redstone, MiningBlock.getMiningBlock(Material.REDSTONE_ORE)); + assertEquals(redstone, MiningBlock.getMiningBlock(Material.DEEPSLATE_REDSTONE_ORE)); + assertEquals(lapis, MiningBlock.getMiningBlock(Material.LAPIS_ORE)); + assertEquals(lapis, MiningBlock.getMiningBlock(Material.DEEPSLATE_LAPIS_ORE)); + } + + // --- XP and level stored correctly after construction --- + + @Test + void constructor_xpAndLevel_shouldBeStoredCorrectly() { + MiningBlock block = new MiningBlock(Material.EMERALD_ORE, 7, 2); + assertEquals(7, block.getXp()); + assertEquals(2, block.getMinLevel()); + assertTrue(block.getMaterials().contains(Material.EMERALD_ORE)); + } + + @Test + void constructor_zeroXp_shouldWork() { + MiningBlock block = new MiningBlock(Material.STONE, 0, 0); + assertEquals(0, block.getXp()); + } + + @Test + void constructor_zeroMinLevel_shouldWork() { + MiningBlock block = new MiningBlock(Material.STONE, 1, 0); + assertEquals(0, block.getMinLevel()); + } + + // --- getMaterials returns new list --- + + @Test + void getMaterials_shouldReturnCorrectMaterialTypes() { + MiningBlock block = new MiningBlock( + new Material[]{Material.COPPER_ORE, Material.DEEPSLATE_COPPER_ORE}, + 2, 0 + ); + ArrayList materials = block.getMaterials(); + assertEquals(2, materials.size()); + assertTrue(materials.contains(Material.COPPER_ORE)); + assertTrue(materials.contains(Material.DEEPSLATE_COPPER_ORE)); + } } diff --git a/src/test/java/de/chafficplugins/mininglevels/api/MiningFlowIntegrationTest.java b/src/test/java/de/chafficplugins/mininglevels/api/MiningFlowIntegrationTest.java new file mode 100644 index 0000000..1b4ecff --- /dev/null +++ b/src/test/java/de/chafficplugins/mininglevels/api/MiningFlowIntegrationTest.java @@ -0,0 +1,540 @@ +package de.chafficplugins.mininglevels.api; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import org.bukkit.Material; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.MockBukkit; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Integration tests that wire together MiningLevel, MiningBlock, and MiningPlayer + * using the actual bundled default configurations, and test the full mining + * progression flow. + *

+ * Note: The actual {@code alterXp()}, {@code xpChange()}, and {@code levelUp()} + * methods are coupled to the plugin instance and CrucialLib (for messaging, + * sounds, and config). Since the plugin can't be loaded in tests (CrucialLib + * isn't available), these tests replicate the core state-transition logic from + * those methods to validate the domain integration. + * + * @see MiningPlayer#alterXp(int) + * @see MiningLevel#levelUp(MiningPlayer) + */ +class MiningFlowIntegrationTest { + + @BeforeAll + static void setUpServer() { + MockBukkit.mock(); + } + + @AfterAll + static void tearDown() { + MockBukkit.unmock(); + } + + @BeforeEach + void setUp() { + MiningLevel.miningLevels.clear(); + MiningBlock.miningBlocks.clear(); + MiningPlayer.miningPlayers.clear(); + + loadDefaultLevels(); + loadDefaultBlocks(); + } + + // ---- Deserialization integration: JSON โ†’ model classes ---- + + @Test + void defaultLevels_shouldDeserializeIntoMiningLevelObjects() { + assertEquals(10, MiningLevel.miningLevels.size()); + for (int i = 0; i < MiningLevel.miningLevels.size(); i++) { + MiningLevel level = MiningLevel.get(i); + assertNotNull(level, "Level at ordinal " + i + " should exist"); + assertEquals(i, level.getOrdinal()); + assertNotNull(level.getName()); + } + } + + @Test + void defaultBlocks_shouldDeserializeIntoMiningBlockObjects() { + assertEquals(9, MiningBlock.miningBlocks.size()); + for (MiningBlock block : MiningBlock.miningBlocks) { + assertFalse(block.getMaterials().isEmpty(), "Each block should have at least one material"); + assertTrue(block.getXp() > 0, "Each block should have positive XP"); + assertTrue(block.getMinLevel() >= 0, "Each block should have non-negative minLevel"); + } + } + + @Test + void defaultLevels_shouldHaveIncreasingXpThresholds() { + int prevXp = 0; + for (MiningLevel level : MiningLevel.miningLevels) { + assertTrue(level.getNextLevelXP() > prevXp, + "Level " + level.getName() + " XP (" + level.getNextLevelXP() + + ") should exceed previous (" + prevXp + ")"); + prevXp = level.getNextLevelXP(); + } + } + + @Test + void defaultLevels_skillsShouldProgressMonotonically() { + float prevInstant = -1, prevExtra = -1; + int prevMaxOre = -1, prevHaste = -1; + for (MiningLevel level : MiningLevel.miningLevels) { + assertTrue(level.getInstantBreakProbability() >= prevInstant, + "instantBreakProbability should not decrease at level " + level.getName()); + assertTrue(level.getExtraOreProbability() >= prevExtra, + "extraOreProbability should not decrease at level " + level.getName()); + assertTrue(level.getMaxExtraOre() >= prevMaxOre, + "maxExtraOre should not decrease at level " + level.getName()); + assertTrue(level.getHasteLevel() >= prevHaste, + "hasteLevel should not decrease at level " + level.getName()); + prevInstant = level.getInstantBreakProbability(); + prevExtra = level.getExtraOreProbability(); + prevMaxOre = level.getMaxExtraOre(); + prevHaste = level.getHasteLevel(); + } + } + + // ---- Block lookup integration ---- + + @Test + void getMiningBlock_shouldFindAllDefaultMaterials() { + Material[] expectedMaterials = { + Material.STONE, Material.COAL_ORE, Material.DEEPSLATE_COAL_ORE, + Material.IRON_ORE, Material.DEEPSLATE_IRON_ORE, + Material.GOLD_ORE, Material.DEEPSLATE_GOLD_ORE, + Material.DIAMOND_ORE, Material.DEEPSLATE_DIAMOND_ORE, + Material.EMERALD_ORE, Material.DEEPSLATE_EMERALD_ORE, + Material.LAPIS_ORE, Material.DEEPSLATE_LAPIS_ORE, + Material.REDSTONE_ORE, Material.DEEPSLATE_REDSTONE_ORE, + Material.COPPER_ORE, Material.DEEPSLATE_COPPER_ORE + }; + for (Material mat : expectedMaterials) { + assertNotNull(MiningBlock.getMiningBlock(mat), + "Should find a MiningBlock for " + mat.name()); + } + } + + @Test + void getMiningBlock_regularAndDeepslateVariants_shouldShareSameBlock() { + MiningBlock iron = MiningBlock.getMiningBlock(Material.IRON_ORE); + MiningBlock deepslateIron = MiningBlock.getMiningBlock(Material.DEEPSLATE_IRON_ORE); + assertNotNull(iron); + assertSame(iron, deepslateIron, "Regular and deepslate iron ore should be the same MiningBlock"); + + MiningBlock diamond = MiningBlock.getMiningBlock(Material.DIAMOND_ORE); + MiningBlock deepslateDiamond = MiningBlock.getMiningBlock(Material.DEEPSLATE_DIAMOND_ORE); + assertSame(diamond, deepslateDiamond, "Regular and deepslate diamond ore should be the same MiningBlock"); + } + + @Test + void getMiningBlock_nonMiningBlock_shouldReturnNull() { + assertNull(MiningBlock.getMiningBlock(Material.DIRT)); + assertNull(MiningBlock.getMiningBlock(Material.GRASS_BLOCK)); + assertNull(MiningBlock.getMiningBlock(Material.OAK_LOG)); + } + + // ---- Level requirement gating ---- + + @Test + void levelGating_level0Player_shouldOnlyAccessLevel0Blocks() { + MiningPlayer player = new MiningPlayer(UUID.randomUUID(), 0, 0); + Set accessible = getAccessibleMaterials(player); + + // Level 0 blocks: STONE (minLevel 0), REDSTONE_ORE (0), COAL_ORE (0) + assertTrue(accessible.contains(Material.STONE)); + assertTrue(accessible.contains(Material.COAL_ORE)); + assertTrue(accessible.contains(Material.REDSTONE_ORE)); + + // Level 1+ blocks should be inaccessible + assertFalse(accessible.contains(Material.IRON_ORE)); + assertFalse(accessible.contains(Material.GOLD_ORE)); + assertFalse(accessible.contains(Material.DIAMOND_ORE)); + } + + @Test + void levelGating_level2Player_shouldAccessLevel0Through2Blocks() { + MiningPlayer player = new MiningPlayer(UUID.randomUUID(), 2, 0); + + // minLevel 0 + assertTrue(canMine(player, Material.STONE)); + assertTrue(canMine(player, Material.COAL_ORE)); + // minLevel 1 + assertTrue(canMine(player, Material.IRON_ORE)); + // minLevel 2 + assertTrue(canMine(player, Material.GOLD_ORE)); + assertTrue(canMine(player, Material.COPPER_ORE)); + // minLevel 3 โ€” too high + assertFalse(canMine(player, Material.LAPIS_ORE)); + assertFalse(canMine(player, Material.EMERALD_ORE)); + // minLevel 5 โ€” too high + assertFalse(canMine(player, Material.DIAMOND_ORE)); + } + + @Test + void levelGating_maxLevelPlayer_shouldAccessAllBlocks() { + int maxOrdinal = MiningLevel.getMaxLevel().getOrdinal(); + MiningPlayer player = new MiningPlayer(UUID.randomUUID(), maxOrdinal, 0); + + for (MiningBlock block : MiningBlock.miningBlocks) { + for (Material mat : block.getMaterials()) { + assertTrue(canMine(player, mat), + "Max level player should be able to mine " + mat.name()); + } + } + } + + // ---- XP accumulation and level-up flow ---- + + @Test + void miningFlow_coalOre_shouldAccumulateXp() { + MiningPlayer player = new MiningPlayer(UUID.randomUUID(), 0, 0); + MiningBlock coal = MiningBlock.getMiningBlock(Material.COAL_ORE); + assertNotNull(coal); + + simulateMining(player, coal); + assertEquals(10, player.getXp()); // coal gives 10 XP + + simulateMining(player, coal); + assertEquals(20, player.getXp()); + } + + @Test + void miningFlow_shouldLevelUpWhenXpReachesThreshold() { + MiningPlayer player = new MiningPlayer(UUID.randomUUID(), 0, 0); + assertEquals(0, player.getLevel().getOrdinal()); + + // Level 1 requires 100 XP. Coal gives 10 XP. Mine 10 coal ores. + MiningBlock coal = MiningBlock.getMiningBlock(Material.COAL_ORE); + for (int i = 0; i < 10; i++) { + simulateMining(player, coal); + } + // Should have leveled up to ordinal 1, with overflow XP = 0 + assertEquals(1, player.getLevel().getOrdinal(), "Should have leveled up to ordinal 1"); + assertEquals(0, player.getXp(), "Overflow XP should be 0 (exactly 100 XP)"); + } + + @Test + void miningFlow_shouldCarryOverflowXpAfterLevelUp() { + MiningPlayer player = new MiningPlayer(UUID.randomUUID(), 0, 95); + // 95 XP + 10 XP from coal = 105 XP, threshold is 100 โ†’ level up, overflow = 5 + MiningBlock coal = MiningBlock.getMiningBlock(Material.COAL_ORE); + simulateMining(player, coal); + + assertEquals(1, player.getLevel().getOrdinal()); + assertEquals(5, player.getXp(), "Should carry 5 XP overflow into next level"); + } + + @Test + void miningFlow_highXpBlock_shouldCauseMultiLevelJump() { + MiningPlayer player = new MiningPlayer(UUID.randomUUID(), 0, 0); + // Diamond gives 1000 XP. + // Level 0โ†’1: 100 XP (overflow 900) + // Level 1โ†’2: 300 XP (overflow 600) + // Level 2โ†’3: 600 XP (overflow 0) + // But player can't mine diamond at level 0 (minLevel 5), so use gold instead. + // Gold gives 100 XP. Start at level 2 (minLevel 2 for gold). + // Level 2โ†’3: need 600 XP. But that requires mining 6 gold ores. + // Let's just test with manual XP to verify multi-level jump logic. + player.setXp(0); + player.setLevel(0); + + // Simulate getting 1000 XP at once (e.g., admin command or multiple blocks) + addXpWithLevelUp(player, 1000); + + // 1000 XP should cascade: 100โ†’300โ†’600 = levels 0,1,2 consumed, overflow 0 + assertEquals(3, player.getLevel().getOrdinal(), + "1000 XP from level 0 should reach ordinal 3"); + assertEquals(0, player.getXp(), "Should have 0 overflow after consuming exactly 1000 XP"); + } + + @Test + void miningFlow_exactlyEnoughXpForOneLevel_shouldLevelUpWithZeroOverflow() { + MiningPlayer player = new MiningPlayer(UUID.randomUUID(), 0, 0); + addXpWithLevelUp(player, 100); // exactly level 0 threshold + + assertEquals(1, player.getLevel().getOrdinal()); + assertEquals(0, player.getXp()); + } + + @Test + void miningFlow_oneLessXpThanNeeded_shouldNotLevelUp() { + MiningPlayer player = new MiningPlayer(UUID.randomUUID(), 0, 0); + addXpWithLevelUp(player, 99); + + assertEquals(0, player.getLevel().getOrdinal()); + assertEquals(99, player.getXp()); + } + + @Test + void miningFlow_atMaxLevel_shouldNotLevelUpFurther() { + int maxOrdinal = MiningLevel.getMaxLevel().getOrdinal(); + MiningPlayer player = new MiningPlayer(UUID.randomUUID(), maxOrdinal, 0); + addXpWithLevelUp(player, 999999); + + assertEquals(maxOrdinal, player.getLevel().getOrdinal(), "Should stay at max level"); + assertEquals(999999, player.getXp(), "XP should accumulate without leveling"); + } + + // ---- Progressive unlock through mining ---- + + @Test + void progressiveUnlock_miningCoalShouldEventuallyUnlockIronOre() { + MiningPlayer player = new MiningPlayer(UUID.randomUUID(), 0, 0); + MiningBlock coal = MiningBlock.getMiningBlock(Material.COAL_ORE); + assertFalse(canMine(player, Material.IRON_ORE), "Should not mine iron at level 0"); + + // Mine coal until level 1 (iron requires minLevel 1) + // Level 0 threshold: 100 XP, coal gives 10 XP โ†’ 10 coal ores + for (int i = 0; i < 10; i++) { + simulateMining(player, coal); + } + + assertEquals(1, player.getLevel().getOrdinal()); + assertTrue(canMine(player, Material.IRON_ORE), "Should mine iron at level 1"); + } + + @Test + void progressiveUnlock_shouldUnlockBlocksAtCorrectLevels() { + MiningPlayer player = new MiningPlayer(UUID.randomUUID(), 0, 0); + + // Track which level each block becomes accessible + // minLevel 0: STONE, COAL_ORE, REDSTONE_ORE + // minLevel 1: IRON_ORE + // minLevel 2: GOLD_ORE, COPPER_ORE + // minLevel 3: LAPIS_ORE, EMERALD_ORE + // minLevel 5: DIAMOND_ORE + + int[] thresholds = {100, 300, 600, 1000, 2000, 6000}; + Material[][] unlocksByLevel = { + {}, // nothing new at level 0โ†’1 transition besides iron + {Material.IRON_ORE, Material.DEEPSLATE_IRON_ORE}, + {Material.GOLD_ORE, Material.COPPER_ORE}, + {Material.LAPIS_ORE, Material.EMERALD_ORE}, + {}, // nothing new at level 4 + {Material.DIAMOND_ORE, Material.DEEPSLATE_DIAMOND_ORE} + }; + + for (int targetLevel = 1; targetLevel <= 5; targetLevel++) { + // Advance to target level + while (player.getLevel().getOrdinal() < targetLevel) { + addXpWithLevelUp(player, player.getLevel().getNextLevelXP() - player.getXp()); + } + + for (Material mat : unlocksByLevel[targetLevel]) { + assertTrue(canMine(player, mat), + mat.name() + " should be accessible at level " + targetLevel); + } + } + } + + // ---- Full progression simulation ---- + + @Test + void fullProgression_shouldReachMaxLevelByMiningEnoughBlocks() { + MiningPlayer player = new MiningPlayer(UUID.randomUUID(), 0, 0); + int maxOrdinal = MiningLevel.getMaxLevel().getOrdinal(); + + // Progress through all levels using available blocks + while (player.getLevel().getOrdinal() < maxOrdinal) { + // Find the highest-XP block the player can mine + MiningBlock bestBlock = getBestMineableBlock(player); + assertNotNull(bestBlock, + "Player at level " + player.getLevel().getOrdinal() + + " should always have at least one mineable block"); + simulateMining(player, bestBlock); + } + + assertEquals(maxOrdinal, player.getLevel().getOrdinal()); + } + + @Test + void fullProgression_levelNavigationShouldWorkThroughEntireChain() { + // Test that getNext()/getBefore() chains work across all default levels + MiningLevel first = MiningLevel.get(0); + assertNotNull(first); + + // Walk forward + MiningLevel current = first; + for (int i = 1; i < MiningLevel.miningLevels.size(); i++) { + MiningLevel next = current.getNext(); + assertEquals(i, next.getOrdinal(), "getNext() from ordinal " + (i - 1)); + current = next; + } + // At max level, getNext() should return self + assertSame(current, current.getNext(), "getNext() at max level should return self"); + + // Walk backward + for (int i = MiningLevel.miningLevels.size() - 2; i >= 0; i--) { + MiningLevel before = current.getBefore(); + assertEquals(i, before.getOrdinal(), "getBefore() from ordinal " + (i + 1)); + current = before; + } + // At min level, getBefore() should return self + assertSame(current, current.getBefore(), "getBefore() at min level should return self"); + } + + // ---- Multi-player scenarios ---- + + @Test + void multiPlayer_independentProgression() { + MiningPlayer alice = new MiningPlayer(UUID.randomUUID(), 0, 0); + MiningPlayer bob = new MiningPlayer(UUID.randomUUID(), 0, 0); + + MiningBlock coal = MiningBlock.getMiningBlock(Material.COAL_ORE); + // Alice mines 10 coal (100 XP โ†’ level 1) + for (int i = 0; i < 10; i++) { + simulateMining(alice, coal); + } + + assertEquals(1, alice.getLevel().getOrdinal()); + assertEquals(0, bob.getLevel().getOrdinal(), "Bob should still be level 0"); + assertEquals(0, bob.getXp(), "Bob should still have 0 XP"); + } + + @Test + void multiPlayer_lookupByUUID_shouldReturnCorrectPlayer() { + UUID aliceId = UUID.randomUUID(); + UUID bobId = UUID.randomUUID(); + MiningPlayer alice = new MiningPlayer(aliceId, 0, 0); + MiningPlayer bob = new MiningPlayer(bobId, 3, 500); + + assertSame(alice, MiningPlayer.getMiningPlayer(aliceId)); + assertSame(bob, MiningPlayer.getMiningPlayer(bobId)); + assertEquals(0, MiningPlayer.getMiningPlayer(aliceId).getLevel().getOrdinal()); + assertEquals(3, MiningPlayer.getMiningPlayer(bobId).getLevel().getOrdinal()); + } + + // ---- XP values from defaults ---- + + @Test + void defaultXpValues_shouldMatchExpectedHierarchy() { + // Diamond > Gold > Iron > Coal/Emerald > Copper > Stone/Redstone/Lapis + int diamondXp = MiningBlock.getMiningBlock(Material.DIAMOND_ORE).getXp(); + int goldXp = MiningBlock.getMiningBlock(Material.GOLD_ORE).getXp(); + int ironXp = MiningBlock.getMiningBlock(Material.IRON_ORE).getXp(); + int coalXp = MiningBlock.getMiningBlock(Material.COAL_ORE).getXp(); + int copperXp = MiningBlock.getMiningBlock(Material.COPPER_ORE).getXp(); + int stoneXp = MiningBlock.getMiningBlock(Material.STONE).getXp(); + + assertTrue(diamondXp > goldXp, "Diamond should give more XP than gold"); + assertTrue(goldXp > ironXp, "Gold should give more XP than iron"); + assertTrue(ironXp > coalXp, "Iron should give more XP than coal"); + assertTrue(coalXp > copperXp, "Coal should give more XP than copper"); + assertTrue(copperXp > stoneXp, "Copper should give more XP than stone"); + } + + @Test + void defaultBlocks_allMinLevelsShouldMapToExistingLevels() { + for (MiningBlock block : MiningBlock.miningBlocks) { + MiningLevel requiredLevel = MiningLevel.get(block.getMinLevel()); + assertNotNull(requiredLevel, + "Block with minLevel " + block.getMinLevel() + + " should map to an existing MiningLevel"); + } + } + + // ---- Helpers ---- + + /** + * Simulates the core mining flow: checks level requirement, adds XP, and + * triggers level-up if threshold is reached. Replicates the state-transition + * logic from {@code MiningEvents.onBlockBreak()}, {@code MiningPlayer.alterXp()}, + * {@code MiningPlayer.xpChange()}, and {@code MiningLevel.levelUp()}. + * + * @return true if the block was mined, false if level was too low + */ + private boolean simulateMining(MiningPlayer player, MiningBlock block) { + // Level check (from MiningEvents.onBlockBreak, line 76) + if (player.getLevel().getOrdinal() < block.getMinLevel()) { + return false; + } + // XP addition (from MiningPlayer.alterXp, line 118) + addXpWithLevelUp(player, block.getXp()); + return true; + } + + /** + * Adds XP and processes level-ups. Replicates the logic from + * {@code MiningPlayer.xpChange()} (line 149) and + * {@code MiningLevel.levelUp()} (lines 240-242). + */ + private void addXpWithLevelUp(MiningPlayer player, int xp) { + player.setXp(player.getXp() + xp); + // Level-up loop (from xpChange line 149 + levelUp lines 240-242) + while (player.getXp() >= player.getLevel().getNextLevelXP() + && player.getLevel().getOrdinal() + 1 < MiningLevel.miningLevels.size()) { + int overflow = player.getXp() - player.getLevel().getNextLevelXP(); + player.setLevel(player.getLevel().getNext()); + player.setXp(overflow); + } + } + + private boolean canMine(MiningPlayer player, Material material) { + MiningBlock block = MiningBlock.getMiningBlock(material); + return block != null && player.getLevel().getOrdinal() >= block.getMinLevel(); + } + + private Set getAccessibleMaterials(MiningPlayer player) { + Set accessible = new HashSet<>(); + for (MiningBlock block : MiningBlock.miningBlocks) { + if (player.getLevel().getOrdinal() >= block.getMinLevel()) { + accessible.addAll(block.getMaterials()); + } + } + return accessible; + } + + private MiningBlock getBestMineableBlock(MiningPlayer player) { + MiningBlock best = null; + for (MiningBlock block : MiningBlock.miningBlocks) { + if (player.getLevel().getOrdinal() >= block.getMinLevel()) { + if (best == null || block.getXp() > best.getXp()) { + best = block; + } + } + } + return best; + } + + private void loadDefaultLevels() { + try (InputStream is = getClass().getResourceAsStream("/defaults/levels.json")) { + assertNotNull(is, "levels.json not found on classpath"); + try (Reader reader = new InputStreamReader(is)) { + ArrayList levels = new Gson().fromJson(reader, + new TypeToken>() {}.getType()); + MiningLevel.miningLevels.addAll(levels); + } + } catch (IOException e) { + fail("Failed to load levels.json: " + e.getMessage()); + } + } + + private void loadDefaultBlocks() { + try (InputStream is = getClass().getResourceAsStream("/defaults/blocks.json")) { + assertNotNull(is, "blocks.json not found on classpath"); + try (Reader reader = new InputStreamReader(is)) { + ArrayList blocks = new Gson().fromJson(reader, + new TypeToken>() {}.getType()); + MiningBlock.miningBlocks.addAll(blocks); + } + } catch (IOException e) { + fail("Failed to load blocks.json: " + e.getMessage()); + } + } +} diff --git a/src/test/java/de/chafficplugins/mininglevels/api/MiningLevelTest.java b/src/test/java/de/chafficplugins/mininglevels/api/MiningLevelTest.java index 6112687..a155220 100644 --- a/src/test/java/de/chafficplugins/mininglevels/api/MiningLevelTest.java +++ b/src/test/java/de/chafficplugins/mininglevels/api/MiningLevelTest.java @@ -1,11 +1,15 @@ package de.chafficplugins.mininglevels.api; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockbukkit.mockbukkit.MockBukkit; +import java.util.ArrayList; + import static org.junit.jupiter.api.Assertions.*; class MiningLevelTest { @@ -33,6 +37,8 @@ void setUp() { MiningLevel.miningLevels.add(level3); } + // --- get() --- + @Test void get_shouldReturnCorrectLevel() { MiningLevel level = MiningLevel.get(0); @@ -41,12 +47,33 @@ void get_shouldReturnCorrectLevel() { assertEquals(0, level.getOrdinal()); } + @Test + void get_eachOrdinal_shouldReturnCorrectLevel() { + assertEquals("Beginner", MiningLevel.get(0).getName()); + assertEquals("Apprentice", MiningLevel.get(1).getName()); + assertEquals("Expert", MiningLevel.get(2).getName()); + assertEquals("Master", MiningLevel.get(3).getName()); + } + @Test void get_shouldReturnNullForInvalidOrdinal() { assertNull(MiningLevel.get(99)); assertNull(MiningLevel.get(-1)); } + @Test + void get_shouldReturnNullForOrdinalJustOutOfRange() { + assertNull(MiningLevel.get(4)); + } + + @Test + void get_emptyList_shouldReturnNull() { + MiningLevel.miningLevels.clear(); + assertNull(MiningLevel.get(0)); + } + + // --- getNext() --- + @Test void getNext_shouldReturnNextLevel() { MiningLevel level = MiningLevel.get(0); @@ -65,6 +92,35 @@ void getNext_atMaxLevel_shouldReturnSelf() { assertEquals(maxLevel, next); } + @Test + void getNext_chainThroughAllLevels() { + MiningLevel level = MiningLevel.get(0); + assertNotNull(level); + assertEquals("Beginner", level.getName()); + + level = level.getNext(); + assertEquals("Apprentice", level.getName()); + + level = level.getNext(); + assertEquals("Expert", level.getName()); + + level = level.getNext(); + assertEquals("Master", level.getName()); + + // at max, getNext returns self + level = level.getNext(); + assertEquals("Master", level.getName()); + } + + @Test + void getNext_secondToLast_shouldReturnLast() { + MiningLevel expert = MiningLevel.get(2); + assertNotNull(expert); + assertEquals("Master", expert.getNext().getName()); + } + + // --- getBefore() --- + @Test void getBefore_shouldReturnPreviousLevel() { MiningLevel level = MiningLevel.get(2); @@ -83,6 +139,35 @@ void getBefore_atMinLevel_shouldReturnSelf() { assertEquals(minLevel, before); } + @Test + void getBefore_chainThroughAllLevels() { + MiningLevel level = MiningLevel.get(3); + assertNotNull(level); + assertEquals("Master", level.getName()); + + level = level.getBefore(); + assertEquals("Expert", level.getName()); + + level = level.getBefore(); + assertEquals("Apprentice", level.getName()); + + level = level.getBefore(); + assertEquals("Beginner", level.getName()); + + // at min, getBefore returns self + level = level.getBefore(); + assertEquals("Beginner", level.getName()); + } + + @Test + void getBefore_secondLevel_shouldReturnFirst() { + MiningLevel apprentice = MiningLevel.get(1); + assertNotNull(apprentice); + assertEquals("Beginner", apprentice.getBefore().getName()); + } + + // --- getNextLevelXP / setNextLevelXP --- + @Test void getNextLevelXP_shouldReturnCorrectValue() { MiningLevel level = MiningLevel.get(0); @@ -90,6 +175,14 @@ void getNextLevelXP_shouldReturnCorrectValue() { assertEquals(100, level.getNextLevelXP()); } + @Test + void getNextLevelXP_eachLevel_shouldReturnCorrectValue() { + assertEquals(100, MiningLevel.get(0).getNextLevelXP()); + assertEquals(300, MiningLevel.get(1).getNextLevelXP()); + assertEquals(500, MiningLevel.get(2).getNextLevelXP()); + assertEquals(1000, MiningLevel.get(3).getNextLevelXP()); + } + @Test void setNextLevelXP_shouldUpdateValue() { MiningLevel level = MiningLevel.get(0); @@ -98,6 +191,24 @@ void setNextLevelXP_shouldUpdateValue() { assertEquals(200, level.getNextLevelXP()); } + @Test + void setNextLevelXP_toZero_shouldWork() { + MiningLevel level = MiningLevel.get(0); + assertNotNull(level); + level.setNextLevelXP(0); + assertEquals(0, level.getNextLevelXP()); + } + + @Test + void setNextLevelXP_toLargeValue_shouldWork() { + MiningLevel level = MiningLevel.get(0); + assertNotNull(level); + level.setNextLevelXP(Integer.MAX_VALUE); + assertEquals(Integer.MAX_VALUE, level.getNextLevelXP()); + } + + // --- getMaxLevel --- + @Test void getMaxLevel_shouldReturnLastLevel() { MiningLevel maxLevel = MiningLevel.getMaxLevel(); @@ -106,6 +217,26 @@ void getMaxLevel_shouldReturnLastLevel() { assertEquals(3, maxLevel.getOrdinal()); } + @Test + void getMaxLevel_singleLevel_shouldReturnThatLevel() { + MiningLevel.miningLevels.clear(); + MiningLevel only = new MiningLevel("Only", 100, 0); + MiningLevel.miningLevels.add(only); + assertEquals(only, MiningLevel.getMaxLevel()); + } + + @Test + void getMaxLevel_twoLevels_shouldReturnSecond() { + MiningLevel.miningLevels.clear(); + MiningLevel first = new MiningLevel("First", 100, 0); + MiningLevel second = new MiningLevel("Second", 200, 1); + MiningLevel.miningLevels.add(first); + MiningLevel.miningLevels.add(second); + assertEquals(second, MiningLevel.getMaxLevel()); + } + + // --- equals --- + @Test void equals_sameLevels_shouldBeEqual() { MiningLevel level1 = MiningLevel.get(0); @@ -120,6 +251,32 @@ void equals_differentLevels_shouldNotBeEqual() { assertNotEquals(level1, level2); } + @Test + void equals_null_shouldReturnFalse() { + MiningLevel level = MiningLevel.get(0); + assertNotNull(level); + assertNotEquals(level, null); + } + + @Test + void equals_differentType_shouldReturnFalse() { + MiningLevel level = MiningLevel.get(0); + assertNotNull(level); + assertNotEquals(level, "not a level"); + assertNotEquals(level, 42); + } + + @Test + void equals_sameOrdinalDifferentName_shouldBeEqual() { + MiningLevel.miningLevels.clear(); + MiningLevel a = new MiningLevel("LevelA", 100, 0); + MiningLevel b = new MiningLevel("LevelB", 200, 0); + // equals is based on ordinal only + assertEquals(a, b); + } + + // --- instantBreakProbability --- + @Test void instantBreakProbability_defaultIsZero() { MiningLevel level = MiningLevel.get(0); @@ -135,6 +292,23 @@ void setInstantBreakProbability_validRange() { assertEquals(50, level.getInstantBreakProbability()); } + @Test + void setInstantBreakProbability_zero_shouldWork() { + MiningLevel level = MiningLevel.get(0); + assertNotNull(level); + level.setInstantBreakProbability(50); + level.setInstantBreakProbability(0); + assertEquals(0, level.getInstantBreakProbability()); + } + + @Test + void setInstantBreakProbability_hundred_shouldWork() { + MiningLevel level = MiningLevel.get(0); + assertNotNull(level); + level.setInstantBreakProbability(100); + assertEquals(100, level.getInstantBreakProbability()); + } + @Test void setInstantBreakProbability_outOfRange_shouldNotChange() { MiningLevel level = MiningLevel.get(0); @@ -146,6 +320,16 @@ void setInstantBreakProbability_outOfRange_shouldNotChange() { assertEquals(50, level.getInstantBreakProbability()); } + @Test + void setInstantBreakProbability_fractional_shouldWork() { + MiningLevel level = MiningLevel.get(0); + assertNotNull(level); + level.setInstantBreakProbability(33.5f); + assertEquals(33.5f, level.getInstantBreakProbability()); + } + + // --- extraOreProbability --- + @Test void extraOreProbability_defaultIsZero() { MiningLevel level = MiningLevel.get(0); @@ -161,6 +345,23 @@ void setExtraOreProbability_validRange() { assertEquals(25, level.getExtraOreProbability()); } + @Test + void setExtraOreProbability_zero_shouldWork() { + MiningLevel level = MiningLevel.get(0); + assertNotNull(level); + level.setExtraOreProbability(50); + level.setExtraOreProbability(0); + assertEquals(0, level.getExtraOreProbability()); + } + + @Test + void setExtraOreProbability_hundred_shouldWork() { + MiningLevel level = MiningLevel.get(0); + assertNotNull(level); + level.setExtraOreProbability(100); + assertEquals(100, level.getExtraOreProbability()); + } + @Test void setExtraOreProbability_outOfRange_shouldNotChange() { MiningLevel level = MiningLevel.get(0); @@ -172,6 +373,16 @@ void setExtraOreProbability_outOfRange_shouldNotChange() { assertEquals(25, level.getExtraOreProbability()); } + @Test + void setExtraOreProbability_fractional_shouldWork() { + MiningLevel level = MiningLevel.get(0); + assertNotNull(level); + level.setExtraOreProbability(12.75f); + assertEquals(12.75f, level.getExtraOreProbability()); + } + + // --- maxExtraOre --- + @Test void maxExtraOre_defaultIsZero() { MiningLevel level = MiningLevel.get(0); @@ -187,6 +398,15 @@ void setMaxExtraOre_validValue() { assertEquals(5, level.getMaxExtraOre()); } + @Test + void setMaxExtraOre_zero_shouldWork() { + MiningLevel level = MiningLevel.get(0); + assertNotNull(level); + level.setMaxExtraOre(5); + level.setMaxExtraOre(0); + assertEquals(0, level.getMaxExtraOre()); + } + @Test void setMaxExtraOre_negative_shouldNotChange() { MiningLevel level = MiningLevel.get(0); @@ -196,6 +416,16 @@ void setMaxExtraOre_negative_shouldNotChange() { assertEquals(5, level.getMaxExtraOre()); } + @Test + void setMaxExtraOre_largeValue_shouldWork() { + MiningLevel level = MiningLevel.get(0); + assertNotNull(level); + level.setMaxExtraOre(999); + assertEquals(999, level.getMaxExtraOre()); + } + + // --- hasteLevel --- + @Test void hasteLevel_defaultIsZero() { MiningLevel level = MiningLevel.get(0); @@ -211,6 +441,15 @@ void setHasteLevel_validValue() { assertEquals(3, level.getHasteLevel()); } + @Test + void setHasteLevel_zero_shouldWork() { + MiningLevel level = MiningLevel.get(0); + assertNotNull(level); + level.setHasteLevel(3); + level.setHasteLevel(0); + assertEquals(0, level.getHasteLevel()); + } + @Test void setHasteLevel_negative_shouldNotChange() { MiningLevel level = MiningLevel.get(0); @@ -220,8 +459,185 @@ void setHasteLevel_negative_shouldNotChange() { assertEquals(3, level.getHasteLevel()); } + @Test + void setHasteLevel_largeValue_shouldWork() { + MiningLevel level = MiningLevel.get(0); + assertNotNull(level); + level.setHasteLevel(255); + assertEquals(255, level.getHasteLevel()); + } + + // --- rewards --- + + @Test + void getRewards_default_shouldBeEmpty() { + MiningLevel level = MiningLevel.get(0); + assertNotNull(level); + ItemStack[] rewards = level.getRewards(); + assertNotNull(rewards); + assertEquals(0, rewards.length); + } + + @Test + void addReward_singleItem_shouldBeRetrievable() { + MiningLevel level = MiningLevel.get(0); + assertNotNull(level); + level.addReward(new ItemStack(Material.DIAMOND, 5)); + ItemStack[] rewards = level.getRewards(); + assertEquals(1, rewards.length); + assertEquals(Material.DIAMOND, rewards[0].getType()); + assertEquals(5, rewards[0].getAmount()); + } + + @Test + void addReward_multipleItems_shouldAllBeRetrievable() { + MiningLevel level = MiningLevel.get(0); + assertNotNull(level); + level.addReward(new ItemStack(Material.DIAMOND, 5)); + level.addReward(new ItemStack(Material.IRON_INGOT, 10)); + level.addReward(new ItemStack(Material.GOLD_INGOT, 3)); + + ItemStack[] rewards = level.getRewards(); + assertEquals(3, rewards.length); + assertEquals(Material.DIAMOND, rewards[0].getType()); + assertEquals(Material.IRON_INGOT, rewards[1].getType()); + assertEquals(Material.GOLD_INGOT, rewards[2].getType()); + } + + @Test + void setRewards_shouldReplaceExisting() { + MiningLevel level = MiningLevel.get(0); + assertNotNull(level); + level.addReward(new ItemStack(Material.DIAMOND, 5)); + assertEquals(1, level.getRewards().length); + + ArrayList newRewards = new ArrayList<>(); + newRewards.add(new ItemStack(Material.EMERALD, 20)); + newRewards.add(new ItemStack(Material.NETHERITE_INGOT, 1)); + level.setRewards(newRewards); + + ItemStack[] rewards = level.getRewards(); + assertEquals(2, rewards.length); + assertEquals(Material.EMERALD, rewards[0].getType()); + assertEquals(20, rewards[0].getAmount()); + assertEquals(Material.NETHERITE_INGOT, rewards[1].getType()); + } + + @Test + void setRewards_emptyList_shouldClearRewards() { + MiningLevel level = MiningLevel.get(0); + assertNotNull(level); + level.addReward(new ItemStack(Material.DIAMOND, 5)); + assertEquals(1, level.getRewards().length); + + level.setRewards(new ArrayList<>()); + assertEquals(0, level.getRewards().length); + } + + // --- getName(Reward) --- + + @Test + void getName_normalItem_shouldReturnMaterialName() { + Reward reward = new Reward(new ItemStack(Material.DIAMOND, 1)); + assertEquals("DIAMOND", MiningLevel.getName(reward)); + } + + @Test + void getName_differentMaterials_shouldReturnCorrectNames() { + assertEquals("IRON_INGOT", MiningLevel.getName(new Reward(new ItemStack(Material.IRON_INGOT, 1)))); + assertEquals("GOLD_INGOT", MiningLevel.getName(new Reward(new ItemStack(Material.GOLD_INGOT, 1)))); + assertEquals("EMERALD", MiningLevel.getName(new Reward(new ItemStack(Material.EMERALD, 1)))); + } + + // --- constructor --- + + @Test + void constructor_shouldStoreNameAndOrdinal() { + MiningLevel level = new MiningLevel("TestLevel", 500, 99); + assertEquals("TestLevel", level.getName()); + assertEquals(500, level.getNextLevelXP()); + assertEquals(99, level.getOrdinal()); + } + + @Test + void constructor_emptyName_shouldWork() { + MiningLevel level = new MiningLevel("", 100, 0); + assertEquals("", level.getName()); + } + + @Test + void constructor_zeroXP_shouldWork() { + MiningLevel level = new MiningLevel("Zero", 0, 0); + assertEquals(0, level.getNextLevelXP()); + } + + // --- list size --- + @Test void levelListSize_shouldBeFour() { assertEquals(4, MiningLevel.miningLevels.size()); } + + // --- navigation with single level --- + + @Test + void singleLevel_getNext_shouldReturnSelf() { + MiningLevel.miningLevels.clear(); + MiningLevel only = new MiningLevel("Only", 100, 0); + MiningLevel.miningLevels.add(only); + assertEquals(only, only.getNext()); + } + + @Test + void singleLevel_getBefore_shouldReturnSelf() { + MiningLevel.miningLevels.clear(); + MiningLevel only = new MiningLevel("Only", 100, 0); + MiningLevel.miningLevels.add(only); + assertEquals(only, only.getBefore()); + } + + // --- skills are independent per level --- + + @Test + void allSkillProperties_independentPerLevel() { + MiningLevel level0 = MiningLevel.get(0); + MiningLevel level1 = MiningLevel.get(1); + assertNotNull(level0); + assertNotNull(level1); + + level0.setHasteLevel(1); + level0.setInstantBreakProbability(10); + level0.setExtraOreProbability(5); + level0.setMaxExtraOre(2); + + level1.setHasteLevel(3); + level1.setInstantBreakProbability(25); + level1.setExtraOreProbability(15); + level1.setMaxExtraOre(5); + + assertEquals(1, level0.getHasteLevel()); + assertEquals(10, level0.getInstantBreakProbability()); + assertEquals(5, level0.getExtraOreProbability()); + assertEquals(2, level0.getMaxExtraOre()); + + assertEquals(3, level1.getHasteLevel()); + assertEquals(25, level1.getInstantBreakProbability()); + assertEquals(15, level1.getExtraOreProbability()); + assertEquals(5, level1.getMaxExtraOre()); + } + + @Test + void rewards_independentPerLevel() { + MiningLevel level0 = MiningLevel.get(0); + MiningLevel level1 = MiningLevel.get(1); + assertNotNull(level0); + assertNotNull(level1); + + level0.addReward(new ItemStack(Material.DIAMOND, 1)); + level1.addReward(new ItemStack(Material.EMERALD, 5)); + level1.addReward(new ItemStack(Material.GOLD_INGOT, 3)); + + assertEquals(1, level0.getRewards().length); + assertEquals(2, level1.getRewards().length); + } } diff --git a/src/test/java/de/chafficplugins/mininglevels/api/MiningPlayerTest.java b/src/test/java/de/chafficplugins/mininglevels/api/MiningPlayerTest.java index ed3044d..44a2299 100644 --- a/src/test/java/de/chafficplugins/mininglevels/api/MiningPlayerTest.java +++ b/src/test/java/de/chafficplugins/mininglevels/api/MiningPlayerTest.java @@ -1,10 +1,14 @@ package de.chafficplugins.mininglevels.api; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockbukkit.mockbukkit.MockBukkit; +import org.mockbukkit.mockbukkit.ServerMock; +import org.mockbukkit.mockbukkit.entity.PlayerMock; import java.util.UUID; @@ -12,11 +16,12 @@ class MiningPlayerTest { + private static ServerMock server; private UUID playerUUID; @BeforeAll static void setUpServer() { - MockBukkit.mock(); + server = MockBukkit.mock(); } @AfterAll @@ -34,6 +39,8 @@ void setUp() { playerUUID = UUID.randomUUID(); } + // --- constructor --- + @Test void constructor_shouldCreatePlayer() { MiningPlayer player = new MiningPlayer(playerUUID, 0, 0); @@ -57,6 +64,27 @@ void constructor_duplicatePlayer_shouldThrow() { new MiningPlayer(playerUUID, 0, 0)); } + @Test + void constructor_withXp_shouldStoreXp() { + MiningPlayer player = new MiningPlayer(playerUUID, 0, 75); + assertEquals(75, player.getXp()); + } + + @Test + void constructor_withHigherLevel_shouldStoreLevel() { + MiningPlayer player = new MiningPlayer(playerUUID, 2, 0); + assertEquals("Expert", player.getLevel().getName()); + } + + @Test + void constructor_withLevelAndXp_shouldStoreBoth() { + MiningPlayer player = new MiningPlayer(playerUUID, 1, 150); + assertEquals("Apprentice", player.getLevel().getName()); + assertEquals(150, player.getXp()); + } + + // --- getMiningPlayer --- + @Test void getMiningPlayer_existingPlayer_shouldReturn() { new MiningPlayer(playerUUID, 0, 0); @@ -70,6 +98,27 @@ void getMiningPlayer_nonExistingPlayer_shouldReturnNull() { assertNull(MiningPlayer.getMiningPlayer(UUID.randomUUID())); } + @Test + void getMiningPlayer_afterMultipleCreations_shouldFindEach() { + UUID uuid1 = UUID.randomUUID(); + UUID uuid2 = UUID.randomUUID(); + UUID uuid3 = UUID.randomUUID(); + new MiningPlayer(uuid1, 0, 10); + new MiningPlayer(uuid2, 1, 20); + new MiningPlayer(uuid3, 2, 30); + + assertEquals(10, MiningPlayer.getMiningPlayer(uuid1).getXp()); + assertEquals(20, MiningPlayer.getMiningPlayer(uuid2).getXp()); + assertEquals(30, MiningPlayer.getMiningPlayer(uuid3).getXp()); + } + + @Test + void getMiningPlayer_emptyList_shouldReturnNull() { + assertNull(MiningPlayer.getMiningPlayer(UUID.randomUUID())); + } + + // --- notExists --- + @Test void notExists_newPlayer_shouldReturnTrue() { assertTrue(MiningPlayer.notExists(UUID.randomUUID())); @@ -81,6 +130,15 @@ void notExists_existingPlayer_shouldReturnFalse() { assertFalse(MiningPlayer.notExists(playerUUID)); } + @Test + void notExists_afterCreation_shouldReturnFalse() { + assertTrue(MiningPlayer.notExists(playerUUID)); + new MiningPlayer(playerUUID, 0, 0); + assertFalse(MiningPlayer.notExists(playerUUID)); + } + + // --- setXp --- + @Test void setXp_shouldSetWithoutLevelCheck() { MiningPlayer player = new MiningPlayer(playerUUID, 0, 0); @@ -93,8 +151,44 @@ void setXp_canSetAboveThreshold() { MiningPlayer player = new MiningPlayer(playerUUID, 0, 0); player.setXp(999); assertEquals(999, player.getXp()); + // Level should NOT change since setXp doesn't trigger level check + assertEquals("Beginner", player.getLevel().getName()); } + @Test + void setXp_zeroXp_shouldWork() { + MiningPlayer player = new MiningPlayer(playerUUID, 0, 50); + player.setXp(0); + assertEquals(0, player.getXp()); + } + + @Test + void setXp_negativeXp_shouldWork() { + MiningPlayer player = new MiningPlayer(playerUUID, 0, 50); + player.setXp(-10); + assertEquals(-10, player.getXp()); + } + + @Test + void setXp_largeValue_shouldWork() { + MiningPlayer player = new MiningPlayer(playerUUID, 0, 0); + player.setXp(Integer.MAX_VALUE); + assertEquals(Integer.MAX_VALUE, player.getXp()); + } + + @Test + void setXp_multipleTimes_shouldOverwrite() { + MiningPlayer player = new MiningPlayer(playerUUID, 0, 0); + player.setXp(10); + assertEquals(10, player.getXp()); + player.setXp(50); + assertEquals(50, player.getXp()); + player.setXp(0); + assertEquals(0, player.getXp()); + } + + // --- setLevel --- + @Test void setLevel_byOrdinal_shouldUpdateLevel() { MiningPlayer player = new MiningPlayer(playerUUID, 0, 0); @@ -111,6 +205,40 @@ void setLevel_byMiningLevel_shouldUpdateLevel() { assertEquals("Expert", player.getLevel().getName()); } + @Test + void setLevel_toZero_shouldWork() { + MiningPlayer player = new MiningPlayer(playerUUID, 2, 0); + player.setLevel(0); + assertEquals("Beginner", player.getLevel().getName()); + } + + @Test + void setLevel_toMaxLevel_shouldWork() { + MiningPlayer player = new MiningPlayer(playerUUID, 0, 0); + player.setLevel(2); + assertEquals("Expert", player.getLevel().getName()); + } + + @Test + void setLevel_multipleTimes_shouldUpdateEachTime() { + MiningPlayer player = new MiningPlayer(playerUUID, 0, 0); + player.setLevel(1); + assertEquals("Apprentice", player.getLevel().getName()); + player.setLevel(2); + assertEquals("Expert", player.getLevel().getName()); + player.setLevel(0); + assertEquals("Beginner", player.getLevel().getName()); + } + + @Test + void setLevel_doesNotAffectXp() { + MiningPlayer player = new MiningPlayer(playerUUID, 0, 50); + player.setLevel(2); + assertEquals(50, player.getXp()); + } + + // --- getLevel --- + @Test void getLevel_shouldReturnCorrectMiningLevel() { MiningPlayer player = new MiningPlayer(playerUUID, 1, 50); @@ -120,10 +248,88 @@ void getLevel_shouldReturnCorrectMiningLevel() { assertEquals(1, level.getOrdinal()); } + @Test + void getLevel_afterSetLevel_shouldReflectChange() { + MiningPlayer player = new MiningPlayer(playerUUID, 0, 0); + assertEquals("Beginner", player.getLevel().getName()); + player.setLevel(2); + assertEquals("Expert", player.getLevel().getName()); + } + + // --- getPlayer / getOfflinePlayer --- + + @Test + void getPlayer_onlinePlayer_shouldReturnPlayer() { + PlayerMock mockPlayer = server.addPlayer(); + MiningPlayer mp = new MiningPlayer(mockPlayer.getUniqueId(), 0, 0); + assertNotNull(mp.getPlayer()); + assertEquals(mockPlayer.getUniqueId(), mp.getPlayer().getUniqueId()); + } + + @Test + void getOfflinePlayer_shouldReturnOfflinePlayer() { + MiningPlayer mp = new MiningPlayer(playerUUID, 0, 0); + assertNotNull(mp.getOfflinePlayer()); + assertEquals(playerUUID, mp.getOfflinePlayer().getUniqueId()); + } + + // --- addRewards --- + + @Test + void addRewards_singleReward_shouldBeStored() { + PlayerMock mockPlayer = server.addPlayer(); + MiningPlayer mp = new MiningPlayer(mockPlayer.getUniqueId(), 0, 0); + mp.addRewards(new ItemStack(Material.DIAMOND, 5)); + // claim returns 1 if rewards were claimed + int result = mp.claim(); + assertEquals(1, result); + } + + @Test + void addRewards_multipleRewards_shouldAllBeStored() { + PlayerMock mockPlayer = server.addPlayer(); + MiningPlayer mp = new MiningPlayer(mockPlayer.getUniqueId(), 0, 0); + mp.addRewards( + new ItemStack(Material.DIAMOND, 5), + new ItemStack(Material.IRON_INGOT, 10), + new ItemStack(Material.GOLD_INGOT, 3) + ); + int result = mp.claim(); + assertEquals(1, result); + } + + // --- claim --- + + @Test + void claim_noRewards_shouldReturnZero() { + PlayerMock mockPlayer = server.addPlayer(); + MiningPlayer mp = new MiningPlayer(mockPlayer.getUniqueId(), 0, 0); + assertEquals(0, mp.claim()); + } + + @Test + void claim_withRewards_shouldReturnOne() { + PlayerMock mockPlayer = server.addPlayer(); + MiningPlayer mp = new MiningPlayer(mockPlayer.getUniqueId(), 0, 0); + mp.addRewards(new ItemStack(Material.DIAMOND, 1)); + assertEquals(1, mp.claim()); + } + + @Test + void claim_afterClaiming_shouldReturnZero() { + PlayerMock mockPlayer = server.addPlayer(); + MiningPlayer mp = new MiningPlayer(mockPlayer.getUniqueId(), 0, 0); + mp.addRewards(new ItemStack(Material.DIAMOND, 1)); + assertEquals(1, mp.claim()); + // no more rewards + assertEquals(0, mp.claim()); + } + + // --- equals --- + @Test void equals_sameUUID_shouldBeEqual() { MiningPlayer player1 = new MiningPlayer(playerUUID, 0, 0); - // Can't create another with same UUID (throws), so test self-equality assertEquals(player1, player1); } @@ -140,6 +346,24 @@ void equals_nonMiningPlayerObject_shouldReturnFalse() { assertNotEquals(player, "not a player"); } + @Test + void equals_null_shouldReturnFalse() { + MiningPlayer player = new MiningPlayer(playerUUID, 0, 0); + assertNotEquals(player, null); + } + + @Test + void equals_sameUUIDDifferentLevelAndXp_shouldBeEqual() { + // equals is UUID-based, so same UUID = equal regardless of level/xp + // But we can't create two with the same UUID, so test via self-equality + MiningPlayer player = new MiningPlayer(playerUUID, 0, 0); + player.setLevel(2); + player.setXp(999); + assertEquals(player, player); + } + + // --- multiplePlayers --- + @Test void multiplePlayers_shouldAllBeTracked() { UUID uuid1 = UUID.randomUUID(); @@ -156,6 +380,27 @@ void multiplePlayers_shouldAllBeTracked() { assertNotNull(MiningPlayer.getMiningPlayer(uuid3)); } + @Test + void multiplePlayers_eachHasCorrectState() { + UUID uuid1 = UUID.randomUUID(); + UUID uuid2 = UUID.randomUUID(); + + new MiningPlayer(uuid1, 0, 10); + new MiningPlayer(uuid2, 2, 400); + + MiningPlayer p1 = MiningPlayer.getMiningPlayer(uuid1); + MiningPlayer p2 = MiningPlayer.getMiningPlayer(uuid2); + + assertNotNull(p1); + assertNotNull(p2); + assertEquals("Beginner", p1.getLevel().getName()); + assertEquals(10, p1.getXp()); + assertEquals("Expert", p2.getLevel().getName()); + assertEquals(400, p2.getXp()); + } + + // --- level boundary scenarios --- + @Test void playerWithLevel0_shouldHaveBeginnerLevel() { MiningPlayer player = new MiningPlayer(playerUUID, 0, 0); @@ -169,10 +414,52 @@ void playerWithMaxLevel_shouldHaveMaxLevel() { assertEquals("Expert", player.getLevel().getName()); } + // --- XP and level combinations --- + @Test - void setXp_zeroXp_shouldWork() { - MiningPlayer player = new MiningPlayer(playerUUID, 0, 50); - player.setXp(0); - assertEquals(0, player.getXp()); + void setXp_doesNotChangeLevel() { + MiningPlayer player = new MiningPlayer(playerUUID, 0, 0); + player.setXp(9999); + assertEquals("Beginner", player.getLevel().getName()); + } + + @Test + void setLevel_doesNotChangeXp() { + MiningPlayer player = new MiningPlayer(playerUUID, 0, 75); + player.setLevel(2); + assertEquals(75, player.getXp()); + assertEquals("Expert", player.getLevel().getName()); + } + + @Test + void player_setAndGetUUID_shouldBeConsistent() { + MiningPlayer player = new MiningPlayer(playerUUID, 0, 0); + UUID retrieved = player.getUUID(); + assertEquals(playerUUID, retrieved); + assertSame(playerUUID, retrieved); + } + + // --- many players stress --- + + @Test + void manyPlayers_shouldAllBeTrackable() { + for (int i = 0; i < 100; i++) { + new MiningPlayer(UUID.randomUUID(), i % 3, i); + } + assertEquals(100, MiningPlayer.miningPlayers.size()); + } + + @Test + void manyPlayers_lookupShouldFindAll() { + UUID[] uuids = new UUID[50]; + for (int i = 0; i < 50; i++) { + uuids[i] = UUID.randomUUID(); + new MiningPlayer(uuids[i], i % 3, i * 10); + } + for (int i = 0; i < 50; i++) { + MiningPlayer found = MiningPlayer.getMiningPlayer(uuids[i]); + assertNotNull(found); + assertEquals(i * 10, found.getXp()); + } } } diff --git a/src/test/java/de/chafficplugins/mininglevels/api/RewardTest.java b/src/test/java/de/chafficplugins/mininglevels/api/RewardTest.java index 3fc2e18..b82292c 100644 --- a/src/test/java/de/chafficplugins/mininglevels/api/RewardTest.java +++ b/src/test/java/de/chafficplugins/mininglevels/api/RewardTest.java @@ -24,6 +24,8 @@ static void tearDown() { MockBukkit.unmock(); } + // --- constructor --- + @Test void constructor_shouldStoreTypeAndAmount() { ItemStack item = new ItemStack(Material.DIAMOND, 10); @@ -31,6 +33,22 @@ void constructor_shouldStoreTypeAndAmount() { assertEquals(10, reward.getAmount()); } + @Test + void constructor_singleItem_shouldHaveAmountOne() { + ItemStack item = new ItemStack(Material.EMERALD, 1); + Reward reward = new Reward(item); + assertEquals(1, reward.getAmount()); + } + + @Test + void constructor_largeAmount_shouldStore() { + ItemStack item = new ItemStack(Material.DIAMOND, 64); + Reward reward = new Reward(item); + assertEquals(64, reward.getAmount()); + } + + // --- getItemStack --- + @Test void getItemStack_shouldReturnCorrectMaterialAndAmount() { ItemStack item = new ItemStack(Material.IRON_INGOT, 5); @@ -41,10 +59,94 @@ void getItemStack_shouldReturnCorrectMaterialAndAmount() { } @Test - void getItemStack_singleItem_shouldHaveAmountOne() { - ItemStack item = new ItemStack(Material.EMERALD, 1); + void getItemStack_shouldReturnNewInstance() { + ItemStack item = new ItemStack(Material.DIAMOND, 10); Reward reward = new Reward(item); - assertEquals(1, reward.getAmount()); + ItemStack result1 = reward.getItemStack(); + ItemStack result2 = reward.getItemStack(); + // Each call returns a new instance + assertNotSame(result1, result2); + // But they should be equal in content + assertEquals(result1.getType(), result2.getType()); + assertEquals(result1.getAmount(), result2.getAmount()); + } + + // --- different material types --- + + @Test + void reward_diamond_shouldPreserveMaterial() { + Reward reward = new Reward(new ItemStack(Material.DIAMOND, 3)); + assertEquals(Material.DIAMOND, reward.getItemStack().getType()); + assertEquals(3, reward.getAmount()); + } + + @Test + void reward_ironIngot_shouldPreserveMaterial() { + Reward reward = new Reward(new ItemStack(Material.IRON_INGOT, 16)); + assertEquals(Material.IRON_INGOT, reward.getItemStack().getType()); + assertEquals(16, reward.getAmount()); + } + + @Test + void reward_goldIngot_shouldPreserveMaterial() { + Reward reward = new Reward(new ItemStack(Material.GOLD_INGOT, 8)); + assertEquals(Material.GOLD_INGOT, reward.getItemStack().getType()); + assertEquals(8, reward.getAmount()); + } + + @Test + void reward_emerald_shouldPreserveMaterial() { + Reward reward = new Reward(new ItemStack(Material.EMERALD, 32)); assertEquals(Material.EMERALD, reward.getItemStack().getType()); + assertEquals(32, reward.getAmount()); + } + + @Test + void reward_netheriteIngot_shouldPreserveMaterial() { + Reward reward = new Reward(new ItemStack(Material.NETHERITE_INGOT, 2)); + assertEquals(Material.NETHERITE_INGOT, reward.getItemStack().getType()); + assertEquals(2, reward.getAmount()); + } + + @Test + void reward_tool_shouldPreserveMaterial() { + Reward reward = new Reward(new ItemStack(Material.DIAMOND_PICKAXE, 1)); + assertEquals(Material.DIAMOND_PICKAXE, reward.getItemStack().getType()); + assertEquals(1, reward.getAmount()); + } + + @Test + void reward_block_shouldPreserveMaterial() { + Reward reward = new Reward(new ItemStack(Material.DIAMOND_BLOCK, 4)); + assertEquals(Material.DIAMOND_BLOCK, reward.getItemStack().getType()); + assertEquals(4, reward.getAmount()); + } + + // --- getAmount --- + + @Test + void getAmount_shouldMatchConstructorAmount() { + assertEquals(1, new Reward(new ItemStack(Material.DIAMOND, 1)).getAmount()); + assertEquals(16, new Reward(new ItemStack(Material.DIAMOND, 16)).getAmount()); + assertEquals(32, new Reward(new ItemStack(Material.DIAMOND, 32)).getAmount()); + assertEquals(64, new Reward(new ItemStack(Material.DIAMOND, 64)).getAmount()); + } + + // --- round-trip fidelity --- + + @Test + void roundTrip_materialAndAmount_shouldBePreserved() { + Material[] testMaterials = { + Material.DIAMOND, Material.EMERALD, Material.IRON_INGOT, + Material.GOLD_INGOT, Material.NETHERITE_INGOT, Material.COAL, + Material.LAPIS_LAZULI, Material.REDSTONE + }; + for (Material mat : testMaterials) { + ItemStack original = new ItemStack(mat, 7); + Reward reward = new Reward(original); + ItemStack result = reward.getItemStack(); + assertEquals(mat, result.getType(), "Material mismatch for " + mat.name()); + assertEquals(7, result.getAmount(), "Amount mismatch for " + mat.name()); + } } } diff --git a/src/test/java/de/chafficplugins/mininglevels/utils/ConfigStringsTest.java b/src/test/java/de/chafficplugins/mininglevels/utils/ConfigStringsTest.java index 05c72a0..4f80b9f 100644 --- a/src/test/java/de/chafficplugins/mininglevels/utils/ConfigStringsTest.java +++ b/src/test/java/de/chafficplugins/mininglevels/utils/ConfigStringsTest.java @@ -6,11 +6,48 @@ class ConfigStringsTest { + // --- version --- + @Test void crucialLibVersion_shouldBe300() { assertEquals("3.0.0", ConfigStrings.CRUCIAL_LIB_VERSION); } + // --- identifiers --- + + @Test + void localizedIdentifier_shouldBeMininglevels() { + assertEquals("mininglevels", ConfigStrings.LOCALIZED_IDENTIFIER); + } + + @Test + void spigotId_shouldBePositive() { + assertTrue(ConfigStrings.SPIGOT_ID > 0); + assertEquals(100886, ConfigStrings.SPIGOT_ID); + } + + @Test + void bstatsId_shouldBePositive() { + assertTrue(ConfigStrings.BSTATS_ID > 0); + assertEquals(14709, ConfigStrings.BSTATS_ID); + } + + // --- prefix --- + + @Test + void prefix_shouldHaveDefaultValue() { + assertNotNull(ConfigStrings.PREFIX); + assertTrue(ConfigStrings.PREFIX.contains("ML")); + } + + @Test + void prefix_shouldContainBrackets() { + assertTrue(ConfigStrings.PREFIX.contains("[")); + assertTrue(ConfigStrings.PREFIX.contains("]")); + } + + // --- permissions --- + @Test void permissions_shouldNotBeNull() { assertNotNull(ConfigStrings.PERMISSION_ADMIN); @@ -23,6 +60,56 @@ void permissions_shouldNotBeNull() { assertNotNull(ConfigStrings.PERMISSION_DEBUG); } + @Test + void permissionAdmin_shouldBeWildcard() { + assertEquals("mininglevels.*", ConfigStrings.PERMISSION_ADMIN); + } + + @Test + void permissions_shouldStartWithMininglevels() { + assertTrue(ConfigStrings.PERMISSION_ADMIN.startsWith("mininglevels.")); + assertTrue(ConfigStrings.PERMISSION_SET_LEVEL.startsWith("mininglevels.")); + assertTrue(ConfigStrings.PERMISSION_SET_XP.startsWith("mininglevels.")); + assertTrue(ConfigStrings.PERMISSION_LEVEL.startsWith("mininglevels.")); + assertTrue(ConfigStrings.PERMISSION_RELOAD.startsWith("mininglevels.")); + assertTrue(ConfigStrings.PERMISSION_EDITOR.startsWith("mininglevels.")); + assertTrue(ConfigStrings.PERMISSIONS_LEADERBOARD.startsWith("mininglevels.")); + assertTrue(ConfigStrings.PERMISSION_DEBUG.startsWith("mininglevels.")); + } + + @Test + void permissions_shouldHaveCorrectValues() { + assertEquals("mininglevels.setlevel", ConfigStrings.PERMISSION_SET_LEVEL); + assertEquals("mininglevels.setxp", ConfigStrings.PERMISSION_SET_XP); + assertEquals("mininglevels.level", ConfigStrings.PERMISSION_LEVEL); + assertEquals("mininglevels.reload", ConfigStrings.PERMISSION_RELOAD); + assertEquals("mininglevels.editor", ConfigStrings.PERMISSION_EDITOR); + assertEquals("mininglevels.leaderboard", ConfigStrings.PERMISSIONS_LEADERBOARD); + assertEquals("mininglevels.debug", ConfigStrings.PERMISSION_DEBUG); + } + + @Test + void permissions_shouldAllBeDistinct() { + String[] perms = { + ConfigStrings.PERMISSION_ADMIN, + ConfigStrings.PERMISSION_SET_LEVEL, + ConfigStrings.PERMISSION_SET_XP, + ConfigStrings.PERMISSION_LEVEL, + ConfigStrings.PERMISSION_RELOAD, + ConfigStrings.PERMISSION_EDITOR, + ConfigStrings.PERMISSIONS_LEADERBOARD, + ConfigStrings.PERMISSION_DEBUG + }; + for (int i = 0; i < perms.length; i++) { + for (int j = i + 1; j < perms.length; j++) { + assertNotEquals(perms[i], perms[j], + "Duplicate permission: " + perms[i]); + } + } + } + + // --- config keys --- + @Test void configKeys_shouldNotBeNull() { assertNotNull(ConfigStrings.LVL_UP_SOUND); @@ -35,6 +122,32 @@ void configKeys_shouldNotBeNull() { assertNotNull(ConfigStrings.ADMIN_DEBUG); } + @Test + void configKeys_shouldHaveCorrectValues() { + assertEquals("levelup_sound", ConfigStrings.LVL_UP_SOUND); + assertEquals("max_level_xp_drops", ConfigStrings.MAX_LEVEL_XP_DROPS); + assertEquals("level_with.player_placed_blocks", ConfigStrings.LEVEL_WITH_PLAYER_PLACED_BLOCKS); + assertEquals("level_with.generated_blocks", ConfigStrings.LEVEL_WITH_GENERATED_BLOCKS); + assertEquals("level_progression_messages", ConfigStrings.LEVEL_PROGRESSION_MESSAGES); + assertEquals("destroy_mining_blocks_on_explode", ConfigStrings.DESTROY_MINING_BLOCKS_ON_EXPLODE); + assertEquals("mining_items", ConfigStrings.MINING_ITEMS); + assertEquals("admin.debug", ConfigStrings.ADMIN_DEBUG); + } + + @Test + void configKeys_shouldNotBeEmpty() { + assertFalse(ConfigStrings.LVL_UP_SOUND.isEmpty()); + assertFalse(ConfigStrings.MAX_LEVEL_XP_DROPS.isEmpty()); + assertFalse(ConfigStrings.LEVEL_WITH_PLAYER_PLACED_BLOCKS.isEmpty()); + assertFalse(ConfigStrings.LEVEL_WITH_GENERATED_BLOCKS.isEmpty()); + assertFalse(ConfigStrings.LEVEL_PROGRESSION_MESSAGES.isEmpty()); + assertFalse(ConfigStrings.DESTROY_MINING_BLOCKS_ON_EXPLODE.isEmpty()); + assertFalse(ConfigStrings.MINING_ITEMS.isEmpty()); + assertFalse(ConfigStrings.ADMIN_DEBUG.isEmpty()); + } + + // --- message keys --- + @Test void messageKeys_shouldNotBeNull() { assertNotNull(ConfigStrings.NO_PERMISSION); @@ -51,18 +164,91 @@ void messageKeys_shouldNotBeNull() { } @Test - void prefix_shouldHaveDefaultValue() { - assertNotNull(ConfigStrings.PREFIX); - assertTrue(ConfigStrings.PREFIX.contains("ML")); + void messageKeys_shouldNotBeEmpty() { + assertFalse(ConfigStrings.NO_PERMISSION.isEmpty()); + assertFalse(ConfigStrings.NEW_LEVEL.isEmpty()); + assertFalse(ConfigStrings.PLAYER_NOT_EXIST.isEmpty()); + assertFalse(ConfigStrings.XP_RECEIVED.isEmpty()); + assertFalse(ConfigStrings.LEVEL_OF.isEmpty()); + assertFalse(ConfigStrings.LEVEL_UNLOCKED.isEmpty()); + assertFalse(ConfigStrings.LEVEL_NEEDED.isEmpty()); + assertFalse(ConfigStrings.LEVEL_DROPPED.isEmpty()); + assertFalse(ConfigStrings.XP_GAINED.isEmpty()); + assertFalse(ConfigStrings.RELOAD_SUCCESSFUL.isEmpty()); + assertFalse(ConfigStrings.ERROR_OCCURRED.isEmpty()); } @Test - void localizedIdentifier_shouldBeMininglevels() { - assertEquals("mininglevels", ConfigStrings.LOCALIZED_IDENTIFIER); + void skillChangeMessages_shouldNotBeNull() { + assertNotNull(ConfigStrings.HASTELVL_CHANGE); + assertNotNull(ConfigStrings.INSTANT_BREAK_CHANGE); + assertNotNull(ConfigStrings.EXTRA_ORE_CHANGE); + assertNotNull(ConfigStrings.MAX_EXTRA_ORE_CHANGE); } @Test - void permissionAdmin_shouldBeWildcard() { - assertEquals("mininglevels.*", ConfigStrings.PERMISSION_ADMIN); + void rewardMessages_shouldNotBeNull() { + assertNotNull(ConfigStrings.REWARDS_LIST); + assertNotNull(ConfigStrings.CLAIM_YOUR_REWARD); + assertNotNull(ConfigStrings.NO_REWARDS); + assertNotNull(ConfigStrings.REWARDS_CLAIMED); + assertNotNull(ConfigStrings.NO_MORE_SPACE); + } + + @Test + void uiMessages_shouldNotBeNull() { + assertNotNull(ConfigStrings.CURRENT_LEVEL); + assertNotNull(ConfigStrings.CURRENT_XP); + assertNotNull(ConfigStrings.CURRENT_HASTE_LEVEL); + assertNotNull(ConfigStrings.CURRENT_INSTANT_BREAK_LEVEL); + assertNotNull(ConfigStrings.CURRENT_EXTRA_ORE_LEVEL); + assertNotNull(ConfigStrings.CURRENT_MAX_EXTRA_ORE); + } + + @Test + void usageMessages_shouldNotBeNull() { + assertNotNull(ConfigStrings.USAGE_SET_LEVEL); + assertNotNull(ConfigStrings.USAGE_SET_XP); + assertNotNull(ConfigStrings.USAGE_LEVEL); + } + + @Test + void miscMessages_shouldNotBeNull() { + assertNotNull(ConfigStrings.NO_CONSOLE_COMMAND); + assertNotNull(ConfigStrings.ONLY_BLOCKS_ALLOWED); + assertNotNull(ConfigStrings.CANT_DELETE_LEVEL); + assertNotNull(ConfigStrings.LEADERBOARD_HEADER); + assertNotNull(ConfigStrings.CLOSE); + } + + // --- all message keys are distinct --- + + @Test + void messageKeys_shouldAllBeDistinct() { + String[] keys = { + ConfigStrings.NO_PERMISSION, ConfigStrings.NEW_LEVEL, + ConfigStrings.PLAYER_NOT_EXIST, ConfigStrings.XP_RECEIVED, + ConfigStrings.LEVEL_OF, ConfigStrings.LEVEL_UNLOCKED, + ConfigStrings.HASTELVL_CHANGE, ConfigStrings.INSTANT_BREAK_CHANGE, + ConfigStrings.EXTRA_ORE_CHANGE, ConfigStrings.MAX_EXTRA_ORE_CHANGE, + ConfigStrings.REWARDS_LIST, ConfigStrings.CLAIM_YOUR_REWARD, + ConfigStrings.LEVEL_DROPPED, ConfigStrings.XP_GAINED, + ConfigStrings.RELOAD_SUCCESSFUL, ConfigStrings.ERROR_OCCURRED, + ConfigStrings.NO_CONSOLE_COMMAND, ConfigStrings.CURRENT_LEVEL, + ConfigStrings.CURRENT_XP, ConfigStrings.CURRENT_HASTE_LEVEL, + ConfigStrings.CURRENT_INSTANT_BREAK_LEVEL, ConfigStrings.CURRENT_EXTRA_ORE_LEVEL, + ConfigStrings.CURRENT_MAX_EXTRA_ORE, ConfigStrings.ONLY_BLOCKS_ALLOWED, + ConfigStrings.CANT_DELETE_LEVEL, ConfigStrings.NO_REWARDS, + ConfigStrings.REWARDS_CLAIMED, ConfigStrings.NO_MORE_SPACE, + ConfigStrings.LEVEL_NEEDED, ConfigStrings.LEADERBOARD_HEADER, + ConfigStrings.USAGE_SET_LEVEL, ConfigStrings.USAGE_SET_XP, + ConfigStrings.USAGE_LEVEL, ConfigStrings.CLOSE + }; + for (int i = 0; i < keys.length; i++) { + for (int j = i + 1; j < keys.length; j++) { + assertNotEquals(keys[i], keys[j], + "Duplicate message key: " + keys[i]); + } + } } } diff --git a/src/test/java/de/chafficplugins/mininglevels/utils/MathUtilsTest.java b/src/test/java/de/chafficplugins/mininglevels/utils/MathUtilsTest.java index 2c2f610..b828b12 100644 --- a/src/test/java/de/chafficplugins/mininglevels/utils/MathUtilsTest.java +++ b/src/test/java/de/chafficplugins/mininglevels/utils/MathUtilsTest.java @@ -33,4 +33,73 @@ void randomDouble_smallRange_shouldReturnValueInRange() { assertTrue(result >= 1 && result < 3, "Expected value in [1, 3), got: " + result); } + + @RepeatedTest(50) + void randomDouble_zeroToOne_shouldReturnValueInRange() { + double result = MathUtils.randomDouble(0, 1); + assertTrue(result >= 0 && result < 1, + "Expected value in [0, 1), got: " + result); + } + + @RepeatedTest(30) + void randomDouble_largeRange_shouldReturnValueInRange() { + double result = MathUtils.randomDouble(-1_000_000, 1_000_000); + assertTrue(result >= -1_000_000 && result < 1_000_000, + "Expected value in [-1000000, 1000000), got: " + result); + } + + @RepeatedTest(30) + void randomDouble_verySmallRange_shouldReturnValueInRange() { + double result = MathUtils.randomDouble(0.5, 0.5001); + assertTrue(result >= 0.5 && result < 0.5001, + "Expected value in [0.5, 0.5001), got: " + result); + } + + @RepeatedTest(30) + void randomDouble_negativeRange_shouldReturnValueInRange() { + double result = MathUtils.randomDouble(-100, -50); + assertTrue(result >= -100 && result < -50, + "Expected value in [-100, -50), got: " + result); + } + + @Test + void randomDouble_zeroRange_atZero_shouldReturnZero() { + double result = MathUtils.randomDouble(0, 0); + assertEquals(0.0, result, 0.001); + } + + @Test + void randomDouble_zeroRange_atNegative_shouldReturnThatValue() { + double result = MathUtils.randomDouble(-7.5, -7.5); + assertEquals(-7.5, result, 0.001); + } + + @Test + void randomDouble_producesDistinctValues() { + // Over 100 calls, we should see at least 2 distinct values + double first = MathUtils.randomDouble(0, 1000); + boolean foundDifferent = false; + for (int i = 0; i < 100; i++) { + double next = MathUtils.randomDouble(0, 1000); + if (Math.abs(next - first) > 0.001) { + foundDifferent = true; + break; + } + } + assertTrue(foundDifferent, "Expected distinct values over 100 calls"); + } + + @RepeatedTest(30) + void randomDouble_integerBounds_shouldReturnValueInRange() { + double result = MathUtils.randomDouble(10, 20); + assertTrue(result >= 10 && result < 20, + "Expected value in [10, 20), got: " + result); + } + + @RepeatedTest(20) + void randomDouble_fractionalBounds_shouldReturnValueInRange() { + double result = MathUtils.randomDouble(1.5, 2.5); + assertTrue(result >= 1.5 && result < 2.5, + "Expected value in [1.5, 2.5), got: " + result); + } }