From 4fc9c735f2d399cdb4c600daf8e64f6281611cf2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Feb 2026 09:18:29 +0000 Subject: [PATCH 1/6] test: significantly expand test suite from 66 to 498 tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MiningLevelTest: 27 → 63 tests (rewards, getName, navigation chains, boundary values, single-level edge cases, skill independence) - MiningPlayerTest: 16 → 48 tests (addRewards, claim, getPlayer, getOfflinePlayer, negative XP, stress tests with 100 players) - MiningBlockTest: 10 → 34 tests (setMaterials, multi-material blocks, non-block rejection for tools/items, boundary setMinLevel) - RewardTest: 3 → 14 tests (round-trip fidelity across 8 materials, new instance per call, tools and blocks as rewards) - MathUtilsTest: 4 → 14 tests (zero/negative/fractional/large ranges, distinct values check, ~310 repeated runs total) - ConfigStringsTest: 6 → 25 tests (exact value checks, format validation, distinctness checks for all permissions and message keys) https://claude.ai/code/session_01EyodQnKSvWcNzA6W8pCmyu --- .../mininglevels/api/MiningBlockTest.java | 237 ++++++++++ .../mininglevels/api/MiningLevelTest.java | 416 ++++++++++++++++++ .../mininglevels/api/MiningPlayerTest.java | 299 ++++++++++++- .../mininglevels/api/RewardTest.java | 108 ++++- .../mininglevels/utils/ConfigStringsTest.java | 221 +++++++++- .../mininglevels/utils/MathUtilsTest.java | 69 +++ 6 files changed, 1334 insertions(+), 16 deletions(-) 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/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..b03e295 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,112 @@ 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); + } + + // --- download URLs --- + + @Test + void downloadUrl_shouldNotBeNull() { + assertNotNull(ConfigStrings.DOWNLOAD_URL); + assertFalse(ConfigStrings.DOWNLOAD_URL.isEmpty()); + } + + @Test + void jsonFileIds_shouldNotBeNull() { + assertNotNull(ConfigStrings.MINING_LEVELS_JSON); + assertNotNull(ConfigStrings.MINING_BLOCKS_JSON); + assertFalse(ConfigStrings.MINING_LEVELS_JSON.isEmpty()); + assertFalse(ConfigStrings.MINING_BLOCKS_JSON.isEmpty()); + } + + @Test + void jsonFileIds_shouldBeDifferent() { + assertNotEquals(ConfigStrings.MINING_LEVELS_JSON, ConfigStrings.MINING_BLOCKS_JSON); + } + + // --- 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); + } } From 2f8046b081625156a49fb91e36c1907ff8a196ab Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Feb 2026 09:26:38 +0000 Subject: [PATCH 2/6] fix: bundle default configs and remove dead Google Drive downloads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bundle default levels.json and blocks.json in src/main/resources/defaults/ so first-run setup copies from JAR instead of downloading from Google Drive - Remove dead Google Drive URL constants (DOWNLOAD_URL, MINING_LEVELS_JSON, MINING_BLOCKS_JSON) that return 404 - Rewrite FileManager to use getResource() + Files.copy() for defaults - Guard onDisable() with null check on fileManager to prevent NPE when startup fails partway through initialization - Update installation docs to reflect bundled defaults and data/ directory Fixes the same class of startup regression reported in MyTrip where CrucialAPI→CrucialLib migration left dead download URLs that break first-run bootstrap. https://claude.ai/code/session_01EyodQnKSvWcNzA6W8pCmyu --- docs/installation.md | 10 ++- .../mininglevels/MiningLevels.java | 12 +-- .../mininglevels/io/FileManager.java | 50 ++++++----- .../mininglevels/utils/ConfigStrings.java | 4 - src/main/resources/defaults/blocks.json | 87 +++++++++++++++++++ src/main/resources/defaults/levels.json | 57 ++++++++++++ .../mininglevels/utils/ConfigStringsTest.java | 21 ----- 7 files changed, 186 insertions(+), 55 deletions(-) create mode 100644 src/main/resources/defaults/blocks.json create mode 100644 src/main/resources/defaults/levels.json 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..f11432d --- /dev/null +++ b/src/main/resources/defaults/blocks.json @@ -0,0 +1,87 @@ +[ + { + "materials": [ + "COAL_ORE", + "DEEPSLATE_COAL_ORE" + ], + "xp": 1, + "minLevel": 0 + }, + { + "materials": [ + "COPPER_ORE", + "DEEPSLATE_COPPER_ORE" + ], + "xp": 1, + "minLevel": 0 + }, + { + "materials": [ + "IRON_ORE", + "DEEPSLATE_IRON_ORE" + ], + "xp": 2, + "minLevel": 0 + }, + { + "materials": [ + "REDSTONE_ORE", + "DEEPSLATE_REDSTONE_ORE" + ], + "xp": 1, + "minLevel": 0 + }, + { + "materials": [ + "GOLD_ORE", + "DEEPSLATE_GOLD_ORE" + ], + "xp": 3, + "minLevel": 1 + }, + { + "materials": [ + "LAPIS_ORE", + "DEEPSLATE_LAPIS_ORE" + ], + "xp": 2, + "minLevel": 1 + }, + { + "materials": [ + "DIAMOND_ORE", + "DEEPSLATE_DIAMOND_ORE" + ], + "xp": 5, + "minLevel": 2 + }, + { + "materials": [ + "EMERALD_ORE", + "DEEPSLATE_EMERALD_ORE" + ], + "xp": 5, + "minLevel": 3 + }, + { + "materials": [ + "NETHER_GOLD_ORE" + ], + "xp": 2, + "minLevel": 1 + }, + { + "materials": [ + "NETHER_QUARTZ_ORE" + ], + "xp": 2, + "minLevel": 0 + }, + { + "materials": [ + "ANCIENT_DEBRIS" + ], + "xp": 10, + "minLevel": 4 + } +] diff --git a/src/main/resources/defaults/levels.json b/src/main/resources/defaults/levels.json new file mode 100644 index 0000000..099f02d --- /dev/null +++ b/src/main/resources/defaults/levels.json @@ -0,0 +1,57 @@ +[ + { + "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": 500, + "ordinal": 2, + "instantBreakProbability": 10.0, + "extraOreProbability": 15.0, + "maxExtraOre": 1, + "hasteLevel": 1, + "commands": [], + "rewards": [] + }, + { + "name": "4", + "nextLevelXP": 1000, + "ordinal": 3, + "instantBreakProbability": 15.0, + "extraOreProbability": 20.0, + "maxExtraOre": 2, + "hasteLevel": 1, + "commands": [], + "rewards": [] + }, + { + "name": "5", + "nextLevelXP": 2000, + "ordinal": 4, + "instantBreakProbability": 20.0, + "extraOreProbability": 25.0, + "maxExtraOre": 3, + "hasteLevel": 2, + "commands": [], + "rewards": [] + } +] diff --git a/src/test/java/de/chafficplugins/mininglevels/utils/ConfigStringsTest.java b/src/test/java/de/chafficplugins/mininglevels/utils/ConfigStringsTest.java index b03e295..4f80b9f 100644 --- a/src/test/java/de/chafficplugins/mininglevels/utils/ConfigStringsTest.java +++ b/src/test/java/de/chafficplugins/mininglevels/utils/ConfigStringsTest.java @@ -221,27 +221,6 @@ void miscMessages_shouldNotBeNull() { assertNotNull(ConfigStrings.CLOSE); } - // --- download URLs --- - - @Test - void downloadUrl_shouldNotBeNull() { - assertNotNull(ConfigStrings.DOWNLOAD_URL); - assertFalse(ConfigStrings.DOWNLOAD_URL.isEmpty()); - } - - @Test - void jsonFileIds_shouldNotBeNull() { - assertNotNull(ConfigStrings.MINING_LEVELS_JSON); - assertNotNull(ConfigStrings.MINING_BLOCKS_JSON); - assertFalse(ConfigStrings.MINING_LEVELS_JSON.isEmpty()); - assertFalse(ConfigStrings.MINING_BLOCKS_JSON.isEmpty()); - } - - @Test - void jsonFileIds_shouldBeDifferent() { - assertNotEquals(ConfigStrings.MINING_LEVELS_JSON, ConfigStrings.MINING_BLOCKS_JSON); - } - // --- all message keys are distinct --- @Test From cd2a870708fd3e9dca1c38f3b5dd0e6c0ec7972e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Feb 2026 09:29:35 +0000 Subject: [PATCH 3/6] fix: restore original default configs from git history MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace placeholder defaults with the actual originals recovered from the initial commit (2fe705a). The files were removed in 53476af when Google Drive downloads were introduced. - levels.json: 10 levels with original XP curve (100→150000) - blocks.json: 9 block entries with original XP values Added empty commands[] and rewards[] arrays to levels.json for compatibility with the reward system added after the initial commit. https://claude.ai/code/session_01EyodQnKSvWcNzA6W8pCmyu --- src/main/resources/defaults/blocks.json | 68 ++++++++++-------------- src/main/resources/defaults/levels.json | 69 ++++++++++++++++++++++--- 2 files changed, 89 insertions(+), 48 deletions(-) diff --git a/src/main/resources/defaults/blocks.json b/src/main/resources/defaults/blocks.json index f11432d..681db4b 100644 --- a/src/main/resources/defaults/blocks.json +++ b/src/main/resources/defaults/blocks.json @@ -1,87 +1,73 @@ [ { "materials": [ - "COAL_ORE", - "DEEPSLATE_COAL_ORE" + "STONE" ], "xp": 1, "minLevel": 0 }, { "materials": [ - "COPPER_ORE", - "DEEPSLATE_COPPER_ORE" + "DEEPSLATE_REDSTONE_ORE", + "REDSTONE_ORE" ], "xp": 1, "minLevel": 0 }, { "materials": [ - "IRON_ORE", - "DEEPSLATE_IRON_ORE" + "DEEPSLATE_LAPIS_ORE", + "LAPIS_ORE" ], - "xp": 2, - "minLevel": 0 + "xp": 1, + "minLevel": 3 }, { "materials": [ - "REDSTONE_ORE", - "DEEPSLATE_REDSTONE_ORE" + "DEEPSLATE_COAL_ORE", + "COAL_ORE" ], - "xp": 1, + "xp": 10, "minLevel": 0 }, { "materials": [ - "GOLD_ORE", - "DEEPSLATE_GOLD_ORE" + "DEEPSLATE_GOLD_ORE", + "GOLD_ORE" ], - "xp": 3, - "minLevel": 1 + "xp": 100, + "minLevel": 2 }, { "materials": [ - "LAPIS_ORE", - "DEEPSLATE_LAPIS_ORE" + "DEEPSLATE_EMERALD_ORE", + "EMERALD_ORE" ], - "xp": 2, - "minLevel": 1 + "xp": 10, + "minLevel": 3 }, { "materials": [ - "DIAMOND_ORE", - "DEEPSLATE_DIAMOND_ORE" + "DEEPSLATE_COPPER_ORE", + "COPPER_ORE" ], "xp": 5, "minLevel": 2 }, { "materials": [ - "EMERALD_ORE", - "DEEPSLATE_EMERALD_ORE" - ], - "xp": 5, - "minLevel": 3 - }, - { - "materials": [ - "NETHER_GOLD_ORE" + "DEEPSLATE_IRON_ORE", + "IRON_ORE" ], - "xp": 2, + "xp": 50, "minLevel": 1 }, { "materials": [ - "NETHER_QUARTZ_ORE" + "DEEPSLATE_DIAMOND_ORE", + "DIAMOND_ORE" ], - "xp": 2, - "minLevel": 0 - }, - { - "materials": [ - "ANCIENT_DEBRIS" - ], - "xp": 10, - "minLevel": 4 + "xp": 1000, + "minLevel": 5 } ] diff --git a/src/main/resources/defaults/levels.json b/src/main/resources/defaults/levels.json index 099f02d..1a7278b 100644 --- a/src/main/resources/defaults/levels.json +++ b/src/main/resources/defaults/levels.json @@ -23,12 +23,12 @@ }, { "name": "3", - "nextLevelXP": 500, + "nextLevelXP": 600, "ordinal": 2, "instantBreakProbability": 10.0, - "extraOreProbability": 15.0, - "maxExtraOre": 1, - "hasteLevel": 1, + "extraOreProbability": 10.0, + "maxExtraOre": 2, + "hasteLevel": 0, "commands": [], "rewards": [] }, @@ -39,7 +39,7 @@ "instantBreakProbability": 15.0, "extraOreProbability": 20.0, "maxExtraOre": 2, - "hasteLevel": 1, + "hasteLevel": 0, "commands": [], "rewards": [] }, @@ -48,8 +48,63 @@ "nextLevelXP": 2000, "ordinal": 4, "instantBreakProbability": 20.0, - "extraOreProbability": 25.0, - "maxExtraOre": 3, + "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": [] From 3b0d683d5ba4f3070e4cdfa44c05e69020e767e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Feb 2026 10:06:03 +0000 Subject: [PATCH 4/6] Add validation tests for bundled default config files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bundled defaults (levels.json, blocks.json) were previously untested — all existing tests construct their own data manually, so wrong default values went undetected. These 13 tests load the JSON from the classpath and validate structure, value ranges, material validity, and cross-references between blocks and levels. https://claude.ai/code/session_01EyodQnKSvWcNzA6W8pCmyu --- .../mininglevels/api/DefaultConfigTest.java | 205 ++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 src/test/java/de/chafficplugins/mininglevels/api/DefaultConfigTest.java 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); + } + } + } +} From 56d8a23e42b77a95f0fc9c46f82252a63a42d713 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Feb 2026 10:20:00 +0000 Subject: [PATCH 5/6] Add integration tests for the mining progression flow 25 tests that wire together MiningLevel, MiningBlock, and MiningPlayer using the actual bundled default configs. Tests cover: - JSON deserialization into model classes - Block lookup by material (regular + deepslate variants) - Level requirement gating (can't mine above your level) - XP accumulation, level-up thresholds, overflow carry - Multi-level jumps from large XP gains - Progressive block unlocks as player levels up - Full progression from level 1 to max - Multi-player independence - Default data cross-reference validity The core xpChange/levelUp logic is replicated in test helpers because the actual methods are coupled to the plugin instance and CrucialLib (messaging, sounds, config), which aren't available in tests. https://claude.ai/code/session_01EyodQnKSvWcNzA6W8pCmyu --- .../api/MiningFlowIntegrationTest.java | 540 ++++++++++++++++++ 1 file changed, 540 insertions(+) create mode 100644 src/test/java/de/chafficplugins/mininglevels/api/MiningFlowIntegrationTest.java 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()); + } + } +} From e8101a9bed500c881b5faad78960e2b66d9e1754 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Feb 2026 11:55:54 +0000 Subject: [PATCH 6/6] chore: add issue and PR templates https://claude.ai/code/session_01EyodQnKSvWcNzA6W8pCmyu --- .github/ISSUE_TEMPLATE/bug_report.yml | 69 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 8 +++ .github/ISSUE_TEMPLATE/feature_request.yml | 33 +++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 25 ++++++++ 4 files changed, 135 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md 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)