diff --git a/src/main/java/world/bentobox/challenges/tasks/TryToComplete.java b/src/main/java/world/bentobox/challenges/tasks/TryToComplete.java index 15fa3da2..40333969 100644 --- a/src/main/java/world/bentobox/challenges/tasks/TryToComplete.java +++ b/src/main/java/world/bentobox/challenges/tasks/TryToComplete.java @@ -1993,7 +1993,8 @@ private boolean hasRequiredTeamPresence() * Checks if two items match, considering the ignore-metadata setting. For potion-like materials * that are in the ignore-metadata set, compares the base potion type while ignoring other metadata. * For non-potion materials in the ignore-metadata set, uses type-only comparison. For materials - * not in the ignore-metadata set, uses full similarity comparison. + * not in the ignore-metadata set, uses full similarity comparison, except that the anvil + * repair cost component is ignored. * * @param candidate candidate item from inventory * @param required required item template @@ -2012,10 +2013,11 @@ private static boolean itemsMatch(ItemStack candidate, ItemStack required, Set 0) { + ((Repairable) meta).setRepairCost(repairCost); + } + book.setItemMeta(meta); + return book; + } + + @Test + void testInventoryChallengeEnchantedBookFromAnvilMatches() { + // Required: Unbreaking III book with no repair cost (as configured by an admin) + InventoryRequirements req = new InventoryRequirements(); + ItemStack requiredBook = createEnchantedBook(Enchantment.UNBREAKING, 3, 0); + req.setRequiredItems(Collections.singletonList(requiredBook)); + req.setTakeItems(true); + challenge.setRequirements(req); + + // Player holds an Unbreaking III book that was combined in an anvil (has repair_cost) + ItemStack anvilBook = createEnchantedBook(Enchantment.UNBREAKING, 3, 1); + assertFalse(anvilBook.isSimilar(requiredBook), "Sanity: strict isSimilar must differ on repair cost"); + when(inv.getContents()).thenReturn(new ItemStack[] { anvilBook }); + when(player.getInventory()).thenReturn(inv); + + assertTrue(TryToComplete.complete(addon, user, challenge, world, topLabel, permissionPrefix)); + verify(user, never()).getTranslation(any(World.class), eq("challenges.errors.not-enough-items"), any(), any()); + assertEquals(0, anvilBook.getAmount(), "The anvil-made book should have been taken"); + } + + @Test + void testInventoryChallengeEnchantedBookRequiredHasRepairCost() { + // Required item itself was created from an anvil-made book; player holds a clean one + InventoryRequirements req = new InventoryRequirements(); + ItemStack requiredBook = createEnchantedBook(Enchantment.UNBREAKING, 3, 2); + req.setRequiredItems(Collections.singletonList(requiredBook)); + challenge.setRequirements(req); + + ItemStack cleanBook = createEnchantedBook(Enchantment.UNBREAKING, 3, 0); + when(inv.getContents()).thenReturn(new ItemStack[] { cleanBook }); + when(player.getInventory()).thenReturn(inv); + + assertTrue(TryToComplete.complete(addon, user, challenge, world, topLabel, permissionPrefix)); + } + + @Test + void testInventoryChallengeEnchantedBookWrongLevelStillFails() { + // Ignoring repair cost must not loosen the enchantment check itself + InventoryRequirements req = new InventoryRequirements(); + ItemStack requiredBook = createEnchantedBook(Enchantment.UNBREAKING, 3, 0); + req.setRequiredItems(Collections.singletonList(requiredBook)); + challenge.setRequirements(req); + + ItemStack lowerBook = createEnchantedBook(Enchantment.UNBREAKING, 2, 1); + when(inv.getContents()).thenReturn(new ItemStack[] { lowerBook }); + when(player.getInventory()).thenReturn(inv); + // The "missing items" message prettifies the enchanted book, which needs vararg translations + Mockito.doAnswer(invocation -> invocation.getArgument(0, String.class)) + .when(user).getTranslationOrNothing(anyString(), Mockito.any(String[].class)); + + assertFalse(TryToComplete.complete(addon, user, challenge, world, topLabel, permissionPrefix)); + verify(user).getTranslation(any(World.class), eq("challenges.errors.not-enough-items"), eq("[items]"), + any()); + } + + @Test + void testUtilsIsSimilarIgnoringRepairCost() { + ItemStack clean = createEnchantedBook(Enchantment.UNBREAKING, 3, 0); + ItemStack anvil = createEnchantedBook(Enchantment.UNBREAKING, 3, 5); + ItemStack other = createEnchantedBook(Enchantment.MENDING, 1, 5); + + assertTrue(Utils.isSimilarIgnoringRepairCost(clean, anvil)); + assertTrue(Utils.isSimilarIgnoringRepairCost(anvil, clean)); + assertTrue(Utils.isSimilarIgnoringRepairCost(anvil, anvil.clone())); + assertFalse(Utils.isSimilarIgnoringRepairCost(anvil, other)); + assertFalse(Utils.isSimilarIgnoringRepairCost(null, clean)); + assertFalse(Utils.isSimilarIgnoringRepairCost(clean, null)); + + // The original item must not be modified + assertEquals(5, ((Repairable) anvil.getItemMeta()).getRepairCost()); + } + + @Test + void testUtilsWithoutRepairCost() { + ItemStack clean = createEnchantedBook(Enchantment.UNBREAKING, 3, 0); + ItemStack anvil = createEnchantedBook(Enchantment.UNBREAKING, 3, 5); + ItemStack plain = new ItemStack(Material.DIRT); + + // Items without a repair cost are returned as-is + assertTrue(clean == Utils.withoutRepairCost(clean)); + assertTrue(plain == Utils.withoutRepairCost(plain)); + assertNull(Utils.withoutRepairCost(null)); + + ItemStack stripped = Utils.withoutRepairCost(anvil); + assertFalse(((Repairable) stripped.getItemMeta()).hasRepairCost()); + assertTrue(stripped.isSimilar(clean)); + assertEquals(5, ((Repairable) anvil.getItemMeta()).getRepairCost(), "Original must be untouched"); + } + + @Test + void testGroupEqualItemsMergesAnvilBooks() { + ItemStack clean = createEnchantedBook(Enchantment.UNBREAKING, 3, 0); + ItemStack anvil = createEnchantedBook(Enchantment.UNBREAKING, 3, 3); + List grouped = Utils.groupEqualItems(Arrays.asList(clean, anvil), Set.of()); + assertEquals(1, grouped.size()); + assertEquals(2, grouped.get(0).getAmount()); + } }