diff --git a/pom.xml b/pom.xml index 8d7473b..5ebe28a 100644 --- a/pom.xml +++ b/pom.xml @@ -69,7 +69,7 @@ -LOCAL - 2.26.0 + 2.27.0 BentoBoxWorld_Level bentobox-world https://sonarcloud.io diff --git a/src/main/java/world/bentobox/level/commands/IslandDonateCommand.java b/src/main/java/world/bentobox/level/commands/IslandDonateCommand.java index fa76d70..d4d603e 100644 --- a/src/main/java/world/bentobox/level/commands/IslandDonateCommand.java +++ b/src/main/java/world/bentobox/level/commands/IslandDonateCommand.java @@ -1,10 +1,13 @@ package world.bentobox.level.commands; +import java.util.EnumMap; import java.util.List; +import java.util.Map; import java.util.Optional; import org.bukkit.Material; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; import world.bentobox.bentobox.api.commands.CompositeCommand; import world.bentobox.bentobox.api.commands.ConfirmableCommand; @@ -17,13 +20,17 @@ import world.bentobox.level.util.Utils; /** - * Command: /island donate [hand [amount]] - * Opens a donation GUI or donates blocks from hand. + * Command: /island donate [hand [amount]] [inv] + * Opens a donation GUI, or donates blocks from the player's hand, or + * donates every donatable block from the player's inventory. * * @author tastybento */ public class IslandDonateCommand extends ConfirmableCommand { + private static final String MATERIAL_PLACEHOLDER = "[material]"; + private static final String POINTS_PLACEHOLDER = "[points]"; + private final Level addon; public IslandDonateCommand(Level addon, CompositeCommand parent) { @@ -65,6 +72,11 @@ public boolean execute(User user, String label, List args) { return handleHandDonation(user, island, args); } + // Handle "inv" subcommand (accepts English "inv" or the localized keyword) + if (!args.isEmpty() && isInvKeyword(user, args.get(0))) { + return handleInvDonation(user, island); + } + // No args - open GUI DonationPanel.openPanel(addon, getWorld(), user, island); return true; @@ -111,8 +123,8 @@ private boolean handleHandDonation(User user, Island island, List args) String prompt = user.getTranslation("island.donate.hand.confirm-prompt", TextVariables.NUMBER, String.valueOf(previewAmount), - "[material]", Utils.prettifyObject(material, user), - "[points]", Utils.formatNumber(user, previewPoints)); + MATERIAL_PLACEHOLDER, Utils.prettifyObject(material, user), + POINTS_PLACEHOLDER, Utils.formatNumber(user, previewPoints)); askConfirmation(user, prompt, () -> performHandDonation(user, island, material, blockValue, finalRequested)); return true; @@ -138,18 +150,110 @@ private void performHandDonation(User user, Island island, Material material, in user.sendMessage("island.donate.hand.success", TextVariables.NUMBER, String.valueOf(amount), - "[material]", Utils.prettifyObject(material, user), - "[points]", Utils.formatNumber(user, points)); + MATERIAL_PLACEHOLDER, Utils.prettifyObject(material, user), + POINTS_PLACEHOLDER, Utils.formatNumber(user, points)); + } + + /** + * Handle the /island donate inv subcommand. Scans the player's inventory for + * blocks with a positive donation value, shows a per-material breakdown plus + * the total, and asks for confirmation. Items with no value or that aren't + * donatable blocks remain in the inventory. + */ + private boolean handleInvDonation(User user, Island island) { + Map totals = collectDonatableTotals(user.getPlayer().getInventory()); + + if (totals.isEmpty()) { + user.sendMessage("island.donate.empty"); + return false; + } + + long totalPoints = 0L; + StringBuilder prompt = new StringBuilder( + user.getTranslation("island.donate.inv.confirm-header")); + for (Map.Entry e : totals.entrySet()) { + int value = addon.getBlockConfig().getValue(getWorld(), e.getKey()); + long points = (long) value * e.getValue(); + totalPoints += points; + prompt.append('\n').append(user.getTranslation("island.donate.inv.confirm-line", + TextVariables.NUMBER, String.valueOf(e.getValue()), + MATERIAL_PLACEHOLDER, Utils.prettifyObject(e.getKey(), user), + POINTS_PLACEHOLDER, Utils.formatNumber(user, points))); + } + prompt.append('\n').append(user.getTranslation("island.donate.inv.confirm-total", + POINTS_PLACEHOLDER, Utils.formatNumber(user, totalPoints))); + + askConfirmation(user, prompt.toString(), () -> performInvDonation(user, island)); + return true; + } + + private void performInvDonation(User user, Island island) { + PlayerInventory pInv = user.getPlayer().getInventory(); + ItemStack[] contents = pInv.getStorageContents(); + Map donated = new EnumMap<>(Material.class); + long totalPoints = 0L; + + for (int i = 0; i < contents.length; i++) { + ItemStack item = contents[i]; + Integer value = donationValue(item); + if (value == null) { + continue; + } + int amount = item.getAmount(); + long points = (long) value * amount; + donated.merge(item.getType(), amount, Integer::sum); + totalPoints += points; + addon.getManager().donateBlocks(island, user.getUniqueId(), item.getType().name(), amount, points); + contents[i] = null; + } + pInv.setStorageContents(contents); + + if (donated.isEmpty()) { + user.sendMessage("island.donate.empty"); + return; + } + int totalBlocks = donated.values().stream().mapToInt(Integer::intValue).sum(); + user.sendMessage("island.donate.success", + POINTS_PLACEHOLDER, Utils.formatNumber(user, totalPoints), + TextVariables.NUMBER, String.valueOf(totalBlocks)); + addon.getManager().recalculateAfterDonation(island); + } + + private Map collectDonatableTotals(PlayerInventory pInv) { + Map totals = new EnumMap<>(Material.class); + for (ItemStack item : pInv.getStorageContents()) { + if (donationValue(item) != null) { + totals.merge(item.getType(), item.getAmount(), Integer::sum); + } + } + return totals; + } + + /** + * @return the per-block donation value if the item is a donatable block with a + * positive configured value, or null otherwise + */ + private Integer donationValue(ItemStack item) { + if (item == null || item.getType().isAir() || !item.getType().isBlock()) { + return null; + } + Integer value = addon.getBlockConfig().getValue(getWorld(), item.getType()); + return (value != null && value > 0) ? value : null; } @Override public Optional> tabComplete(User user, String alias, List args) { + // BentoBox includes the command label as args.get(0); the user-typed args start at index 1. String lastArg = !args.isEmpty() ? args.get(args.size() - 1) : ""; String handKeyword = user.getTranslation("island.donate.hand.keyword"); - if (args.size() <= 1) { - return Optional.of(Util.tabLimit(List.of(handKeyword), lastArg)); + String invKeyword = user.getTranslation("island.donate.inv.keyword"); + + // First user-arg slot: suggest "hand" and "inv". + if (args.size() <= 2) { + return Optional.of(Util.tabLimit(List.of(handKeyword, invKeyword), lastArg)); } - if (args.size() == 2 && isHandKeyword(user, args.get(0)) && user.isPlayer()) { + // Second user-arg slot after "hand": suggest the held count. + if (args.size() == 3 && isHandKeyword(user, args.get(1)) && user.isPlayer()) { int held = user.getPlayer().getInventory().getItemInMainHand().getAmount(); if (held > 0) { return Optional.of(Util.tabLimit(List.of(String.valueOf(held)), lastArg)); @@ -162,4 +266,9 @@ private boolean isHandKeyword(User user, String arg) { String localized = user.getTranslation("island.donate.hand.keyword"); return "hand".equalsIgnoreCase(arg) || localized.equalsIgnoreCase(arg); } + + private boolean isInvKeyword(User user, String arg) { + String localized = user.getTranslation("island.donate.inv.keyword"); + return "inv".equalsIgnoreCase(arg) || localized.equalsIgnoreCase(arg); + } } diff --git a/src/main/resources/locales/cs.yml b/src/main/resources/locales/cs.yml index 35ec78b..7c1da5b 100644 --- a/src/main/resources/locales/cs.yml +++ b/src/main/resources/locales/cs.yml @@ -67,6 +67,11 @@ island: success: "Donated [number] x [material] for [points] permanent points!" not-block: "You must be holding a placeable block to donate." confirm-prompt: "About to DESTROY [number] x [material] for [points] permanent points." + inv: + keyword: "inv" + confirm-header: "Tyto bloky budou ZNIČENY z tvého inventáře:" + confirm-line: "[number] x [material] = [points] bodů" + confirm-total: "Celkem: [points] trvalých bodů." detail: description: "zobrazit podrobnosti o blocích vašeho ostrova" top: diff --git a/src/main/resources/locales/de.yml b/src/main/resources/locales/de.yml index b05dab9..75ebec9 100644 --- a/src/main/resources/locales/de.yml +++ b/src/main/resources/locales/de.yml @@ -68,6 +68,11 @@ island: success: "Donated [number] x [material] for [points] permanent points!" not-block: "You must be holding a placeable block to donate." confirm-prompt: "About to DESTROY [number] x [material] for [points] permanent points." + inv: + keyword: "inv" + confirm-header: "Diese Blöcke werden aus deinem Inventar ZERSTÖRT:" + confirm-line: "[number] x [material] = [points] Punkte" + confirm-total: "Gesamt: [points] permanente Punkte." detail: description: "zeigt Details der Blöcke deiner Insel" top: diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index ff81550..e18ba4a 100755 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -52,7 +52,7 @@ island: in-progress: "Island level calculation is in progress..." time-out: "The level calculation took too long. Please try again later." donate: - parameters: "[hand [amount]]" + parameters: "[hand [amount]] [inv]" description: "donate blocks to permanently raise island level" must-be-on-island: "You must be on your island to donate blocks." no-permission: "You do not have permission to donate blocks on this island." @@ -72,6 +72,11 @@ island: success: "Donated [number] x [material] for [points] permanent points!" not-block: "You must be holding a placeable block to donate." confirm-prompt: "About to DESTROY [number] x [material] for [points] permanent points." + inv: + keyword: "inv" + confirm-header: "About to DESTROY these blocks from your inventory:" + confirm-line: "[number] x [material] = [points] points" + confirm-total: "Total: [points] permanent points." detail: description: "shows detail of your island blocks" top: diff --git a/src/main/resources/locales/es.yml b/src/main/resources/locales/es.yml index 0d93e3f..8571223 100644 --- a/src/main/resources/locales/es.yml +++ b/src/main/resources/locales/es.yml @@ -65,6 +65,11 @@ island: success: "Donated [number] x [material] for [points] permanent points!" not-block: "You must be holding a placeable block to donate." confirm-prompt: "About to DESTROY [number] x [material] for [points] permanent points." + inv: + keyword: "inv" + confirm-header: "Estos bloques serán DESTRUIDOS de tu inventario:" + confirm-line: "[number] x [material] = [points] puntos" + confirm-total: "Total: [points] puntos permanentes." detail: description: "muestra el detalle de los bloques de tu isla" top: diff --git a/src/main/resources/locales/fr.yml b/src/main/resources/locales/fr.yml index e1265d1..d939b6b 100644 --- a/src/main/resources/locales/fr.yml +++ b/src/main/resources/locales/fr.yml @@ -67,6 +67,11 @@ island: success: "Donated [number] x [material] for [points] permanent points!" not-block: "You must be holding a placeable block to donate." confirm-prompt: "About to DESTROY [number] x [material] for [points] permanent points." + inv: + keyword: "inv" + confirm-header: "Ces blocs vont être DÉTRUITS de votre inventaire :" + confirm-line: "[number] x [material] = [points] points" + confirm-total: "Total : [points] points permanents." top: description: affiche le top 10 gui-title: "Top 10" diff --git a/src/main/resources/locales/hu.yml b/src/main/resources/locales/hu.yml index 7be400a..e9113e5 100644 --- a/src/main/resources/locales/hu.yml +++ b/src/main/resources/locales/hu.yml @@ -68,6 +68,11 @@ island: success: "Donated [number] x [material] for [points] permanent points!" not-block: "You must be holding a placeable block to donate." confirm-prompt: "About to DESTROY [number] x [material] for [points] permanent points." + inv: + keyword: "inv" + confirm-header: "Ezeket a blokkokat MEGSEMMISÍTI a leltáradból:" + confirm-line: "[number] x [material] = [points] pont" + confirm-total: "Összesen: [points] állandó pont." detail: description: "megmutatja a szigeted blokkjainak részleteit" top: diff --git a/src/main/resources/locales/id.yml b/src/main/resources/locales/id.yml index 539666b..ff360f0 100644 --- a/src/main/resources/locales/id.yml +++ b/src/main/resources/locales/id.yml @@ -65,6 +65,11 @@ island: success: "Donated [number] x [material] for [points] permanent points!" not-block: "You must be holding a placeable block to donate." confirm-prompt: "About to DESTROY [number] x [material] for [points] permanent points." + inv: + keyword: "inv" + confirm-header: "Akan MENGHANCURKAN blok-blok ini dari inventaris Anda:" + confirm-line: "[number] x [material] = [points] poin" + confirm-total: "Total: [points] poin permanen." top: description: menunjukkan Sepuluh Besar gui-title: " Sepuluh Besar" diff --git a/src/main/resources/locales/ko.yml b/src/main/resources/locales/ko.yml index f846737..073e26c 100644 --- a/src/main/resources/locales/ko.yml +++ b/src/main/resources/locales/ko.yml @@ -68,6 +68,11 @@ island: success: "Donated [number] x [material] for [points] permanent points!" not-block: "You must be holding a placeable block to donate." confirm-prompt: "About to DESTROY [number] x [material] for [points] permanent points." + inv: + keyword: "inv" + confirm-header: "인벤토리에서 다음 블록을 파괴합니다:" + confirm-line: "[number] x [material] = [points] 점" + confirm-total: "총: [points] 영구 점수." detail: description: "섬 블록의 세부 정보를 표시합니다" top: diff --git a/src/main/resources/locales/lv.yml b/src/main/resources/locales/lv.yml index 7ec2474..a9f6feb 100644 --- a/src/main/resources/locales/lv.yml +++ b/src/main/resources/locales/lv.yml @@ -68,6 +68,11 @@ island: success: "Donated [number] x [material] for [points] permanent points!" not-block: "You must be holding a placeable block to donate." confirm-prompt: "About to DESTROY [number] x [material] for [points] permanent points." + inv: + keyword: "inv" + confirm-header: "Šie bloki tiks IZNĪCINĀTI no tavas somas:" + confirm-line: "[number] x [material] = [points] punkti" + confirm-total: "Kopā: [points] pastāvīgi punkti." detail: description: "rāda tavas salas bloku detaļas" top: diff --git a/src/main/resources/locales/nl.yml b/src/main/resources/locales/nl.yml index 324b896..9ddd410 100644 --- a/src/main/resources/locales/nl.yml +++ b/src/main/resources/locales/nl.yml @@ -65,6 +65,11 @@ island: success: "Donated [number] x [material] for [points] permanent points!" not-block: "You must be holding a placeable block to donate." confirm-prompt: "About to DESTROY [number] x [material] for [points] permanent points." + inv: + keyword: "inv" + confirm-header: "Deze blokken worden VERNIETIGD uit je inventaris:" + confirm-line: "[number] x [material] = [points] punten" + confirm-total: "Totaal: [points] permanente punten." top: description: Toon de Top tien gui-title: " Top tien" diff --git a/src/main/resources/locales/pl.yml b/src/main/resources/locales/pl.yml index da6ad80..67b6940 100644 --- a/src/main/resources/locales/pl.yml +++ b/src/main/resources/locales/pl.yml @@ -65,6 +65,11 @@ island: success: "Donated [number] x [material] for [points] permanent points!" not-block: "You must be holding a placeable block to donate." confirm-prompt: "About to DESTROY [number] x [material] for [points] permanent points." + inv: + keyword: "inv" + confirm-header: "Te bloki zostaną ZNISZCZONE z twojego ekwipunku:" + confirm-line: "[number] x [material] = [points] punktów" + confirm-total: "Łącznie: [points] punktów stałych." top: description: pokauje Top 10 wysp gui-title: "Top 10" diff --git a/src/main/resources/locales/pt.yml b/src/main/resources/locales/pt.yml index 57fbabe..ffb289c 100644 --- a/src/main/resources/locales/pt.yml +++ b/src/main/resources/locales/pt.yml @@ -68,6 +68,11 @@ island: success: "Donated [number] x [material] for [points] permanent points!" not-block: "You must be holding a placeable block to donate." confirm-prompt: "About to DESTROY [number] x [material] for [points] permanent points." + inv: + keyword: "inv" + confirm-header: "Estes blocos serão DESTRUÍDOS do seu inventário:" + confirm-line: "[number] x [material] = [points] pontos" + confirm-total: "Total: [points] pontos permanentes." detail: description: "mostra os detalhes dos blocos da sua ilha" top: diff --git a/src/main/resources/locales/ru.yml b/src/main/resources/locales/ru.yml index 2dc53da..e483b21 100644 --- a/src/main/resources/locales/ru.yml +++ b/src/main/resources/locales/ru.yml @@ -68,6 +68,11 @@ island: success: "Donated [number] x [material] for [points] permanent points!" not-block: "You must be holding a placeable block to donate." confirm-prompt: "About to DESTROY [number] x [material] for [points] permanent points." + inv: + keyword: "inv" + confirm-header: "Эти блоки будут УНИЧТОЖЕНЫ из вашего инвентаря:" + confirm-line: "[number] x [material] = [points] очков" + confirm-total: "Всего: [points] постоянных очков." detail: description: показать информацию о блоках на вашем острове top: diff --git a/src/main/resources/locales/tr.yml b/src/main/resources/locales/tr.yml index 689f9b8..2f8ae2b 100644 --- a/src/main/resources/locales/tr.yml +++ b/src/main/resources/locales/tr.yml @@ -72,6 +72,11 @@ island: success: "Donated [number] x [material] for [points] permanent points!" not-block: "You must be holding a placeable block to donate." confirm-prompt: "About to DESTROY [number] x [material] for [points] permanent points." + inv: + keyword: "inv" + confirm-header: "Envanterinizden bu bloklar YOK EDİLECEK:" + confirm-line: "[number] x [material] = [points] puan" + confirm-total: "Toplam: [points] kalıcı puan." detail: description: "adanın blok ayrıntılarını gösterir" top: diff --git a/src/main/resources/locales/uk.yml b/src/main/resources/locales/uk.yml index 9ac33d3..d8286b3 100644 --- a/src/main/resources/locales/uk.yml +++ b/src/main/resources/locales/uk.yml @@ -65,6 +65,11 @@ island: success: "Пожертвовано [number] x [material] за [points] постійних очок!" not-block: "Ви повинні тримати блок, який можна розмістити." confirm-prompt: "Буде ЗНИЩЕНО [number] x [material] за [points] постійних очок." + inv: + keyword: "inv" + confirm-header: "Ці блоки будуть ЗНИЩЕНІ з вашого інвентарю:" + confirm-line: "[number] x [material] = [points] очок" + confirm-total: "Всього: [points] постійних очок." top: description: показати першу десятку gui-title: "& Десятка Кращих" diff --git a/src/main/resources/locales/vi.yml b/src/main/resources/locales/vi.yml index 6d27ce2..9c19d5d 100644 --- a/src/main/resources/locales/vi.yml +++ b/src/main/resources/locales/vi.yml @@ -71,6 +71,11 @@ island: success: "Donated [number] x [material] for [points] permanent points!" not-block: "You must be holding a placeable block to donate." confirm-prompt: "About to DESTROY [number] x [material] for [points] permanent points." + inv: + keyword: "inv" + confirm-header: "Sắp PHÁ HỦY những khối này từ kho đồ của bạn:" + confirm-line: "[number] x [material] = [points] điểm" + confirm-total: "Tổng: [points] điểm vĩnh viễn." detail: description: "hiển thị chi tiết các khối trên đảo của bạn" top: diff --git a/src/main/resources/locales/zh-CN.yml b/src/main/resources/locales/zh-CN.yml index 1901208..6dd71c6 100644 --- a/src/main/resources/locales/zh-CN.yml +++ b/src/main/resources/locales/zh-CN.yml @@ -63,6 +63,11 @@ island: success: "Donated [number] x [material] for [points] permanent points!" not-block: "You must be holding a placeable block to donate." confirm-prompt: "About to DESTROY [number] x [material] for [points] permanent points." + inv: + keyword: "inv" + confirm-header: "即将从你的物品栏销毁以下方块:" + confirm-line: "[number] x [material] = [points] 点" + confirm-total: "共计: [points] 永久点数。" top: description: 显示前十名 diff --git a/src/test/java/world/bentobox/level/commands/IslandDonateCommandTest.java b/src/test/java/world/bentobox/level/commands/IslandDonateCommandTest.java index 7ba90bd..40771de 100644 --- a/src/test/java/world/bentobox/level/commands/IslandDonateCommandTest.java +++ b/src/test/java/world/bentobox/level/commands/IslandDonateCommandTest.java @@ -4,14 +4,19 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.UUID; import org.bukkit.Location; @@ -65,6 +70,10 @@ protected void setUp() throws Exception { when(user.getTranslation(anyString(), anyString(), anyString())).thenAnswer(i -> i.getArgument(0, String.class)); when(user.getTranslation(anyString(), anyString(), anyString(), anyString(), anyString())).thenAnswer(i -> i.getArgument(0, String.class)); when(user.getTranslation("island.donate.hand.keyword")).thenReturn("hand"); + when(user.getTranslation("island.donate.inv.keyword")).thenReturn("inv"); + when(user.getTranslationOrNothing(anyString())).thenReturn(""); + when(user.getTranslationOrNothing(anyString(), anyString(), anyString())).thenReturn(""); + when(user.getLocale()).thenReturn(Locale.US); when(user.getLocation()).thenReturn(location); when(player.getInventory()).thenReturn(inventory); @@ -156,9 +165,87 @@ void testExecuteHandBlockNoValue() { @Test void testTabCompleteNoArgs() { - // When no args, should suggest "hand" + // When no args, should suggest "hand" and "inv" var result = cmd.tabComplete(user, "donate", Collections.emptyList()); assertTrue(result.isPresent()); assertTrue(result.get().contains("hand")); + assertTrue(result.get().contains("inv")); + } + + @Test + void testTabCompleteFirstArgFromBentoBoxFlow() { + // BentoBox passes the leaf command label as args.get(0); the partial first + // user arg sits in args.get(1). Empty string = bare "/island donate ". + var result = cmd.tabComplete(user, "donate", List.of("donate", "")); + assertTrue(result.isPresent()); + assertTrue(result.get().contains("hand")); + assertTrue(result.get().contains("inv")); + } + + @Test + void testTabCompleteSecondArgAfterHandSuggestsHeldAmount() { + ItemStack stack = mock(ItemStack.class); + when(stack.getAmount()).thenReturn(7); + when(inventory.getItemInMainHand()).thenReturn(stack); + + var result = cmd.tabComplete(user, "donate", List.of("donate", "hand", "")); + assertTrue(result.isPresent()); + assertTrue(result.get().contains("7")); + } + + @Test + void testTabCompleteAfterInvSuggestsNothing() { + var result = cmd.tabComplete(user, "donate", List.of("donate", "inv", "")); + assertTrue(result.isPresent()); + assertTrue(result.get().isEmpty()); + } + + @Test + void testExecuteInvEmptyInventory() { + when(inventory.getStorageContents()).thenReturn(new ItemStack[] { null, null, null }); + + assertFalse(cmd.execute(user, "donate", List.of("inv"))); + verify(user).sendMessage("island.donate.empty"); + verify(manager, never()).donateBlocks(any(), any(UUID.class), anyString(), anyInt(), anyLong()); + } + + @Test + void testExecuteInvNoValuableBlocks() { + // Stone with no value, sword (not a block) + ItemStack stone = mock(ItemStack.class); + when(stone.getType()).thenReturn(Material.STONE); + when(stone.getAmount()).thenReturn(5); + ItemStack sword = mock(ItemStack.class); + when(sword.getType()).thenReturn(Material.DIAMOND_SWORD); + when(inventory.getStorageContents()).thenReturn(new ItemStack[] { stone, sword }); + when(blockConfig.getValue(any(), eq(Material.STONE))).thenReturn(null); + + assertFalse(cmd.execute(user, "donate", List.of("inv"))); + verify(user).sendMessage("island.donate.empty"); + verify(manager, never()).donateBlocks(any(), any(UUID.class), anyString(), anyInt(), anyLong()); + } + + @Test + void testExecuteInvShowsConfirmationPrompt() { + ItemStack diamond = mock(ItemStack.class); + when(diamond.getType()).thenReturn(Material.DIAMOND_BLOCK); + when(diamond.getAmount()).thenReturn(2); + ItemStack gold = mock(ItemStack.class); + when(gold.getType()).thenReturn(Material.GOLD_BLOCK); + when(gold.getAmount()).thenReturn(3); + // Non-donatable item is ignored, not destroyed + ItemStack sword = mock(ItemStack.class); + when(sword.getType()).thenReturn(Material.DIAMOND_SWORD); + + when(inventory.getStorageContents()) + .thenReturn(new ItemStack[] { diamond, sword, gold }); + when(blockConfig.getValue(any(), eq(Material.DIAMOND_BLOCK))).thenReturn(100); + when(blockConfig.getValue(any(), eq(Material.GOLD_BLOCK))).thenReturn(50); + + assertTrue(cmd.execute(user, "donate", List.of("inv"))); + // The confirmation header should have been requested via getTranslation + verify(user).getTranslation("island.donate.inv.confirm-header"); + // No donation yet — only confirmation requested + verify(manager, never()).donateBlocks(any(), any(UUID.class), anyString(), anyInt(), anyLong()); } }