diff --git a/src/main/java/com/gmail/nossr50/util/skills/SkillTools.java b/src/main/java/com/gmail/nossr50/util/skills/SkillTools.java index fe2743985..4c14c9bbe 100644 --- a/src/main/java/com/gmail/nossr50/util/skills/SkillTools.java +++ b/src/main/java/com/gmail/nossr50/util/skills/SkillTools.java @@ -1,6 +1,5 @@ package com.gmail.nossr50.util.skills; -import com.gmail.nossr50.api.exceptions.InvalidSkillException; import com.gmail.nossr50.config.experience.ExperienceConfig; import com.gmail.nossr50.datatypes.skills.PrimarySkillType; import com.gmail.nossr50.datatypes.skills.SubSkillType; @@ -18,6 +17,7 @@ import java.util.Collections; import java.util.EnumMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; @@ -27,15 +27,16 @@ import org.jetbrains.annotations.VisibleForTesting; public class SkillTools { private final mcMMO pluginRef; + // TODO: Java has immutable types now, switch to those // TODO: Figure out which ones we don't need, this was copy pasted from a diff branch public final @NotNull ImmutableList LOCALIZED_SKILL_NAMES; public final @NotNull ImmutableList FORMATTED_SUBSKILL_NAMES; public final @NotNull ImmutableSet EXACT_SUBSKILL_NAMES; public final @NotNull ImmutableList CHILD_SKILLS; - public final static @NotNull ImmutableList NON_CHILD_SKILLS; - public final static @NotNull ImmutableList SALVAGE_PARENTS; - public final static @NotNull ImmutableList SMELTING_PARENTS; + public static final @NotNull ImmutableList NON_CHILD_SKILLS; + public static final @NotNull ImmutableList SALVAGE_PARENTS; + public static final @NotNull ImmutableList SMELTING_PARENTS; public final @NotNull ImmutableList COMBAT_SKILLS; public final @NotNull ImmutableList GATHERING_SKILLS; public final @NotNull ImmutableList MISC_SKILLS; @@ -44,73 +45,141 @@ public class SkillTools { private final @NotNull ImmutableMap superAbilityParentRelationshipMap; private final @NotNull ImmutableMap> primarySkillChildrenMap; - // The map below is for the super abilities which require readying a tool, its everything except blast mining private final ImmutableMap mainActivatedAbilityChildMap; private final ImmutableMap primarySkillToolMap; static { + // Build NON_CHILD_SKILLS once from the enum values ArrayList tempNonChildSkills = new ArrayList<>(); for (PrimarySkillType primarySkillType : PrimarySkillType.values()) { - if (primarySkillType != PrimarySkillType.SALVAGE - && primarySkillType != PrimarySkillType.SMELTING) { + if (!isChildSkill(primarySkillType)) { tempNonChildSkills.add(primarySkillType); } } - NON_CHILD_SKILLS = ImmutableList.copyOf(tempNonChildSkills); - SALVAGE_PARENTS = ImmutableList.of(PrimarySkillType.REPAIR, PrimarySkillType.FISHING); - SMELTING_PARENTS = ImmutableList.of(PrimarySkillType.MINING, PrimarySkillType.REPAIR); + + SALVAGE_PARENTS = ImmutableList.of( + PrimarySkillType.REPAIR, + PrimarySkillType.FISHING + ); + SMELTING_PARENTS = ImmutableList.of( + PrimarySkillType.MINING, + PrimarySkillType.REPAIR + ); } - public SkillTools(@NotNull mcMMO pluginRef) throws InvalidSkillException { + public SkillTools(@NotNull mcMMO pluginRef) { this.pluginRef = pluginRef; /* * Setup subskill -> parent relationship map */ - EnumMap tempSubParentMap = new EnumMap<>( - SubSkillType.class); - - //Super hacky and disgusting - for (PrimarySkillType primarySkillType1 : PrimarySkillType.values()) { - for (SubSkillType subSkillType : SubSkillType.values()) { - String[] splitSubSkillName = subSkillType.toString().split("_"); - - if (primarySkillType1.toString().equalsIgnoreCase(splitSubSkillName[0])) { - //Parent Skill Found - tempSubParentMap.put(subSkillType, primarySkillType1); - } - } - } - - subSkillParentRelationshipMap = ImmutableMap.copyOf(tempSubParentMap); + this.subSkillParentRelationshipMap = buildSubSkillParentMap(); /* * Setup primary -> (collection) subskill map */ - - EnumMap> tempPrimaryChildMap = new EnumMap<>( - PrimarySkillType.class); - - //Init the empty Hash Sets - for (PrimarySkillType primarySkillType1 : PrimarySkillType.values()) { - tempPrimaryChildMap.put(primarySkillType1, new HashSet<>()); - } - - //Fill in the hash sets - for (SubSkillType subSkillType : SubSkillType.values()) { - PrimarySkillType parentSkill = subSkillParentRelationshipMap.get(subSkillType); - - //Add this subskill as a child - tempPrimaryChildMap.get(parentSkill).add(subSkillType); - } - - primarySkillChildrenMap = ImmutableMap.copyOf(tempPrimaryChildMap); + this.primarySkillChildrenMap = buildPrimarySkillChildrenMap(subSkillParentRelationshipMap); /* * Setup primary -> tooltype map */ - EnumMap tempToolMap = new EnumMap<>(PrimarySkillType.class); + this.primarySkillToolMap = buildPrimarySkillToolMap(); + + /* + * Setup ability -> primary map + * Setup primary -> ability map + */ + var abilityMaps = buildSuperAbilityMaps(); + this.superAbilityParentRelationshipMap = abilityMaps.superAbilityParentRelationshipMap(); + this.mainActivatedAbilityChildMap = abilityMaps.mainActivatedAbilityChildMap(); + + /* + * Build child skill list + */ + this.CHILD_SKILLS = buildChildSkills(); + + /* + * Build categorized skill lists + */ + this.COMBAT_SKILLS = buildCombatSkills(); + this.GATHERING_SKILLS = ImmutableList.of( + PrimarySkillType.EXCAVATION, + PrimarySkillType.FISHING, + PrimarySkillType.HERBALISM, + PrimarySkillType.MINING, + PrimarySkillType.WOODCUTTING + ); + this.MISC_SKILLS = ImmutableList.of( + PrimarySkillType.ACROBATICS, + PrimarySkillType.ALCHEMY, + PrimarySkillType.REPAIR, + PrimarySkillType.SALVAGE, + PrimarySkillType.SMELTING + ); + + /* + * Build formatted/localized/etc string lists + */ + this.LOCALIZED_SKILL_NAMES = ImmutableList.copyOf(buildLocalizedPrimarySkillNames()); + this.FORMATTED_SUBSKILL_NAMES = ImmutableList.copyOf(buildFormattedSubSkillNameList()); + this.EXACT_SUBSKILL_NAMES = ImmutableSet.copyOf(buildExactSubSkillNameList()); + } + + @VisibleForTesting + @NotNull + ImmutableMap buildSubSkillParentMap() { + EnumMap tempSubParentMap = + new EnumMap<>(SubSkillType.class); + + // SubSkillType names use a convention: _SOMETHING + for (SubSkillType subSkillType : SubSkillType.values()) { + String enumName = subSkillType.name(); + int underscoreIndex = enumName.indexOf('_'); + String parentPrefix = underscoreIndex == -1 + ? enumName + : enumName.substring(0, underscoreIndex); + + for (PrimarySkillType primarySkillType : PrimarySkillType.values()) { + if (primarySkillType.name().equalsIgnoreCase(parentPrefix)) { + tempSubParentMap.put(subSkillType, primarySkillType); + break; + } + } + } + + return ImmutableMap.copyOf(tempSubParentMap); + } + + @VisibleForTesting + @NotNull + ImmutableMap> buildPrimarySkillChildrenMap( + ImmutableMap subParentMap) { + + EnumMap> tempPrimaryChildMap = + new EnumMap<>(PrimarySkillType.class); + + // Initialize empty sets + for (PrimarySkillType primarySkillType : PrimarySkillType.values()) { + tempPrimaryChildMap.put(primarySkillType, new HashSet<>()); + } + + // Fill sets + for (SubSkillType subSkillType : SubSkillType.values()) { + PrimarySkillType parentSkill = subParentMap.get(subSkillType); + if (parentSkill != null) { + tempPrimaryChildMap.get(parentSkill).add(subSkillType); + } + } + + return ImmutableMap.copyOf(tempPrimaryChildMap); + } + + @VisibleForTesting + @NotNull + ImmutableMap buildPrimarySkillToolMap() { + EnumMap tempToolMap = + new EnumMap<>(PrimarySkillType.class); tempToolMap.put(PrimarySkillType.AXES, ToolType.AXE); tempToolMap.put(PrimarySkillType.WOODCUTTING, ToolType.AXE); @@ -120,55 +189,63 @@ public class SkillTools { tempToolMap.put(PrimarySkillType.HERBALISM, ToolType.HOE); tempToolMap.put(PrimarySkillType.MINING, ToolType.PICKAXE); - primarySkillToolMap = ImmutableMap.copyOf(tempToolMap); + return ImmutableMap.copyOf(tempToolMap); + } - /* - * Setup ability -> primary map - * Setup primary -> ability map - */ + /** + * Holder for the two super ability maps, so we can build them in one pass. + */ + @VisibleForTesting + record SuperAbilityMaps( + @NotNull ImmutableMap superAbilityParentRelationshipMap, + @NotNull ImmutableMap mainActivatedAbilityChildMap) { + } - EnumMap tempAbilityParentRelationshipMap = new EnumMap<>( - SuperAbilityType.class); - EnumMap tempMainActivatedAbilityChildMap = new EnumMap<>( - PrimarySkillType.class); + @VisibleForTesting + @NotNull + SuperAbilityMaps buildSuperAbilityMaps() { + final Map tempAbilityParentRelationshipMap = + new EnumMap<>(SuperAbilityType.class); + final Map tempMainActivatedAbilityChildMap = + new EnumMap<>(PrimarySkillType.class); for (SuperAbilityType superAbilityType : SuperAbilityType.values()) { - try { - PrimarySkillType parent = getSuperAbilityParent(superAbilityType); - tempAbilityParentRelationshipMap.put(superAbilityType, parent); + final PrimarySkillType parent = getSuperAbilityParent(superAbilityType); + tempAbilityParentRelationshipMap.put(superAbilityType, parent); - if (superAbilityType != SuperAbilityType.BLAST_MINING) { - //This map is used only for abilities that have a tool readying phase, so blast mining is ignored - tempMainActivatedAbilityChildMap.put(parent, superAbilityType); - } - } catch (InvalidSkillException e) { - e.printStackTrace(); + // This map is used only for abilities that have a tool readying phase, + // so Blast Mining is ignored. + if (superAbilityType != SuperAbilityType.BLAST_MINING) { + tempMainActivatedAbilityChildMap.put(parent, superAbilityType); } } - superAbilityParentRelationshipMap = ImmutableMap.copyOf(tempAbilityParentRelationshipMap); - mainActivatedAbilityChildMap = ImmutableMap.copyOf(tempMainActivatedAbilityChildMap); - - /* - * Build child skill and nonchild skill lists - */ + return new SuperAbilityMaps( + ImmutableMap.copyOf(tempAbilityParentRelationshipMap), + ImmutableMap.copyOf(tempMainActivatedAbilityChildMap) + ); + } + @VisibleForTesting + @NotNull + ImmutableList buildChildSkills() { List childSkills = new ArrayList<>(); - for (PrimarySkillType primarySkillType : PrimarySkillType.values()) { if (isChildSkill(primarySkillType)) { childSkills.add(primarySkillType); } } + return ImmutableList.copyOf(childSkills); + } - CHILD_SKILLS = ImmutableList.copyOf(childSkills); + @VisibleForTesting + @NotNull + ImmutableList buildCombatSkills() { + var gameVersion = mcMMO.getCompatibilityManager().getMinecraftGameVersion(); - /* - * Build categorized skill lists - */ - if (mcMMO.getCompatibilityManager().getMinecraftGameVersion().isAtLeast(1, 21, 11)) { + if (gameVersion.isAtLeast(1, 21, 11)) { // We are in a game version with Spears and Maces - COMBAT_SKILLS = ImmutableList.of( + return ImmutableList.of( PrimarySkillType.ARCHERY, PrimarySkillType.AXES, PrimarySkillType.CROSSBOWS, @@ -177,10 +254,11 @@ public class SkillTools { PrimarySkillType.SPEARS, PrimarySkillType.TAMING, PrimarySkillType.TRIDENTS, - PrimarySkillType.UNARMED); - } else if (mcMMO.getCompatibilityManager().getMinecraftGameVersion().isAtLeast(1, 21, 0)) { + PrimarySkillType.UNARMED + ); + } else if (gameVersion.isAtLeast(1, 21, 0)) { // We are in a game version with Maces - COMBAT_SKILLS = ImmutableList.of( + return ImmutableList.of( PrimarySkillType.ARCHERY, PrimarySkillType.AXES, PrimarySkillType.CROSSBOWS, @@ -188,42 +266,23 @@ public class SkillTools { PrimarySkillType.SWORDS, PrimarySkillType.TAMING, PrimarySkillType.TRIDENTS, - PrimarySkillType.UNARMED); + PrimarySkillType.UNARMED + ); } else { // No Maces in this version - COMBAT_SKILLS = ImmutableList.of( + return ImmutableList.of( PrimarySkillType.ARCHERY, PrimarySkillType.AXES, PrimarySkillType.CROSSBOWS, PrimarySkillType.SWORDS, PrimarySkillType.TAMING, PrimarySkillType.TRIDENTS, - PrimarySkillType.UNARMED); + PrimarySkillType.UNARMED + ); } - GATHERING_SKILLS = ImmutableList.of( - PrimarySkillType.EXCAVATION, - PrimarySkillType.FISHING, - PrimarySkillType.HERBALISM, - PrimarySkillType.MINING, - PrimarySkillType.WOODCUTTING); - MISC_SKILLS = ImmutableList.of( - PrimarySkillType.ACROBATICS, - PrimarySkillType.ALCHEMY, - PrimarySkillType.REPAIR, - PrimarySkillType.SALVAGE, - PrimarySkillType.SMELTING); - - /* - * Build formatted/localized/etc string lists - */ - - LOCALIZED_SKILL_NAMES = ImmutableList.copyOf(buildLocalizedPrimarySkillNames()); - FORMATTED_SUBSKILL_NAMES = ImmutableList.copyOf(buildFormattedSubSkillNameList()); - EXACT_SUBSKILL_NAMES = ImmutableSet.copyOf(buildExactSubSkillNameList()); } - private @NotNull PrimarySkillType getSuperAbilityParent(SuperAbilityType superAbilityType) - throws InvalidSkillException { + private @NotNull PrimarySkillType getSuperAbilityParent(SuperAbilityType superAbilityType) { return switch (superAbilityType) { case BERSERK -> PrimarySkillType.UNARMED; case GREEN_TERRA -> PrimarySkillType.HERBALISM; @@ -241,7 +300,7 @@ public class SkillTools { } /** - * Makes a list of the "nice" version of sub skill names Used in tab completion mostly + * Makes a list of the "nice" version of sub skill names. Used in tab completion mostly. * * @return a list of formatted sub skill names */ @@ -284,9 +343,12 @@ public class SkillTools { } /** - * Matches a string of a skill to a skill This is NOT case sensitive First it checks the locale - * file and tries to match by the localized name of the skill Then if nothing is found it checks - * against the hard coded "name" of the skill, which is just its name in English + * Matches a string of a skill to a skill. + * This is NOT case-sensitive. + *

+ * First it checks the locale file and tries to match by the localized name of the skill. + * Then if nothing is found it checks against the hard coded "name" of the skill, + * which is just its name in English. * * @param skillName target skill name * @return the matching PrimarySkillType if one is found, otherwise null @@ -294,8 +356,9 @@ public class SkillTools { public PrimarySkillType matchSkill(String skillName) { if (!pluginRef.getGeneralConfig().getLocale().equalsIgnoreCase("en_US")) { for (PrimarySkillType type : PrimarySkillType.values()) { - if (skillName.equalsIgnoreCase(LocaleLoader.getString( - StringUtils.getCapitalized(type.name()) + ".SkillName"))) { + String localized = LocaleLoader.getString( + StringUtils.getCapitalized(type.name()) + ".SkillName"); + if (skillName.equalsIgnoreCase(localized)) { return type; } } @@ -309,15 +372,15 @@ public class SkillTools { if (!skillName.equalsIgnoreCase("all")) { pluginRef.getLogger() - .warning("Invalid mcMMO skill (" + skillName + ")"); //TODO: Localize + .warning("Invalid mcMMO skill (" + skillName + ")"); // TODO: Localize } return null; } /** - * Gets the PrimarySkillStype to which a SubSkillType belongs Return null if it does not belong - * to one.. which should be impossible in most circumstances + * Gets the PrimarySkillType to which a SubSkillType belongs. + * Returns null if it does not belong to one (which should be impossible in most circumstances). * * @param subSkillType target subskill * @return the PrimarySkillType of this SubSkill, null if it doesn't exist @@ -327,8 +390,8 @@ public class SkillTools { } /** - * Gets the PrimarySkillStype to which a SuperAbilityType belongs Return null if it does not - * belong to one.. which should be impossible in most circumstances + * Gets the PrimarySkillType to which a SuperAbilityType belongs. + * Returns null if it does not belong to one (which should be impossible in most circumstances). * * @param superAbilityType target super ability * @return the PrimarySkillType of this SuperAbilityType, null if it doesn't exist @@ -338,16 +401,15 @@ public class SkillTools { } public SuperAbilityType getSuperAbility(PrimarySkillType primarySkillType) { - if (mainActivatedAbilityChildMap.get(primarySkillType) == null) { - return null; - } - return mainActivatedAbilityChildMap.get(primarySkillType); } public boolean isSuperAbilityUnlocked(PrimarySkillType primarySkillType, Player player) { - SuperAbilityType superAbilityType = mcMMO.p.getSkillTools() - .getSuperAbility(primarySkillType); + SuperAbilityType superAbilityType = getSuperAbility(primarySkillType); + if (superAbilityType == null) { + return false; + } + SubSkillType subSkillType = superAbilityType.getSubSkillTypeDefinition(); return RankUtils.hasUnlockedSubskill(player, subSkillType); } @@ -380,7 +442,6 @@ public class SkillTools { return ExperienceConfig.getInstance().getFormulaSkillModifier(primarySkillType); } - // TODO: This is a little "hacky", we probably need to add something to distinguish child skills in the enum, or to use another enum for them public static boolean isChildSkill(PrimarySkillType primarySkillType) { return switch (primarySkillType) { case SALVAGE, SMELTING -> true; @@ -404,8 +465,10 @@ public class SkillTools { } public boolean canCombatSkillsTrigger(PrimarySkillType primarySkillType, Entity target) { - return (target instanceof Player || (target instanceof Tameable - && ((Tameable) target).isTamed())) ? getPVPEnabled(primarySkillType) + boolean isPlayerOrTamed = (target instanceof Player) + || (target instanceof Tameable && ((Tameable) target).isTamed()); + return isPlayerOrTamed + ? getPVPEnabled(primarySkillType) : getPVEEnabled(primarySkillType); } @@ -422,7 +485,7 @@ public class SkillTools { } public int getLevelCap(@NotNull PrimarySkillType primarySkillType) { - return mcMMO.p.getGeneralConfig().getLevelCap(primarySkillType); + return pluginRef.getGeneralConfig().getLevelCap(primarySkillType); } /** @@ -457,17 +520,12 @@ public class SkillTools { } public @NotNull ImmutableList getChildSkillParents( - PrimarySkillType childSkill) - throws IllegalArgumentException { - switch (childSkill) { - case SALVAGE -> { - return SALVAGE_PARENTS; - } - case SMELTING -> { - return SMELTING_PARENTS; - } + PrimarySkillType childSkill) throws IllegalArgumentException { + return switch (childSkill) { + case SALVAGE -> SALVAGE_PARENTS; + case SMELTING -> SMELTING_PARENTS; default -> throw new IllegalArgumentException( "Skill " + childSkill + " is not a child skill"); - } + }; } } diff --git a/src/test/java/com/gmail/nossr50/util/skills/SkillToolsTest.java b/src/test/java/com/gmail/nossr50/util/skills/SkillToolsTest.java new file mode 100644 index 000000000..0d7f43715 --- /dev/null +++ b/src/test/java/com/gmail/nossr50/util/skills/SkillToolsTest.java @@ -0,0 +1,371 @@ +package com.gmail.nossr50.util.skills; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.gmail.nossr50.config.GeneralConfig; +import com.gmail.nossr50.datatypes.skills.PrimarySkillType; +import com.gmail.nossr50.datatypes.skills.SubSkillType; +import com.gmail.nossr50.datatypes.skills.SuperAbilityType; +import com.gmail.nossr50.datatypes.skills.ToolType; +import com.gmail.nossr50.locale.LocaleLoader; +import com.gmail.nossr50.mcMMO; +import com.gmail.nossr50.util.compat.CompatibilityManager; +import com.gmail.nossr50.util.platform.MinecraftGameVersion; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +@TestInstance(Lifecycle.PER_CLASS) +class SkillToolsTest { + + private static final @NotNull Logger logger = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); + + private static MockedStatic mockedMcMMO; + private static MockedStatic mockedLocaleLoader; + + private GeneralConfig generalConfig; + private CompatibilityManager compatibilityManager; + + @BeforeAll + void setUpAll() { + // Static mcMMO + LocaleLoader mocks + mockedMcMMO = Mockito.mockStatic(mcMMO.class); + mockedLocaleLoader = Mockito.mockStatic(LocaleLoader.class); + + // Plugin instance + mcMMO.p = mock(mcMMO.class); + when(mcMMO.p.getLogger()).thenReturn(logger); + + // General config + generalConfig = mock(GeneralConfig.class); + when(mcMMO.p.getGeneralConfig()).thenReturn(generalConfig); + when(generalConfig.getLocale()).thenReturn("en_US"); + + // Compatibility manager + game version + compatibilityManager = mock(CompatibilityManager.class); + when(mcMMO.getCompatibilityManager()).thenReturn(compatibilityManager); + + // LocaleLoader – just echo key back to keep things simple/deterministic + mockedLocaleLoader.when(() -> LocaleLoader.getString(anyString())) + .thenAnswer(invocation -> invocation.getArgument(0)); + } + + @AfterAll + void tearDownAll() { + mockedLocaleLoader.close(); + mockedMcMMO.close(); + } + + private SkillTools newSkillToolsForVersion(int major, int minor, int patch) throws Exception { + when(compatibilityManager.getMinecraftGameVersion()) + .thenReturn(new MinecraftGameVersion(major, minor, patch)); + return new SkillTools(mcMMO.p); + } + + // ------------------------------------------------------------------------ + // NON_CHILD_SKILLS / isChildSkill / CHILD_SKILLS + // ------------------------------------------------------------------------ + + @Test + void nonChildSkillsShouldContainAllPrimarySkillsExceptSalvageAndSmelting() { + List expected = Arrays.stream(PrimarySkillType.values()) + .filter(t -> t != PrimarySkillType.SALVAGE && t != PrimarySkillType.SMELTING) + .collect(Collectors.toList()); + + assertThat(SkillTools.NON_CHILD_SKILLS) + .containsExactlyInAnyOrderElementsOf(expected); + } + + @Test + void isChildSkillShouldReturnTrueOnlyForSalvageAndSmelting() { + for (PrimarySkillType type : PrimarySkillType.values()) { + boolean isChild = SkillTools.isChildSkill(type); + + if (type == PrimarySkillType.SALVAGE || type == PrimarySkillType.SMELTING) { + assertThat(isChild) + .as("%s should be considered a child skill", type) + .isTrue(); + } else { + assertThat(isChild) + .as("%s should NOT be considered a child skill", type) + .isFalse(); + } + } + } + + @Test + void childSkillsListShouldMatchIsChildSkillClassification() throws Exception { + SkillTools skillTools = newSkillToolsForVersion(1, 21, 11); + + List expectedChildren = Arrays.stream(PrimarySkillType.values()) + .filter(SkillTools::isChildSkill) + .collect(Collectors.toList()); + + assertThat(skillTools.getChildSkills()) + .containsExactlyInAnyOrderElementsOf(expectedChildren); + } + + // ------------------------------------------------------------------------ + // Child skill parents (SALVAGE_PARENTS / SMELTING_PARENTS / getChildSkillParents) + // ------------------------------------------------------------------------ + + @Test + void childSkillParentsShouldMatchStaticParentLists() throws Exception { + SkillTools skillTools = newSkillToolsForVersion(1, 21, 11); + + assertThat(skillTools.getChildSkillParents(PrimarySkillType.SALVAGE)) + .as("SALVAGE parents") + .containsExactlyElementsOf(SkillTools.SALVAGE_PARENTS); + + assertThat(skillTools.getChildSkillParents(PrimarySkillType.SMELTING)) + .as("SMELTING parents") + .containsExactlyElementsOf(SkillTools.SMELTING_PARENTS); + } + + @Test + void getChildSkillParentsShouldThrowForNonChildSkill() throws Exception { + SkillTools skillTools = newSkillToolsForVersion(1, 21, 11); + + assertThatThrownBy(() -> skillTools.getChildSkillParents(PrimarySkillType.MINING)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("is not a child skill"); + } + + // ------------------------------------------------------------------------ + // Super ability ↔ primary skill relationships + // ------------------------------------------------------------------------ + + @Test + void superAbilityParentMappingShouldMatchDefinedSwitch() throws Exception { + SkillTools skillTools = newSkillToolsForVersion(1, 21, 11); + + assertThat(skillTools.getPrimarySkillBySuperAbility(SuperAbilityType.BERSERK)) + .isEqualTo(PrimarySkillType.UNARMED); + assertThat(skillTools.getPrimarySkillBySuperAbility(SuperAbilityType.GREEN_TERRA)) + .isEqualTo(PrimarySkillType.HERBALISM); + assertThat(skillTools.getPrimarySkillBySuperAbility(SuperAbilityType.TREE_FELLER)) + .isEqualTo(PrimarySkillType.WOODCUTTING); + assertThat(skillTools.getPrimarySkillBySuperAbility(SuperAbilityType.SUPER_BREAKER)) + .isEqualTo(PrimarySkillType.MINING); + assertThat(skillTools.getPrimarySkillBySuperAbility(SuperAbilityType.BLAST_MINING)) + .isEqualTo(PrimarySkillType.MINING); + assertThat(skillTools.getPrimarySkillBySuperAbility(SuperAbilityType.SKULL_SPLITTER)) + .isEqualTo(PrimarySkillType.AXES); + assertThat(skillTools.getPrimarySkillBySuperAbility(SuperAbilityType.SERRATED_STRIKES)) + .isEqualTo(PrimarySkillType.SWORDS); + assertThat(skillTools.getPrimarySkillBySuperAbility(SuperAbilityType.GIGA_DRILL_BREAKER)) + .isEqualTo(PrimarySkillType.EXCAVATION); + assertThat(skillTools.getPrimarySkillBySuperAbility(SuperAbilityType.SUPER_SHOTGUN)) + .isEqualTo(PrimarySkillType.CROSSBOWS); + assertThat(skillTools.getPrimarySkillBySuperAbility(SuperAbilityType.TRIDENTS_SUPER_ABILITY)) + .isEqualTo(PrimarySkillType.TRIDENTS); + assertThat(skillTools.getPrimarySkillBySuperAbility(SuperAbilityType.EXPLOSIVE_SHOT)) + .isEqualTo(PrimarySkillType.ARCHERY); + assertThat(skillTools.getPrimarySkillBySuperAbility(SuperAbilityType.MACES_SUPER_ABILITY)) + .isEqualTo(PrimarySkillType.MACES); + assertThat(skillTools.getPrimarySkillBySuperAbility(SuperAbilityType.SPEARS_SUPER_ABILITY)) + .isEqualTo(PrimarySkillType.SPEARS); + } + + @Test + void mainActivatedAbilityChildMapShouldOmitBlastMiningAndMapOthersBackToAbility() throws Exception { + SkillTools skillTools = newSkillToolsForVersion(1, 21, 11); + + // All super abilities EXCEPT BLAST_MINING should be discoverable via getSuperAbility() + assertThat(skillTools.getSuperAbility(PrimarySkillType.MINING)) + .as("MINING should not expose BLAST_MINING as the 'main' tool-readied ability") + .isEqualTo(SuperAbilityType.SUPER_BREAKER); + + assertThat(skillTools.getSuperAbility(PrimarySkillType.UNARMED)) + .isEqualTo(SuperAbilityType.BERSERK); + assertThat(skillTools.getSuperAbility(PrimarySkillType.HERBALISM)) + .isEqualTo(SuperAbilityType.GREEN_TERRA); + assertThat(skillTools.getSuperAbility(PrimarySkillType.WOODCUTTING)) + .isEqualTo(SuperAbilityType.TREE_FELLER); + assertThat(skillTools.getSuperAbility(PrimarySkillType.AXES)) + .isEqualTo(SuperAbilityType.SKULL_SPLITTER); + assertThat(skillTools.getSuperAbility(PrimarySkillType.SWORDS)) + .isEqualTo(SuperAbilityType.SERRATED_STRIKES); + assertThat(skillTools.getSuperAbility(PrimarySkillType.EXCAVATION)) + .isEqualTo(SuperAbilityType.GIGA_DRILL_BREAKER); + assertThat(skillTools.getSuperAbility(PrimarySkillType.CROSSBOWS)) + .isEqualTo(SuperAbilityType.SUPER_SHOTGUN); + assertThat(skillTools.getSuperAbility(PrimarySkillType.TRIDENTS)) + .isEqualTo(SuperAbilityType.TRIDENTS_SUPER_ABILITY); + assertThat(skillTools.getSuperAbility(PrimarySkillType.ARCHERY)) + .isEqualTo(SuperAbilityType.EXPLOSIVE_SHOT); + assertThat(skillTools.getSuperAbility(PrimarySkillType.MACES)) + .isEqualTo(SuperAbilityType.MACES_SUPER_ABILITY); + assertThat(skillTools.getSuperAbility(PrimarySkillType.SPEARS)) + .isEqualTo(SuperAbilityType.SPEARS_SUPER_ABILITY); + + // Skills without a main activated ability should return null + assertThat(skillTools.getSuperAbility(PrimarySkillType.REPAIR)).isNull(); + assertThat(skillTools.getSuperAbility(PrimarySkillType.FISHING)).isNull(); + } + + // ------------------------------------------------------------------------ + // Sub-skill → primary-skill mapping (name prefix convention) + // ------------------------------------------------------------------------ + + @Test + void primarySkillBySubSkillShouldFollowNamePrefixConvention() throws Exception { + SkillTools skillTools = newSkillToolsForVersion(1, 21, 11); + + for (SubSkillType sub : SubSkillType.values()) { + PrimarySkillType parent = skillTools.getPrimarySkillBySubSkill(sub); + + assertThat(parent) + .as("SubSkill %s should have a parent PrimarySkillType", sub) + .isNotNull(); + + String subName = sub.name().toUpperCase(Locale.ENGLISH); + String parentPrefix = parent.name().toUpperCase(Locale.ENGLISH); + + assertThat(subName.startsWith(parentPrefix)) + .as("SubSkill %s should start with its parent skill name %s", subName, parentPrefix) + .isTrue(); + } + } + + // ------------------------------------------------------------------------ + // primarySkillToolMap + // ------------------------------------------------------------------------ + + @Test + void primarySkillToolTypeMappingShouldMatchDefinition() throws Exception { + SkillTools skillTools = newSkillToolsForVersion(1, 21, 11); + + assertThat(skillTools.getPrimarySkillToolType(PrimarySkillType.AXES)) + .isEqualTo(ToolType.AXE); + assertThat(skillTools.getPrimarySkillToolType(PrimarySkillType.WOODCUTTING)) + .isEqualTo(ToolType.AXE); + assertThat(skillTools.getPrimarySkillToolType(PrimarySkillType.UNARMED)) + .isEqualTo(ToolType.FISTS); + assertThat(skillTools.getPrimarySkillToolType(PrimarySkillType.SWORDS)) + .isEqualTo(ToolType.SWORD); + assertThat(skillTools.getPrimarySkillToolType(PrimarySkillType.EXCAVATION)) + .isEqualTo(ToolType.SHOVEL); + assertThat(skillTools.getPrimarySkillToolType(PrimarySkillType.HERBALISM)) + .isEqualTo(ToolType.HOE); + assertThat(skillTools.getPrimarySkillToolType(PrimarySkillType.MINING)) + .isEqualTo(ToolType.PICKAXE); + + // And any skill not explicitly mapped should currently return null + assertThat(skillTools.getPrimarySkillToolType(PrimarySkillType.FISHING)).isNull(); + assertThat(skillTools.getPrimarySkillToolType(PrimarySkillType.TAMING)).isNull(); + } + + // ------------------------------------------------------------------------ + // Combat / Gathering / Misc groupings by Minecraft version + // ------------------------------------------------------------------------ + + @Test + void combatGatheringMiscGroupingsShouldMatchDefinitionForModernSpearsAndMacesVersion() + throws Exception { + SkillTools skillTools = newSkillToolsForVersion(1, 21, 11); + + assertThat(skillTools.getCombatSkills()) + .containsExactly( + PrimarySkillType.ARCHERY, + PrimarySkillType.AXES, + PrimarySkillType.CROSSBOWS, + PrimarySkillType.MACES, + PrimarySkillType.SWORDS, + PrimarySkillType.SPEARS, + PrimarySkillType.TAMING, + PrimarySkillType.TRIDENTS, + PrimarySkillType.UNARMED + ); + + assertThat(skillTools.getGatheringSkills()) + .containsExactly( + PrimarySkillType.EXCAVATION, + PrimarySkillType.FISHING, + PrimarySkillType.HERBALISM, + PrimarySkillType.MINING, + PrimarySkillType.WOODCUTTING + ); + + assertThat(skillTools.getMiscSkills()) + .containsExactly( + PrimarySkillType.ACROBATICS, + PrimarySkillType.ALCHEMY, + PrimarySkillType.REPAIR, + PrimarySkillType.SALVAGE, + PrimarySkillType.SMELTING + ); + } + + @Test + void combatSkillsShouldMatchDefinitionForVersionWithMacesButWithoutSpears() throws Exception { + SkillTools skillTools = newSkillToolsForVersion(1, 21, 0); + + assertThat(skillTools.getCombatSkills()) + .containsExactly( + PrimarySkillType.ARCHERY, + PrimarySkillType.AXES, + PrimarySkillType.CROSSBOWS, + PrimarySkillType.MACES, + PrimarySkillType.SWORDS, + PrimarySkillType.TAMING, + PrimarySkillType.TRIDENTS, + PrimarySkillType.UNARMED + ); + } + + @Test + void combatSkillsShouldMatchDefinitionForVersionWithoutMacesOrSpears() throws Exception { + SkillTools skillTools = newSkillToolsForVersion(1, 20, 4); + + assertThat(skillTools.getCombatSkills()) + .containsExactly( + PrimarySkillType.ARCHERY, + PrimarySkillType.AXES, + PrimarySkillType.CROSSBOWS, + PrimarySkillType.SWORDS, + PrimarySkillType.TAMING, + PrimarySkillType.TRIDENTS, + PrimarySkillType.UNARMED + ); + } + + // ------------------------------------------------------------------------ + // LOCALIZED_SKILL_NAMES basic sanity (size + uniqueness, not content) + // ------------------------------------------------------------------------ + + @Test + void localizedSkillNamesShouldContainOneEntryPerPrimarySkillAndBeSorted() throws Exception { + SkillTools skillTools = newSkillToolsForVersion(1, 21, 11); + + List names = new ArrayList<>(skillTools.LOCALIZED_SKILL_NAMES); + + // One per PrimarySkillType + assertThat(names).hasSize(PrimarySkillType.values().length); + + // No duplicates + assertThat(new HashSet<>(names)).hasSize(names.size()); + + // Sorted ascending + List sorted = new ArrayList<>(names); + Collections.sort(sorted); + assertThat(names).isEqualTo(sorted); + } +}