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);
+ }
}