diff --git a/src/main/java/world/bentobox/challenges/ChallengesManager.java b/src/main/java/world/bentobox/challenges/ChallengesManager.java index 3677c76..f27b6db 100644 --- a/src/main/java/world/bentobox/challenges/ChallengesManager.java +++ b/src/main/java/world/bentobox/challenges/ChallengesManager.java @@ -604,9 +604,23 @@ public class ChallengesManager * @param challengeID - challengeID */ private void setChallengeComplete(@NonNull String storageDataID, @NonNull String challengeID) + { + this.setChallengeComplete(storageDataID, challengeID, 1); + } + + + /** + * Sets the challenge with given ID as complete and increments the number of times it has been + * completed + * + * @param storageDataID - playerData ID + * @param challengeID - challengeID + * @param count - how many times challenge is completed + */ + private void setChallengeComplete(@NonNull String storageDataID, @NonNull String challengeID, int count) { this.addPlayerData(storageDataID); - this.playerCacheData.get(storageDataID).setChallengeDone(challengeID); + this.playerCacheData.get(storageDataID).addChallengeDone(challengeID, count); // Save this.savePlayerData(storageDataID); } @@ -842,9 +856,9 @@ public class ChallengesManager * @param world - World where completion must be called. * @param challenge - That must be completed. */ - public void setChallengeComplete(User user, World world, Challenge challenge) + public void setChallengeComplete(User user, World world, Challenge challenge, int completionCount) { - this.setChallengeComplete(user.getUniqueId(), world, challenge); + this.setChallengeComplete(user.getUniqueId(), world, challenge, completionCount); } @@ -854,13 +868,14 @@ public class ChallengesManager * @param world - World where completion must be called. * @param challenge - That must be completed. */ - public void setChallengeComplete(UUID userID, World world, Challenge challenge) + public void setChallengeComplete(UUID userID, World world, Challenge challenge, int completionCount) { String storageID = this.getDataUniqueID(userID, Util.getWorld(world)); this.setChallengeComplete(storageID, challenge.getUniqueId()); this.addLogEntry(storageID, new LogEntry.Builder("COMPLETE"). data("user-id", userID.toString()). data("challenge-id", challenge.getUniqueId()). + data("completion-count", Integer.toString(completionCount)). build()); // Fire event that user completes challenge @@ -868,7 +883,7 @@ public class ChallengesManager new ChallengeCompletedEvent(challenge.getUniqueId(), userID, false, - 1)); + completionCount)); } diff --git a/src/main/java/world/bentobox/challenges/commands/CompleteChallengeCommand.java b/src/main/java/world/bentobox/challenges/commands/CompleteChallengeCommand.java index b9f7d5a..4837dc9 100644 --- a/src/main/java/world/bentobox/challenges/commands/CompleteChallengeCommand.java +++ b/src/main/java/world/bentobox/challenges/commands/CompleteChallengeCommand.java @@ -112,15 +112,14 @@ public class CompleteChallengeCommand extends CompositeCommand }); break; -// TODO: not implemented YET -// case 4: -// // Suggest a number of completions. -// if (lastString.isEmpty() || lastString.matches("[0-9]*")) -// { -// returnList.addAll(Util.tabLimit(Collections.singletonList(""), lastString)); -// } -// -// break; + case 4: + // Suggest a number of completions. + if (lastString.isEmpty() || lastString.matches("[0-9]*")) + { + returnList.addAll(Util.tabLimit(Collections.singletonList(""), lastString)); + } + + break; default: { returnList.addAll(Util.tabLimit(Collections.singletonList("help"), lastString)); diff --git a/src/main/java/world/bentobox/challenges/database/object/ChallengesPlayerData.java b/src/main/java/world/bentobox/challenges/database/object/ChallengesPlayerData.java index 4f81405..e8984c7 100644 --- a/src/main/java/world/bentobox/challenges/database/object/ChallengesPlayerData.java +++ b/src/main/java/world/bentobox/challenges/database/object/ChallengesPlayerData.java @@ -217,8 +217,21 @@ public class ChallengesPlayerData implements DataObject */ public void setChallengeDone(@NonNull String challengeName) { - int times = challengeStatus.getOrDefault(challengeName, 0) + 1; - challengeStatus.put(challengeName, times); + this.addChallengeDone(challengeName, 1); + } + + + /** + * Mark a challenge as having been completed. Will increment the number of times and + * timestamp + * + * @param challengeName - unique challenge name + * @param times - how many new times should be added + */ + public void addChallengeDone(@NonNull String challengeName, int times) + { + int newTimes = challengeStatus.getOrDefault(challengeName, 0) + times; + challengeStatus.put(challengeName, newTimes); challengesTimestamp.put(challengeName, System.currentTimeMillis()); } diff --git a/src/main/java/world/bentobox/challenges/panel/user/ChallengesGUI.java b/src/main/java/world/bentobox/challenges/panel/user/ChallengesGUI.java index 69220ab..29d52ff 100644 --- a/src/main/java/world/bentobox/challenges/panel/user/ChallengesGUI.java +++ b/src/main/java/world/bentobox/challenges/panel/user/ChallengesGUI.java @@ -7,6 +7,7 @@ import org.bukkit.World; import org.bukkit.inventory.ItemStack; import java.util.List; +import net.wesjd.anvilgui.AnvilGUI; import world.bentobox.bentobox.api.panels.PanelItem; import world.bentobox.bentobox.api.panels.builders.PanelBuilder; import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; @@ -356,14 +357,47 @@ public class ChallengesGUI extends CommonGUI description(GuiUtils.stringSplit(this.generateChallengeDescription(challenge, this.user.getPlayer()), this.addon.getChallengesSettings().getLoreLineLength())). clickHandler((panel, user1, clickType, slot) -> { - if (TryToComplete.complete(this.addon, - this.user, - challenge, - this.world, - this.topLabel, - this.permissionPrefix)) + + // Add ability to input how many repeats player should do. + // Do not open if challenge is not repeatable. + if (clickType.isRightClick() && challenge.isRepeatable()) { - panel.getInventory().setItem(slot, this.getChallengeButton(challenge).getItem()); + new AnvilGUI(this.addon.getPlugin(), + this.user.getPlayer(), + "1", + (player, reply) -> { + try + { + if (TryToComplete.complete(this.addon, + this.user, + challenge, + this.world, + this.topLabel, + this.permissionPrefix, + Integer.parseInt(reply))) + { + panel.getInventory().setItem(slot, this.getChallengeButton(challenge).getItem()); + } + } + catch (Exception e) + { + this.user.sendMessage("challenges.errors.not-a-integer", "[value]", reply); + } + + return reply; + }); + } + else + { + if (TryToComplete.complete(this.addon, + this.user, + challenge, + this.world, + this.topLabel, + this.permissionPrefix)) + { + panel.getInventory().setItem(slot, this.getChallengeButton(challenge).getItem()); + } } return true; diff --git a/src/main/java/world/bentobox/challenges/tasks/TryToComplete.java b/src/main/java/world/bentobox/challenges/tasks/TryToComplete.java index 596d991..cf7e060 100644 --- a/src/main/java/world/bentobox/challenges/tasks/TryToComplete.java +++ b/src/main/java/world/bentobox/challenges/tasks/TryToComplete.java @@ -4,9 +4,11 @@ package world.bentobox.challenges.tasks; + import org.bukkit.*; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; +import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; @@ -23,7 +25,6 @@ import world.bentobox.challenges.ChallengesManager; import world.bentobox.challenges.database.object.Challenge; import world.bentobox.challenges.database.object.Challenge.ChallengeType; import world.bentobox.challenges.database.object.ChallengeLevel; -import world.bentobox.challenges.utils.GuiUtils; /** @@ -182,9 +183,32 @@ public class TryToComplete World world, String topLabel, String permissionPrefix) + { + return TryToComplete.complete(addon, user, challenge, world, topLabel, permissionPrefix, 1); + } + + + /** + * This static method allows complete challenge and get result about completion. + * @param addon - Challenges Addon. + * @param user - User who performs challenge. + * @param challenge - Challenge that should be completed. + * @param world - World where completion may occur. + * @param topLabel - Label of the top command. + * @param permissionPrefix - Permission prefix for GameMode addon. + * @param maxTimes - Integer that represents how many times user wants to complete challenges. + * @return true, if challenge is completed, otherwise false. + */ + public static boolean complete(ChallengesAddon addon, + User user, + Challenge challenge, + World world, + String topLabel, + String permissionPrefix, + int maxTimes) { return new TryToComplete(addon, user, challenge, world, topLabel, permissionPrefix). - build().meetsRequirements; + build(maxTimes).meetsRequirements; } @@ -197,17 +221,41 @@ public class TryToComplete * This method checks if challenge can be done, and complete it, if it is possible. * @return ChallengeResult object, that contains completion status. */ - public ChallengeResult build() + public ChallengeResult build(int maxTimes) { // Check if can complete challenge - ChallengeResult result = this.checkIfCanCompleteChallenge(); + ChallengeResult result = this.checkIfCanCompleteChallenge(maxTimes); - if (!result.meetsRequirements) + if (!result.isMeetsRequirements()) { return result; } - if (!result.repeat) + this.fullFillRequirements(result); + + // Validation to avoid rewarding if something goes wrong in removing requirements. + + if (!result.isMeetsRequirements()) + { + if (result.removedItems != null) + { + result.removedItems.forEach((item, amount) -> + { + ItemStack returnItem = item.clone(); + returnItem.setAmount(amount); + + this.user.getInventory().addItem(returnItem).forEach((k, v) -> + this.user.getWorld().dropItem(this.user.getLocation(), v)); + }); + } + + // Entities and blocks will not be restored. + + return result; + } + + // If challenge was not completed then reward items for completing it first time. + if (!result.wasCompleted()) { // Item rewards for (ItemStack reward : this.challenge.getRewardItems()) @@ -230,7 +278,11 @@ public class TryToComplete // Run commands this.runCommands(this.challenge.getRewardCommands()); - this.user.sendMessage("challenges.messages.you-completed-challenge", "[value]", this.challenge.getFriendlyName()); + // Send message about first completion only if it is completed only once. + if (result.getFactor() == 1) + { + this.user.sendMessage("challenges.messages.you-completed-challenge", "[value]", this.challenge.getFriendlyName()); + } if (this.addon.getChallengesSettings().isBroadcastMessages()) { @@ -257,36 +309,58 @@ public class TryToComplete 20); } } - else + + if (result.wasCompleted() || result.getFactor() > 1) { + int rewardFactor = result.getFactor() - (result.wasCompleted() ? 0 : 1); + // Item Repeat Rewards for (ItemStack reward : this.challenge.getRepeatItemReward()) { // Clone is necessary because otherwise it will chane reward itemstack // amount. - this.user.getInventory().addItem(reward.clone()).forEach((k, v) -> - this.user.getWorld().dropItem(this.user.getLocation(), v)); + + for (int i = 0; i < rewardFactor; i++) + { + this.user.getInventory().addItem(reward.clone()).forEach((k, v) -> + this.user.getWorld().dropItem(this.user.getLocation(), v)); + } } // Money Repeat Reward if (this.addon.isEconomyProvided()) { - this.addon.getEconomyProvider().deposit(this.user, this.challenge.getRepeatMoneyReward()); + this.addon.getEconomyProvider().deposit(this.user, + this.challenge.getRepeatMoneyReward() * rewardFactor); } // Experience Repeat Reward - this.user.getPlayer().giveExp(this.challenge.getRepeatExperienceReward()); + this.user.getPlayer().giveExp( + this.challenge.getRepeatExperienceReward() * rewardFactor); // Run commands - this.runCommands(this.challenge.getRepeatRewardCommands()); + for (int i = 0; i < rewardFactor; i++) + { + this.runCommands(this.challenge.getRepeatRewardCommands()); + } - this.user.sendMessage("challenges.messages.you-repeated-challenge", "[value]", this.challenge.getFriendlyName()); + if (result.getFactor() > 1) + { + this.user.sendMessage("challenges.messages.you-repeated-challenge-multiple", + "[value]", this.challenge.getFriendlyName(), + "[count]", Integer.toString(result.getFactor())); + } + else + { + this.user.sendMessage("challenges.messages.you-repeated-challenge", "[value]", this.challenge.getFriendlyName()); + } } // Mark as complete - this.manager.setChallengeComplete(this.user, this.world, this.challenge); + this.manager.setChallengeComplete(this.user, this.world, this.challenge, result.getFactor()); - if (!result.repeat) + // Check level completion. + if (!result.wasCompleted()) { ChallengeLevel level = this.manager.getLevel(this.challenge); @@ -350,11 +424,77 @@ public class TryToComplete } + /** + * This method full fills all challenge type requirements, that is not full filled yet. + * @param result Challenge Results + */ + private void fullFillRequirements(ChallengeResult result) + { + if (this.challenge.getChallengeType().equals(ChallengeType.ISLAND)) + { + if (result.meetsRequirements && + this.challenge.isRemoveEntities() && + !this.challenge.getRequiredEntities().isEmpty()) + { + this.removeEntities(result.entities, result.getFactor()); + } + + if (result.meetsRequirements && + this.challenge.isRemoveBlocks() && + !this.challenge.getRequiredBlocks().isEmpty()) + { + this.removeBlocks(result.blocks, result.getFactor()); + } + } + else if (this.challenge.getChallengeType().equals(ChallengeType.INVENTORY)) + { + + // If remove items, then remove them + if (this.challenge.isTakeItems()) + { + int sumEverything = result.requiredItems.stream(). + mapToInt(itemStack -> itemStack.getAmount() * result.getFactor()). + sum(); + + Map removedItems = + this.removeItems(result.requiredItems, result.getFactor()); + + int removedAmount = removedItems.values().stream().mapToInt(num -> num).sum(); + + // Something is not removed. + if (sumEverything != removedAmount) + { + this.user.sendMessage("challenges.errors.cannot-remove-items"); + + result.removedItems = removedItems; + result.meetsRequirements = false; + } + } + } + else if (this.challenge.getChallengeType().equals(ChallengeType.OTHER)) + { + if (this.addon.isEconomyProvided() && this.challenge.isTakeMoney()) + { + this.addon.getEconomyProvider().withdraw(this.user, this.challenge.getRequiredMoney()); + } + + if (this.challenge.isTakeExperience() && + this.user.getPlayer().getGameMode() != GameMode.CREATIVE) + { + // Cannot take anything from creative game mode. + this.user.getPlayer().setTotalExperience( + this.user.getPlayer().getTotalExperience() - this.challenge.getRequiredExperience()); + } + } + } + + /** * Checks if a challenge can be completed or not * It returns ChallengeResult. + * @param maxTimes - times that user wanted to complete */ - private ChallengeResult checkIfCanCompleteChallenge() + private ChallengeResult checkIfCanCompleteChallenge(int maxTimes) { ChallengeResult result; @@ -422,21 +562,27 @@ public class TryToComplete } else if (type.equals(ChallengeType.INVENTORY)) { - result = this.checkInventory(); + result = this.checkInventory(this.getAvailableCompletionTimes(maxTimes)); } else if (type.equals(ChallengeType.ISLAND)) { - result = this.checkSurrounding(); + result = this.checkSurrounding(this.getAvailableCompletionTimes(maxTimes)); } else if (type.equals(ChallengeType.OTHER)) { - result = this.checkOthers(); + result = this.checkOthers(this.getAvailableCompletionTimes(maxTimes)); } else { result = EMPTY_RESULT; } + // Mark if challenge is completed. + if (result.isMeetsRequirements()) + { + result.setCompleted(this.manager.isChallengeComplete(this.user, this.world, this.challenge)); + } + // Everything fails till this point. return result; } @@ -452,6 +598,35 @@ public class TryToComplete this.challenge.getRequiredPermissions().stream().allMatch(s -> this.user.hasPermission(s)); } + + /** + * This method checks if it is possible to complete maxTimes current challenge by + * challenge constraints and already completed times. + * @param vantedTimes How many times user wants to complete challenge + * @return how many times user is able complete challenge by its constraints. + */ + private int getAvailableCompletionTimes(int vantedTimes) + { + if (!this.challenge.isRepeatable()) + { + // Challenge is not repeatable + vantedTimes = 1; + } + else if (this.challenge.getMaxTimes() != 0) + { + // Challenge has limitations + long availableTimes = this.challenge.getMaxTimes() - this.manager.getChallengeTimes(this.user, this.world, this.challenge); + + if (availableTimes < vantedTimes) + { + vantedTimes = (int) availableTimes; + } + } + + return vantedTimes; + } + + /** * This method runs all commands from command list. * @param commands List of commands that must be performed. @@ -520,8 +695,9 @@ public class TryToComplete /** * Checks if a inventory challenge can be completed or not * It returns ChallengeResult. + * @param maxTimes - times that user wanted to complete */ - private ChallengeResult checkInventory() + private ChallengeResult checkInventory(int maxTimes) { // Run through inventory List requiredItems = new ArrayList<>(this.challenge.getRequiredItems().size()); @@ -560,77 +736,62 @@ public class TryToComplete } } - int sumEverything = 0; - // Check if all required items are in players inventory. for (ItemStack required : requiredItems) { + int numInInventory; + if (this.canIgnoreMeta(required.getType())) { - int numInInventory = + numInInventory = Arrays.stream(this.user.getInventory().getContents()). filter(Objects::nonNull). filter(i -> i.getType().equals(required.getType())). mapToInt(ItemStack::getAmount). sum(); - - if (numInInventory < required.getAmount()) - { - this.user.sendMessage("challenges.errors.not-enough-items", - "[items]", - Util.prettifyText(required.getType().toString())); - return EMPTY_RESULT; - } } else { - if (!this.user.getInventory().containsAtLeast(required, required.getAmount())) - { - this.user.sendMessage("challenges.errors.not-enough-items", - "[items]", - Util.prettifyText(required.getType().toString())); - return EMPTY_RESULT; - } + numInInventory = + Arrays.stream(this.user.getInventory().getContents()). + filter(Objects::nonNull). + filter(i -> i.isSimilar(required)). + mapToInt(ItemStack::getAmount). + sum(); } - sumEverything += required.getAmount(); - } - - // If remove items, then remove them - if (this.challenge.isTakeItems()) - { - Map removedItems = this.removeItems(requiredItems); - - int removedAmount = removedItems.values().stream().mapToInt(num -> num).sum(); - - // Something is not removed. - if (sumEverything != removedAmount) + if (numInInventory < required.getAmount()) { - this.user.sendMessage("challenges.errors.cannot-remove-items"); - // TODO: Necessary to implement returning removed items. - + this.user.sendMessage("challenges.errors.not-enough-items", + "[items]", + Util.prettifyText(required.getType().toString())); return EMPTY_RESULT; } + + maxTimes = Math.min(maxTimes, numInInventory / required.getAmount()); } } // Return the result - return new ChallengeResult().setMeetsRequirements().setRepeat( - this.manager.isChallengeComplete(this.user, this.world, this.challenge)); + return new ChallengeResult(). + setMeetsRequirements(). + setCompleteFactor(maxTimes). + setRequiredItems(requiredItems); } /** * Removes items from a user's inventory * @param requiredItemList - a list of item stacks to be removed + * @param factor - factor for required items. */ - Map removeItems(List requiredItemList) + Map removeItems(List requiredItemList, int factor) { - Map removed = new HashMap<>(); + Map removed = new HashMap<>(); for (ItemStack required : requiredItemList) { - int amountToBeRemoved = required.getAmount(); + int amountToBeRemoved = required.getAmount() * factor; List itemsInInventory; @@ -651,22 +812,25 @@ public class TryToComplete collect(Collectors.toList()); } - for (ItemStack i : itemsInInventory) + for (ItemStack itemStack : itemsInInventory) { if (amountToBeRemoved > 0) { + ItemStack dummy = itemStack.clone(); + dummy.setAmount(1); + // Remove either the full amount or the remaining amount - if (i.getAmount() >= amountToBeRemoved) + if (itemStack.getAmount() >= amountToBeRemoved) { - i.setAmount(i.getAmount() - amountToBeRemoved); - removed.merge(i.getType(), amountToBeRemoved, Integer::sum); + itemStack.setAmount(itemStack.getAmount() - amountToBeRemoved); + removed.merge(dummy, amountToBeRemoved, Integer::sum); amountToBeRemoved = 0; } else { - removed.merge(i.getType(), i.getAmount(), Integer::sum); - amountToBeRemoved -= i.getAmount(); - i.setAmount(0); + removed.merge(dummy, itemStack.getAmount(), Integer::sum); + amountToBeRemoved -= itemStack.getAmount(); + itemStack.setAmount(0); } } } @@ -713,8 +877,9 @@ public class TryToComplete /** * Checks if a island challenge can be completed or not * It returns ChallengeResult. + * @param factor - times that user wanted to complete */ - private ChallengeResult checkSurrounding() + private ChallengeResult checkSurrounding(int factor) { // Init location in player position. BoundingBox boundingBox = this.user.getPlayer().getBoundingBox().clone(); @@ -754,66 +919,73 @@ public class TryToComplete } } - ChallengeResult result = this.searchForEntities(this.challenge.getRequiredEntities(), boundingBox); + ChallengeResult result = this.searchForEntities(this.challenge.getRequiredEntities(), factor, boundingBox); - if (result.meetsRequirements && !this.challenge.getRequiredBlocks().isEmpty()) + if (result.isMeetsRequirements() && !this.challenge.getRequiredBlocks().isEmpty()) { // Search for items only if entities found - result = this.searchForBlocks(this.challenge.getRequiredBlocks(), boundingBox); + result = this.searchForBlocks(this.challenge.getRequiredBlocks(), result.getFactor(), boundingBox); } - if (result.meetsRequirements && - this.challenge.isRemoveEntities() && - !this.challenge.getRequiredEntities().isEmpty()) - { - this.removeEntities(boundingBox); - } - - if (result.meetsRequirements && - this.challenge.isRemoveBlocks() && - !this.challenge.getRequiredBlocks().isEmpty()) - { - this.removeBlocks(boundingBox); - } - - // Check if challenge is repeated. - result.setRepeat(this.manager.isChallengeComplete(this.user, this.world, this.challenge)); - return result; } /** * This method search required blocks in given challenge boundingBox. - * @param map RequiredBlock Map. + * @param requiredMap RequiredBlock Map. + * @param factor - requirement multilayer. * @param boundingBox Bounding box of island challenge * @return ChallengeResult */ - private ChallengeResult searchForBlocks(Map map, BoundingBox boundingBox) + private ChallengeResult searchForBlocks(Map requiredMap, int factor, BoundingBox boundingBox) { - Map blocks = new EnumMap<>(map); - - if (blocks.isEmpty()) + if (requiredMap.isEmpty()) { - return new ChallengeResult().setMeetsRequirements(); + return new ChallengeResult().setMeetsRequirements().setCompleteFactor(factor); } + Map blocks = new EnumMap<>(requiredMap); + Map blocksFound = new HashMap<>(requiredMap.size()); + + // This queue will contain only blocks whit required type ordered by distance till player. + Queue blockFromWorld = new PriorityQueue<>((o1, o2) -> { + if (o1.getType().equals(o2.getType())) + { + return Double.compare(o1.getLocation().distance(this.user.getLocation()), + o2.getLocation().distance(this.user.getLocation())); + } + else + { + return o1.getType().compareTo(o2.getType()); + } + }); + for (int x = (int) boundingBox.getMinX(); x <= boundingBox.getMaxX(); x++) { for (int y = (int) boundingBox.getMinY(); y <= boundingBox.getMaxY(); y++) { for (int z = (int) boundingBox.getMinZ(); z <= boundingBox.getMaxZ(); z++) { - Material mat = this.user.getWorld().getBlockAt(x, y, z).getType(); - // Remove one - blocks.computeIfPresent(mat, (b, amount) -> amount - 1); - // Remove any that have an amount of 0 - blocks.entrySet().removeIf(en -> en.getValue() <= 0); + Block block = this.user.getWorld().getBlockAt(x, y, z); - if (blocks.isEmpty()) + if (requiredMap.containsKey(block.getType())) { - // Return as soon as it s empty as no point to search more. - return new ChallengeResult().setMeetsRequirements(); + blockFromWorld.add(block); + + blocksFound.putIfAbsent(block.getType(), 1); + blocksFound.computeIfPresent(block.getType(), (reqEntity, amount) -> amount + 1); + + // Remove one + blocks.computeIfPresent(block.getType(), (b, amount) -> amount - 1); + // Remove any that have an amount of 0 + blocks.entrySet().removeIf(en -> en.getValue() <= 0); + + if (blocks.isEmpty() && factor == 1) + { + // Return as soon as it s empty as no point to search more. + return new ChallengeResult().setMeetsRequirements().setCompleteFactor(factor).setBlockQueue(blockFromWorld); + } } } } @@ -821,7 +993,21 @@ public class TryToComplete if (blocks.isEmpty()) { - return new ChallengeResult().setMeetsRequirements(); + if (factor > 1) + { + // Calculate minimal completion count. + + for (Map.Entry entry : blocksFound.entrySet()) + { + factor = Math.min(factor, + entry.getValue() / requiredMap.get(entry.getKey())); + } + } + + // kick garbage collector + blocksFound.clear(); + + return new ChallengeResult().setMeetsRequirements().setCompleteFactor(factor).setBlockQueue(blockFromWorld); } this.user.sendMessage("challenges.errors.not-close-enough", "[number]", String.valueOf(this.challenge.getSearchRadius())); @@ -830,94 +1016,134 @@ public class TryToComplete "[amount]", String.valueOf(v), "[item]", Util.prettifyText(k.toString()))); + + // kick garbage collector + blocks.clear(); + blocksFound.clear(); + requiredMap.clear(); + return EMPTY_RESULT; } /** * This method search required entities in given radius from user position and entity is inside boundingBox. - * @param map RequiredEntities Map. + * @param requiredMap RequiredEntities Map. + * @param factor - requirements multiplier. * @param boundingBox Bounding box of island challenge * @return ChallengeResult */ - private ChallengeResult searchForEntities(Map map, BoundingBox boundingBox) + private ChallengeResult searchForEntities(Map requiredMap, + int factor, + BoundingBox boundingBox) { - Map entities = map.isEmpty() ? new EnumMap<>(EntityType.class) : new EnumMap<>(map); - - if (entities.isEmpty()) + if (requiredMap.isEmpty()) { - return new ChallengeResult().setMeetsRequirements(); + return new ChallengeResult().setMeetsRequirements().setCompleteFactor(factor); } - int searchRadius = this.challenge.getSearchRadius(); + // Collect all entities that could be removed. + Map entitiesFound = new HashMap<>(); + Map minimalRequirements = new EnumMap<>(requiredMap); - this.user.getPlayer().getNearbyEntities(searchRadius, searchRadius, searchRadius).forEach(entity -> { - // Check if entity is inside challenge bounding box - if (boundingBox.contains(entity.getBoundingBox())) + // Create queue that contains all required entities ordered by distance till player. + Queue entityQueue = new PriorityQueue<>((o1, o2) -> { + if (o1.getType().equals(o2.getType())) { - // Look through all the nearby Entities, filtering by type - entities.computeIfPresent(entity.getType(), (reqEntity, amount) -> amount - 1); - entities.entrySet().removeIf(e -> e.getValue() == 0); + return Double.compare(o1.getLocation().distance(this.user.getLocation()), + o2.getLocation().distance(this.user.getLocation())); + } + else + { + return o1.getType().compareTo(o2.getType()); } }); - if (entities.isEmpty()) + this.world.getNearbyEntities(boundingBox).forEach(entity -> { + // Check if entity is inside challenge bounding box + if (requiredMap.containsKey(entity.getType())) + { + entitiesFound.putIfAbsent(entity.getType(), 1); + entitiesFound.computeIfPresent(entity.getType(), (reqEntity, amount) -> amount + 1); + + // Look through all the nearby Entities, filtering by type + minimalRequirements.computeIfPresent(entity.getType(), (reqEntity, amount) -> amount - 1); + minimalRequirements.entrySet().removeIf(e -> e.getValue() == 0); + } + }); + + if (minimalRequirements.isEmpty()) { - return new ChallengeResult().setMeetsRequirements(); + if (factor > 1) + { + // Calculate minimal completion count. + + for (Map.Entry entry : entitiesFound.entrySet()) + { + factor = Math.min(factor, + entry.getValue() / requiredMap.get(entry.getKey())); + } + } + + // Kick garbage collector + entitiesFound.clear(); + + return new ChallengeResult().setMeetsRequirements().setCompleteFactor(factor).setEntityQueue(entityQueue); } - entities.forEach((reqEnt, amount) -> this.user.sendMessage("challenges.errors.you-still-need", + minimalRequirements.forEach((reqEnt, amount) -> this.user.sendMessage("challenges.errors.you-still-need", "[amount]", String.valueOf(amount), "[item]", Util.prettifyText(reqEnt.toString()))); + // Kick garbage collector + entitiesFound.clear(); + minimalRequirements.clear(); + entityQueue.clear(); + return EMPTY_RESULT; } /** * This method removes required block and set air instead of it. - * @param boundingBox Bounding box of island challenge + * @param blockQueue Queue with blocks that could be removed + * @param factor requirement factor for each block type. */ - private void removeBlocks(BoundingBox boundingBox) + private void removeBlocks(Queue blockQueue, int factor) { Map blocks = new EnumMap<>(this.challenge.getRequiredBlocks()); - for (int x = (int) boundingBox.getMinX(); x <= boundingBox.getMaxX(); x++) - { - for (int y = (int) boundingBox.getMinY(); y <= boundingBox.getMaxY(); y++) + // Increase required blocks by factor. + blocks.entrySet().forEach(entry -> entry.setValue(entry.getValue() * factor)); + + blockQueue.forEach(block -> { + if (blocks.containsKey(block.getType())) { - for (int z = (int) boundingBox.getMinZ(); z <= boundingBox.getMaxZ(); z++) - { - Block block = this.user.getWorld().getBlockAt(new Location(this.user.getWorld(), x, y, z)); + blocks.computeIfPresent(block.getType(), (b, amount) -> amount - 1); + blocks.entrySet().removeIf(en -> en.getValue() <= 0); - if (blocks.containsKey(block.getType())) - { - blocks.computeIfPresent(block.getType(), (b, amount) -> amount - 1); - blocks.entrySet().removeIf(en -> en.getValue() <= 0); - - block.setType(Material.AIR); - } - } + block.setType(Material.AIR); } - } + }); } /** * This method removes required entities. - * @param boundingBox Bounding box of island challenge + * @param entityQueue Queue with entities that could be removed + * @param factor requirement factor for each entity type. */ - private void removeEntities(BoundingBox boundingBox) + private void removeEntities(Queue entityQueue, int factor) { Map entities = this.challenge.getRequiredEntities().isEmpty() ? new EnumMap<>(EntityType.class) : new EnumMap<>(this.challenge.getRequiredEntities()); - int searchRadius = this.challenge.getSearchRadius(); + // Increase required entities by factor. + entities.entrySet().forEach(entry -> entry.setValue(entry.getValue() * factor)); - this.user.getPlayer().getNearbyEntities(searchRadius, searchRadius, searchRadius).forEach(entity -> { - // Look through all the nearby Entities, filtering by type - - if (entities.containsKey(entity.getType()) && boundingBox.contains(entity.getBoundingBox())) + // Go through entity queue and remove entities that are requried. + entityQueue.forEach(entity -> { + if (entities.containsKey(entity.getType())) { entities.computeIfPresent(entity.getType(), (reqEntity, amount) -> amount - 1); entities.entrySet().removeIf(e -> e.getValue() == 0); @@ -935,8 +1161,9 @@ public class TryToComplete /** * Checks if a other challenge can be completed or not * It returns ChallengeResult. + * @param factor - times that user wanted to complete */ - private ChallengeResult checkOthers() + private ChallengeResult checkOthers(int factor) { if (!this.addon.isLevelProvided() && this.challenge.getRequiredIslandLevel() != 0) @@ -981,21 +1208,19 @@ public class TryToComplete } else { - if (this.addon.isEconomyProvided() && this.challenge.isTakeMoney()) - { - this.addon.getEconomyProvider().withdraw(this.user, this.challenge.getRequiredMoney()); - } + // calculate factor - if (this.challenge.isTakeExperience() && - this.user.getPlayer().getGameMode() != GameMode.CREATIVE) - { - // Cannot take anything from creative game mode. - this.user.getPlayer().setTotalExperience( - this.user.getPlayer().getTotalExperience() - this.challenge.getRequiredExperience()); - } + if (this.addon.isEconomyProvided() && this.challenge.isTakeMoney()) + { + factor = Math.min(factor, (int) this.addon.getEconomyProvider().getBalance(this.user) / this.challenge.getRequiredMoney()); + } - return new ChallengeResult().setMeetsRequirements(). - setRepeat(this.manager.isChallengeComplete(this.user, this.world, this.challenge)); + if (this.challenge.getRequiredExperience() > 0 && this.challenge.isTakeExperience()) + { + factor = Math.min(factor, this.user.getPlayer().getTotalExperience() / this.challenge.getRequiredExperience()); + } + + return new ChallengeResult().setMeetsRequirements().setCompleteFactor(factor); } return EMPTY_RESULT; @@ -1060,12 +1285,9 @@ public class TryToComplete */ private class ChallengeResult { - private boolean meetsRequirements; - - private boolean repeat; - - /** + * This method sets that challenge meets all requirements at least once. + * @return Current object. */ ChallengeResult setMeetsRequirements() { @@ -1075,12 +1297,145 @@ public class TryToComplete /** - * @param repeat the repeat to set + * Method sets that challenge is completed once already + * @param completed boolean that indicate that challenge has been already completed. + * @return Current object. */ - ChallengeResult setRepeat(boolean repeat) + ChallengeResult setCompleted(boolean completed) { - this.repeat = repeat; + this.completed = completed; return this; } + + + /** + * Method sets how many times challenge can be completed. + * @param factor Integer that represents completion count. + * @return Current object. + */ + ChallengeResult setCompleteFactor(int factor) + { + this.factor = factor; + return this; + } + + + // --------------------------------------------------------------------- + // Section: Requirement memory + // --------------------------------------------------------------------- + + + /** + * Method sets requiredItems for inventory challenge. + * @param requiredItems items that are required by inventory challenge. + * @return Current object. + */ + ChallengeResult setRequiredItems(List requiredItems) + { + this.requiredItems = requiredItems; + return this; + } + + + /** + * Method sets queue that contains all blocks with required material type. + * @param blocks queue that contains required materials from world. + * @return Current object. + */ + ChallengeResult setBlockQueue(Queue blocks) + { + this.blocks = blocks; + return this; + } + + /** + * Method sets queue that contains all entities with required entity type. + * @param entities queue that contains required entities from world. + * @return Current object. + */ + ChallengeResult setEntityQueue(Queue entities) + { + this.entities = entities; + return this; + } + + + // --------------------------------------------------------------------- + // Section: Getters + // --------------------------------------------------------------------- + + + /** + * Returns value of was completed variable. + * @return value of completed variable + */ + boolean wasCompleted() + { + return this.completed; + } + + + /** + * This method returns how many times challenge can be completed. + * @return completion count. + */ + int getFactor() + { + return this.factor; + } + + + /** + * This method returns if challenge requirements has been met at least once. + * @return value of meets requirements variable. + */ + boolean isMeetsRequirements() + { + return this.meetsRequirements; + } + + + // --------------------------------------------------------------------- + // Section: Variables + // --------------------------------------------------------------------- + + + /** + * Boolean that indicate that challenge has already bean completed once before. + */ + private boolean completed; + + /** + * Indicates that challenge can be completed. + */ + private boolean meetsRequirements; + + /** + * Integer that represents how many times challenge is completed + */ + private int factor; + + /** + * List that contains required items for Inventory Challenge + * Necessary as it contains grouped items by type or similarity, not by limit 64. + */ + private List requiredItems; + + /** + * Map that contains removed items and their removed count. + */ + private Map removedItems = null; + + /** + * Queue of blocks that contains all blocks with the same type as requiredBlock from + * challenge requirements. + */ + private Queue blocks; + + /** + * Queue of entities that contains all entities with the same type as requiredEntities from + * challenge requirements. + */ + private Queue entities; } } diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index 0d09812..22706b0 100755 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -319,9 +319,10 @@ challenges: admin: hit-things: 'Hit things to add them to the list of things required. Right click when done.' you-added: 'You added one [thing] to the challenge' - challenge-created: '[challenge] created!' + challenge-created: '[challenge]&r created!' you-completed-challenge: '&2You completed the [value] &r&2challenge!' you-repeated-challenge: '&2You repeated the [value] &r&2challenge!' + you-repeated-challenge-multiple: '&2You repeated the [value] &r&2challenge [count] times!' you-completed-level: '&2You completed the [value] &r&2level!' name-has-completed-challenge: '&5[name] has completed the [value] &r&5challenge!' name-has-completed-level: '&5[name] has completed the [value] &r&5level!' diff --git a/src/test/java/world/bentobox/challenges/tasks/TryToCompleteTest.java b/src/test/java/world/bentobox/challenges/tasks/TryToCompleteTest.java index 00ab34c..b60aff7 100644 --- a/src/test/java/world/bentobox/challenges/tasks/TryToCompleteTest.java +++ b/src/test/java/world/bentobox/challenges/tasks/TryToCompleteTest.java @@ -1,24 +1,27 @@ -/** - * - */ package world.bentobox.challenges.tasks; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.logging.Logger; +import org.bukkit.Bukkit; import org.bukkit.Material; +import org.bukkit.Server; +import org.bukkit.inventory.ItemFactory; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.PlayerInventory; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; import world.bentobox.challenges.ChallengesAddon; @@ -26,147 +29,202 @@ import world.bentobox.bentobox.api.user.User; /** * @author tastybento - * TODO: This test should be fixed. */ @RunWith(PowerMockRunner.class) +@PrepareForTest({ Bukkit.class}) public class TryToCompleteTest { - private User user; - ItemStack[] stacks = { new ItemStack(Material.PAPER, 32), - new ItemStack(Material.ACACIA_BOAT), - null, - null, - new ItemStack(Material.CACTUS, 32), - new ItemStack(Material.CACTUS, 32), - new ItemStack(Material.CACTUS, 32), - new ItemStack(Material.GOLD_BLOCK, 32) - }; - List required; - private ChallengesAddon addon; - private PlayerInventory inv; + private User user; + ItemStack[] stacks = { new ItemStack(Material.PAPER, 32), + new ItemStack(Material.ACACIA_BOAT), + null, + null, + new ItemStack(Material.CACTUS, 32), + new ItemStack(Material.CACTUS, 32), + new ItemStack(Material.CACTUS, 32), + new ItemStack(Material.BRICK_STAIRS, 64), + new ItemStack(Material.BRICK_STAIRS, 64), + new ItemStack(Material.BRICK_STAIRS, 5), + new ItemStack(Material.GOLD_BLOCK, 32) + }; + List required; + private ChallengesAddon addon; + private PlayerInventory inv; - /** - * @throws java.lang.Exception - */ - @Before - public void setUp() throws Exception { - user = mock(User.class); - inv = mock(PlayerInventory.class); - when(inv.getContents()).thenReturn(stacks); - when(user.getInventory()).thenReturn(inv); - addon = mock(ChallengesAddon.class); - required = new ArrayList<>(); - } + /** + * @throws java.lang.Exception + */ + @Before + public void setUp() throws Exception { + user = mock(User.class); + inv = mock(PlayerInventory.class); + when(inv.getContents()).thenReturn(stacks); + when(user.getInventory()).thenReturn(inv); + addon = mock(ChallengesAddon.class); + required = new ArrayList<>(); - /** - * Test method for {@link TryToComplete#removeItems(java.util.List)}. - */ - @Test - @Ignore - public void testRemoveItemsSuccess() { - Material reqMat = Material.PAPER; - int reqQty = 21; - required.add(new ItemStack(reqMat, reqQty)); - TryToComplete x = new TryToComplete(addon); - x.user(user); - Map removed = x.removeItems(required); - assertTrue(removed.get(reqMat) == reqQty); - } + Server server = mock(Server.class); + ItemFactory itemFactory = mock(ItemFactory.class); + when(server.getItemFactory()).thenReturn(itemFactory); - /** - * Test method for {@link TryToComplete#removeItems(java.util.List)}. - */ - @Test - @Ignore - public void testRemoveItemsMax() { - Material reqMat = Material.PAPER; - int reqQty = 50; - required.add(new ItemStack(reqMat, reqQty)); - TryToComplete x = new TryToComplete(addon); - x.user(user); - Map removed = x.removeItems(required); - assertTrue(removed.get(reqMat) == 32); - } + // Test will not work with items that has meta data. + when(itemFactory.getItemMeta(any())).thenReturn(null); + when(itemFactory.equals(null, null)).thenReturn(true); - /** - * Test method for {@link TryToComplete#removeItems(java.util.List)}. - */ - @Test - @Ignore - public void testRemoveItemsZero() { - Material reqMat = Material.PAPER; - int reqQty = 0; - required.add(new ItemStack(reqMat, reqQty)); - TryToComplete x = new TryToComplete(addon); - x.user(user); - Map removed = x.removeItems(required); - assertTrue(removed.get(reqMat) == null); - } + PowerMockito.mockStatic(Bukkit.class); + when(Bukkit.getServer()).thenReturn(server); - /** - * Test method for {@link TryToComplete#removeItems(java.util.List)}. - */ - @Test - @Ignore - public void testRemoveItemsSuccessMultiple() { - required.add(new ItemStack(Material.PAPER, 11)); - required.add(new ItemStack(Material.PAPER, 5)); - required.add(new ItemStack(Material.PAPER, 5)); - TryToComplete x = new TryToComplete(addon); - x.user(user); - Map removed = x.removeItems(required); - assertTrue(removed.get(Material.PAPER) == 21); - } + when(Bukkit.getItemFactory()).thenReturn(itemFactory); + when(Bukkit.getLogger()).thenReturn(Logger.getAnonymousLogger()); + } - /** - * Test method for {@link TryToComplete#removeItems(java.util.List)}. - */ - @Test - @Ignore - public void testRemoveItemsSuccessMultipleOther() { - required.add(new ItemStack(Material.CACTUS, 5)); - required.add(new ItemStack(Material.PAPER, 11)); - required.add(new ItemStack(Material.PAPER, 5)); - required.add(new ItemStack(Material.PAPER, 5)); - required.add(new ItemStack(Material.CACTUS, 5)); - TryToComplete x = new TryToComplete(addon); - x.user(user); - Map removed = x.removeItems(required); - assertTrue(removed.get(Material.PAPER) == 21); - assertTrue(removed.get(Material.CACTUS) == 10); - } + /** + * Test method for {@link TryToComplete#removeItems(java.util.List, int)}. + */ + @Test + public void testRemoveItemsSuccess() { + Material requiredMaterial = Material.PAPER; + int requiredQuantity = 21; - /** - * Test method for {@link TryToComplete#removeItems(java.util.List)}. - */ - @Test - @Ignore - public void testRemoveItemsMultipleOtherFail() { - required.add(new ItemStack(Material.ACACIA_FENCE, 5)); - required.add(new ItemStack(Material.ARROW, 11)); - required.add(new ItemStack(Material.STONE, 5)); - required.add(new ItemStack(Material.BAKED_POTATO, 5)); - required.add(new ItemStack(Material.GHAST_SPAWN_EGG, 5)); - TryToComplete x = new TryToComplete(addon); - x.user(user); - Map removed = x.removeItems(required); - assertTrue(removed.isEmpty()); + this.required.add(new ItemStack(requiredMaterial, requiredQuantity)); + TryToComplete x = new TryToComplete(this.addon); + x.user(this.user); + Map removed = x.removeItems(this.required, 1); - } + assertEquals((int) removed.getOrDefault(new ItemStack(requiredMaterial, 1), 0), requiredQuantity); + } - /** - * Test method for {@link TryToComplete#removeItems(java.util.List)}. - */ - @Test - @Ignore - public void testRemoveItemsFail() { - required.add(new ItemStack(Material.GOLD_BLOCK, 55)); - TryToComplete x = new TryToComplete(addon); - x.user(user); - Map removed = x.removeItems(required); - // It will remove 32, but not any more - assertTrue(removed.get(Material.GOLD_BLOCK) == 32); - // An error will be thrown - Mockito.verify(addon, Mockito.times(1)).logError(Mockito.anyString()); - } + /** + * Test method for {@link TryToComplete#removeItems(java.util.List, int)}. + */ + @Test + public void testRemoveItemsMax() { + Material requiredMaterial = Material.PAPER; + int requiredQuantity = 50; + + this.required.add(new ItemStack(requiredMaterial, requiredQuantity)); + TryToComplete x = new TryToComplete(this.addon); + x.user(this.user); + Map removed = x.removeItems(this.required, 1); + + assertNotEquals((int) removed.getOrDefault(new ItemStack(requiredMaterial, 1), 0), requiredQuantity); + } + + /** + * Test method for {@link TryToComplete#removeItems(java.util.List, int)}. + */ + @Test + public void testRemoveItemsZero() { + Material requiredMaterial = Material.PAPER; + int requiredQuantity = 0; + + this.required.add(new ItemStack(requiredMaterial, requiredQuantity)); + TryToComplete x = new TryToComplete(this.addon); + x.user(this.user); + Map removed = x.removeItems(this.required, 1); + + assertTrue(removed.isEmpty()); + } + + /** + * Test method for {@link TryToComplete#removeItems(java.util.List, int)}. + */ + @Test + public void testRemoveItemsSuccessMultiple() { + required.add(new ItemStack(Material.PAPER, 11)); + required.add(new ItemStack(Material.PAPER, 5)); + required.add(new ItemStack(Material.PAPER, 5)); + TryToComplete x = new TryToComplete(addon); + x.user(user); + Map removed = x.removeItems(required, 1); + + assertEquals((int) removed.getOrDefault(new ItemStack(Material.PAPER, 1), 0), 21); + } + + /** + * Test method for {@link TryToComplete#removeItems(java.util.List, int)}. + */ + @Test + public void testRemoveItemsSuccessMultipleOther() { + required.add(new ItemStack(Material.CACTUS, 5)); + required.add(new ItemStack(Material.PAPER, 11)); + required.add(new ItemStack(Material.PAPER, 5)); + required.add(new ItemStack(Material.PAPER, 5)); + required.add(new ItemStack(Material.CACTUS, 5)); + TryToComplete x = new TryToComplete(addon); + x.user(user); + Map removed = x.removeItems(required, 1); + + assertEquals((int) removed.getOrDefault(new ItemStack(Material.PAPER, 1), 0), 21); + assertEquals((int) removed.getOrDefault(new ItemStack(Material.CACTUS, 1), 0), 10); + } + + /** + * Test method for {@link TryToComplete#removeItems(java.util.List, int)}. + */ + @Test + public void testRemoveItemsMultipleOtherFail() { + required.add(new ItemStack(Material.ACACIA_FENCE, 5)); + required.add(new ItemStack(Material.ARROW, 11)); + required.add(new ItemStack(Material.STONE, 5)); + required.add(new ItemStack(Material.BAKED_POTATO, 5)); + required.add(new ItemStack(Material.GHAST_SPAWN_EGG, 5)); + TryToComplete x = new TryToComplete(addon); + x.user(user); + Map removed = x.removeItems(required, 1); + assertTrue(removed.isEmpty()); + } + + /** + * Test method for {@link TryToComplete#removeItems(java.util.List, int)}. + */ + @Test + public void testRemoveItemsFail() { + ItemStack input = new ItemStack(Material.GOLD_BLOCK, 55); + required.add(input); + TryToComplete x = new TryToComplete(addon); + x.user(user); + Map removed = x.removeItems(required, 1); + + // It will remove 32, but not any more + assertEquals((int) removed.getOrDefault(new ItemStack(Material.GOLD_BLOCK, 1), 0), 32); + + // An error will be thrown + Mockito.verify(addon, Mockito.times(1)).logError(Mockito.anyString()); + } + + + + /** + * Test method for {@link TryToComplete#removeItems(java.util.List, int)}. + */ + @Test + public void testRequireTwoStacks() { + required.add(new ItemStack(Material.BRICK_STAIRS, 64)); + required.add(new ItemStack(Material.BRICK_STAIRS, 64)); + + TryToComplete x = new TryToComplete(addon); + x.user(user); + Map removed = x.removeItems(required, 1); + + // It should remove both stacks + assertEquals((int) removed.getOrDefault(new ItemStack(Material.BRICK_STAIRS, 1), 0), 128); + } + + + /** + * Test method for {@link TryToComplete#removeItems(java.util.List, int)}. + */ + @Test + public void testFactorStacks() { + required.add(new ItemStack(Material.BRICK_STAIRS, 32)); + + TryToComplete x = new TryToComplete(addon); + x.user(user); + Map removed = x.removeItems(required, 4); + + // It should remove both stacks + assertEquals((int) removed.getOrDefault(new ItemStack(Material.BRICK_STAIRS, 1), 0), 128); + } } +