diff --git a/README.md b/README.md index 10b425c..77a0990 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # Spigot Quest Board -A Purpur 26.2 plugin for shared physical quest boards. Players can escrow rewards for requested block items, deliver those blocks at any quest board, and claim delivered or returned items safely. +A Purpur 26.2 plugin for shared physical quest boards. Players can escrow rewards for requested items, deliver those items at any quest board, and claim delivered or returned items safely. The approved behavior is specified in the [OKF knowledge bundle](knowledge/index.md). ## Status -Administrators can register persistent shared quest boards by targeting a block within five blocks and running `/questadmin createboard`. They can instead run `/questadmin createboard physical` to generate a five-wide oak board above the targeted ground anchor, with glowing centered title and browsing-instruction signs above three decorative signs; its visible panel blocks and all five signs are registered. Right-clicking any registered board opens a compact native dashboard with dedicated browsing, creation, and claim screens. A player can request a block and quantity while escrowing the exact reward stack held in their main hand. Every board and `/quests list` show the same active quests with requested blocks, rewards, issuers, and time remaining; custom reward names are shown together with their material type. Players can complete quests at any board or with `/quests complete ` by delivering the required blocks. Exact escrowed rewards are granted immediately, and delivered blocks are held for the issuer. Issuers can cancel their own active quests at any board or with `/quests cancel `. Completed deliveries and rewards from cancelled or seven-day-expired quests are held durably and can be collected at any board or with `/quests claim`; inventory overflow drops at the claimant's feet. +Administrators can register persistent shared quest boards by targeting a block within five blocks and running `/questadmin createboard`. They can instead run `/questadmin createboard physical` to generate a five-wide oak board above the targeted ground anchor, with glowing centered title and browsing-instruction signs above three decorative signs; its visible panel blocks and all five signs are registered. Right-clicking any registered board opens a compact native dashboard with dedicated browsing, creation, and claim screens. A player can request any inventory item material and quantity while escrowing the exact reward stack held in their main hand. Every board and `/quests list` show the same active quests with requested items, rewards, issuers, and time remaining; custom reward names are shown together with their material type. Players can complete quests at any board or with `/quests complete ` by delivering the required items. Exact escrowed rewards are granted immediately, and delivered items are held for the issuer. Issuers can cancel their own active quests at any board or with `/quests cancel `. Completed deliveries and rewards from cancelled or seven-day-expired quests are held durably and can be collected at any board or with `/quests claim`; inventory overflow drops at the claimant's feet. ## Requirements @@ -23,7 +23,7 @@ The plugin JAR is written to `build/libs/`. ## Player commands -Player commands are disabled by default and can be enabled persistently by an administrator. When disabled, players within five blocks of a registered board can still use `/quests create ` with material and quantity autocomplete; other player command forms remain disabled. +Player commands are disabled by default and can be enabled persistently by an administrator. When disabled, players within five blocks of a registered board can still use `/quests create ` with material and quantity autocomplete; other player command forms remain disabled. ```text /quests diff --git a/knowledge/index.md b/knowledge/index.md index 068cf50..bd66897 100644 --- a/knowledge/index.md +++ b/knowledge/index.md @@ -7,7 +7,7 @@ okf_version: "0.1" # Spigot Quest Board Knowledge -This bundle specifies shared physical quest boards, reward escrow, block deliveries, item claims, player commands, administration, persistence, and plugin delivery requirements. +This bundle specifies shared physical quest boards, reward escrow, item deliveries, item claims, player commands, administration, persistence, and plugin delivery requirements. Login guidance is covered by [quest browsing](user-stories/us-004-browse-available-quests.md) and [recurring pending-item reminders](user-stories/us-007-expire-quests-and-claim-held-items.md). diff --git a/knowledge/log.md b/knowledge/log.md index d26c0bf..7fc43ab 100644 --- a/knowledge/log.md +++ b/knowledge/log.md @@ -6,6 +6,13 @@ description: Chronological record of material decisions affecting Spigot Quest B # Spigot Quest Board Knowledge Log +## 2026-09-10T01:38:21Z — Inventory-item quest requests + +- Extended [US-003](user-stories/us-003-create-a-block-delivery-quest.md) and [US-005](user-stories/us-005-deliver-blocks-and-complete-a-quest.md) to support inventory item materials, including shulker shells, rather than only blocks. +- Validation and autocomplete use item eligibility and reject air and non-item materials; command, dialog, notification, and claim wording now describes items. +- Preserved the persistence format and existing escrow, delivery, and claim transitions. +- Confirmed catalog regressions failed before the fix using mocked server material properties, then verified shell creation, persisted settlement and claim acknowledgement, and existing regressions: all 134 tests and the plugin JAR passed `./gradlew clean check jar`. + ## 2026-09-05 — Initial quest-board design - Administrators create physical quest boards, and every board exposes the same global quests and claims. diff --git a/knowledge/user-stories/index.md b/knowledge/user-stories/index.md index 239f8fb..bd2866f 100644 --- a/knowledge/user-stories/index.md +++ b/knowledge/user-stories/index.md @@ -8,9 +8,9 @@ description: Catalog of user stories for the Spigot Quest Board plugin. 1. [US-001: Build and release the plugin](us-001-build-and-release-plugin.md) 2. [US-002: Create and use shared quest boards](us-002-create-and-use-shared-quest-boards.md) -3. [US-003: Create a block-delivery quest](us-003-create-a-block-delivery-quest.md) +3. [US-003: Create an item-delivery quest](us-003-create-a-block-delivery-quest.md) 4. [US-004: Browse available quests](us-004-browse-available-quests.md) — includes login quest counts and board guidance. -5. [US-005: Deliver blocks and complete a quest](us-005-deliver-blocks-and-complete-a-quest.md) +5. [US-005: Deliver items and complete a quest](us-005-deliver-blocks-and-complete-a-quest.md) 6. [US-006: Cancel an owned quest](us-006-cancel-an-owned-quest.md) 7. [US-007: Expire quests and claim held items](us-007-expire-quests-and-claim-held-items.md) — includes recurring login reminders for unclaimed items. 8. [US-008: Control player quest commands](us-008-control-player-quest-commands.md) diff --git a/knowledge/user-stories/us-002-create-and-use-shared-quest-boards.md b/knowledge/user-stories/us-002-create-and-use-shared-quest-boards.md index 796f1a2..bff65ed 100644 --- a/knowledge/user-stories/us-002-create-and-use-shared-quest-boards.md +++ b/knowledge/user-stories/us-002-create-and-use-shared-quest-boards.md @@ -23,5 +23,5 @@ As an **administrator**, I want to turn a targeted block into a quest board so t ## Related -- [US-003: Create a block-delivery quest](us-003-create-a-block-delivery-quest.md) +- [US-003: Create an item-delivery quest](us-003-create-a-block-delivery-quest.md) - [US-004: Browse available quests](us-004-browse-available-quests.md) diff --git a/knowledge/user-stories/us-003-create-a-block-delivery-quest.md b/knowledge/user-stories/us-003-create-a-block-delivery-quest.md index e912e88..cdc6469 100644 --- a/knowledge/user-stories/us-003-create-a-block-delivery-quest.md +++ b/knowledge/user-stories/us-003-create-a-block-delivery-quest.md @@ -1,17 +1,21 @@ --- type: User Story -title: "US-003: Create a block-delivery quest" -description: Let a player request block items while safely escrowing an inventory-funded reward. +title: "US-003: Create an item-delivery quest" +description: Let a player request inventory items while safely escrowing an inventory-funded reward. status: done --- -# US-003: Create a block-delivery quest +# US-003: Create an item-delivery quest -As a **player**, I want to request a block material and quantity in exchange for items I already own so that another player can fulfill my request safely. +As a **player**, I want to request an item material and quantity in exchange for items I already own so that another player can fulfill my request safely. ## Acceptance criteria -- [x] A player can create a quest through a registered quest board by selecting a valid block material, a positive quantity, and reward items from their inventory. +- [x] Requests accept all inventory item materials, including shulker shells and existing block items, while rejecting air, invalid names, and non-item materials. +- [x] Contextual material autocomplete includes non-block items; command and dialog wording describes items rather than only blocks. +- [x] Regression tests verify shell creation, autocomplete, and existing block-item support. + +- [x] A player can create a quest through a registered quest board by selecting a valid item material, a positive quantity, and reward items from their inventory. - [x] Reward items must exist in the issuer's inventory and are removed only when quest creation succeeds. - [x] Removed rewards are held durably in escrow by the quest system. - [x] A created quest records a unique identifier, issuer UUID and current name, requested material and quantity, exact reward items, creation time, and expiration time. @@ -24,5 +28,5 @@ As a **player**, I want to request a block material and quantity in exchange for ## Related - [US-002: Create and use shared quest boards](us-002-create-and-use-shared-quest-boards.md) -- [US-005: Deliver blocks and complete a quest](us-005-deliver-blocks-and-complete-a-quest.md) +- [US-005: Deliver items and complete a quest](us-005-deliver-blocks-and-complete-a-quest.md) - [US-007: Expire quests and claim held items](us-007-expire-quests-and-claim-held-items.md) diff --git a/knowledge/user-stories/us-004-browse-available-quests.md b/knowledge/user-stories/us-004-browse-available-quests.md index 613f181..8535b2a 100644 --- a/knowledge/user-stories/us-004-browse-available-quests.md +++ b/knowledge/user-stories/us-004-browse-available-quests.md @@ -25,5 +25,5 @@ As a **player**, I want to browse current quests so that I can decide which bloc ## Related - [US-002: Create and use shared quest boards](us-002-create-and-use-shared-quest-boards.md) -- [US-005: Deliver blocks and complete a quest](us-005-deliver-blocks-and-complete-a-quest.md) +- [US-005: Deliver items and complete a quest](us-005-deliver-blocks-and-complete-a-quest.md) - [US-013: Show custom reward item names](us-013-show-custom-reward-item-names.md) diff --git a/knowledge/user-stories/us-005-deliver-blocks-and-complete-a-quest.md b/knowledge/user-stories/us-005-deliver-blocks-and-complete-a-quest.md index 05c9bb7..c76e8f8 100644 --- a/knowledge/user-stories/us-005-deliver-blocks-and-complete-a-quest.md +++ b/knowledge/user-stories/us-005-deliver-blocks-and-complete-a-quest.md @@ -1,29 +1,32 @@ --- type: User Story -title: "US-005: Deliver blocks and complete a quest" -description: Let a player deliver requested blocks at a quest board and receive the escrowed reward. +title: "US-005: Deliver items and complete a quest" +description: Let a player deliver requested items at a quest board and receive the escrowed reward. status: done --- -# US-005: Deliver blocks and complete a quest +# US-005: Deliver items and complete a quest -As a **player**, I want to deliver requested blocks to a quest board so that I receive the promised reward and the issuer can claim my delivery. +As a **player**, I want to deliver requested items to a quest board so that I receive the promised reward and the issuer can claim my delivery. ## Acceptance criteria +- [x] Non-block requested items, including shulker shells, can be delivered and held for issuer claiming using existing escrow and rollback protections. +- [x] Delivery notifications and claim messages refer to items, and regression tests verify shell settlement and existing block deliveries. + - [x] An active quest offers a Complete Quest action through every registered board. -- [x] Completion requires the delivering player to possess the full requested quantity of the exact block material. -- [x] Successful completion removes the requested blocks from the delivering player's inventory and stores them as a claim for the issuer. +- [x] Completion requires the delivering player to possess the full requested quantity of the exact item material. +- [x] Successful completion removes the requested items from the delivering player's inventory and stores them as a claim for the issuer. - [x] Successful completion gives the exact escrowed reward items to the delivering player. - [x] Reward items that do not fit are dropped safely at the delivering player's feet with a clear message. -- [x] The issuer is notified immediately when online or on their next login when offline that delivered blocks can be claimed at a quest board. +- [x] The issuer is notified immediately when online or on their next login when offline that delivered items can be claimed at a quest board. - [x] When player commands are enabled, `/quests complete ` provides equivalent behavior with contextual autocomplete. - [x] Completion is atomic and exactly one player can complete a quest, including simultaneous attempts. -- [x] Validation or persistence failure does not consume delivered blocks, release rewards, or change quest state. +- [x] Validation or persistence failure does not consume delivered items, release rewards, or change quest state. - [x] Completed quests are no longer shown as active or completable. - [x] Automated tests verify inventory validation, settlement, overflow, notifications, rollback, and concurrent completion protection. ## Related -- [US-003: Create a block-delivery quest](us-003-create-a-block-delivery-quest.md) +- [US-003: Create an item-delivery quest](us-003-create-a-block-delivery-quest.md) - [US-007: Expire quests and claim held items](us-007-expire-quests-and-claim-held-items.md) diff --git a/knowledge/user-stories/us-007-expire-quests-and-claim-held-items.md b/knowledge/user-stories/us-007-expire-quests-and-claim-held-items.md index 266d60e..eaaebf6 100644 --- a/knowledge/user-stories/us-007-expire-quests-and-claim-held-items.md +++ b/knowledge/user-stories/us-007-expire-quests-and-claim-held-items.md @@ -30,5 +30,5 @@ As a **quest issuer**, I want delivered blocks and returned rewards held at the ## Related -- [US-005: Deliver blocks and complete a quest](us-005-deliver-blocks-and-complete-a-quest.md) +- [US-005: Deliver items and complete a quest](us-005-deliver-blocks-and-complete-a-quest.md) - [US-006: Cancel an owned quest](us-006-cancel-an-owned-quest.md) diff --git a/knowledge/user-stories/us-008-control-player-quest-commands.md b/knowledge/user-stories/us-008-control-player-quest-commands.md index 37349ac..ed1bc56 100644 --- a/knowledge/user-stories/us-008-control-player-quest-commands.md +++ b/knowledge/user-stories/us-008-control-player-quest-commands.md @@ -23,6 +23,6 @@ As an **administrator**, I want to enable or disable player quest commands so th ## Related - [US-002: Create and use shared quest boards](us-002-create-and-use-shared-quest-boards.md) -- [US-003: Create a block-delivery quest](us-003-create-a-block-delivery-quest.md) +- [US-003: Create an item-delivery quest](us-003-create-a-block-delivery-quest.md) - [US-007: Expire quests and claim held items](us-007-expire-quests-and-claim-held-items.md) - [US-012: Create quests by command near a board](us-012-create-quests-near-a-board.md) diff --git a/knowledge/user-stories/us-009-use-a-screen-fitting-quest-board-interface.md b/knowledge/user-stories/us-009-use-a-screen-fitting-quest-board-interface.md index 40d4d12..fadde34 100644 --- a/knowledge/user-stories/us-009-use-a-screen-fitting-quest-board-interface.md +++ b/knowledge/user-stories/us-009-use-a-screen-fitting-quest-board-interface.md @@ -24,7 +24,7 @@ As a **player**, I want the quest-board interface to fit on screen so that I can ## Related - [US-002: Create and use shared quest boards](us-002-create-and-use-shared-quest-boards.md) -- [US-003: Create a block-delivery quest](us-003-create-a-block-delivery-quest.md) +- [US-003: Create an item-delivery quest](us-003-create-a-block-delivery-quest.md) - [US-004: Browse available quests](us-004-browse-available-quests.md) -- [US-005: Deliver blocks and complete a quest](us-005-deliver-blocks-and-complete-a-quest.md) +- [US-005: Deliver items and complete a quest](us-005-deliver-blocks-and-complete-a-quest.md) - [US-007: Expire quests and claim held items](us-007-expire-quests-and-claim-held-items.md) diff --git a/knowledge/user-stories/us-012-create-quests-near-a-board.md b/knowledge/user-stories/us-012-create-quests-near-a-board.md index 88acb8b..6ed0dac 100644 --- a/knowledge/user-stories/us-012-create-quests-near-a-board.md +++ b/knowledge/user-stories/us-012-create-quests-near-a-board.md @@ -11,7 +11,7 @@ As a **player**, I want to use the quest-creation command near a quest board so ## Acceptance criteria -- [x] When global player quest commands are disabled, `/quests create ` remains available to a player within five blocks of a registered board interaction location. +- [x] When global player quest commands are disabled, `/quests create ` remains available to a player within five blocks of a registered board interaction location. - [x] Material and quantity autocomplete remain available for nearby quest creation. - [x] A player farther than five blocks from every registered board is directed to move closer and no quest or escrow change occurs. - [x] Board proximity requires the player and registered location to be in the same world. diff --git a/knowledge/user-stories/us-013-show-custom-reward-item-names.md b/knowledge/user-stories/us-013-show-custom-reward-item-names.md index 44b6fe1..594d322 100644 --- a/knowledge/user-stories/us-013-show-custom-reward-item-names.md +++ b/knowledge/user-stories/us-013-show-custom-reward-item-names.md @@ -22,5 +22,5 @@ As a **player**, I want named reward items identified by both custom name and ma ## Related -- [US-003: Create a block-delivery quest](us-003-create-a-block-delivery-quest.md) +- [US-003: Create an item-delivery quest](us-003-create-a-block-delivery-quest.md) - [US-004: Browse available quests](us-004-browse-available-quests.md) diff --git a/src/main/java/games/dmg/spigotquestboard/BukkitBlockMaterialCatalog.java b/src/main/java/games/dmg/spigotquestboard/BukkitItemMaterialCatalog.java similarity index 78% rename from src/main/java/games/dmg/spigotquestboard/BukkitBlockMaterialCatalog.java rename to src/main/java/games/dmg/spigotquestboard/BukkitItemMaterialCatalog.java index f37a19a..91df6da 100644 --- a/src/main/java/games/dmg/spigotquestboard/BukkitBlockMaterialCatalog.java +++ b/src/main/java/games/dmg/spigotquestboard/BukkitItemMaterialCatalog.java @@ -6,11 +6,11 @@ import java.util.Locale; import java.util.Optional; import org.bukkit.Material; -final class BukkitBlockMaterialCatalog implements BlockMaterialCatalog { +final class BukkitItemMaterialCatalog implements ItemMaterialCatalog { @Override - public Optional normalizeBlock(String input) { + public Optional normalizeItem(String input) { Material material = Material.matchMaterial(input == null ? "" : input); - if (material == null || material.isAir() || !material.isBlock()) { + if (material == null || material.isAir() || !material.isItem()) { return Optional.empty(); } return Optional.of(material.name()); @@ -20,7 +20,7 @@ final class BukkitBlockMaterialCatalog implements BlockMaterialCatalog { public List suggest(String prefix) { String normalizedPrefix = prefix.toUpperCase(Locale.ROOT); return Arrays.stream(Material.values()) - .filter(material -> material.isBlock() && !material.isAir()) + .filter(material -> material.isItem() && !material.isAir()) .map(Material::name) .filter(name -> name.startsWith(normalizedPrefix)) .sorted() diff --git a/src/main/java/games/dmg/spigotquestboard/BlockMaterialCatalog.java b/src/main/java/games/dmg/spigotquestboard/ItemMaterialCatalog.java similarity index 60% rename from src/main/java/games/dmg/spigotquestboard/BlockMaterialCatalog.java rename to src/main/java/games/dmg/spigotquestboard/ItemMaterialCatalog.java index acb1d1f..026f155 100644 --- a/src/main/java/games/dmg/spigotquestboard/BlockMaterialCatalog.java +++ b/src/main/java/games/dmg/spigotquestboard/ItemMaterialCatalog.java @@ -3,7 +3,7 @@ package games.dmg.spigotquestboard; import java.util.List; import java.util.Optional; -interface BlockMaterialCatalog { - Optional normalizeBlock(String input); +interface ItemMaterialCatalog { + Optional normalizeItem(String input); List suggest(String prefix); } diff --git a/src/main/java/games/dmg/spigotquestboard/QuestBoardDialogUi.java b/src/main/java/games/dmg/spigotquestboard/QuestBoardDialogUi.java index 0d48a02..e63d2ca 100644 --- a/src/main/java/games/dmg/spigotquestboard/QuestBoardDialogUi.java +++ b/src/main/java/games/dmg/spigotquestboard/QuestBoardDialogUi.java @@ -113,7 +113,7 @@ final class QuestBoardDialogUi implements QuestBoardUi { + "metadata, is escrowed only if the quest saves successfully.", List.of(action("Create quest", QuestBoardDialogSpec.Route.SUBMIT_CREATE)), List.of( - new QuestBoardDialogSpec.Input("requested_material", "Requested block", "", 64), + new QuestBoardDialogSpec.Input("requested_material", "Requested item", "", 64), new QuestBoardDialogSpec.Input("requested_quantity", "Quantity", "64", 10) ), true @@ -221,7 +221,7 @@ final class QuestBoardDialogUi implements QuestBoardUi { } ActionButton collect = callbackButton( "Collect pending claims", - "Collect delivered blocks and returned rewards", + "Collect delivered items and returned rewards", 250, respondingPlayer -> submitClaim(respondingPlayer) ); @@ -247,7 +247,7 @@ final class QuestBoardDialogUi implements QuestBoardUi { private ActionButton renderQuestAction(QuestBoardDialogSpec.Action action) { return switch (action.route()) { case COMPLETE -> callbackButton( - action.label(), "Deliver the requested blocks", 180, + action.label(), "Deliver the requested items", 180, player -> submitCompletion(player, action.questId()) ); case CANCEL -> callbackButton( @@ -328,7 +328,7 @@ final class QuestBoardDialogUi implements QuestBoardUi { } return claims.stream().map(claim -> { String kind = claim.type() == QuestClaimType.DELIVERED_BLOCKS - ? "DELIVERED BLOCKS" : "RETURNED REWARD (" + claim.source().name() + ")"; + ? "DELIVERED ITEMS" : "RETURNED REWARD (" + claim.source().name() + ")"; String items = claim.items().stream() .map(item -> item.amount() + " " + item.material()) .reduce((left, right) -> left + ", " + right) @@ -382,7 +382,7 @@ final class QuestBoardDialogUi implements QuestBoardUi { player.sendMessage(exception.getMessage()); } catch (IOException exception) { player.sendMessage( - "The quest could not be saved. Your delivered blocks were restored." + "The quest could not be saved. Your delivered items were restored." ); } } diff --git a/src/main/java/games/dmg/spigotquestboard/QuestClaimController.java b/src/main/java/games/dmg/spigotquestboard/QuestClaimController.java index ee8320c..115e3a7 100644 --- a/src/main/java/games/dmg/spigotquestboard/QuestClaimController.java +++ b/src/main/java/games/dmg/spigotquestboard/QuestClaimController.java @@ -55,7 +55,7 @@ final class QuestClaimController implements QuestClaimGateway { private static String description(QuestClaim claim) { if (claim.type() == QuestClaimType.DELIVERED_BLOCKS) { - return "Collected delivered blocks from completed quest " + claim.questId() + "."; + return "Collected delivered items from completed quest " + claim.questId() + "."; } String reason = claim.source() == QuestClaimSource.EXPIRATION ? "expired" : "cancelled"; diff --git a/src/main/java/games/dmg/spigotquestboard/QuestCommand.java b/src/main/java/games/dmg/spigotquestboard/QuestCommand.java index 0996bbf..2984890 100644 --- a/src/main/java/games/dmg/spigotquestboard/QuestCommand.java +++ b/src/main/java/games/dmg/spigotquestboard/QuestCommand.java @@ -148,7 +148,7 @@ final class QuestCommand implements CommandExecutor, TabCompleter { sender.sendMessage(exception.getMessage()); } catch (IOException exception) { sender.sendMessage( - "The quest could not be saved. Your delivered blocks were restored." + "The quest could not be saved. Your delivered items were restored." ); } return true; @@ -223,7 +223,7 @@ final class QuestCommand implements CommandExecutor, TabCompleter { ); } if (arguments.length == 2 && "create".equalsIgnoreCase(arguments[0])) { - return creator.suggestBlockMaterials(arguments[1]); + return creator.suggestItemMaterials(arguments[1]); } if (arguments.length == 3 && "create".equalsIgnoreCase(arguments[0])) { return startsWith(QUANTITIES, arguments[2]); @@ -267,7 +267,7 @@ final class QuestCommand implements CommandExecutor, TabCompleter { private static void usage(CommandSender sender) { sender.sendMessage( - "Usage: /quests [list] | /quests create | " + "Usage: /quests [list] | /quests create | " + "/quests complete | /quests cancel | /quests claim" ); sender.sendMessage("Hold the entire reward stack in your main hand; its exact metadata will be escrowed."); diff --git a/src/main/java/games/dmg/spigotquestboard/QuestCreationController.java b/src/main/java/games/dmg/spigotquestboard/QuestCreationController.java index c250c5b..f1e407b 100644 --- a/src/main/java/games/dmg/spigotquestboard/QuestCreationController.java +++ b/src/main/java/games/dmg/spigotquestboard/QuestCreationController.java @@ -8,12 +8,12 @@ import org.bukkit.entity.Player; final class QuestCreationController implements QuestCreationGateway { private final QuestService quests; - private final BlockMaterialCatalog materials; + private final ItemMaterialCatalog materials; private final HeldRewardInventory rewards; QuestCreationController( QuestService quests, - BlockMaterialCatalog materials, + ItemMaterialCatalog materials, HeldRewardInventory rewards ) { this.quests = Objects.requireNonNull(quests, "quests"); @@ -29,9 +29,9 @@ final class QuestCreationController implements QuestCreationGateway { if (requestedAmount <= 0) { throw new IllegalArgumentException("Requested quantity must be positive"); } - String material = materials.normalizeBlock(requestedMaterial) + String material = materials.normalizeItem(requestedMaterial) .orElseThrow(() -> new IllegalArgumentException( - "Requested material must be a valid block" + "Requested material must be a valid item" )); HeldRewardInventory.RemovedReward removed = rewards.remove(player); try { @@ -46,7 +46,7 @@ final class QuestCreationController implements QuestCreationGateway { } @Override - public List suggestBlockMaterials(String prefix) { + public List suggestItemMaterials(String prefix) { return materials.suggest(prefix == null ? "" : prefix); } } diff --git a/src/main/java/games/dmg/spigotquestboard/QuestCreationGateway.java b/src/main/java/games/dmg/spigotquestboard/QuestCreationGateway.java index d21e8b1..82a5c64 100644 --- a/src/main/java/games/dmg/spigotquestboard/QuestCreationGateway.java +++ b/src/main/java/games/dmg/spigotquestboard/QuestCreationGateway.java @@ -9,5 +9,5 @@ interface QuestCreationGateway { Quest create(Player player, String requestedMaterial, int requestedAmount, Instant createdAt) throws IOException; - List suggestBlockMaterials(String prefix); + List suggestItemMaterials(String prefix); } diff --git a/src/main/java/games/dmg/spigotquestboard/QuestService.java b/src/main/java/games/dmg/spigotquestboard/QuestService.java index 08d205c..06db31b 100644 --- a/src/main/java/games/dmg/spigotquestboard/QuestService.java +++ b/src/main/java/games/dmg/spigotquestboard/QuestService.java @@ -33,7 +33,7 @@ final class QuestService implements QuestBrowser { String material = Objects.requireNonNull(requestedMaterial, "requestedMaterial") .trim().toUpperCase(Locale.ROOT); if (material.isEmpty() || "AIR".equals(material)) { - throw new IllegalArgumentException("Requested material must be a block"); + throw new IllegalArgumentException("Requested material must be a valid item"); } Quest quest = new Quest( UUID.randomUUID(), issuerId, issuerName, material, requestedAmount, @@ -90,7 +90,7 @@ final class QuestService implements QuestBrowser { IssuerNotification notification = new IssuerNotification( UUID.randomUUID(), active.id(), active.issuerId(), "Quest " + active.id() + " was completed. Your delivered " - + active.requestedMaterial() + " blocks can be claimed at a quest board.", + + active.requestedMaterial() + " items can be claimed at a quest board.", completedAt ); diff --git a/src/main/java/games/dmg/spigotquestboard/SpigotQuestBoardPlugin.java b/src/main/java/games/dmg/spigotquestboard/SpigotQuestBoardPlugin.java index 670eda3..ab77f59 100644 --- a/src/main/java/games/dmg/spigotquestboard/SpigotQuestBoardPlugin.java +++ b/src/main/java/games/dmg/spigotquestboard/SpigotQuestBoardPlugin.java @@ -37,7 +37,7 @@ public final class SpigotQuestBoardPlugin extends JavaPlugin { } QuestCreationGateway creator = new QuestCreationController( - quests, new BukkitBlockMaterialCatalog(), new BukkitHeldRewardInventory() + quests, new BukkitItemMaterialCatalog(), new BukkitHeldRewardInventory() ); Clock clock = Clock.systemUTC(); BukkitIssuerNotifier notifier = new BukkitIssuerNotifier( diff --git a/src/test/java/games/dmg/spigotquestboard/BukkitItemMaterialCatalogTest.java b/src/test/java/games/dmg/spigotquestboard/BukkitItemMaterialCatalogTest.java new file mode 100644 index 0000000..eda53c0 --- /dev/null +++ b/src/test/java/games/dmg/spigotquestboard/BukkitItemMaterialCatalogTest.java @@ -0,0 +1,100 @@ +package games.dmg.spigotquestboard; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Optional; +import org.bukkit.Material; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +final class BukkitItemMaterialCatalogTest { + private final BukkitItemMaterialCatalog catalog = new BukkitItemMaterialCatalog(); + + private MockedStatic materials; + + @BeforeEach + void provideServerMaterialProperties() { + // Purpur delegates material properties to server-only registries. + String[] names = {"SHULKER_SHELL", "SHULKER_BOX", "STONE", "AIR", "CAVE_AIR", "VOID_AIR", "WATER", "FIRE"}; + Material[] fixtures = new Material[names.length]; + for (int i = 0; i < names.length; i++) { + Material material = Mockito.mock(Material.class); + Mockito.when(material.name()).thenReturn(names[i]); + Mockito.when(material.isItem()).thenReturn(i <= 3); + Mockito.when(material.isBlock()).thenReturn(i != 0); + Mockito.when(material.isAir()).thenReturn(i >= 3 && i <= 5); + fixtures[i] = material; + } + materials = Mockito.mockStatic(Material.class); + materials.when(Material::values).thenReturn(fixtures); + for (int i = 0; i < names.length; i++) { + String name = names[i]; + Material material = fixtures[i]; + materials.when(() -> Material.matchMaterial(name)).thenReturn(material); + materials.when(() -> Material.matchMaterial(name.toLowerCase(java.util.Locale.ROOT))) + .thenReturn(material); + } + } + + @AfterEach + void closeMaterialFixtures() { + materials.close(); + } + + @Test + void acceptsShellsAndExistingBlockItems() { + assertEquals(Optional.of("SHULKER_SHELL"), catalog.normalizeItem("shulker_shell")); + assertEquals(Optional.of("SHULKER_BOX"), catalog.normalizeItem("shulker_box")); + assertEquals(Optional.of("STONE"), catalog.normalizeItem("stone")); + } + + @Test + void rejectsAirInvalidAndNonItemMaterials() { + for (String input : new String[] {"AIR", "CAVE_AIR", "VOID_AIR", "WATER", "FIRE", "invalid", ""}) { + assertTrue(catalog.normalizeItem(input).isEmpty(), input); + } + assertTrue(catalog.normalizeItem(null).isEmpty()); + } + + @Test + void createsPersistsAndSettlesShellRequest(@org.junit.jupiter.api.io.TempDir java.nio.file.Path directory) + throws Exception { + QuestRepository repository = new YamlQuestRepository(directory.resolve("quests.yml")); + QuestService service = new QuestService(repository); + org.bukkit.entity.Player player = Mockito.mock(org.bukkit.entity.Player.class); + java.util.UUID issuer = java.util.UUID.randomUUID(); + Mockito.when(player.getUniqueId()).thenReturn(issuer); + Mockito.when(player.getName()).thenReturn("Issuer"); + EscrowItem reward = new EscrowItem("DIAMOND", 1, null); + HeldRewardInventory inventory = ignored -> new HeldRewardInventory.RemovedReward() { + @Override public EscrowItem item() { return reward; } + @Override public void rollback() { throw new AssertionError("Unexpected rollback"); } + }; + QuestCreationController controller = new QuestCreationController(service, catalog, inventory); + assertTrue(controller.suggestItemMaterials("shulker_").contains("SHULKER_SHELL")); + Quest quest = controller.create(player, "shulker_shell", 16, java.time.Instant.EPOCH); + service = new QuestService(repository); + EscrowItem delivery = new EscrowItem("SHULKER_SHELL", 16, null); + QuestCompletion completion = service.complete( + quest.id(), delivery, java.time.Instant.EPOCH.plusSeconds(1) + ); + assertEquals(java.util.List.of(reward), completion.reward()); + service = new QuestService(repository); + QuestClaim claim = service.claimsFor(issuer).getFirst(); + assertEquals(java.util.List.of(delivery), claim.items()); + assertEquals(QuestStatus.COMPLETED, service.state().quests().get(quest.id()).status()); + service.acknowledgeClaim(issuer, claim.id()); + assertTrue(new QuestService(repository).claimsFor(issuer).isEmpty()); + } + + @Test + void suggestsShellsAndBoxesButNotNonItems() { + assertTrue(catalog.suggest("shulker_").contains("SHULKER_SHELL")); + assertTrue(catalog.suggest("shulker_").contains("SHULKER_BOX")); + assertTrue(catalog.suggest("WATER").stream().noneMatch("WATER"::equals)); + } +} diff --git a/src/test/java/games/dmg/spigotquestboard/QuestBoardDialogUiTest.java b/src/test/java/games/dmg/spigotquestboard/QuestBoardDialogUiTest.java index a71e283..074d288 100644 --- a/src/test/java/games/dmg/spigotquestboard/QuestBoardDialogUiTest.java +++ b/src/test/java/games/dmg/spigotquestboard/QuestBoardDialogUiTest.java @@ -267,7 +267,7 @@ final class QuestBoardDialogUiTest { } @Test - void boardListingDistinguishesDeliveredBlocksFromReturnedRewards() { + void boardListingDistinguishesDeliveredItemsFromReturnedRewards() { RecordingCreator creator = new RecordingCreator(false); Player player = mock(Player.class); UUID owner = UUID.randomUUID(); @@ -301,7 +301,7 @@ final class QuestBoardDialogUiTest { String listing = ui.claimListingText(player); - org.junit.jupiter.api.Assertions.assertTrue(listing.contains("DELIVERED BLOCKS — 2 STONE")); + org.junit.jupiter.api.Assertions.assertTrue(listing.contains("DELIVERED ITEMS — 2 STONE")); org.junit.jupiter.api.Assertions.assertTrue( listing.contains("RETURNED REWARD (EXPIRATION) — 1 DIAMOND") ); @@ -412,7 +412,7 @@ final class QuestBoardDialogUiTest { } @Override - public List suggestBlockMaterials(String prefix) { + public List suggestItemMaterials(String prefix) { return List.of(); } } diff --git a/src/test/java/games/dmg/spigotquestboard/QuestClaimControllerTest.java b/src/test/java/games/dmg/spigotquestboard/QuestClaimControllerTest.java index 6c4c745..6363ea0 100644 --- a/src/test/java/games/dmg/spigotquestboard/QuestClaimControllerTest.java +++ b/src/test/java/games/dmg/spigotquestboard/QuestClaimControllerTest.java @@ -29,7 +29,7 @@ final class QuestClaimControllerTest { assertEquals(new ClaimCollectionResult(3, 6), result); assertTrue(service.claimsFor(issuer).isEmpty()); - verify(player).sendMessage(contains("delivered blocks from completed quest")); + verify(player).sendMessage(contains("delivered items from completed quest")); verify(player).sendMessage(contains("returned reward from cancelled quest")); verify(player).sendMessage(contains("returned reward from expired quest")); verify(player).sendMessage(contains("dropped at your feet, protected for you")); diff --git a/src/test/java/games/dmg/spigotquestboard/QuestCommandTest.java b/src/test/java/games/dmg/spigotquestboard/QuestCommandTest.java index 1ac6a51..12b4285 100644 --- a/src/test/java/games/dmg/spigotquestboard/QuestCommandTest.java +++ b/src/test/java/games/dmg/spigotquestboard/QuestCommandTest.java @@ -417,7 +417,7 @@ final class QuestCommandTest { } @Override - public List suggestBlockMaterials(String prefix) { + public List suggestItemMaterials(String prefix) { suggestionPrefix = prefix; return List.of("STONE", "STONE_BRICKS"); } diff --git a/src/test/java/games/dmg/spigotquestboard/QuestCreationControllerTest.java b/src/test/java/games/dmg/spigotquestboard/QuestCreationControllerTest.java index dd1f2ef..4b1da75 100644 --- a/src/test/java/games/dmg/spigotquestboard/QuestCreationControllerTest.java +++ b/src/test/java/games/dmg/spigotquestboard/QuestCreationControllerTest.java @@ -71,9 +71,9 @@ final class QuestCreationControllerTest { private static QuestCreationController controller( MemoryQuestRepository repository, HeldRewardInventory inventory ) throws IOException { - BlockMaterialCatalog catalog = new BlockMaterialCatalog() { + ItemMaterialCatalog catalog = new ItemMaterialCatalog() { @Override - public Optional normalizeBlock(String input) { + public Optional normalizeItem(String input) { return "stone".equalsIgnoreCase(input) ? Optional.of("STONE") : Optional.empty(); }