diff --git a/src/main/java/world/bentobox/challenges/panel/CommonPanel.java b/src/main/java/world/bentobox/challenges/panel/CommonPanel.java new file mode 100644 index 0000000..6e9e760 --- /dev/null +++ b/src/main/java/world/bentobox/challenges/panel/CommonPanel.java @@ -0,0 +1,957 @@ +// +// Created by BONNe +// Copyright - 2021 +// + + +package world.bentobox.challenges.panel; + + +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.inventory.ItemStack; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; + +import java.util.*; +import java.util.stream.Collectors; + +import world.bentobox.bentobox.api.panels.PanelItem; +import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.util.Util; +import world.bentobox.challenges.ChallengesAddon; +import world.bentobox.challenges.database.object.Challenge; +import world.bentobox.challenges.database.object.ChallengeLevel; +import world.bentobox.challenges.database.object.requirements.*; +import world.bentobox.challenges.managers.ChallengesManager; +import world.bentobox.challenges.utils.Constants; +import world.bentobox.challenges.utils.LevelStatus; +import world.bentobox.challenges.utils.Utils; + + +/** + * This class contains common methods for all panels. + */ +public abstract class CommonPanel +{ + /** + * This is default constructor for all classes that extends CommonPanel. + * + * @param addon ChallengesAddon instance. + * @param user User who opens panel. + */ + protected CommonPanel(ChallengesAddon addon, User user, World world, String topLabel, String permissionPrefix) + { + this.addon = addon; + this.world = world; + this.manager = addon.getChallengesManager(); + this.user = user; + + this.topLabel = topLabel; + this.permissionPrefix = permissionPrefix; + + this.parentPanel = null; + + this.returnButton = new PanelItemBuilder(). + name(this.user.getTranslation(Constants.BUTTON + "quit.name")). + description(this.user.getTranslationOrNothing(Constants.BUTTON + "quit.description")). + description(""). + description(this.user.getTranslationOrNothing(Constants.TIPS + "click-to-quit")). + icon(Material.OAK_DOOR). + clickHandler((panel, user1, clickType, i) -> { + this.user.closeInventory(); + return true; + }).build(); + } + + + /** + * This is default constructor for all classes that extends CommonPanel. + * + * @param parentPanel Parent panel of current panel. + */ + protected CommonPanel(@NonNull CommonPanel parentPanel) + { + this.addon = parentPanel.addon; + this.manager = parentPanel.manager; + this.user = parentPanel.user; + this.world = parentPanel.world; + + this.topLabel = parentPanel.topLabel; + this.permissionPrefix = parentPanel.permissionPrefix; + + this.parentPanel = parentPanel; + + this.returnButton = new PanelItemBuilder(). + name(this.user.getTranslation(Constants.BUTTON + "return.name")). + description(this.user.getTranslationOrNothing(Constants.BUTTON + "return.description")). + description(""). + description(this.user.getTranslationOrNothing(Constants.TIPS + "click-to-return")). + icon(Material.OAK_DOOR). + clickHandler((panel, user1, clickType, i) -> { + this.parentPanel.build(); + return true; + }).build(); + } + + + /** + * This method allows building panel. + */ + protected abstract void build(); + + + +// --------------------------------------------------------------------- +// Section: Common methods +// --------------------------------------------------------------------- + + + /** + * This method generates and returns given challenge description. It is used here to avoid multiple + * duplicates, as it would be nice to have single place where challenge could be generated. + * @param challenge Challenge which description must be generated. + * @param target target player. + * @return List of strings that will be used in challenges description. + */ + protected List generateChallengeDescription(Challenge challenge, @Nullable User target) + { + // Some values to avoid over checking. + final boolean isCompletedOnce = target != null && + this.manager.isChallengeComplete(target.getUniqueId(), this.world, challenge); + + final long doneTimes = target != null && challenge.isRepeatable() ? + this.manager.getChallengeTimes(target, this.world, challenge) : (isCompletedOnce ? 0 : 1); + + boolean isCompletedAll = isCompletedOnce && + (!challenge.isRepeatable() || + challenge.getMaxTimes() > 0 && doneTimes >= challenge.getMaxTimes()); + + final String reference = Constants.DESCRIPTIONS + "challenge."; + + // Get description in single string + String description = Util.translateColorCodes(String.join("\n", + challenge.getDescription())); + // Non-memory optimal code used for easier debugging and nicer code layout for my eye :) + // Get status in single string + String status = this.generateChallengeStatus(isCompletedOnce, + isCompletedAll, + doneTimes, + challenge.getMaxTimes()); + // Get requirements in single string + String requirements = isCompletedAll ? "" : this.generateRequirements(challenge, target); + // Get rewards in single string + String rewards = isCompletedAll ? "" : this.generateRewards(challenge, isCompletedOnce); + + if (!description.replaceAll("(?m)^[ \\t]*\\r?\\n", "").isEmpty()) + { + String returnString = this.user.getTranslationOrNothing(reference + "lore", + Constants.REQUIREMENTS, requirements, + Constants.REWARDS, rewards, + Constants.STATUS, status); + + // remove empty lines from the generated text. + List collect = + Arrays.stream(returnString.replaceAll("(?m)^[ \\t]*\\r?\\n", ""). + split("\n")). + collect(Collectors.toList()); + + // find and replace description from collected blocks. + + for (int i = 0; i < collect.size(); i++) + { + if (collect.get(i).contains(Constants.DESCRIPTION)) + { + collect.set(i, collect.get(i).replace(Constants.DESCRIPTION, description)); + } + } + + return collect; + } + else + { + String returnString = this.user.getTranslationOrNothing(reference + "lore", + Constants.DESCRIPTION, description, + Constants.REQUIREMENTS, requirements, + Constants.REWARDS, rewards, + Constants.STATUS, status); + + // Remove empty lines and returns as a list. + + return Arrays.stream(returnString.replaceAll("(?m)^[ \\t]*\\r?\\n", ""). + split("\n")). + collect(Collectors.toList()); + } + } + + + /** + * This method generate requirements description for given challenge. + * @param challenge Challenge which requirements must be generated. + * @return Lore message with requirements. + */ + private String generateRequirements(Challenge challenge, @Nullable User target) + { + final String reference = Constants.DESCRIPTIONS + "challenge.requirements."; + + String environment; + + if (challenge.getEnvironment().isEmpty() || challenge.getEnvironment().size() == 3) + { + // If challenge can be completed everywhere, do not display requirement. + environment = ""; + } + else if (challenge.getEnvironment().size() == 1) + { + environment = this.user.getTranslationOrNothing(reference + "environment-single", + Constants.ENVIRONMENT, + Utils.prettifyObject(challenge.getEnvironment().iterator().next(), this.user)); + } + else + { + StringBuilder builder = new StringBuilder(); + builder.append(this.user.getTranslationOrNothing(reference + "environment-title")); + challenge.getEnvironment().stream().sorted().forEach(en -> + { + builder.append("\n"); + builder.append(this.user.getTranslationOrNothing(reference + "environment-single", + Constants.ENVIRONMENT, + Utils.prettifyObject(en, this.user))); + }); + + environment = builder.toString(); + } + + String permissions; + + if (!challenge.getRequirements().getRequiredPermissions().isEmpty()) + { + // Yes list duplication for complete menu. + List missingPermissions = challenge.getRequirements().getRequiredPermissions().stream(). + filter(permission -> target == null || !target.hasPermission(permission)). + sorted(). + collect(Collectors.toList()); + + StringBuilder permissionBuilder = new StringBuilder(); + + if (missingPermissions.size() == 1) + { + permissionBuilder.append(this.user.getTranslationOrNothing(reference + "permission-single", + Constants.PERMISSION, missingPermissions.get(0))); + } + else if (!missingPermissions.isEmpty()) + { + permissionBuilder.append(this.user.getTranslationOrNothing(reference + "permissions-title")); + missingPermissions.forEach(permission -> + { + permissionBuilder.append("\n"); + permissionBuilder.append(this.user.getTranslationOrNothing(reference + "permissions-list", + Constants.PERMISSION, permission)); + }); + } + + permissions = permissionBuilder.toString(); + } + else + { + permissions = ""; + } + + String typeRequirement = switch (challenge.getChallengeType()) { + case INVENTORY_TYPE -> this.generateInventoryChallenge(challenge.getRequirements()); + case ISLAND_TYPE -> this.generateIslandChallenge(challenge.getRequirements()); + case OTHER_TYPE -> this.generateOtherChallenge(challenge.getRequirements()); + case STATISTIC_TYPE -> this.generateStatisticChallenge(challenge.getRequirements()); + }; + + return this.user.getTranslationOrNothing(reference + "lore", + Constants.ENVIRONMENT, environment, + Constants.TYPE_REQUIREMENT, typeRequirement, + Constants.PERMISSIONS, permissions); + } + + + /** + * This method generates lore message for island requirement. + * @param requirement Island Requirement. + * @return Requirement lore message. + */ + private String generateIslandChallenge(IslandRequirements requirement) + { + final String reference = Constants.DESCRIPTIONS + "challenge.requirements.island."; + + String blocks; + + if (!requirement.getRequiredBlocks().isEmpty()) + { + StringBuilder builder = new StringBuilder(); + builder.append(this.user.getTranslationOrNothing(reference + "blocks-title")); + requirement.getRequiredBlocks().entrySet().stream(). + sorted(Map.Entry.comparingByKey()). + forEach(entry -> + { + builder.append("\n"); + builder.append(this.user.getTranslationOrNothing(reference + "blocks-value", + Constants.NUMBER, String.valueOf(entry.getValue()), + Constants.MATERIAL, Utils.prettifyObject(entry.getKey(), this.user))); + }); + + blocks = builder.toString(); + } + else + { + blocks = ""; + } + + String entities; + + if (!requirement.getRequiredEntities().isEmpty()) + { + StringBuilder builder = new StringBuilder(); + builder.append(this.user.getTranslationOrNothing(reference + "entities-title")); + requirement.getRequiredEntities().entrySet().stream(). + sorted(Map.Entry.comparingByKey()). + forEach(entry -> + { + builder.append("\n"); + builder.append(this.user.getTranslationOrNothing(reference + "entities-value", + Constants.NUMBER, String.valueOf(entry.getValue()), + Constants.ENTITY, Utils.prettifyObject(entry.getKey(), this.user))); + }); + + entities = builder.toString(); + } + else + { + entities = ""; + } + + String searchRadius = this.user.getTranslationOrNothing(reference + "search-radius", + Constants.NUMBER, String.valueOf(requirement.getSearchRadius())); + + String warningBlocks = requirement.isRemoveBlocks() ? + this.user.getTranslationOrNothing(reference + "warning-block") : ""; + String warningEntities = requirement.isRemoveEntities() ? + this.user.getTranslationOrNothing(reference + "warning-entity") : ""; + + return this.user.getTranslationOrNothing(reference + "lore", + "[blocks]", blocks, + "[entities]", entities, + "[warning-block]", warningBlocks, + "[warning-entity]", warningEntities, + "[search-radius]", searchRadius); + } + + + /** + * This method generates lore message for inventory requirement. + * @param requirement Inventory Requirement. + * @return Requirement lore message. + */ + private String generateInventoryChallenge(InventoryRequirements requirement) + { + final String reference = Constants.DESCRIPTIONS + "challenge.requirements.inventory."; + + String items; + + if (!requirement.getRequiredItems().isEmpty()) + { + StringBuilder builder = new StringBuilder(); + builder.append(this.user.getTranslationOrNothing(reference + "item-title")); + Utils.groupEqualItems(requirement.getRequiredItems()).stream(). + sorted(Comparator.comparing(ItemStack::getType)). + forEach(itemStack -> + { + builder.append("\n"); + builder.append(this.user.getTranslationOrNothing(reference + "item-list", + "[item]", Utils.prettifyObject(itemStack, this.user))); + }); + + items = builder.toString(); + } + else + { + items = ""; + } + + String warning = requirement.isTakeItems() ? + this.user.getTranslationOrNothing(reference + "warning") : ""; + + return this.user.getTranslationOrNothing(reference + "lore", + "[items]", items, + "[warning]", warning); + } + + + /** + * This method generates lore message for other requirement. + * @param requirement Other Requirement. + * @return Requirement lore message. + */ + private String generateOtherChallenge(OtherRequirements requirement) + { + final String reference = Constants.DESCRIPTIONS + "challenge.requirements.other."; + + String experience = requirement.getRequiredExperience() <= 0 ? "" : + this.user.getTranslationOrNothing(reference + "experience", + "[number]", String.valueOf(requirement.getRequiredExperience())); + + String experienceWarning = requirement.getRequiredExperience() > 0 && requirement.isTakeExperience() ? + this.user.getTranslationOrNothing(reference + "experience-warning") : ""; + + String money = !this.addon.isEconomyProvided() || requirement.getRequiredMoney() <= 0 ? "" : + this.user.getTranslationOrNothing(reference + "money", + "[number]", String.valueOf(requirement.getRequiredMoney())); + + String moneyWarning = this.addon.isEconomyProvided() && + requirement.getRequiredMoney() > 0 && + requirement.isTakeMoney() ? + this.user.getTranslationOrNothing(reference + "money-warning") : ""; + + String level = !this.addon.isLevelProvided() || requirement.getRequiredIslandLevel() <= 0 ? "" : + this.user.getTranslationOrNothing(reference, + "[number]", String.valueOf(requirement.getRequiredIslandLevel())); + + return this.user.getTranslationOrNothing(reference + "lore", + "[experience]", experience, + "[experience-warning]", experienceWarning, + "[money]", money, + "[money-warning]", moneyWarning, + "[level]", level); + } + + + /** + * This method generates lore message for Statistic requirement. + * @param requirement Statistic Requirement. + * @return Requirement lore message. + */ + private String generateStatisticChallenge(StatisticRequirements requirement) + { + final String reference = Constants.DESCRIPTIONS + "challenge.requirements.statistic."; + + String statistic; + + if (requirement.getStatistic() == null) + { + // Challenges by default comes with empty statistic field. + return ""; + } + + switch (requirement.getStatistic().getType()) + { + case UNTYPED -> { + statistic = this.user.getTranslationOrNothing(reference + "statistic", + "[statistic]", Utils.prettifyObject(requirement.getStatistic(), this.user), + "[number]", String.valueOf(requirement.getAmount())); + } + case ITEM, BLOCK -> { + if (requirement.getAmount() > 1) + { + statistic = this.user.getTranslationOrNothing(reference + "multiple-target", + "[statistic]", Utils.prettifyObject(requirement.getStatistic(), this.user), + "[number]", String.valueOf(requirement.getAmount()), + "[target]", Utils.prettifyObject(requirement.getMaterial(), this.user)); + } + else + { + statistic = this.user.getTranslationOrNothing(reference + "single-target", + "[statistic]", Utils.prettifyObject(requirement.getStatistic(), this.user), + "[target]", Utils.prettifyObject(requirement.getMaterial(), this.user)); + } + } + case ENTITY -> { + if (requirement.getAmount() > 1) + { + statistic = this.user.getTranslationOrNothing(reference + "multiple-target", + "[statistic]", Utils.prettifyObject(requirement.getStatistic(), this.user), + "[number]", String.valueOf(requirement.getAmount()), + "[target]", Utils.prettifyObject(requirement.getEntity(), this.user)); + } + else + { + statistic = this.user.getTranslationOrNothing(reference + "single-target", + "[statistic]", Utils.prettifyObject(requirement.getStatistic(), this.user), + "[target]", Utils.prettifyObject(requirement.getEntity(), this.user)); + } + } + default -> { + statistic = ""; + } + } + + String warning = requirement.isReduceStatistic() ? + this.user.getTranslationOrNothing(reference + "warning") : ""; + + return this.user.getTranslationOrNothing(reference + "lore", + "[statistic]", statistic, + "[warning]", warning); + } + + + /** + * This message generates challenge status description. + * @param completedOnce Indicate that challenge is completed at least one time. + * @param completedAll Indicate that challenge is not repeatable anymore. + * @param completionCount Number of completion count. + * @param maxCompletions Number of max completion count. + * @return String with a text that will be generated for status. + */ + private String generateChallengeStatus(boolean completedOnce, + boolean completedAll, + long completionCount, + int maxCompletions) + { + final String reference = Constants.DESCRIPTIONS + "challenge.status."; + + if (completedAll) + { + if (maxCompletions > 1) + { + return this.user.getTranslationOrNothing(reference + "completed-times-reached", + Constants.MAX, String.valueOf(maxCompletions)); + } + else + { + return this.user.getTranslationOrNothing(reference + "completed"); + } + } + else if (completedOnce) + { + if (maxCompletions > 0) + { + return this.user.getTranslationOrNothing(reference + "completed-times-of", + Constants.MAX, String.valueOf(maxCompletions), + Constants.NUMBER, String.valueOf(completionCount)); + } + else + { + return this.user.getTranslationOrNothing(reference + "completed-times", + Constants.NUMBER, String.valueOf(completionCount)); + } + } + else + { + return ""; + } + } + + + /** + * This method creates reward lore text. + * @param challenge Challenge which reward lore must be generated. + * @param isRepeating Boolean that indicate if it is repeating reward or first time. + * @return Reward text. + */ + private String generateRewards(Challenge challenge, boolean isRepeating) + { + if (isRepeating) + { + return this.generateRepeatReward(challenge); + } + else + { + return this.generateReward(challenge); + } + } + + + /** + * This method creates repeat reward lore text. + * @param challenge Challenge which reward lore must be generated. + * @return Reward text. + */ + private String generateRepeatReward(Challenge challenge) + { + final String reference = Constants.DESCRIPTIONS + "challenge.rewards."; + + String items; + + if (!challenge.getRepeatItemReward().isEmpty()) + { + StringBuilder builder = new StringBuilder(); + builder.append(this.user.getTranslationOrNothing(reference + "item-title")); + Utils.groupEqualItems(challenge.getRepeatItemReward()).stream(). + sorted(Comparator.comparing(ItemStack::getType)). + forEach(itemStack -> + { + builder.append("\n"); + builder.append(this.user.getTranslationOrNothing(reference + "item-list", + "[item]", Utils.prettifyObject(itemStack, this.user))); + }); + + items = builder.toString(); + } + else + { + items = ""; + } + + String experience = challenge.getRepeatExperienceReward() <= 0 ? "" : + this.user.getTranslationOrNothing(reference + "experience", + "[number]", String.valueOf(challenge.getRepeatExperienceReward())); + + String money = !this.addon.isEconomyProvided() || challenge.getRepeatMoneyReward() <= 0 ? "" : + this.user.getTranslationOrNothing(reference + "money", + "[number]", String.valueOf(challenge.getRepeatMoneyReward())); + + String commands; + + if (!challenge.getRepeatRewardCommands().isEmpty()) + { + StringBuilder permissionBuilder = new StringBuilder(); + + if (!challenge.getRepeatRewardCommands().isEmpty()) + { + permissionBuilder.append(this.user.getTranslationOrNothing(reference + "commands-title")); + + challenge.getRepeatRewardCommands().forEach(command -> + { + permissionBuilder.append("\n"); + permissionBuilder.append(this.user.getTranslationOrNothing(reference + "command", + "[command]", command)); + }); + } + + commands = permissionBuilder.toString(); + } + else + { + commands = ""; + } + + if (challenge.getRepeatRewardText().isEmpty() && + items.isEmpty() && + experience.isEmpty() && + money.isEmpty() && + commands.isEmpty()) + { + // If everything is empty, do not return anything. + return ""; + } + + return this.user.getTranslationOrNothing(reference + "lore", + "[text]", Util.translateColorCodes(String.join("\n", challenge.getRepeatRewardText())), + "[items]", items, + "[experience]", experience, + "[money]", money, + "[commands]", commands); + } + + + /** + * This method creates reward lore text. + * @param challenge Challenge which reward lore must be generated. + * @return Reward text. + */ + private String generateReward(Challenge challenge) + { + final String reference = Constants.DESCRIPTIONS + "challenge.rewards."; + + String items; + + if (!challenge.getRewardItems().isEmpty()) + { + StringBuilder builder = new StringBuilder(); + builder.append(this.user.getTranslationOrNothing(reference + "item-title")); + Utils.groupEqualItems(challenge.getRewardItems()).stream(). + sorted(Comparator.comparing(ItemStack::getType)). + forEach(itemStack -> + { + builder.append("\n"); + builder.append(this.user.getTranslationOrNothing(reference + "item-list", + "[item]", Utils.prettifyObject(itemStack, this.user))); + }); + + items = builder.toString(); + } + else + { + items = ""; + } + + String experience = challenge.getRewardExperience() <= 0 ? "" : + this.user.getTranslationOrNothing(reference + "experience", + "[number]", String.valueOf(challenge.getRewardExperience())); + + String money = !this.addon.isEconomyProvided() || challenge.getRewardMoney() <= 0 ? "" : + this.user.getTranslationOrNothing(reference + "money", + "[number]", String.valueOf(challenge.getRewardMoney())); + + String commands; + + if (!challenge.getRewardCommands().isEmpty()) + { + StringBuilder permissionBuilder = new StringBuilder(); + + if (!challenge.getRewardCommands().isEmpty()) + { + permissionBuilder.append(this.user.getTranslationOrNothing(reference + "commands-title")); + + challenge.getRewardCommands().forEach(command -> + { + permissionBuilder.append("\n"); + permissionBuilder.append(this.user.getTranslationOrNothing(reference + "command", + "[command]", command)); + }); + } + + commands = permissionBuilder.toString(); + } + else + { + commands = ""; + } + + if (challenge.getRewardText().isEmpty() && + items.isEmpty() && + experience.isEmpty() && + money.isEmpty() && + commands.isEmpty()) + { + // If everything is empty, do not return anything. + return ""; + } + + return this.user.getTranslationOrNothing(reference + "lore", + "[text]", Util.translateColorCodes(String.join("\n", challenge.getRewardText())), + "[items]", items, + "[experience]", experience, + "[money]", money, + "[commands]", commands); + } + + + /** + * This method generates level description string. + * @param level Level which string must be generated. + * @return List with generated description. + */ + protected List generateLevelDescription(ChallengeLevel level) + { + final String reference = Constants.DESCRIPTIONS + "level."; + + // Non-memory optimal code used for easier debugging and nicer code layout for my eye :) + // Get status in single string + String status = ""; + // Get requirements in single string + String waiver = this.user.getTranslationOrNothing(reference + "waiver", + "[number]", String.valueOf(level.getWaiverAmount())); + // Get rewards in single string + String rewards = this.generateReward(level); + + String returnString = this.user.getTranslation(reference + "lore", + "[text]", Util.translateColorCodes(level.getUnlockMessage()), + "[waiver]", waiver, + "[rewards]", rewards, + "[status]", status); + + // Remove empty lines and returns as a list. + + return Arrays.stream(returnString.replaceAll("(?m)^[ \\t]*\\r?\\n", ""). + split("\n")). + collect(Collectors.toList()); + } + + + /** + * This method generates level description string. + * @param levelStatus Level which string must be generated. + * @param user User who calls generation. + * @return List with generated description. + */ + protected List generateLevelDescription(LevelStatus levelStatus, User user) + { + ChallengeLevel level = levelStatus.getLevel(); + + final String reference = Constants.DESCRIPTIONS + "level."; + + // Non-memory optimal code used for easier debugging and nicer code layout for my eye :) + // Get status in single string + String status = this.generateLevelStatus(levelStatus); + // Get requirements in single string + String waiver = !levelStatus.isUnlocked() || levelStatus.isComplete() ? "" : + this.user.getTranslationOrNothing(reference + "waiver", + "[number]", String.valueOf(level.getWaiverAmount())); + // Get rewards in single string + String rewards = !levelStatus.isUnlocked() ? "" : this.generateReward(level); + + String returnString = this.user.getTranslation(reference + "lore", + "[text]", Util.translateColorCodes(levelStatus.getLevel().getUnlockMessage()), + "[waiver]", waiver, + "[rewards]", rewards, + "[status]", status); + + // Remove empty lines and returns as a list. + + return Arrays.stream(returnString.replaceAll("(?m)^[ \\t]*\\r?\\n", ""). + split("\n")). + collect(Collectors.toList()); + } + + + /** + * This method generates level status description. + * @param levelStatus Level status which description must be generated. + * @return Level status text. + */ + private String generateLevelStatus(LevelStatus levelStatus) + { + final String reference = Constants.DESCRIPTIONS + "level.status."; + + if (!levelStatus.isUnlocked()) + { + return this.user.getTranslationOrNothing(reference + "locked") + "\n" + + this.user.getTranslationOrNothing(reference + "missing-challenges", + "[number]", String.valueOf(levelStatus.getNumberOfChallengesStillToDo())); + } + else if (levelStatus.isComplete()) + { + return this.user.getTranslationOrNothing(reference + "completed"); + } + else + { + ChallengeLevel level = levelStatus.getLevel(); + // Check if unlock message should appear. + int doneChallenges = (int) level.getChallenges().stream(). + filter(challenge -> this.addon.getChallengesManager().isChallengeComplete(user.getUniqueId(), world, challenge)). + count(); + + return this.user.getTranslation(reference + "completed-challenges-of", + "[number]", String.valueOf(doneChallenges), + "[max]", String.valueOf(level.getChallenges().size())); + } + } + + + /** + * This method creates reward lore text. + * @param level ChallengeLevel which reward lore must be generated. + * @return Reward text. + */ + private String generateReward(ChallengeLevel level) + { + final String reference = Constants.DESCRIPTIONS + "level.rewards."; + + String items; + + if (!level.getRewardItems().isEmpty()) + { + StringBuilder builder = new StringBuilder(); + builder.append(this.user.getTranslationOrNothing(reference + "item-title")); + Utils.groupEqualItems(level.getRewardItems()).stream(). + sorted(Comparator.comparing(ItemStack::getType)). + forEach(itemStack -> + { + builder.append("\n"); + builder.append(this.user.getTranslationOrNothing(reference + "item-list", + "[item]", Utils.prettifyObject(itemStack, this.user))); + }); + + items = builder.toString(); + } + else + { + items = ""; + } + + String experience = level.getRewardExperience() <= 0 ? "" : + this.user.getTranslationOrNothing(reference + "experience", + "[number]", String.valueOf(level.getRewardExperience())); + + String money = !this.addon.isEconomyProvided() || level.getRewardMoney() <= 0 ? "" : + this.user.getTranslationOrNothing(reference + "money", + "[number]", String.valueOf(level.getRewardMoney())); + + String commands; + + if (!level.getRewardCommands().isEmpty()) + { + StringBuilder permissionBuilder = new StringBuilder(); + + if (!level.getRewardCommands().isEmpty()) + { + permissionBuilder.append(this.user.getTranslationOrNothing(reference + "commands-title")); + + level.getRewardCommands().forEach(command -> + { + permissionBuilder.append("\n"); + permissionBuilder.append(this.user.getTranslationOrNothing(reference + "command", + "[command]", command)); + }); + } + + commands = permissionBuilder.toString(); + } + else + { + commands = ""; + } + + if (level.getRewardText().isEmpty() && + items.isEmpty() && + experience.isEmpty() && + money.isEmpty() && + commands.isEmpty()) + { + // If everything is empty, do not return anything. + return ""; + } + + return this.user.getTranslationOrNothing(reference + "lore", + "[text]", Util.translateColorCodes(String.join("\n", level.getRewardText())), + "[items]", items, + "[experience]", experience, + "[money]", money, + "[commands]", commands); + } + + + +// --------------------------------------------------------------------- +// Section: Variables +// --------------------------------------------------------------------- + + + /** + * This variable stores parent gui. + */ + @Nullable + protected final CommonPanel parentPanel; + + /** + * Variable stores Challenges addon. + */ + protected final ChallengesAddon addon; + + /** + * Variable stores Challenges addon manager. + */ + protected final ChallengesManager manager; + + /** + * Variable stores world in which panel is referred to. + */ + protected final World world; + + /** + * Variable stores user who created this panel. + */ + protected final User user; + + /** + * Variable stores top label of command from which panel was called. + */ + protected final String topLabel; + + /** + * Variable stores permission prefix of command from which panel was called. + */ + protected final String permissionPrefix; + + /** + * This object holds PanelItem that allows to return to previous panel. + */ + protected PanelItem returnButton; +} diff --git a/src/main/java/world/bentobox/challenges/panel/user/ChallengesPanel.java b/src/main/java/world/bentobox/challenges/panel/user/ChallengesPanel.java new file mode 100644 index 0000000..38c3c14 --- /dev/null +++ b/src/main/java/world/bentobox/challenges/panel/user/ChallengesPanel.java @@ -0,0 +1,751 @@ +// +// Created by BONNe +// Copyright - 2021 +// + + +package world.bentobox.challenges.panel.user; + + +import org.bukkit.World; +import org.bukkit.inventory.ItemStack; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import world.bentobox.bentobox.api.panels.PanelItem; +import world.bentobox.bentobox.api.panels.TemplatedPanel; +import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; +import world.bentobox.bentobox.api.panels.builders.TemplatedPanelBuilder; +import world.bentobox.bentobox.api.panels.reader.ItemTemplateRecord; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.util.Util; +import world.bentobox.challenges.ChallengesAddon; +import world.bentobox.challenges.config.SettingsUtils; +import world.bentobox.challenges.database.object.Challenge; +import world.bentobox.challenges.panel.CommonPanel; +import world.bentobox.challenges.tasks.TryToComplete; +import world.bentobox.challenges.utils.Constants; +import world.bentobox.challenges.utils.LevelStatus; + + +/** + * Main challenges panel builder. + */ +public class ChallengesPanel extends CommonPanel +{ + private ChallengesPanel(ChallengesAddon addon, + World world, + User user, + String topLabel, + String permissionPrefix) + { + super(addon, user, world, topLabel, permissionPrefix); + this.updateLevelList(); + this.containsChallenges = this.manager.hasAnyChallengeData(this.world); + } + + + /** + * Open the Challenges GUI. + * + * @param addon the addon + * @param world the world + * @param user the user + * @param topLabel the top label + * @param permissionPrefix the permission prefix + */ + public static void open(ChallengesAddon addon, + World world, + User user, + String topLabel, + String permissionPrefix) + { + new ChallengesPanel(addon, world, user, topLabel, permissionPrefix).build(); + } + + + protected void build() + { + // Do not open gui if there is no challenges. + if (!this.containsChallenges) + { + this.addon.logError("There are no challenges set up!"); + this.user.sendMessage(Constants.ERRORS + "no-challenges"); + return; + } + + // Create lists for builder. + this.updateFreeChallengeList(); + this.updateChallengeList(); + // this.updateLevelList(); + + // Start building panel. + TemplatedPanelBuilder panelBuilder = new TemplatedPanelBuilder(); + + // Set main template. + panelBuilder.template("main_panel", new File(this.addon.getDataFolder(), "panels")); + panelBuilder.user(this.user); + panelBuilder.world(this.user.getWorld()); + + // Register button builders + panelBuilder.registerTypeBuilder("CHALLENGE", this::createChallengeButton); + panelBuilder.registerTypeBuilder("LEVEL", this::createLevelButton); + + panelBuilder.registerTypeBuilder("UNASSIGNED_CHALLENGES", this::createFreeChallengesButton); + + panelBuilder.registerTypeBuilder("NEXT", this::createNextButton); + panelBuilder.registerTypeBuilder("PREVIOUS", this::createPreviousButton); + + // Register unknown type builder. + panelBuilder.build(); + } + + + private void updateFreeChallengeList() + { + this.freeChallengeList = this.manager.getFreeChallenges(this.world); + + if (this.addon.getChallengesSettings().isRemoveCompleteOneTimeChallenges()) + { + this.freeChallengeList.removeIf(challenge -> !challenge.isRepeatable() && + this.manager.isChallengeComplete(this.user, this.world, challenge)); + } + + // Remove all undeployed challenges if VisibilityMode is set to Hidden. + if (this.addon.getChallengesSettings().getVisibilityMode().equals(SettingsUtils.VisibilityMode.HIDDEN)) + { + this.freeChallengeList.removeIf(challenge -> !challenge.isDeployed()); + } + } + + + private void updateChallengeList() + { + if (this.lastSelectedLevel != null) + { + this.challengeList = this.manager.getLevelChallenges(this.lastSelectedLevel.getLevel()); + + if (this.addon.getChallengesSettings().isRemoveCompleteOneTimeChallenges()) + { + this.challengeList.removeIf(challenge -> !challenge.isRepeatable() && + this.manager.isChallengeComplete(this.user, this.world, challenge)); + } + + // Remove all undeployed challenges if VisibilityMode is set to Hidden. + if (this.addon.getChallengesSettings().getVisibilityMode().equals(SettingsUtils.VisibilityMode.HIDDEN)) + { + this.challengeList.removeIf(challenge -> !challenge.isDeployed()); + } + } + else + { + this.challengeList = this.freeChallengeList; + } + } + + + private void updateLevelList() + { + this.levelList = this.manager.getAllChallengeLevelStatus(this.user, this.world); + + for (LevelStatus levelStatus : this.levelList) + { + if (levelStatus.isUnlocked()) + { + this.lastSelectedLevel = levelStatus; + } + else + { + break; + } + } + } + + + @Nullable + private PanelItem createChallengeButton(ItemTemplateRecord template, TemplatedPanel.ItemSlot slot) + { + if (this.challengeList.isEmpty()) + { + // Does not contain any free challenges. + return null; + } + + Challenge levelChallenge; + + // Check if that is a specific free challenge + if (template.dataMap().containsKey("id")) + { + String id = (String) template.dataMap().get("id"); + + // Find a challenge with given Id; + levelChallenge = this.challengeList.stream(). + filter(challenge -> challenge.getUniqueId().equals(id)). + findFirst(). + orElse(null); + + if (levelChallenge == null) + { + // There is no challenge in the list with specific id. + return null; + } + } + else + { + int index = this.challengeIndex * slot.amountMap().getOrDefault("CHALLENGE", 1) + slot.slot(); + + if (index >= this.challengeList.size()) + { + // Out of index. + return null; + } + + levelChallenge = this.challengeList.get(index); + } + + return this.createChallengeButton(template, levelChallenge); + } + + + @NonNull + private PanelItem createChallengeButton(ItemTemplateRecord template, @NonNull Challenge challenge) + { + PanelItemBuilder builder = new PanelItemBuilder(); + + // Template specification are always more important than dynamic content. + builder.icon(template.icon() != null ? template.icon().clone() : challenge.getIcon()); + + // Template specific title is always more important than challenge name. + if (template.title() != null && !template.title().isBlank()) + { + builder.name(this.user.getTranslation(this.world, template.title(), + Constants.CHALLENGE, challenge.getFriendlyName())); + } + else + { + builder.name(Util.translateColorCodes(challenge.getFriendlyName())); + } + + if (template.description() != null && !template.description().isBlank()) + { + // TODO: adding parameters could be useful. + builder.description(this.user.getTranslation(this.world, template.description())); + } + else + { + builder.description(this.generateChallengeDescription(challenge, this.user)); + } + + // Add Click handler + builder.clickHandler((panel, user, clickType, i) -> { + for (ItemTemplateRecord.ActionRecords action : template.actions()) + { + if (clickType == action.clickType()) + { + switch (action.actionType().toUpperCase()) + { + case "COMPLETE": + if (TryToComplete.complete(this.addon, + this.user, + challenge, + this.world, + this.topLabel, + this.permissionPrefix)) + { + panel.getInventory().setItem(i, + this.createChallengeButton(template, challenge).getItem()); + } + break; + case "COMPLETE_MAX": + if (challenge.isRepeatable()) + { + if (TryToComplete.complete(this.addon, + this.user, + challenge, + this.world, + this.topLabel, + this.permissionPrefix, + Integer.MAX_VALUE)) + { + panel.getInventory().setItem(i, + this.createChallengeButton(template, challenge).getItem()); + } + } + break; + case "MULTIPLE_PANEL": + if (challenge.isRepeatable()) + { + MultiplePanel.open(this.addon, this.user, value -> + { + TryToComplete.complete(this.addon, + this.user, + challenge, + this.world, + this.topLabel, + this.permissionPrefix, + value); + + this.build(); + }); + } + break; + } + } + } + + return true; + }); + + // Collect tooltips. + List tooltips = template.actions().stream(). + filter(action -> action.tooltip() != null). + filter(action -> challenge.isRepeatable() || "COMPLETE".equalsIgnoreCase(action.actionType())). + map(action -> this.user.getTranslation(this.world, action.tooltip())). + filter(text -> !text.isBlank()). + collect(Collectors.toCollection(() -> new ArrayList<>(template.actions().size()))); + + // Add tooltips. + if (!tooltips.isEmpty()) + { + // Empty line and tooltips. + builder.description(""); + builder.description(tooltips); + } + + // Glow the icon. + builder.glow(this.addon.getChallengesSettings().isAddCompletedGlow() && + this.manager.isChallengeComplete(this.user, this.world, challenge)); + + // Click Handlers are managed by custom addon buttons. + return builder.build(); + } + + + @Nullable + private PanelItem createLevelButton(ItemTemplateRecord template, TemplatedPanel.ItemSlot slot) + { + if (this.levelList.isEmpty()) + { + // Does not contain any levels. + return null; + } + + LevelStatus level; + + // Check if that is a specific level + if (template.dataMap().containsKey("id")) + { + String id = (String) template.dataMap().get("id"); + + // Find a challenge with given Id; + level = this.levelList.stream(). + filter(levelStatus -> levelStatus.getLevel().getUniqueId().equals(id)). + findFirst(). + orElse(null); + + if (level == null) + { + // There is no challenge in the list with specific id. + return null; + } + } + else + { + int index = this.levelIndex * slot.amountMap().getOrDefault("LEVEL", 1) + slot.slot(); + + if (index >= this.levelList.size()) + { + // Out of index. + return null; + } + + level = this.levelList.get(index); + } + + return this.createLevelButton(template, level); + } + + + @NonNull + private PanelItem createLevelButton(ItemTemplateRecord template, @NonNull LevelStatus level) + { + PanelItemBuilder builder = new PanelItemBuilder(); + + // Template specification are always more important than dynamic content. + if (template.icon() != null) + { + builder.icon(template.icon().clone()); + } + else + { + if (level.isUnlocked()) + { + builder.icon(level.getLevel().getIcon()); + } + else if (level.getLevel().getLockedIcon() != null) + { + // Clone will prevent issues with description storing. + // It can be done only here as it can be null. + builder.icon(level.getLevel().getLockedIcon().clone()); + } + else + { + builder.icon(this.addon.getChallengesSettings().getLockedLevelIcon()); + } + } + + if (template.title() != null && !template.title().isBlank()) + { + builder.name(this.user.getTranslation(this.world, template.title(), + Constants.LEVEL, level.getLevel().getFriendlyName())); + } + else + { + builder.name(Util.translateColorCodes(level.getLevel().getFriendlyName())); + } + + if (template.description() != null && !template.description().isBlank()) + { + // TODO: adding parameters could be useful. + builder.description(this.user.getTranslation(this.world, template.description())); + } + else + { + // TODO: Complete description generate. + builder.description(this.generateLevelDescription(level, this.user)); + } + + // Add click handler + builder.clickHandler((panel, user, clickType, i) -> { + if (level != this.lastSelectedLevel && level.isUnlocked()) + { + this.lastSelectedLevel = level; + this.challengeIndex = 0; + + this.build(); + } + + return true; + }); + + // Collect tooltips. + List tooltips = template.actions().stream(). + filter(action -> action.tooltip() != null). + filter(action -> level != this.lastSelectedLevel && level.isUnlocked()). + map(action -> this.user.getTranslation(this.world, action.tooltip())). + filter(text -> !text.isBlank()). + collect(Collectors.toCollection(() -> new ArrayList<>(template.actions().size()))); + + // Add tooltips. + if (!tooltips.isEmpty()) + { + // Empty line and tooltips. + builder.description(""); + builder.description(tooltips); + } + + // Glow the icon. + builder.glow(level == this.lastSelectedLevel || + level.isUnlocked() && + this.addon.getChallengesSettings().isAddCompletedGlow() && + this.manager.isLevelCompleted(this.user, this.world, level.getLevel())); + + // Click Handlers are managed by custom addon buttons. + return builder.build(); + } + + + @Nullable + private PanelItem createFreeChallengesButton(@NonNull ItemTemplateRecord template, TemplatedPanel.ItemSlot slot) + { + if (this.freeChallengeList.isEmpty()) + { + // There are no free challenges for selection. + return null; + } + + PanelItemBuilder builder = new PanelItemBuilder(); + + if (template.icon() != null) + { + builder.icon(template.icon().clone()); + } + + if (template.title() != null) + { + builder.name(this.user.getTranslation(this.world, template.title())); + } + + if (template.description() != null) + { + builder.description(this.user.getTranslation(this.world, template.description())); + } + + // Add ClickHandler + builder.clickHandler((panel, user, clickType, i) -> + { + if (this.lastSelectedLevel != null) + { + this.lastSelectedLevel = null; + this.build(); + } + + // Always return true. + return true; + }); + + // Collect tooltips. + List tooltips = template.actions().stream(). + filter(action -> action.tooltip() != null). + filter(action -> this.lastSelectedLevel == null). + map(action -> this.user.getTranslation(this.world, action.tooltip())). + filter(text -> !text.isBlank()). + collect(Collectors.toCollection(() -> new ArrayList<>(template.actions().size()))); + + // Add tooltips. + if (!tooltips.isEmpty()) + { + // Empty line and tooltips. + builder.description(""); + builder.description(tooltips); + } + + return builder.build(); + } + + + @Nullable + private PanelItem createNextButton(@NonNull ItemTemplateRecord template, TemplatedPanel.ItemSlot slot) + { + String target = template.dataMap().getOrDefault("target", "").toString().toUpperCase(); + + int nextPageIndex; + + switch (target) + { + case "CHALLENGE" -> { + int size = this.challengeList.size(); + + if (size <= slot.amountMap().getOrDefault("CHALLENGE", 1) || + 1.0 * size / slot.amountMap().getOrDefault("CHALLENGE", 1) <= this.challengeIndex + 1) + { + // There are no next elements + return null; + } + + nextPageIndex = this.challengeIndex + 2; + } + case "LEVEL" -> { + int size = this.levelList.size(); + + if (size <= slot.amountMap().getOrDefault("LEVEL", 1) || + 1.0 * size / slot.amountMap().getOrDefault("LEVEL", 1) <= this.levelIndex + 1) + { + // There are no next elements + return null; + } + + nextPageIndex = this.levelIndex + 2; + } + default -> { + // If not assigned to any type, return null. + return null; + } + } + + PanelItemBuilder builder = new PanelItemBuilder(); + + if (template.icon() != null) + { + ItemStack clone = template.icon().clone(); + + if ((Boolean) template.dataMap().getOrDefault("indexing", false)) + { + clone.setAmount(nextPageIndex); + } + + builder.icon(clone); + } + + if (template.title() != null) + { + builder.name(this.user.getTranslation(this.world, template.title())); + } + + if (template.description() != null) + { + builder.description(this.user.getTranslation(this.world, template.description()), + Constants.NUMBER, String.valueOf(nextPageIndex)); + } + + // Add ClickHandler + builder.clickHandler((panel, user, clickType, i) -> + { + // Next button ignores click type currently. + switch (target) + { + case "CHALLENGE" -> this.challengeIndex++; + case "LEVEL" -> this.levelIndex++; + } + + this.build(); + + // Always return true. + return true; + }); + + // Collect tooltips. + List tooltips = template.actions().stream(). + filter(action -> action.tooltip() != null). + map(action -> this.user.getTranslation(this.world, action.tooltip())). + filter(text -> !text.isBlank()). + collect(Collectors.toCollection(() -> new ArrayList<>(template.actions().size()))); + + // Add tooltips. + if (!tooltips.isEmpty()) + { + // Empty line and tooltips. + builder.description(""); + builder.description(tooltips); + } + + return builder.build(); + } + + + @Nullable + private PanelItem createPreviousButton(@NonNull ItemTemplateRecord template, TemplatedPanel.ItemSlot slot) + { + String target = template.dataMap().getOrDefault("target", "").toString().toUpperCase(); + + int previousPageIndex; + + if ("CHALLENGE".equals(target)) + { + if (this.challengeIndex == 0) + { + // There are no next elements + return null; + } + + previousPageIndex = this.challengeIndex; + } + else if ("LEVEL".equals(target)) + { + if (this.levelIndex == 0) + { + // There are no next elements + return null; + } + + previousPageIndex = this.levelIndex; + } + else + { + // If not assigned to any type, return null. + return null; + } + + PanelItemBuilder builder = new PanelItemBuilder(); + + if (template.icon() != null) + { + ItemStack clone = template.icon().clone(); + + if ((Boolean) template.dataMap().getOrDefault("indexing", false)) + { + clone.setAmount(previousPageIndex); + } + + builder.icon(clone); + } + + if (template.title() != null) + { + builder.name(this.user.getTranslation(this.world, template.title())); + } + + if (template.description() != null) + { + builder.description(this.user.getTranslation(this.world, template.description()), + Constants.NUMBER, String.valueOf(previousPageIndex)); + } + + // Add ClickHandler + builder.clickHandler((panel, user, clickType, i) -> + { + // Next button ignores click type currently. + switch (target) + { + case "CHALLENGE" -> this.challengeIndex--; + case "LEVEL" -> this.levelIndex--; + } + + this.build(); + + // Always return true. + return true; + }); + + // Collect tooltips. + List tooltips = template.actions().stream(). + filter(action -> action.tooltip() != null). + map(action -> this.user.getTranslation(this.world, action.tooltip())). + filter(text -> !text.isBlank()). + collect(Collectors.toCollection(() -> new ArrayList<>(template.actions().size()))); + + // Add tooltips. + if (!tooltips.isEmpty()) + { + // Empty line and tooltips. + builder.description(""); + builder.description(tooltips); + } + + return builder.build(); + } + + +// --------------------------------------------------------------------- +// Section: Variables +// --------------------------------------------------------------------- + + /** + * This boolean indicates if in the world there exist challenges for displaying in GUI. + */ + private final boolean containsChallenges; + + /** + * This list contains free challenges in current Panel. + */ + private List freeChallengeList; + + /** + * This will be used if levels are more than 18. + */ + private int levelIndex; + + /** + * This list contains all information about level completion in current world. + */ + private List levelList; + + /** + * This will be used if free challenges are more than 18. + */ + private int challengeIndex; + + /** + * This list contains challenges in current Panel. + */ + private List challengeList; + + /** + * This indicates last selected level. + */ + private LevelStatus lastSelectedLevel; +} diff --git a/src/main/java/world/bentobox/challenges/panel/user/GameModePanel.java b/src/main/java/world/bentobox/challenges/panel/user/GameModePanel.java new file mode 100644 index 0000000..cb30698 --- /dev/null +++ b/src/main/java/world/bentobox/challenges/panel/user/GameModePanel.java @@ -0,0 +1,388 @@ +// +// Created by BONNe +// Copyright - 2021 +// + + +package world.bentobox.challenges.panel.user; + + +import org.bukkit.World; +import org.bukkit.inventory.ItemStack; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import world.bentobox.bentobox.api.addons.GameModeAddon; +import world.bentobox.bentobox.api.commands.CompositeCommand; +import world.bentobox.bentobox.api.panels.PanelItem; +import world.bentobox.bentobox.api.panels.TemplatedPanel; +import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; +import world.bentobox.bentobox.api.panels.builders.TemplatedPanelBuilder; +import world.bentobox.bentobox.api.panels.reader.ItemTemplateRecord; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.util.Util; +import world.bentobox.challenges.ChallengesAddon; +import world.bentobox.challenges.panel.CommonPanel; +import world.bentobox.challenges.utils.Constants; + + +/** + * Main challenges panel builder. + */ +public class GameModePanel extends CommonPanel +{ + private GameModePanel(ChallengesAddon addon, + World world, + User user, + List addonList, + boolean adminMode) + { + super(addon, user, world, null, null); + this.addonList = addonList; + this.adminMode = adminMode; + } + + + /** + * Open the Challenges GUI. + * + * @param addon the addon + * @param world the world + * @param user the user + * @param addonList List of gamemode addons + * @param adminMode Indicate if admin mode. + */ + public static void open(ChallengesAddon addon, + World world, + User user, + List addonList, + boolean adminMode) + { + new GameModePanel(addon, world, user, addonList, adminMode).build(); + } + + + protected void build() + { + // Start building panel. + TemplatedPanelBuilder panelBuilder = new TemplatedPanelBuilder(); + + // Set main template. + panelBuilder.template("gamemode_panel", new File(this.addon.getDataFolder(), "panels")); + panelBuilder.user(this.user); + panelBuilder.world(this.user.getWorld()); + + // Register button builders + panelBuilder.registerTypeBuilder("GAMEMODE", this::createGameModeButton); + + panelBuilder.registerTypeBuilder("NEXT", this::createNextButton); + panelBuilder.registerTypeBuilder("PREVIOUS", this::createPreviousButton); + + // Register unknown type builder. + panelBuilder.build(); + } + + + @Nullable + private PanelItem createGameModeButton(ItemTemplateRecord template, TemplatedPanel.ItemSlot slot) + { + if (this.addonList.isEmpty()) + { + // Does not contain any free challenges. + return null; + } + + GameModeAddon gameModeAddon; + + // Check if that is a specific free challenge + if (template.dataMap().containsKey("id")) + { + String id = (String) template.dataMap().get("id"); + + // Find a challenge with given Id; + gameModeAddon = this.addonList.stream(). + filter(gamemode -> gamemode.getDescription().getName().equals(id)). + findFirst(). + orElse(null); + + if (gameModeAddon == null) + { + // There is no gamemode in the list with specific id. + return null; + } + } + else + { + int index = this.addonIndex * slot.amountMap().getOrDefault("GAMEMODE", 1) + slot.slot(); + + if (index >= this.addonList.size()) + { + // Out of index. + return null; + } + + gameModeAddon = this.addonList.get(index); + } + + return this.createGameModeButton(template, gameModeAddon); + } + + + @NonNull + private PanelItem createGameModeButton(ItemTemplateRecord template, @NonNull GameModeAddon gameModeAddon) + { + PanelItemBuilder builder = new PanelItemBuilder(); + + // Template specification are always more important than dynamic content. + builder.icon(template.icon() != null ? + template.icon().clone() : + new ItemStack(gameModeAddon.getDescription().getIcon())); + + // Template specific title is always more important than challenge name. + if (template.title() != null && !template.title().isBlank()) + { + builder.name(this.user.getTranslation(this.world, template.title(), + Constants.GAMEMODE, gameModeAddon.getDescription().getName())); + } + else + { + builder.name(Util.translateColorCodes(gameModeAddon.getDescription().getName())); + } + + if (template.description() != null && !template.description().isBlank()) + { + // TODO: adding parameters could be useful. + builder.description(this.user.getTranslation(this.world, template.description())); + } + else + { + builder.description(gameModeAddon.getDescription().getDescription()); + } + + // Add Click handler + builder.clickHandler((panel, user, clickType, i) -> { + for (ItemTemplateRecord.ActionRecords action : template.actions()) + { + if (clickType == action.clickType()) + { + Optional command; + + if (this.adminMode) + { + command = gameModeAddon.getAdminCommand(); + } + else + { + command = gameModeAddon.getPlayerCommand(); + } + + command.ifPresent(compositeCommand -> + user.performCommand(compositeCommand.getTopLabel() + " challenges")); + } + } + + return true; + }); + + // Collect tooltips. + List tooltips = template.actions().stream(). + filter(action -> action.tooltip() != null). + map(action -> this.user.getTranslation(this.world, action.tooltip())). + filter(text -> !text.isBlank()). + collect(Collectors.toCollection(() -> new ArrayList<>(template.actions().size()))); + + // Add tooltips. + if (!tooltips.isEmpty()) + { + // Empty line and tooltips. + builder.description(""); + builder.description(tooltips); + } + + // Glow the icon. + builder.glow(gameModeAddon.inWorld(this.user.getWorld())); + + // Click Handlers are managed by custom addon buttons. + return builder.build(); + } + + + @Nullable + private PanelItem createNextButton(@NonNull ItemTemplateRecord template, TemplatedPanel.ItemSlot slot) + { + String target = template.dataMap().getOrDefault("target", "").toString().toUpperCase(); + + int nextPageIndex; + + if ("GAMEMODE".equals(target)) + { + int size = this.addonList.size(); + + if (size <= slot.amountMap().getOrDefault("GAMEMODE", 1) || + 1.0 * size / slot.amountMap().getOrDefault("GAMEMODE", 1) <= this.addonIndex + 1) + { + // There are no next elements + return null; + } + + nextPageIndex = this.addonIndex + 2; + } + else + {// If not assigned to any type, return null. + return null; + } + + PanelItemBuilder builder = new PanelItemBuilder(); + + if (template.icon() != null) + { + ItemStack clone = template.icon().clone(); + + if ((Boolean) template.dataMap().getOrDefault("indexing", false)) + { + clone.setAmount(nextPageIndex); + } + + builder.icon(clone); + } + + if (template.title() != null) + { + builder.name(this.user.getTranslation(this.world, template.title())); + } + + if (template.description() != null) + { + builder.description(this.user.getTranslation(this.world, template.description()), + Constants.NUMBER, String.valueOf(nextPageIndex)); + } + + // Add ClickHandler + builder.clickHandler((panel, user, clickType, i) -> + { + this.addonIndex++; + this.build(); + // Always return true. + return true; + }); + + // Collect tooltips. + List tooltips = template.actions().stream(). + filter(action -> action.tooltip() != null). + map(action -> this.user.getTranslation(this.world, action.tooltip())). + filter(text -> !text.isBlank()). + collect(Collectors.toCollection(() -> new ArrayList<>(template.actions().size()))); + + // Add tooltips. + if (!tooltips.isEmpty()) + { + // Empty line and tooltips. + builder.description(""); + builder.description(tooltips); + } + + return builder.build(); + } + + + @Nullable + private PanelItem createPreviousButton(@NonNull ItemTemplateRecord template, TemplatedPanel.ItemSlot slot) + { + String target = template.dataMap().getOrDefault("target", "").toString().toUpperCase(); + + int previousPageIndex; + + if ("GAMEMODE".equals(target)) + { + if (this.addonIndex == 0) + { + // There are no next elements + return null; + } + + previousPageIndex = this.addonIndex; + } + else + { + // If not assigned to any type, return null. + return null; + } + + PanelItemBuilder builder = new PanelItemBuilder(); + + if (template.icon() != null) + { + ItemStack clone = template.icon().clone(); + + if ((Boolean) template.dataMap().getOrDefault("indexing", false)) + { + clone.setAmount(previousPageIndex); + } + + builder.icon(clone); + } + + if (template.title() != null) + { + builder.name(this.user.getTranslation(this.world, template.title())); + } + + if (template.description() != null) + { + builder.description(this.user.getTranslation(this.world, template.description()), + Constants.NUMBER, String.valueOf(previousPageIndex)); + } + + // Add ClickHandler + builder.clickHandler((panel, user, clickType, i) -> + { + // Next button ignores click type currently. + this.addonIndex--; + this.build(); + // Always return true. + return true; + }); + + // Collect tooltips. + List tooltips = template.actions().stream(). + filter(action -> action.tooltip() != null). + map(action -> this.user.getTranslation(this.world, action.tooltip())). + filter(text -> !text.isBlank()). + collect(Collectors.toCollection(() -> new ArrayList<>(template.actions().size()))); + + // Add tooltips. + if (!tooltips.isEmpty()) + { + // Empty line and tooltips. + builder.description(""); + builder.description(tooltips); + } + + return builder.build(); + } + + +// --------------------------------------------------------------------- +// Section: Variables +// --------------------------------------------------------------------- + + /** + * This will be used if free challenges are more than 18. + */ + private int addonIndex; + + /** + * This list contains challenges in current Panel. + */ + private final List addonList; + + /** + * Indicate if gui is for players or admins. + */ + private final boolean adminMode; +} diff --git a/src/main/java/world/bentobox/challenges/panel/user/MultiplePanel.java b/src/main/java/world/bentobox/challenges/panel/user/MultiplePanel.java new file mode 100644 index 0000000..657c7f5 --- /dev/null +++ b/src/main/java/world/bentobox/challenges/panel/user/MultiplePanel.java @@ -0,0 +1,282 @@ +// +// Created by BONNe +// Copyright - 2021 +// + + +package world.bentobox.challenges.panel.user; + + +import org.bukkit.inventory.ItemStack; +import org.eclipse.jdt.annotation.NonNull; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import world.bentobox.bentobox.api.panels.PanelItem; +import world.bentobox.bentobox.api.panels.TemplatedPanel; +import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; +import world.bentobox.bentobox.api.panels.builders.TemplatedPanelBuilder; +import world.bentobox.bentobox.api.panels.reader.ItemTemplateRecord; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.challenges.ChallengesAddon; +import world.bentobox.challenges.panel.ConversationUtils; +import world.bentobox.challenges.utils.Constants; + + +public class MultiplePanel +{ + private MultiplePanel(ChallengesAddon addon, User user, Consumer action) + { + this.addon = addon; + this.user = user; + this.action = action; + } + + + public static void open(ChallengesAddon addon, User user, Consumer action) + { + new MultiplePanel(addon, user, action).build(); + } + + +// --------------------------------------------------------------------- +// Section: Methods +// --------------------------------------------------------------------- + + + private void build() + { + // Start building panel. + TemplatedPanelBuilder panelBuilder = new TemplatedPanelBuilder(); + + // Set main template. + panelBuilder.template("multiple_panel", new File(this.addon.getDataFolder(), "panels")); + panelBuilder.user(this.user); + panelBuilder.world(this.user.getWorld()); + + // Register button builders + panelBuilder.registerTypeBuilder("INCREASE", this::createIncreaseButton); + panelBuilder.registerTypeBuilder("REDUCE", this::createReduceButton); + panelBuilder.registerTypeBuilder("ACCEPT", this::createValueButton); + + // Register unknown type builder. + panelBuilder.build(); + } + + + @NonNull + private PanelItem createIncreaseButton(ItemTemplateRecord template, TemplatedPanel.ItemSlot itemSlot) + { + int increaseValue = (int) template.dataMap().getOrDefault("value", 1); + + PanelItemBuilder builder = new PanelItemBuilder(); + + if (template.icon() != null) + { + ItemStack clone = template.icon().clone(); + clone.setAmount(increaseValue); + builder.icon(clone); + } + + if (template.title() != null) + { + builder.name(this.user.getTranslation(template.title())); + } + + if (template.description() != null) + { + builder.description(this.user.getTranslation(template.description(), + Constants.NUMBER, String.valueOf(increaseValue))); + } + + // Add ClickHandler + builder.clickHandler((panel, user, clickType, i) -> + { + this.completionValue += increaseValue; + this.build(); + + // Always return true. + return true; + }); + + // Collect tooltips. + List tooltips = template.actions().stream(). + filter(action -> action.tooltip() != null). + map(action -> this.user.getTranslation(action.tooltip())). + filter(text -> !text.isBlank()). + collect(Collectors.toCollection(() -> new ArrayList<>(template.actions().size()))); + + // Add tooltips. + if (!tooltips.isEmpty()) + { + // Empty line and tooltips. + builder.description(""); + builder.description(tooltips); + } + + return builder.build(); + } + + + @NonNull + private PanelItem createReduceButton(ItemTemplateRecord template, TemplatedPanel.ItemSlot itemSlot) + { + int decreaseValue = (int) template.dataMap().getOrDefault("value", 1); + + PanelItemBuilder builder = new PanelItemBuilder(); + + if (template.icon() != null) + { + ItemStack clone = template.icon().clone(); + clone.setAmount(decreaseValue); + builder.icon(clone); + } + + if (template.title() != null) + { + builder.name(this.user.getTranslation(template.title())); + } + + if (template.description() != null) + { + builder.description(this.user.getTranslation(template.description(), + Constants.NUMBER, String.valueOf(decreaseValue))); + } + + // Add ClickHandler + builder.clickHandler((panel, user, clickType, i) -> + { + this.completionValue = Math.max(this.completionValue - decreaseValue, 1); + this.build(); + + // Always return true. + return true; + }); + + // Collect tooltips. + List tooltips = template.actions().stream(). + filter(action -> action.tooltip() != null). + map(action -> this.user.getTranslation(action.tooltip())). + filter(text -> !text.isBlank()). + collect(Collectors.toCollection(() -> new ArrayList<>(template.actions().size()))); + + // Add tooltips. + if (!tooltips.isEmpty()) + { + // Empty line and tooltips. + builder.description(""); + builder.description(tooltips); + } + + return builder.build(); + } + + + @NonNull + private PanelItem createValueButton(ItemTemplateRecord template, TemplatedPanel.ItemSlot itemSlot) + { + PanelItemBuilder builder = new PanelItemBuilder(); + + if (template.icon() != null) + { + ItemStack clone = template.icon().clone(); + clone.setAmount(this.completionValue); + builder.icon(clone); + } + + if (template.title() != null) + { + builder.name(this.user.getTranslation(template.title())); + } + + if (template.description() != null) + { + builder.description(this.user.getTranslation(template.description(), + Constants.NUMBER, String.valueOf(completionValue))); + } + + // Add ClickHandler + builder.clickHandler((panel, user, clickType, i) -> + { + for (ItemTemplateRecord.ActionRecords actionRecords : template.actions()) + { + if (clickType == actionRecords.clickType()) + { + if (actionRecords.actionType().equalsIgnoreCase("input")) + { + // Input consumer. + Consumer numberConsumer = number -> + { + if (number != null) + { + this.completionValue = number.intValue(); + } + + // reopen panel + this.build(); + }; + + ConversationUtils.createNumericInput(numberConsumer, + this.user, + this.user.getTranslation(Constants.CONVERSATIONS + "input-number"), + 1, + 2000); + } + else if (actionRecords.actionType().equalsIgnoreCase("accept")) + { + this.action.accept(this.completionValue); + } + } + } + + // Always return true. + return true; + }); + + // Collect tooltips. + List tooltips = template.actions().stream(). + filter(action -> action.tooltip() != null). + map(action -> this.user.getTranslation(action.tooltip())). + filter(text -> !text.isBlank()). + collect(Collectors.toCollection(() -> new ArrayList<>(template.actions().size()))); + + // Add tooltips. + if (!tooltips.isEmpty()) + { + // Empty line and tooltips. + builder.description(""); + builder.description(tooltips); + } + + return builder.build(); + } + + +// --------------------------------------------------------------------- +// Section: Variables +// --------------------------------------------------------------------- + + /** + * Variable stores user who created this panel. + */ + private final User user; + + /** + * This variable holds action that will be performed on accept. + */ + private final Consumer action; + + /** + * Variable stores Challenges addon. + */ + protected final ChallengesAddon addon; + + /** + * Local storing of selected value. + */ + private int completionValue = 1; +} diff --git a/src/main/resources/panels/gamemode_panel.yml b/src/main/resources/panels/gamemode_panel.yml new file mode 100644 index 0000000..0003d7d --- /dev/null +++ b/src/main/resources/panels/gamemode_panel.yml @@ -0,0 +1,49 @@ +gamemode_panel: + title: challenges.gui.titles.gamemode-gui + type: INVENTORY + background: + icon: BLACK_STAINED_GLASS_PANE + title: "&b&r" # Empty text + border: + icon: BLACK_STAINED_GLASS_PANE + title: "&b&r" # Empty text + force-shown: [] + content: + 2: + 1: + icon: TIPPED_ARROW:INSTANT_HEAL::::1 + title: challenges.gui.buttons.previous.name + description: challenges.gui.buttons.previous.description + data: + type: PREVIOUS + target: GAMEMODE + indexing: true + action: + left: + tooltip: challenges.gui.tips.click-to-select + 2: gamemode + 3: gamemode + 4: gamemode + 5: gamemode + 6: gamemode + 7: gamemode + 8: gamemode + 9: + icon: TIPPED_ARROW:JUMP::::1 + title: challenges.gui.buttons.next.name + description: challenges.gui.buttons.next.description + data: + type: NEXT + target: GAMEMODE + indexing: true + action: + left: + tooltip: challenges.gui.tips.click-to-select + reusable: + gamemode: + data: + type: GAMEMODE + actions: + left: + type: SELECT + tooltip: challenges.gui.tips.click-to-select \ No newline at end of file diff --git a/src/main/resources/panels/main_panel.yml b/src/main/resources/panels/main_panel.yml new file mode 100644 index 0000000..cd8538e --- /dev/null +++ b/src/main/resources/panels/main_panel.yml @@ -0,0 +1,110 @@ +main_panel: + title: challenges.gui.titles.player-gui + type: INVENTORY + background: + icon: BLACK_STAINED_GLASS_PANE + title: "&b&r" # Empty text + border: + icon: BLACK_STAINED_GLASS_PANE + title: "&b&r" # Empty text + force-shown: [4] + content: + 2: + 2: challenge_button + 3: challenge_button + 4: challenge_button + 5: challenge_button + 6: challenge_button + 7: challenge_button + 8: challenge_button + 3: + 1: + icon: TIPPED_ARROW:INSTANT_HEAL::::1 + title: challenges.gui.buttons.previous.name + description: challenges.gui.buttons.previous.description + data: + type: PREVIOUS + target: CHALLENGE + indexing: true + action: + left: + tooltip: challenges.gui.tips.click-to-select + 2: challenge_button + 3: challenge_button + 4: challenge_button + 5: challenge_button + 6: challenge_button + 7: challenge_button + 8: challenge_button + 9: + icon: TIPPED_ARROW:JUMP::::1 + title: challenges.gui.buttons.next.name + description: challenges.gui.buttons.next.description + data: + type: NEXT + target: CHALLENGE + indexing: true + action: + left: + tooltip: challenges.gui.tips.click-to-select + 5: + 1: + icon: TIPPED_ARROW:INSTANT_HEAL::::1 + title: challenges.gui.buttons.previous.name + description: challenges.gui.buttons.previous.description + data: + type: PREVIOUS + target: LEVEL + indexing: true + action: + left: + tooltip: challenges.gui.tips.click-to-select + 2: level_button + 3: level_button + 4: level_button + 5: level_button + 6: level_button + 7: level_button + 8: level_button + 9: + icon: TIPPED_ARROW:JUMP::::1 + title: challenges.gui.buttons.next.name + description: challenges.gui.buttons.next.description + data: + type: NEXT + target: LEVEL + indexing: true + action: + left: + tooltip: challenges.gui.tips.click-to-select + 6: + 5: + icon: IRON_BARS + title: challenges.gui.buttons.free-challenges.name + description: challenges.gui.buttons.free-challenges.description + data: + type: UNASSIGNED_CHALLENGES + action: + left: + tooltip: challenges.gui.tips.click-to-select + reusable: + challenge_button: + data: + type: CHALLENGE + actions: + left: + type: COMPLETE + tooltip: challenges.gui.tips.click-to-complete + right: + type: MULTIPLE_PANEL + tooltip: challenges.gui.tips.right-click-multiple-open + shift_left: + type: COMPLETE_MAX + tooltip: challenges.gui.tips.shift-left-click-to-complete-all + level_button: + data: + type: LEVEL + actions: + left: + type: SELECT + tooltip: challenges.gui.tips.click-to-select \ No newline at end of file diff --git a/src/main/resources/panels/multiple_panel.yml b/src/main/resources/panels/multiple_panel.yml new file mode 100644 index 0000000..ac68144 --- /dev/null +++ b/src/main/resources/panels/multiple_panel.yml @@ -0,0 +1,58 @@ +multiple_panel: + title: challenges.gui.titles.multiple-gui + type: HOPPER + content: + 1: + 1: + icon: RED_STAINED_GLASS_PANE + title: challenges.gui.buttons.reduce.name + description: challenges.gui.buttons.reduce.description + data: + type: REDUCE + value: 5 + actions: + left: + tooltip: challenges.gui.tips.click-to-reduce + 2: + icon: ORANGE_STAINED_GLASS_PANE + title: challenges.gui.buttons.reduce.name + description: challenges.gui.buttons.reduce.description + data: + type: REDUCE + value: 1 + actions: + left: + tooltip: challenges.gui.tips.click-to-reduce + 3: + icon: GREEN_STAINED_GLASS_PANE + title: challenges.gui.buttons.accept.name + description: challenges.gui.buttons.accept.description + data: + type: ACCEPT + actions: + left: + type: ACCEPT + tooltip: challenges.gui.tips.left-click-to-accept + right: + type: INPUT + tooltip: challenges.gui.tips.right-click-to-write + 4: + icon: BLUE_STAINED_GLASS_PANE + title: challenges.gui.buttons.increase.name + description: challenges.gui.buttons.increase.description + data: + type: INCREASE + value: 1 + actions: + left: + tooltip: challenges.gui.tips.click-to-increase + 5: + icon: MAGENTA_STAINED_GLASS_PANE + title: challenges.gui.buttons.increase.name + description: challenges.gui.buttons.increase.description + data: + type: INCREASE + value: 5 + actions: + left: + tooltip: challenges.gui.tips.click-to-increase \ No newline at end of file