diff --git a/pom.xml b/pom.xml index 3da436b1..1de3c829 100644 --- a/pom.xml +++ b/pom.xml @@ -44,14 +44,14 @@ 1.21.3-R0.1-SNAPSHOT 1.2.3-SNAPSHOT - 2.7.1-SNAPSHOT + 3.2.4-SNAPSHOT 2.6.3 1.7 1.2.0 ${build.version}-SNAPSHOT - 1.4.0 + 1.5.0 -LOCAL BentoBoxWorld_Challenges @@ -117,6 +117,11 @@ minecraft-repo https://libraries.minecraft.net/ + + + placeholderapi + https://repo.extendedclip.com/releases/ + @@ -198,6 +203,13 @@ commons-math3 3.6.1 + + + me.clip + placeholderapi + 2.11.6 + provided + diff --git a/src/main/java/world/bentobox/challenges/ChallengesAddon.java b/src/main/java/world/bentobox/challenges/ChallengesAddon.java index ee9340b8..847bbd98 100644 --- a/src/main/java/world/bentobox/challenges/ChallengesAddon.java +++ b/src/main/java/world/bentobox/challenges/ChallengesAddon.java @@ -15,8 +15,8 @@ import world.bentobox.bentobox.database.DatabaseSetup.DatabaseType; import world.bentobox.bentobox.hooks.VaultHook; import world.bentobox.bentobox.managers.RanksManager; -import world.bentobox.challenges.commands.ChallengesPlayerCommand; import world.bentobox.challenges.commands.ChallengesGlobalPlayerCommand; +import world.bentobox.challenges.commands.ChallengesPlayerCommand; import world.bentobox.challenges.commands.admin.ChallengesAdminCommand; import world.bentobox.challenges.commands.admin.ChallengesGlobalAdminCommand; import world.bentobox.challenges.config.Settings; diff --git a/src/main/java/world/bentobox/challenges/database/object/Challenge.java b/src/main/java/world/bentobox/challenges/database/object/Challenge.java index c595a0a7..e20d9b35 100644 --- a/src/main/java/world/bentobox/challenges/database/object/Challenge.java +++ b/src/main/java/world/bentobox/challenges/database/object/Challenge.java @@ -1,7 +1,11 @@ package world.bentobox.challenges.database.object; -import java.util.*; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; import org.bukkit.Material; @@ -159,6 +163,12 @@ public enum ChallengeType @Expose private List rewardItems = new ArrayList<>(); + /** + * Hide the list of reward items in the GUI + */ + @Expose + private Boolean hideRewardItems; + /** * Experience point reward */ @@ -853,37 +863,49 @@ public Challenge clone() try { clone = new Challenge(); + // Copy primitive and String fields clone.setUniqueId(this.uniqueId); clone.setFriendlyName(this.friendlyName); clone.setDeployed(this.deployed); + clone.setOrder(this.order); + clone.setLevel(this.level); + + // Clone collections (Strings are immutable so shallow copy is fine) clone.setDescription(new ArrayList<>(this.description)); + clone.setEnvironment(new HashSet<>(this.environment)); + clone.setRewardCommands(new ArrayList<>(this.rewardCommands)); + clone.setIgnoreRewardMetaData(new HashSet<>(this.ignoreRewardMetaData)); + + // Clone mutable objects (ItemStacks, etc.) clone.setIcon(this.icon.clone()); - clone.setOrder(this.order); + clone.setRewardItems(this.rewardItems.stream().map(ItemStack::clone) + .collect(Collectors.toCollection(() -> new ArrayList<>(this.rewardItems.size())))); + clone.setRepeatItemReward(this.repeatItemReward.stream().map(ItemStack::clone) + .collect(Collectors.toCollection(() -> new ArrayList<>(this.repeatItemReward.size())))); + clone.setRepeatRewardCommands(new ArrayList<>(this.repeatRewardCommands)); + + // Clone enum field clone.setChallengeType(ChallengeType.valueOf(this.challengeType.name())); - clone.setEnvironment(new HashSet<>(this.environment)); - clone.setLevel(this.level); + + // Clone boolean and numeric fields clone.setRemoveWhenCompleted(this.removeWhenCompleted); - clone.setRequirements(this.requirements.copy()); - clone.setRewardText(this.rewardText); - clone.setRewardItems( - this.rewardItems.stream(). - map(ItemStack::clone). - collect(Collectors.toCollection(() -> new ArrayList<>(this.rewardItems.size())))); clone.setRewardExperience(this.rewardExperience); clone.setRewardMoney(this.rewardMoney); - clone.setRewardCommands(new ArrayList<>(this.rewardCommands)); clone.setRepeatable(this.repeatable); + clone.setTimeout(this.timeout); clone.setRepeatRewardText(this.repeatRewardText); clone.setMaxTimes(this.maxTimes); clone.setRepeatExperienceReward(this.repeatExperienceReward); - clone.setRepeatItemReward( - this.repeatItemReward.stream(). - map(ItemStack::clone). - collect(Collectors.toCollection(() -> new ArrayList<>(this.repeatItemReward.size())))); clone.setRepeatMoneyReward(this.repeatMoneyReward); - clone.setRepeatRewardCommands(new ArrayList<>(this.repeatRewardCommands)); - clone.setTimeout(this.timeout); - clone.setIgnoreRewardMetaData(new HashSet<>(this.ignoreRewardMetaData)); + + // Copy custom objects + clone.setRequirements(this.requirements.copy()); + + // Clone the remaining String field + clone.setRewardText(this.rewardText); + + // Clone the Boolean field + clone.setHideRewardItems(this.hideRewardItems); } catch (Exception e) { @@ -895,4 +917,18 @@ public Challenge clone() return clone; } + + /** + * @return the hideRewardItems + */ + public boolean isHideRewardItems() { + return Objects.requireNonNullElse(hideRewardItems, false); + } + + /** + * @param hideRewardItems the hideRewardItems to set + */ + public void setHideRewardItems(Boolean hideRewardItems) { + this.hideRewardItems = hideRewardItems; + } } \ No newline at end of file diff --git a/src/main/java/world/bentobox/challenges/database/object/ChallengeLevel.java b/src/main/java/world/bentobox/challenges/database/object/ChallengeLevel.java index 4a7ae9fc..f9d2982f 100644 --- a/src/main/java/world/bentobox/challenges/database/object/ChallengeLevel.java +++ b/src/main/java/world/bentobox/challenges/database/object/ChallengeLevel.java @@ -1,7 +1,11 @@ package world.bentobox.challenges.database.object; -import java.util.*; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; import org.bukkit.Material; @@ -70,7 +74,8 @@ public ChallengeLevel() @ConfigComment("") @ConfigComment("The number of undone challenges that can be left on this level before") - @ConfigComment("unlocking next level.") + @ConfigComment("unlocking next level. Give players more with the permission") + @ConfigComment("[gamemode].challenges.waiver-add.x where x is a number") @Expose private int waiverAmount = 1; diff --git a/src/main/java/world/bentobox/challenges/database/object/adapters/AdvancementsAdapter.java b/src/main/java/world/bentobox/challenges/database/object/adapters/AdvancementsAdapter.java new file mode 100644 index 00000000..91a12001 --- /dev/null +++ b/src/main/java/world/bentobox/challenges/database/object/adapters/AdvancementsAdapter.java @@ -0,0 +1,46 @@ +package world.bentobox.challenges.database.object.adapters; + + +import java.lang.reflect.Type; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.StreamSupport; + +import org.bukkit.Bukkit; +import org.bukkit.advancement.Advancement; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + + +public class AdvancementsAdapter implements JsonSerializer, JsonDeserializer +{ + @Override + public JsonElement serialize(Advancement src, Type typeOfSrc, JsonSerializationContext context) + { + JsonObject result = new JsonObject(); + result.add("name", new JsonPrimitive(src.getKey().getKey())); + return result; + } + + @Override + public Advancement deserialize(JsonElement json, + Type typeOfT, + JsonDeserializationContext context) + throws JsonParseException + { + JsonObject jsonObject = json.getAsJsonObject(); + String name = jsonObject.get("name").getAsString(); + + return StreamSupport + .stream(Spliterators.spliteratorUnknownSize(Bukkit.advancementIterator(), Spliterator.ORDERED), false) + .filter(a -> a.getKey().getKey().equals(name)).findFirst().orElse(null); + } + +} \ No newline at end of file diff --git a/src/main/java/world/bentobox/challenges/database/object/adapters/AdvancementsListAdapter.java b/src/main/java/world/bentobox/challenges/database/object/adapters/AdvancementsListAdapter.java new file mode 100644 index 00000000..4dfe2952 --- /dev/null +++ b/src/main/java/world/bentobox/challenges/database/object/adapters/AdvancementsListAdapter.java @@ -0,0 +1,39 @@ +package world.bentobox.challenges.database.object.adapters; + +import com.google.gson.*; +import org.bukkit.advancement.Advancement; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +public class AdvancementsListAdapter implements JsonSerializer>, JsonDeserializer> { + + // Reuse your existing adapter for individual advancements + private final AdvancementsAdapter advancementAdapter = new AdvancementsAdapter(); + + @Override + public JsonElement serialize(List src, Type typeOfSrc, JsonSerializationContext context) { + JsonArray array = new JsonArray(); + for (Advancement advancement : src) { + // Serialize each advancement using existing adapter + JsonElement element = advancementAdapter.serialize(advancement, advancement.getClass(), context); + array.add(element); + } + return array; + } + + @Override + public List deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + List advancements = new ArrayList<>(); + JsonArray array = json.getAsJsonArray(); + for (JsonElement element : array) { + Advancement advancement = advancementAdapter.deserialize(element, Advancement.class, context); + if (advancement != null) { + advancements.add(advancement); + } + } + return advancements; + } +} \ No newline at end of file diff --git a/src/main/java/world/bentobox/challenges/database/object/adapters/EntityCompatibilityAdapter.java b/src/main/java/world/bentobox/challenges/database/object/adapters/EntityCompatibilityAdapter.java index 8abcf5f6..25f9d02a 100644 --- a/src/main/java/world/bentobox/challenges/database/object/adapters/EntityCompatibilityAdapter.java +++ b/src/main/java/world/bentobox/challenges/database/object/adapters/EntityCompatibilityAdapter.java @@ -7,12 +7,20 @@ package world.bentobox.challenges.database.object.adapters; -import com.google.gson.*; -import org.bukkit.entity.EntityType; import java.lang.reflect.Type; import java.util.EnumMap; import java.util.Map; +import org.bukkit.entity.EntityType; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + import world.bentobox.bentobox.BentoBox; diff --git a/src/main/java/world/bentobox/challenges/database/object/adapters/TypeMigrationAdapter.java b/src/main/java/world/bentobox/challenges/database/object/adapters/TypeMigrationAdapter.java index 2c8c434c..a720ea00 100644 --- a/src/main/java/world/bentobox/challenges/database/object/adapters/TypeMigrationAdapter.java +++ b/src/main/java/world/bentobox/challenges/database/object/adapters/TypeMigrationAdapter.java @@ -7,9 +7,16 @@ package world.bentobox.challenges.database.object.adapters; -import com.google.gson.*; import java.lang.reflect.Type; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + import world.bentobox.challenges.database.object.Challenge; diff --git a/src/main/java/world/bentobox/challenges/database/object/requirements/CheckPapi.java b/src/main/java/world/bentobox/challenges/database/object/requirements/CheckPapi.java new file mode 100644 index 00000000..20f5b151 --- /dev/null +++ b/src/main/java/world/bentobox/challenges/database/object/requirements/CheckPapi.java @@ -0,0 +1,287 @@ +package world.bentobox.challenges.database.object.requirements; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.bukkit.entity.Player; + +import me.clip.placeholderapi.PlaceholderAPI; +import world.bentobox.bentobox.BentoBox; + +public class CheckPapi { + + /** + * Evaluates the given formula by first replacing PAPI placeholders using the provided Player, + * then parsing and evaluating one or more conditions. + *

+ * The formula may contain conditions comparing numeric or string values. + * Operands may contain spaces. The grammar for a condition is: + *

+     *     leftOperand operator rightOperand
+     * 
+ * where the leftOperand is a sequence of tokens (separated by whitespace) until a valid + * operator is found, and the rightOperand is a sequence of tokens until a boolean operator + * ("AND" or "OR") is encountered or the end of the formula is reached. + *

+ * Supported comparison operators (case sensitive) are: + *

    + *
  • "=" or "==" for equality
  • + *
  • "<>" or "!=" for inequality
  • + *
  • "<=" and ">=" for less than or equal and greater than or equal
  • + *
  • "<" and ">" for less than and greater than
  • + *
+ * + * For strings: + *
    + *
  • "=" for case insensitive equality
  • + *
  • "==" for case-sensitive equality
  • + *
  • "<>" for case-insensitive inequality
  • + *
  • "!=" for case sensitive inequality
  • + *
+ * Boolean connectors "AND" and "OR" (case insensitive) combine multiple conditions; + * AND has higher precedence than OR. + *

+ * Examples: + *

+     *     "%aoneblock_my_island_lifetime_count% >= 1000 AND %aoneblock_my_island_level% >= 100"
+     *     "john smith == tasty bento AND 40 > 20"
+     * 
+ * + * @param player the Player used for placeholder replacement + * @param formula the formula to evaluate + * @return true if the formula evaluates to true, false otherwise. + */ + public static boolean evaluate(Player player, String formula) { + // Replace PAPI placeholders with actual values. + String parsedFormula = PlaceholderAPI.setPlaceholders(player, formula); + + // Tokenize the resulting formula by whitespace. + List tokens = tokenize(parsedFormula); + if (tokens.isEmpty()) { + return false; + } + + try { + Parser parser = new Parser(tokens); + boolean result = parser.parseExpression(); + // If there are extra tokens after parsing the full expression, the formula is malformed. + if (parser.hasNext()) { + return false; + } + return result; + } catch (Exception e) { + // Any error in parsing or evaluating the expression results in false. + return false; + } + } + + /** + * Splits a string into tokens using whitespace as the delimiter. + * + * @param s the string to tokenize. + * @return a list of tokens. + */ + private static List tokenize(String s) { + return new ArrayList<>(Arrays.asList(s.split("\\s+"))); + } + + /** + * A simple recursive descent parser that evaluates the formula. + * It supports multi-token operands for conditions. + */ + private static class Parser { + private final List tokens; + private int pos = 0; + + public Parser(List tokens) { + this.tokens = tokens; + } + + public boolean hasNext() { + return pos < tokens.size(); + } + + public String peek() { + return tokens.get(pos); + } + + public String next() { + return tokens.get(pos++); + } + + /** + * Parses an Expression: + * Expression -> Term { OR Term } + * + * @return the boolean value of the expression. + */ + public boolean parseExpression() { + boolean value = parseTerm(); + while (hasNext() && isOr(peek())) { + next(); // consume "OR" + boolean termValue = parseTerm(); + value = value || termValue; + } + return value; + } + + /** + * Parses a Term: + * Term -> Condition { AND Condition } + * + * @return the boolean value of the term. + */ + public boolean parseTerm() { + boolean value = parseCondition(); + while (hasNext() && isAnd(peek())) { + next(); // consume "AND" + boolean conditionValue = parseCondition(); + value = value && conditionValue; + } + return value; + } + + /** + * Parses a single condition of the form: + * leftOperand operator rightOperand + *

+ * The left operand is built by collecting tokens until a valid operator is found. + * The right operand is built by collecting tokens until a boolean operator ("AND" or "OR") + * is encountered or the end of the token list is reached. + * + * @return the boolean result of the condition. + */ + public boolean parseCondition() { + // Parse left operand. + StringBuilder leftSB = new StringBuilder(); + if (!hasNext()) { + BentoBox.getInstance() + .logError("Challenges PAPI formula error: Expected left operand but reached end of expression"); + return false; + } + // Collect tokens for the left operand until an operator is encountered. + while (hasNext() && !isOperator(peek())) { + if (leftSB.length() > 0) { + leftSB.append(" "); + } + leftSB.append(next()); + } + if (!hasNext()) { + throw new RuntimeException("Operator expected after left operand"); + } + // Next token should be an operator. + String operator = next(); + if (!isValidOperator(operator)) { + throw new RuntimeException("Invalid operator: " + operator); + } + // Parse right operand. + StringBuilder rightSB = new StringBuilder(); + while (hasNext() && !isBooleanOperator(peek())) { + if (rightSB.length() > 0) { + rightSB.append(" "); + } + rightSB.append(next()); + } + String leftOperand = leftSB.toString().trim(); + String rightOperand = rightSB.toString().trim(); + + if (rightOperand.isEmpty()) { + return false; + } + + // Evaluate the condition: + // If both operands can be parsed as numbers, use numeric comparison; + // otherwise, perform string comparison. + Double leftNum = tryParseDouble(leftOperand); + Double rightNum = tryParseDouble(rightOperand); + if (leftNum != null && rightNum != null) { + // Numeric comparison. + switch (operator) { + case "=": + case "==": + return Double.compare(leftNum, rightNum) == 0; + case "<>": + case "!=": + return Double.compare(leftNum, rightNum) != 0; + case "<=": + return leftNum <= rightNum; + case ">=": + return leftNum >= rightNum; + case "<": + return leftNum < rightNum; + case ">": + return leftNum > rightNum; + default: + BentoBox.getInstance().logError("Challenges PAPI formula error: Unsupported operator: " + operator); + return false; + } + } else { + // String comparison. + switch (operator) { + case "=": + return leftOperand.equalsIgnoreCase(rightOperand); + case "==": + return leftOperand.equals(rightOperand); + case "<>": + return !leftOperand.equalsIgnoreCase(rightOperand); + case "!=": + return !leftOperand.equals(rightOperand); + case "<=": + return leftOperand.compareTo(rightOperand) <= 0; + case ">=": + return leftOperand.compareTo(rightOperand) >= 0; + case "<": + return leftOperand.compareTo(rightOperand) < 0; + case ">": + return leftOperand.compareTo(rightOperand) > 0; + default: + BentoBox.getInstance().logError("Challenges PAPI formula error: Unsupported operator: " + operator); + return false; + } + } + } + + /** + * Checks if the given token is one of the valid comparison operators. + */ + private boolean isValidOperator(String token) { + return token.equals("=") || token.equals("==") || token.equals("<>") || token.equals("!=") + || token.equals("<=") || token.equals(">=") || token.equals("<") || token.equals(">"); + } + + /** + * Returns true if the token is a comparison operator. + */ + private boolean isOperator(String token) { + return isValidOperator(token); + } + + /** + * Returns true if the token is a boolean operator ("AND" or "OR"). + */ + private boolean isBooleanOperator(String token) { + return token.equalsIgnoreCase("AND") || token.equalsIgnoreCase("OR"); + } + + private boolean isAnd(String token) { + return token.equalsIgnoreCase("AND"); + } + + private boolean isOr(String token) { + return token.equalsIgnoreCase("OR"); + } + + /** + * Tries to parse the given string as a Double. + * Returns the Double if successful, or null if parsing fails. + */ + private Double tryParseDouble(String s) { + try { + return Double.parseDouble(s); + } catch (NumberFormatException e) { + return null; + } + } + } +} \ No newline at end of file diff --git a/src/main/java/world/bentobox/challenges/database/object/requirements/InventoryRequirements.java b/src/main/java/world/bentobox/challenges/database/object/requirements/InventoryRequirements.java index 1f3686ec..946d0a44 100644 --- a/src/main/java/world/bentobox/challenges/database/object/requirements/InventoryRequirements.java +++ b/src/main/java/world/bentobox/challenges/database/object/requirements/InventoryRequirements.java @@ -7,7 +7,11 @@ package world.bentobox.challenges.database.object.requirements; -import java.util.*; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; import org.bukkit.Material; diff --git a/src/main/java/world/bentobox/challenges/database/object/requirements/IslandRequirements.java b/src/main/java/world/bentobox/challenges/database/object/requirements/IslandRequirements.java index 407c28b1..2cff268d 100644 --- a/src/main/java/world/bentobox/challenges/database/object/requirements/IslandRequirements.java +++ b/src/main/java/world/bentobox/challenges/database/object/requirements/IslandRequirements.java @@ -7,9 +7,15 @@ package world.bentobox.challenges.database.object.requirements; -import java.util.*; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import org.bukkit.Fluid; import org.bukkit.Material; +import org.bukkit.Tag; import org.bukkit.entity.EntityType; import com.google.gson.annotations.Expose; @@ -23,202 +29,235 @@ */ public class IslandRequirements extends Requirements { - /** - * Constructor Requirements creates a new Requirements instance. - */ - public IslandRequirements() - { - // Empty constructor for data loader - } - -// --------------------------------------------------------------------- -// Section: Getters and Setters -// --------------------------------------------------------------------- - - - /** - * Method IslandRequirements#getRequiredBlocks returns the requiredBlocks of this object. - * - * @return the requiredBlocks (type {@code Map}) of this object. - */ - public Map getRequiredBlocks() - { - return requiredBlocks; - } - - - /** - * Method IslandRequirements#setRequiredBlocks sets new value for the requiredBlocks of this object. - * @param requiredBlocks new value for this object. - * - */ - public void setRequiredBlocks(Map requiredBlocks) - { - this.requiredBlocks = requiredBlocks; - } - - - /** - * Method IslandRequirements#isRemoveBlocks returns the removeBlocks of this object. - * - * @return the removeBlocks (type boolean) of this object. - */ - public boolean isRemoveBlocks() - { - return removeBlocks; - } - - - /** - * Method IslandRequirements#setRemoveBlocks sets new value for the removeBlocks of this object. - * @param removeBlocks new value for this object. - * - */ - public void setRemoveBlocks(boolean removeBlocks) - { - this.removeBlocks = removeBlocks; - } - - - /** - * Method IslandRequirements#getRequiredEntities returns the requiredEntities of this object. - * - * @return the requiredEntities (type {@code Map}) of this object. - */ - public Map getRequiredEntities() - { - return requiredEntities; - } - - - /** - * Method IslandRequirements#setRequiredEntities sets new value for the requiredEntities of this object. - * @param requiredEntities new value for this object. - * - */ - public void setRequiredEntities(Map requiredEntities) - { - this.requiredEntities = requiredEntities; - } - - - /** - * Method IslandRequirements#isRemoveEntities returns the removeEntities of this object. - * - * @return the removeEntities (type boolean) of this object. - */ - public boolean isRemoveEntities() - { - return removeEntities; - } - - - /** - * Method IslandRequirements#setRemoveEntities sets new value for the removeEntities of this object. - * @param removeEntities new value for this object. - * - */ - public void setRemoveEntities(boolean removeEntities) - { - this.removeEntities = removeEntities; - } - - - /** - * Method IslandRequirements#getSearchRadius returns the searchRadius of this object. - * - * @return the searchRadius (type int) of this object. - */ - public int getSearchRadius() - { - return searchRadius; - } - - - /** - * Method IslandRequirements#setSearchRadius sets new value for the searchRadius of this object. - * @param searchRadius new value for this object. - * - */ - public void setSearchRadius(int searchRadius) - { - this.searchRadius = searchRadius; - } - -// --------------------------------------------------------------------- -// Section: Other methods -// --------------------------------------------------------------------- - - - /** - * Method isValid returns if given requirement data is valid or not. - * - * @return {@code true} if data is valid, {@code false} otherwise. - */ - @Override - public boolean isValid() - { - return super.isValid() && - this.requiredBlocks != null && this.requiredBlocks.keySet().stream().noneMatch(Objects::isNull) && - this.requiredEntities != null && this.requiredEntities.keySet().stream().noneMatch(Objects::isNull); - } - - - /** - * Method Requirements#copy allows copies Requirements object, to avoid changing content when it is necessary - * to use it. - * @return IslandRequirements copy - */ - @Override - public Requirements copy() - { - IslandRequirements clone = new IslandRequirements(); - clone.setRequiredPermissions(new HashSet<>(this.getRequiredPermissions())); - - clone.setRequiredBlocks(new HashMap<>(this.requiredBlocks)); - clone.setRemoveBlocks(this.removeBlocks); - clone.setRequiredEntities(new HashMap<>(this.requiredEntities)); - clone.setRemoveEntities(this.removeEntities); - - clone.setSearchRadius(this.searchRadius); - - return clone; - } - - -// --------------------------------------------------------------------- -// Section: Variables -// --------------------------------------------------------------------- - - - /** - * Map that contains which materials and how many is necessary around player to complete challenge. - */ - @Expose - private Map requiredBlocks = new EnumMap<>(Material.class); - - /** - * Boolean that indicate if blocks should be removed from world after completion. - */ - @Expose - private boolean removeBlocks; - - /** - * Map that contains which entities and how many is necessary around player to complete challenge. - */ - @Expose - @JsonAdapter(EntityCompatibilityAdapter.class) - private Map requiredEntities = new EnumMap<>(EntityType.class); - - /** - * Boolean that indicate if entities should be removed from world after completion. - */ - @Expose - private boolean removeEntities; - - /** - * Radius for searching distance for blocks and entities. - */ - @Expose - private int searchRadius = 10; + /** + * Constructor Requirements creates a new Requirements instance. + */ + public IslandRequirements() { + // Empty constructor for data loader + } + + // --------------------------------------------------------------------- + // Section: Variables + // --------------------------------------------------------------------- + + + /** + * Map that contains which materials and how many is necessary around player to complete challenge. + */ + @Expose + private Map requiredBlocks = new EnumMap<>(Material.class); + + @Expose + private Map, Integer> requiredMaterialTags = new HashMap<>(); + @Expose + private Map, Integer> requiredFluidTags = new HashMap<>(); + @Expose + private Map, Integer> requiredEntityTypeTags = new HashMap<>(); + + /** + * Boolean that indicate if blocks should be removed from world after completion. + */ + @Expose + private boolean removeBlocks; + + /** + * Map that contains which entities and how many is necessary around player to complete challenge. + */ + @Expose + @JsonAdapter(EntityCompatibilityAdapter.class) + private Map requiredEntities = new EnumMap<>(EntityType.class); + + /** + * Boolean that indicate if entities should be removed from world after completion. + */ + @Expose + private boolean removeEntities; + + /** + * Radius for searching distance for blocks and entities. + */ + @Expose + private int searchRadius = 10; + + // --------------------------------------------------------------------- + // Section: Getters and Setters + // --------------------------------------------------------------------- + + + /** + * Method IslandRequirements#getRequiredBlocks returns the requiredBlocks of this object. + * + * @return the requiredBlocks (type {@code Map}) of this object. + */ + public Map getRequiredBlocks() { + return requiredBlocks; + } + + + /** + * Method IslandRequirements#setRequiredBlocks sets new value for the requiredBlocks of this object. + * @param requiredBlocks new value for this object. + * + */ + public void setRequiredBlocks(Map requiredBlocks) { + this.requiredBlocks = requiredBlocks; + } + + + /** + * Method IslandRequirements#isRemoveBlocks returns the removeBlocks of this object. + * + * @return the removeBlocks (type boolean) of this object. + */ + public boolean isRemoveBlocks() { + return removeBlocks; + } + + + /** + * Method IslandRequirements#setRemoveBlocks sets new value for the removeBlocks of this object. + * @param removeBlocks new value for this object. + * + */ + public void setRemoveBlocks(boolean removeBlocks) { + this.removeBlocks = removeBlocks; + } + + + /** + * Method IslandRequirements#getRequiredEntities returns the requiredEntities of this object. + * + * @return the requiredEntities (type {@code Map}) of this object. + */ + public Map getRequiredEntities() { + return requiredEntities; + } + + + /** + * Method IslandRequirements#setRequiredEntities sets new value for the requiredEntities of this object. + * @param requiredEntities new value for this object. + * + */ + public void setRequiredEntities(Map requiredEntities) { + this.requiredEntities = requiredEntities; + } + + /** + * Method IslandRequirements#isRemoveEntities returns the removeEntities of this object. + * + * @return the removeEntities (type boolean) of this object. + */ + public boolean isRemoveEntities() { + return removeEntities; + } + + + /** + * Method IslandRequirements#setRemoveEntities sets new value for the removeEntities of this object. + * @param removeEntities new value for this object. + * + */ + public void setRemoveEntities(boolean removeEntities) { + this.removeEntities = removeEntities; + } + + + /** + * Method IslandRequirements#getSearchRadius returns the searchRadius of this object. + * + * @return the searchRadius (type int) of this object. + */ + public int getSearchRadius() { + return searchRadius; + } + + + /** + * Method IslandRequirements#setSearchRadius sets new value for the searchRadius of this object. + * @param searchRadius new value for this object. + * + */ + public void setSearchRadius(int searchRadius) { + this.searchRadius = searchRadius; + } + + /** + * Method isValid returns if given requirement data is valid or not. + * + * @return {@code true} if data is valid, {@code false} otherwise. + */ + @Override + public boolean isValid() { + return super.isValid() && this.requiredBlocks != null + && this.requiredBlocks.keySet().stream().noneMatch(Objects::isNull) && this.requiredEntities != null + && this.requiredEntities.keySet().stream().noneMatch(Objects::isNull); + } + + + /** + * Method Requirements#copy allows copies Requirements object, to avoid changing content when it is necessary + * to use it. + * @return IslandRequirements copy + */ + @Override + public Requirements copy() { + IslandRequirements clone = new IslandRequirements(); + clone.setRequiredPermissions(new HashSet<>(this.getRequiredPermissions())); + clone.setRequiredMaterialTags(new HashMap<>(this.requiredMaterialTags)); + clone.setRequiredFluidTags(new HashMap<>(this.requiredFluidTags)); + clone.setRequiredEntityTypeTags(new HashMap<>(this.requiredEntityTypeTags)); + clone.setRequiredBlocks(new HashMap<>(this.requiredBlocks)); + clone.setRemoveBlocks(this.removeBlocks); + clone.setRequiredEntities(new HashMap<>(this.requiredEntities)); + clone.setRemoveEntities(this.removeEntities); + + clone.setSearchRadius(this.searchRadius); + + return clone; + } + + + /** + * @return the requiredMaterialTags + */ + public Map, Integer> getRequiredMaterialTags() { + return requiredMaterialTags; + } + + /** + * @param requiredMaterialTags the requiredMaterialTags to set + */ + public void setRequiredMaterialTags(Map, Integer> requiredMaterialTags) { + this.requiredMaterialTags = requiredMaterialTags; + } + + /** + * @return the requiredFluidTags + */ + public Map, Integer> getRequiredFluidTags() { + return requiredFluidTags; + } + + /** + * @param requiredFluidTags the requiredFluidTags to set + */ + public void setRequiredFluidTags(Map, Integer> requiredFluidTags) { + this.requiredFluidTags = requiredFluidTags; + } + + /** + * @return the requiredEntityTypeTags + */ + public Map, Integer> getRequiredEntityTypeTags() { + return requiredEntityTypeTags; + } + + /** + * @param requiredEntityTypeTags the requiredEntityTypeTags to set + */ + public void setRequiredEntityTypeTags(Map, Integer> requiredEntityTypeTags) { + this.requiredEntityTypeTags = requiredEntityTypeTags; + } + } diff --git a/src/main/java/world/bentobox/challenges/database/object/requirements/OtherRequirements.java b/src/main/java/world/bentobox/challenges/database/object/requirements/OtherRequirements.java index 5f59e7d5..3edb37b5 100644 --- a/src/main/java/world/bentobox/challenges/database/object/requirements/OtherRequirements.java +++ b/src/main/java/world/bentobox/challenges/database/object/requirements/OtherRequirements.java @@ -1,16 +1,23 @@ // // Created by BONNe // Copyright - 2019 +// Enhanced by tastybento // package world.bentobox.challenges.database.object.requirements; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; + +import org.bukkit.advancement.Advancement; import com.google.gson.annotations.Expose; +import com.google.gson.annotations.JsonAdapter; +import world.bentobox.challenges.database.object.adapters.AdvancementsListAdapter; /** * This class contains all necessary requirements to complete other type challenge. @@ -139,18 +146,53 @@ public void setRequiredIslandLevel(long requiredIslandLevel) { this.requiredIslandLevel = requiredIslandLevel; } - + + /** + * @return the papiString + */ + public String getPapiString() { + if (papiString == null) { + papiString = ""; + } + return papiString; + } + + /** + * @param papiString the papiString to set + */ + public void setPapiString(String papiString) { + this.papiString = papiString; + } + + /** + * @return the advancements + */ + public List getAdvancements() { + if (advancements == null) { + advancements = new ArrayList<>(); + } + return advancements; + } + + /** + * @param advancements the advancements to set + */ + public void setAdvancements(List advancements) { + + this.advancements = advancements; + //advancements.stream().map(adv -> adv.getDisplay().getTitle()).collect(Collectors.toList()); + } // --------------------------------------------------------------------- // Section: Other methods // --------------------------------------------------------------------- - /** - * Method Requirements#copy allows copies Requirements object, to avoid changing content when it is necessary - * to use it. - * @return OtherRequirements copy - */ +/** + * Method Requirements#copy allows copies Requirements object, to avoid changing content when it is necessary + * to use it. + * @return OtherRequirements copy + */ @Override public Requirements copy() { @@ -162,6 +204,8 @@ public Requirements copy() clone.setRequiredMoney(this.requiredMoney); clone.setTakeMoney(this.takeMoney); clone.setRequiredIslandLevel(this.requiredIslandLevel); + clone.setPapiString(this.papiString); + clone.setAdvancements(this.advancements); return clone; } @@ -201,4 +245,18 @@ public Requirements copy() */ @Expose private long requiredIslandLevel; + + /** + * Formulas that include math symbols and PAPI placeholders + */ + @Expose + private String papiString; + + /** + * List of advancements + */ + @Expose + @JsonAdapter(AdvancementsListAdapter.class) + private List advancements; + } diff --git a/src/main/java/world/bentobox/challenges/database/object/requirements/Requirements.java b/src/main/java/world/bentobox/challenges/database/object/requirements/Requirements.java index ce8279a4..4dae7f03 100644 --- a/src/main/java/world/bentobox/challenges/database/object/requirements/Requirements.java +++ b/src/main/java/world/bentobox/challenges/database/object/requirements/Requirements.java @@ -83,8 +83,8 @@ public boolean isValid() /** - * This set contains all permission strings that ir required for player to complete challenge. - */ + * This set contains all permission strings that are required for player to complete challenge. + */ @Expose private Set requiredPermissions = new HashSet<>(); } diff --git a/src/main/java/world/bentobox/challenges/database/object/requirements/StatisticRequirements.java b/src/main/java/world/bentobox/challenges/database/object/requirements/StatisticRequirements.java index b90ec701..3df672b2 100644 --- a/src/main/java/world/bentobox/challenges/database/object/requirements/StatisticRequirements.java +++ b/src/main/java/world/bentobox/challenges/database/object/requirements/StatisticRequirements.java @@ -2,227 +2,139 @@ // Created by BONNe // Copyright - 2021 // +// Enhanced by tastybento package world.bentobox.challenges.database.object.requirements; -import com.google.gson.annotations.Expose; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + import org.bukkit.Material; import org.bukkit.Statistic; import org.bukkit.entity.EntityType; import org.eclipse.jdt.annotation.Nullable; +import com.google.gson.annotations.Expose; + +/** + * Requirements for statistics based challenges + */ public class StatisticRequirements extends Requirements { /** - * Constructor Requirements creates a new Requirements instance. + * Record for this requirement + * @param Statistic statistic + * @param EntityType entity + * @param Material material + * @param Integer amount + * @param Boolean reduceStatistic */ - public StatisticRequirements() - { - // Empty constructor + public record StatisticRec(@Expose Statistic statistic, @Expose EntityType entity, @Expose Material material, + @Expose Integer amount, @Expose Boolean reduceStatistic) { } - /** - * This method copies given statistic object. - * @return Copy of this object. + * Type of the statistic field. + * @deprecated Shifting to a list */ - @Override - public Requirements copy() - { - StatisticRequirements requirements = new StatisticRequirements(); - requirements.setStatistic(this.statistic); - requirements.setEntity(this.entity); - requirements.setMaterial(this.material); - requirements.setAmount(this.amount); - requirements.setReduceStatistic(this.reduceStatistic); - - return requirements; - } - - - @Override - public boolean isValid() - { - if (!super.isValid()) - { - return false; - } - - if (this.statistic == null) - { - return false; - } - - return switch (this.statistic.getType()) - { - case ITEM -> this.material != null && this.material.isItem(); - - case BLOCK -> this.material != null && this.material.isBlock(); - - case ENTITY -> this.entity != null; - - case UNTYPED -> true; - - }; - - } - - - // --------------------------------------------------------------------- -// Section: Getters and setters -// --------------------------------------------------------------------- - + @Expose + @Nullable + private Statistic statistic; /** - * Gets statistic. - * - * @return the statistic + * Type of entity for entity related statistics. + * @deprecated Shifting to a list */ + @Expose @Nullable - public Statistic getStatistic() - { - return statistic; - } - + private EntityType entity; /** - * Sets statistic. - * - * @param statistic the statistic + * Type of material for block and item related statistics. + * @deprecated Shifting to a list */ - public void setStatistic(@Nullable Statistic statistic) - { - this.statistic = statistic; - } - + @Expose + @Nullable + private Material material; /** - * Gets entity. - * - * @return the entity + * Amount of the stats. + * @deprecated Shifting to a list */ - @Nullable - public EntityType getEntity() - { - return entity; - } - + @Expose + private Integer amount; /** - * Sets entity. - * - * @param entity the entity + * Indicate that player statistic fields must be adjusted after completing challenges. + * @deprecated Shifting to a list */ - public void setEntity(@Nullable EntityType entity) - { - this.entity = entity; - } - + @Expose + private Boolean reduceStatistic; /** - * Gets material. - * - * @return the material + * List of statistics that must be done for this challenge */ + @Expose @Nullable - public Material getMaterial() - { - return material; - } + private List statisticList; /** - * Sets material. - * - * @param material the material + * Constructor Requirements creates a new Requirements instance. */ - public void setMaterial(@Nullable Material material) + public StatisticRequirements() { - this.material = material; + // Empty constructor } /** - * Gets amount. - * - * @return the amount + * This method copies given statistic object. + * @return Copy of this object. */ - public int getAmount() + @Override + public Requirements copy() { - return amount; + StatisticRequirements requirements = new StatisticRequirements(); + requirements.setStatisticList(this.getRequiredStatistics()); + return requirements; } - /** - * Sets amount. - * - * @param amount the amount - */ - public void setAmount(int amount) + @Override + public boolean isValid() { - this.amount = amount; + // TODO - do something here? + return super.isValid(); } - /** - * Is reduce statistic boolean. - * - * @return the boolean + * @return the statisticList */ - public boolean isReduceStatistic() - { - return reduceStatistic; + public List getRequiredStatistics() { + if (statisticList == null) { + statisticList = new ArrayList<>(); + // Convert old single statistic entries to new list of records + if (statistic != null) { + StatisticRec rec = new StatisticRec(this.statistic, this.entity, this.material, this.amount, + this.reduceStatistic); + statisticList.add(rec); + } + } + return statisticList; } - /** - * Sets reduce statistic. - * - * @param reduceStatistic the reduce statistic + * @param value the statisticList to set */ - public void setReduceStatistic(boolean reduceStatistic) - { - this.reduceStatistic = reduceStatistic; + public void setStatisticList(Collection value) { + // If value is null, assign null; otherwise, create a new ArrayList from value. + this.statisticList = (value == null) ? null : new ArrayList<>(value); } -// --------------------------------------------------------------------- -// Section: Variables -// --------------------------------------------------------------------- - - /** - * Type of the statistic field. - */ - @Expose - @Nullable - private Statistic statistic; - - /** - * Type of entity for entity related statistics. - */ - @Expose - @Nullable - private EntityType entity; - - /** - * Type of material for block and item related statistics. - */ - @Expose - @Nullable - private Material material; - - /** - * Amount of the stats. - */ - @Expose - private int amount; - - /** - * Indicate that player statistic fields must be adjusted after completing challenges. - */ - @Expose - private boolean reduceStatistic; } diff --git a/src/main/java/world/bentobox/challenges/events/ChallengeCompletedEvent.java b/src/main/java/world/bentobox/challenges/events/ChallengeCompletedEvent.java index 50c072ee..f9cfc704 100644 --- a/src/main/java/world/bentobox/challenges/events/ChallengeCompletedEvent.java +++ b/src/main/java/world/bentobox/challenges/events/ChallengeCompletedEvent.java @@ -2,9 +2,10 @@ -import org.bukkit.event.HandlerList; import java.util.UUID; +import org.bukkit.event.HandlerList; + import world.bentobox.bentobox.api.events.BentoBoxEvent; diff --git a/src/main/java/world/bentobox/challenges/events/ChallengeResetAllEvent.java b/src/main/java/world/bentobox/challenges/events/ChallengeResetAllEvent.java index fdd34f45..1988f7fe 100644 --- a/src/main/java/world/bentobox/challenges/events/ChallengeResetAllEvent.java +++ b/src/main/java/world/bentobox/challenges/events/ChallengeResetAllEvent.java @@ -1,9 +1,10 @@ package world.bentobox.challenges.events; -import org.bukkit.event.HandlerList; import java.util.UUID; +import org.bukkit.event.HandlerList; + import world.bentobox.bentobox.api.events.BentoBoxEvent; diff --git a/src/main/java/world/bentobox/challenges/events/ChallengeResetEvent.java b/src/main/java/world/bentobox/challenges/events/ChallengeResetEvent.java index 3c2eea3d..8294a1fb 100644 --- a/src/main/java/world/bentobox/challenges/events/ChallengeResetEvent.java +++ b/src/main/java/world/bentobox/challenges/events/ChallengeResetEvent.java @@ -1,9 +1,10 @@ package world.bentobox.challenges.events; -import org.bukkit.event.HandlerList; import java.util.UUID; +import org.bukkit.event.HandlerList; + import world.bentobox.bentobox.api.events.BentoBoxEvent; diff --git a/src/main/java/world/bentobox/challenges/events/LevelCompletedEvent.java b/src/main/java/world/bentobox/challenges/events/LevelCompletedEvent.java index 0928b1c8..1a21cc34 100644 --- a/src/main/java/world/bentobox/challenges/events/LevelCompletedEvent.java +++ b/src/main/java/world/bentobox/challenges/events/LevelCompletedEvent.java @@ -1,9 +1,10 @@ package world.bentobox.challenges.events; -import org.bukkit.event.HandlerList; import java.util.UUID; +import org.bukkit.event.HandlerList; + import world.bentobox.bentobox.api.events.BentoBoxEvent; diff --git a/src/main/java/world/bentobox/challenges/managers/ChallengesImportManager.java b/src/main/java/world/bentobox/challenges/managers/ChallengesImportManager.java index 4788607d..70a21dd1 100644 --- a/src/main/java/world/bentobox/challenges/managers/ChallengesImportManager.java +++ b/src/main/java/world/bentobox/challenges/managers/ChallengesImportManager.java @@ -9,7 +9,16 @@ import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import org.bukkit.Material; @@ -31,6 +40,7 @@ import world.bentobox.bentobox.api.localization.TextVariables; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.json.BentoboxTypeAdapterFactory; +import world.bentobox.bentobox.database.json.adapters.TagTypeAdapterFactory; import world.bentobox.bentobox.database.objects.DataObject; import world.bentobox.bentobox.util.ItemParser; import world.bentobox.bentobox.util.Util; @@ -41,6 +51,7 @@ import world.bentobox.challenges.database.object.requirements.IslandRequirements; import world.bentobox.challenges.database.object.requirements.OtherRequirements; import world.bentobox.challenges.database.object.requirements.StatisticRequirements; +import world.bentobox.challenges.database.object.requirements.StatisticRequirements.StatisticRec; import world.bentobox.challenges.utils.Constants; import world.bentobox.challenges.utils.Utils; @@ -347,13 +358,12 @@ private void populateRequirements(Challenge challenge, ConfigurationSection sect case STATISTIC_TYPE -> { StatisticRequirements requirements = new StatisticRequirements(); challenge.setRequirements(requirements); - - requirements.setAmount(section.getInt("amount", 0)); - requirements.setReduceStatistic(section.getBoolean("reduce", false)); - - requirements.setStatistic(matchStatistic(section.getString("statistic"))); - requirements.setEntity(matchEntity(section.getString("entity"))); - requirements.setMaterial(matchMaterial(section.getString("material"))); + List list = new ArrayList<>(); + list.add(new StatisticRec(matchStatistic(section.getString("statistic")), + matchEntity(section.getString("entity")), matchMaterial(section.getString("material")), + section.getInt("amount", 0), section.getBoolean("reduce", false))); + // TODO: Add support for multiple stat challenge + requirements.setStatisticList(list); } } @@ -1138,6 +1148,7 @@ private static final class DefaultJSONHandler GsonBuilder builder = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().enableComplexMapKeySerialization(); // Register adapters builder.registerTypeAdapterFactory(new BentoboxTypeAdapterFactory(addon.getPlugin())); + builder.registerTypeAdapterFactory(new TagTypeAdapterFactory()); // Keep null in the database builder.serializeNulls(); // Allow characters like < or > without escaping them diff --git a/src/main/java/world/bentobox/challenges/managers/ChallengesManager.java b/src/main/java/world/bentobox/challenges/managers/ChallengesManager.java index a61e175c..7ae75e50 100644 --- a/src/main/java/world/bentobox/challenges/managers/ChallengesManager.java +++ b/src/main/java/world/bentobox/challenges/managers/ChallengesManager.java @@ -1,7 +1,19 @@ package world.bentobox.challenges.managers; -import java.util.*; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.UUID; import java.util.stream.Collectors; import org.bukkit.Bukkit; @@ -1109,8 +1121,9 @@ private void resetAllChallenges(@NonNull String storageDataID, @NonNull String g * @return Level status - how many challenges still to do on which level */ @NonNull - private List getAllChallengeLevelStatus(String storageDataID, String gameMode) + private List getAllChallengeLevelStatus(User user, World world, String gameMode) { + String storageDataID = this.getDataUniqueID(user, Util.getWorld(world)); this.addPlayerData(storageDataID); ChallengesPlayerData playerData = this.playerCacheData.get(storageDataID); @@ -1123,6 +1136,14 @@ private List getAllChallengeLevelStatus(String storageDataID, Strin int doneChallengeCount = 0; boolean previousUnlocked = true; + // Get user's waiver add from permission + // Get per-user waiver amount + int waiverAdd = user + .getPermissionValue(addon.getPlugin().getIWM().getPermissionPrefix(world) + "challenges.waiver-add", 0); + if (waiverAdd < 0) { + waiverAdd = 0; + } + // For each challenge level, check how many the storageDataID has done for (ChallengeLevel level : challengeLevelList) { @@ -1135,7 +1156,7 @@ private List getAllChallengeLevelStatus(String storageDataID, Strin this.getLevelChallenges(previousLevel); int challengesToDo = previousLevel == null ? 0 : - (previousChallengeList.size() - doneChallengeCount - previousLevel.getWaiverAmount()); + (previousChallengeList.size() - doneChallengeCount - (previousLevel.getWaiverAmount() + waiverAdd)); List challengeList = this.getLevelChallenges(level); @@ -1165,14 +1186,15 @@ private List getAllChallengeLevelStatus(String storageDataID, Strin /** * This method returns LevelStatus object for given challenge level. - * @param storageDataID User which level status must be acquired. + * @param user User which level status must be acquired. * @param world World where level is living. * @param level Level which status must be calculated. * @return LevelStatus of given level. */ @Nullable - private LevelStatus getChallengeLevelStatus(@NonNull String storageDataID, World world, @NonNull ChallengeLevel level) + private LevelStatus getChallengeLevelStatus(User user, World world, @NonNull ChallengeLevel level) { + String storageDataID = this.getDataUniqueID(user.getUniqueId(), Util.getWorld(world)); this.addPlayerData(storageDataID); ChallengesPlayerData playerData = this.playerCacheData.get(storageDataID); @@ -1572,7 +1594,8 @@ public void resetAllChallenges(@NonNull UUID userID, World world, @Nullable UUID this.islandWorldManager.getAddon(world).ifPresent(gameMode -> { this.resetAllChallenges(storageID, gameMode.getDescription().getName()); - this.addLogEntry(storageID, new LogEntry.Builder("RESET_ALL"). + this.addLogEntry(storageID, new LogEntry.Builder("RESET_ALL") + . data(USER_ID, userID.toString()). data(ADMIN_ID, adminID == null ? "ISLAND_RESET" : adminID.toString()). build()); @@ -1638,11 +1661,9 @@ public boolean isLevelCompleted(User user, World world, ChallengeLevel level) */ public boolean isLevelUnlocked(User user, World world, ChallengeLevel level) { - String storageDataID = this.getDataUniqueID(user, Util.getWorld(world)); - this.addPlayerData(storageDataID); - - return this.islandWorldManager.getAddon(world).filter(gameMode -> this.getAllChallengeLevelStatus(storageDataID, gameMode.getDescription().getName()). - stream(). + return this.islandWorldManager.getAddon(world).filter(gameMode -> this + .getAllChallengeLevelStatus(user, world, gameMode.getDescription().getName()) + .stream(). filter(LevelStatus::isUnlocked). anyMatch(lv -> lv.getLevel().equals(level))). isPresent(); @@ -1696,7 +1717,8 @@ public boolean validateLevelCompletion(User user, World world, ChallengeLevel le @Nullable public LevelStatus getChallengeLevelStatus(UUID uniqueId, World world, ChallengeLevel level) { - return this.getChallengeLevelStatus(this.getDataUniqueID(uniqueId, Util.getWorld(world)), world, level); + User user = User.getInstance(uniqueId); + return this.getChallengeLevelStatus(user, world, level); } @@ -1712,9 +1734,8 @@ public List getAllChallengeLevelStatus(User user, World world) { return this.islandWorldManager.getAddon(world).map(gameMode -> this.getAllChallengeLevelStatus( - this.getDataUniqueID(user, Util.getWorld(world)), - gameMode.getDescription().getName())). - orElse(Collections.emptyList()); + user, world, gameMode.getDescription().getName())) + .orElse(Collections.emptyList()); } diff --git a/src/main/java/world/bentobox/challenges/panel/CommonPagedPanel.java b/src/main/java/world/bentobox/challenges/panel/CommonPagedPanel.java index 947c10e5..dec98c8a 100644 --- a/src/main/java/world/bentobox/challenges/panel/CommonPagedPanel.java +++ b/src/main/java/world/bentobox/challenges/panel/CommonPagedPanel.java @@ -7,15 +7,15 @@ package world.bentobox.challenges.panel; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + import org.bukkit.Material; import org.bukkit.World; import org.bukkit.inventory.ItemStack; import org.eclipse.jdt.annotation.NonNull; -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; - import world.bentobox.bentobox.api.panels.PanelItem; import world.bentobox.bentobox.api.panels.builders.PanelBuilder; import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; @@ -103,6 +103,7 @@ else if (this.pageIndex > (size / MAX_ELEMENTS)) { if (!panelBuilder.slotOccupied(index)) { + // Show an item panelBuilder.item(index, this.createElementButton(objectList.get(objectIndex++))); } diff --git a/src/main/java/world/bentobox/challenges/panel/CommonPanel.java b/src/main/java/world/bentobox/challenges/panel/CommonPanel.java index 6709d617..32f531b8 100644 --- a/src/main/java/world/bentobox/challenges/panel/CommonPanel.java +++ b/src/main/java/world/bentobox/challenges/panel/CommonPanel.java @@ -29,6 +29,7 @@ import world.bentobox.challenges.database.object.requirements.IslandRequirements; import world.bentobox.challenges.database.object.requirements.OtherRequirements; import world.bentobox.challenges.database.object.requirements.StatisticRequirements; +import world.bentobox.challenges.database.object.requirements.StatisticRequirements.StatisticRec; import world.bentobox.challenges.managers.ChallengesManager; import world.bentobox.challenges.utils.Constants; import world.bentobox.challenges.utils.LevelStatus; @@ -38,6 +39,8 @@ * This class contains common methods for all panels. */ public abstract class CommonPanel { + private static final long MAXSIZE = 10; + /** * This is default constructor for all classes that extends CommonPanel. * @@ -148,7 +151,7 @@ protected List generateChallengeDescription(Challenge challenge, @Nullab String requirements = isCompletedAll ? "" : this.generateRequirements(challenge, target); // Get rewards in single string String rewards = isCompletedAll ? "" : this.generateRewards(challenge, isCompletedOnce); - // Get coolDown in singe string + // Get coolDown in single string String coolDown = isCompletedAll || challenge.getTimeout() <= 0 ? "" : this.generateCoolDown(challenge, target); if (!description.replaceAll("(?m)^[ \\t]*\\r?\\n", "").isEmpty()) { @@ -284,50 +287,47 @@ private String generateRequirements(Challenge challenge, @Nullable User target) private String generateIslandChallenge(IslandRequirements requirement) { final String reference = Constants.DESCRIPTIONS + "challenge.requirements.island."; - String blocks; - + // Required Blocks + StringBuilder blocks = new StringBuilder(); + blocks.append(getBlocksTagsDescription(requirement, reference)); 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"); + blocks.append("\n"); if (entry.getValue() > 1) { - builder.append(this.user.getTranslationOrNothing(reference + "blocks-value", + blocks.append(this.user.getTranslationOrNothing(reference + "blocks-value", Constants.PARAMETER_NUMBER, String.valueOf(entry.getValue()), Constants.PARAMETER_MATERIAL, Utils.prettifyObject(entry.getKey(), this.user))); } else { - builder.append(this.user.getTranslationOrNothing(reference + "block-value", + blocks.append(this.user.getTranslationOrNothing(reference + "block-value", Constants.PARAMETER_MATERIAL, Utils.prettifyObject(entry.getKey(), this.user))); } }); - - blocks = builder.toString(); - } else { - blocks = ""; + } + // Add title if there is something here + if (!blocks.isEmpty()) { + blocks.insert(0, this.user.getTranslationOrNothing(reference + "blocks-title")); } - String entities; - + StringBuilder entities = new StringBuilder(); + entities.append(getEntityTypeTagsDescription(requirement, reference)); 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"); + entities.append("\n"); if (entry.getValue() > 1) { - builder.append(this.user.getTranslationOrNothing(reference + "entities-value", + entities.append(this.user.getTranslationOrNothing(reference + "entities-value", Constants.PARAMETER_NUMBER, String.valueOf(entry.getValue()), Constants.PARAMETER_ENTITY, Utils.prettifyObject(entry.getKey(), this.user))); } else { - builder.append(this.user.getTranslationOrNothing(reference + "entity-value", + entities.append(this.user.getTranslationOrNothing(reference + "entity-value", Constants.PARAMETER_ENTITY, Utils.prettifyObject(entry.getKey(), this.user))); } }); - - entities = builder.toString(); - } else { - entities = ""; + } + // Add title if there is something here + if (!entities.isEmpty()) { + entities.insert(0, this.user.getTranslationOrNothing(reference + "entities-title")); } String searchRadius = this.user.getTranslationOrNothing(reference + "search-radius", Constants.PARAMETER_NUMBER, @@ -340,10 +340,63 @@ private String generateIslandChallenge(IslandRequirements requirement) { ? this.user.getTranslationOrNothing(reference + "warning-entity") : ""; - return this.user.getTranslationOrNothing(reference + "lore", "[blocks]", blocks, "[entities]", entities, + return this.user.getTranslationOrNothing(reference + "lore", "[blocks]", blocks.toString(), "[entities]", + entities.toString(), "[warning-block]", warningBlocks, "[warning-entity]", warningEntities, "[search-radius]", searchRadius); } + private String getBlocksTagsDescription(IslandRequirements requirement, String reference) { + String tags = ""; + if (!requirement.getRequiredMaterialTags().isEmpty()) { + StringBuilder builder = new StringBuilder(); + requirement.getRequiredMaterialTags().entrySet().stream().limit(MAXSIZE).forEach(entry -> { + builder.append("\n"); + + if (entry.getValue() > 1) { + builder.append(this.user.getTranslationOrNothing(reference + "blocks-value", + Constants.PARAMETER_NUMBER, String.valueOf(entry.getValue()), Constants.PARAMETER_MATERIAL, + Utils.prettifyObject(entry.getKey(), this.user))); + } else { + builder.append(this.user.getTranslationOrNothing(reference + "block-value", + Constants.PARAMETER_MATERIAL, Utils.prettifyObject(entry.getKey(), this.user))); + } + }); + if (requirement.getRequiredMaterialTags().size() > MAXSIZE) { + builder.append("...\n"); + } + tags = builder.toString(); + } + + return tags; + } + + private String getEntityTypeTagsDescription(IslandRequirements requirement, String reference) { + String tags = ""; + if (!requirement.getRequiredEntityTypeTags().isEmpty()) { + StringBuilder builder = new StringBuilder(); + requirement.getRequiredEntityTypeTags().entrySet().stream().limit(MAXSIZE).forEach(entry -> { + builder.append("\n"); + + if (entry.getValue() > 1) { + builder.append(this.user.getTranslationOrNothing(reference + "blocks-value", + Constants.PARAMETER_NUMBER, String.valueOf(entry.getValue()), Constants.PARAMETER_MATERIAL, + Utils.prettifyObject(entry.getKey(), this.user))); + } else { + builder.append(this.user.getTranslationOrNothing(reference + "block-value", + Constants.PARAMETER_MATERIAL, Utils.prettifyObject(entry.getKey(), this.user))); + } + }); + if (requirement.getRequiredEntityTypeTags().size() > MAXSIZE) { + builder.append("...\n"); + } + + tags = builder.toString(); + } + + return tags; + } + + /** * This method generates lore message for inventory requirement. * @@ -415,7 +468,7 @@ private String generateOtherChallenge(OtherRequirements requirement) { } /** - * This method generates lore message for Statistic requirement. + * This method generates lore message for Statistic requirements. * * @param requirement Statistic Requirement. * @return Requirement lore message. @@ -423,48 +476,49 @@ private String generateOtherChallenge(OtherRequirements requirement) { private String generateStatisticChallenge(StatisticRequirements requirement) { final String reference = Constants.DESCRIPTIONS + "challenge.requirements.statistic."; - String statistic; - - if (requirement.getStatistic() == null) { + if (requirement.getRequiredStatistics().isEmpty()) { // 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())); + StringBuilder statistics = new StringBuilder(); + for (StatisticRec s : requirement.getRequiredStatistics()) { + String statistic = switch (s.statistic().getType()) { + case UNTYPED -> this.user.getTranslationOrNothing(reference + "statistic", "[statistic]", + Utils.prettifyObject(s.statistic(), this.user), "[number]", String.valueOf(s.amount())); 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)); + if (s.amount() > 1) { + yield this.user.getTranslationOrNothing(reference + "multiple-target", "[statistic]", + Utils.prettifyObject(s.statistic(), this.user), "[number]", String.valueOf(s.amount()), + "[target]", Utils.prettifyObject(s.material(), this.user)); } else { - statistic = this.user.getTranslationOrNothing(reference + "single-target", "[statistic]", - Utils.prettifyObject(requirement.getStatistic(), this.user), "[target]", - Utils.prettifyObject(requirement.getMaterial(), this.user)); + yield this.user.getTranslationOrNothing(reference + "single-target", "[statistic]", + Utils.prettifyObject(s.statistic(), this.user), "[target]", + Utils.prettifyObject(s.material(), 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)); + if (s.amount() > 1) { + yield this.user.getTranslationOrNothing(reference + "multiple-target", "[statistic]", + Utils.prettifyObject(s.statistic(), this.user), "[number]", String.valueOf(s.amount()), + "[target]", Utils.prettifyObject(s.entity(), this.user)); } else { - statistic = this.user.getTranslationOrNothing(reference + "single-target", "[statistic]", - Utils.prettifyObject(requirement.getStatistic(), this.user), "[target]", - Utils.prettifyObject(requirement.getEntity(), this.user)); + yield this.user.getTranslationOrNothing(reference + "single-target", "[statistic]", + Utils.prettifyObject(s.statistic(), this.user), "[target]", + Utils.prettifyObject(s.entity(), this.user)); } } - default -> statistic = ""; - } + default -> ""; + }; - String warning = requirement.isReduceStatistic() ? this.user.getTranslationOrNothing(reference + "warning") - : ""; + String warning = s.reduceStatistic() ? this.user.getTranslationOrNothing(reference + "warning") + : ""; + statistics.append(this.user.getTranslationOrNothing(reference + "lore", "[statistic]", statistic, "[warning]", + warning)); + statistics.append("\n"); - return this.user.getTranslationOrNothing(reference + "lore", "[statistic]", statistic, "[warning]", warning); + } + return statistics.toString(); } /** @@ -607,7 +661,7 @@ private String generateReward(Challenge challenge) { String items; - if (!challenge.getRewardItems().isEmpty()) { + if (!challenge.getRewardItems().isEmpty() && !challenge.isHideRewardItems()) { StringBuilder builder = new StringBuilder(); builder.append(this.user.getTranslationOrNothing(reference + "item-title")); Utils.groupEqualItems(challenge.getRewardItems(), challenge.getIgnoreRewardMetaData()).stream() @@ -688,10 +742,17 @@ protected List generateLevelDescription(ChallengeLevel level) { // my eye :) // Get status in single string String status = ""; + // Get per-user waiver amount + int waiverAdd = user.getPermissionValue( + addon.getPlugin().getIWM().getPermissionPrefix(world) + "challenges.waiver-add", 0); + if (waiverAdd < 0) { + waiverAdd = 0; + } + waiverAdd += level.getWaiverAmount(); // Get requirements in single string String waiver = this.manager.isLastLevel(level, this.world) ? "" : this.user.getTranslationOrNothing(reference + "waiver", "[number]", - String.valueOf(level.getWaiverAmount())); + String.valueOf(waiverAdd)); // Get rewards in single string String rewards = this.generateReward(level); @@ -721,11 +782,18 @@ protected List generateLevelDescription(LevelStatus levelStatus, User us // my eye :) // Get status in single string String status = this.generateLevelStatus(levelStatus); + // Get per-user waiver amount + int waiverAdd = user + .getPermissionValue(addon.getPlugin().getIWM().getPermissionPrefix(world) + "challenges.waiver-add", 0); + if (waiverAdd < 0) { + waiverAdd = 0; + } + waiverAdd += level.getWaiverAmount(); // Get requirements in single string String waiver = this.manager.isLastLevel(level, this.world) || !levelStatus.isUnlocked() || levelStatus.isComplete() ? "" : this.user.getTranslationOrNothing(reference + "waiver", "[number]", - String.valueOf(level.getWaiverAmount())); + String.valueOf(waiverAdd)); // Get rewards in single string String rewards = !levelStatus.isUnlocked() ? "" : this.generateReward(level); diff --git a/src/main/java/world/bentobox/challenges/panel/ConversationUtils.java b/src/main/java/world/bentobox/challenges/panel/ConversationUtils.java index 270528f9..e4ca2e90 100644 --- a/src/main/java/world/bentobox/challenges/panel/ConversationUtils.java +++ b/src/main/java/world/bentobox/challenges/panel/ConversationUtils.java @@ -7,16 +7,24 @@ package world.bentobox.challenges.panel; -import org.bukkit.ChatColor; -import org.bukkit.conversations.*; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.function.Consumer; import java.util.function.Function; +import org.bukkit.ChatColor; +import org.bukkit.conversations.ConversationAbandonedListener; +import org.bukkit.conversations.ConversationContext; +import org.bukkit.conversations.ConversationFactory; +import org.bukkit.conversations.MessagePrompt; +import org.bukkit.conversations.NumericPrompt; +import org.bukkit.conversations.Prompt; +import org.bukkit.conversations.StringPrompt; +import org.bukkit.conversations.ValidatingPrompt; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.user.User; import world.bentobox.challenges.utils.Constants; diff --git a/src/main/java/world/bentobox/challenges/panel/admin/AbstractManageEnumPanel.java b/src/main/java/world/bentobox/challenges/panel/admin/AbstractManageEnumPanel.java new file mode 100644 index 00000000..da5a6ffd --- /dev/null +++ b/src/main/java/world/bentobox/challenges/panel/admin/AbstractManageEnumPanel.java @@ -0,0 +1,161 @@ +package world.bentobox.challenges.panel.admin; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import org.bukkit.inventory.ItemStack; + +import lv.id.bonne.panelutils.PanelUtils; +import world.bentobox.bentobox.api.panels.PanelItem; +import world.bentobox.bentobox.api.panels.builders.PanelBuilder; +import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; +import world.bentobox.challenges.panel.CommonPagedPanel; +import world.bentobox.challenges.panel.CommonPanel; +import world.bentobox.challenges.panel.ConversationUtils; +import world.bentobox.challenges.utils.Constants; +import world.bentobox.challenges.utils.Utils; + +/** + * Abstract class that works with any enum type. + * This class contains all the common logic: filtering, building the panel, creating the element buttons, etc. + * @author tastybento + */ +public abstract class AbstractManageEnumPanel> extends CommonPagedPanel { + + protected final Map itemsMap; + protected final List itemList; + protected final Set selectedItems; + protected List filterElements; + + protected AbstractManageEnumPanel(CommonPanel parentGUI, Map itemsMap) { + super(parentGUI); + this.itemsMap = itemsMap; + this.itemList = new ArrayList<>(itemsMap.keySet()); + // Sort by the enum name (alphabetical order) + this.itemList.sort(Comparator.comparing(Enum::name)); + this.selectedItems = new HashSet<>(); + this.filterElements = this.itemList; + } + + /** + * Update the filter list based on the search string. + */ + @Override + protected void updateFilters() { + if (this.searchString == null || this.searchString.isBlank()) { + this.filterElements = this.itemList; + } else { + this.filterElements = this.itemList.stream() + .filter(element -> element.name().toLowerCase().contains(this.searchString.toLowerCase())) + .distinct().collect(Collectors.toList()); + } + } + + /** + * Creates a button for an element. + */ + @Override + protected PanelItem createElementButton(T element) { + final String reference = getElementTranslationPrefix(); + List description = new ArrayList<>(); + + if (selectedItems.contains(element)) { + description.add(this.user.getTranslation(reference + "selected")); + } + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "left-click-to-choose")); + + if (selectedItems.contains(element)) { + description.add(this.user.getTranslation(Constants.TIPS + "right-click-to-deselect")); + } else { + description.add(this.user.getTranslation(Constants.TIPS + "right-click-to-select")); + } + return new PanelItemBuilder() + .name(this.user.getTranslation(reference + "name", getElementPlaceholder(), + Utils.prettifyObject(element, this.user))) + .icon(getElementIcon(element, itemsMap.get(element))).description(description) + .clickHandler((panel, user1, clickType, slot) -> { + // On right click, toggle selection. + if (clickType.isRightClick()) { + if (!selectedItems.add(element)) { + selectedItems.remove(element); + } + this.build(); + } else { + // On left click, open a numeric input conversation. + Consumer numberConsumer = number -> { + if (number != null) { + itemsMap.put(element, number.intValue()); + } + this.build(); + }; + + ConversationUtils.createNumericInput(numberConsumer, this.user, + this.user.getTranslation(Constants.CONVERSATIONS + "input-number"), 1, + Integer.MAX_VALUE); + } + return true; + }).glow(selectedItems.contains(element)).build(); + } + + /** + * Build the panel. + */ + @Override + protected void build() { + PanelBuilder panelBuilder = new PanelBuilder().user(this.user) + .name(this.user.getTranslation(getPanelTitleKey())); + + // Create a border. + PanelUtils.fillBorder(panelBuilder); + + // Add the functional buttons (buttons like add, remove, etc.) + addFunctionalButtons(panelBuilder); + + // Populate the panel with the filtered items. + populateElements(panelBuilder, this.filterElements); + + // Add the return button. + panelBuilder.item(getReturnButtonSlot(), this.returnButton); + + panelBuilder.build(); + } + + protected int getReturnButtonSlot() { + return 44; + } + + // --- Abstract methods that concrete subclasses must implement --- + + /** + * Returns the ItemStack icon for a given element. + */ + protected abstract ItemStack getElementIcon(T element, int count); + + /** + * Returns the translation prefix for element buttons (e.g. "button.material." or "button.entity."). + */ + protected abstract String getElementTranslationPrefix(); + + /** + * Returns the placeholder used in translation for the element (e.g. "[material]" or "[entity]"). + */ + protected abstract String getElementPlaceholder(); + + /** + * Returns the translation key for the panel title. + */ + protected abstract String getPanelTitleKey(); + + /** + * Adds all the functional (non-element) buttons to the panel. + */ + protected abstract void addFunctionalButtons(PanelBuilder panelBuilder); +} diff --git a/src/main/java/world/bentobox/challenges/panel/admin/EditChallengePanel.java b/src/main/java/world/bentobox/challenges/panel/admin/EditChallengePanel.java index 147f8f7b..e843223d 100644 --- a/src/main/java/world/bentobox/challenges/panel/admin/EditChallengePanel.java +++ b/src/main/java/world/bentobox/challenges/panel/admin/EditChallengePanel.java @@ -8,11 +8,13 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.stream.Collectors; import org.bukkit.Material; import org.bukkit.World; +import org.bukkit.advancement.Advancement; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryCloseEvent; import org.bukkit.inventory.ItemStack; @@ -35,9 +37,8 @@ import world.bentobox.challenges.panel.util.EnvironmentSelector; import world.bentobox.challenges.panel.util.ItemSelector; import world.bentobox.challenges.panel.util.MultiBlockSelector; -import world.bentobox.challenges.panel.util.SingleBlockSelector; -import world.bentobox.challenges.panel.util.SingleEntitySelector; -import world.bentobox.challenges.panel.util.StatisticSelector; +import world.bentobox.challenges.panel.util.SingleAdvancementSelector; +import world.bentobox.challenges.panel.util.SingleAdvancementSelector.Mode; import world.bentobox.challenges.utils.Constants; import world.bentobox.challenges.utils.Utils; @@ -60,10 +61,10 @@ public class EditChallengePanel extends CommonPanel { * @param challenge - challenge that needs editing */ private EditChallengePanel(ChallengesAddon addon, User user, World world, String topLabel, String permissionPrefix, - Challenge challenge) { - super(addon, user, world, topLabel, permissionPrefix); - this.challenge = challenge; - this.currentMenuType = MenuType.PROPERTIES; + Challenge challenge) { + super(addon, user, world, topLabel, permissionPrefix); + this.challenge = challenge; + this.currentMenuType = MenuType.PROPERTIES; } /** @@ -71,10 +72,10 @@ private EditChallengePanel(ChallengesAddon addon, User user, World world, String * @param challenge challenge that needs editing. */ private EditChallengePanel(CommonPanel panel, Challenge challenge) { - super(panel); - this.challenge = challenge; - // Default panel should be Properties. - this.currentMenuType = MenuType.PROPERTIES; + super(panel); + this.challenge = challenge; + // Default panel should be Properties. + this.currentMenuType = MenuType.PROPERTIES; } /** @@ -88,8 +89,8 @@ private EditChallengePanel(CommonPanel panel, Challenge challenge) { * @param challenge - challenge that needs editing */ public static void open(ChallengesAddon addon, User user, World world, String topLabel, String permissionPrefix, - Challenge challenge) { - new EditChallengePanel(addon, user, world, topLabel, permissionPrefix, challenge).build(); + Challenge challenge) { + new EditChallengePanel(addon, user, world, topLabel, permissionPrefix, challenge).build(); } /** @@ -99,7 +100,7 @@ public static void open(ChallengesAddon addon, User user, World world, String to * @param challenge - challenge that needs editing */ public static void open(CommonPanel panel, Challenge challenge) { - new EditChallengePanel(panel, challenge).build(); + new EditChallengePanel(panel, challenge).build(); } // --------------------------------------------------------------------- @@ -111,37 +112,37 @@ public static void open(CommonPanel panel, Challenge challenge) { */ @Override protected void build() { - PanelBuilder panelBuilder = new PanelBuilder().user(this.user).name(this.user - .getTranslation(Constants.TITLE + "edit-challenge", "[challenge]", this.challenge.getFriendlyName())); - - PanelUtils.fillBorder(panelBuilder); - - panelBuilder.item(2, this.createMenuButton(MenuType.PROPERTIES)); - panelBuilder.item(4, this.createMenuButton(MenuType.REQUIREMENTS)); - panelBuilder.item(6, this.createMenuButton(MenuType.REWARDS)); - - if (this.currentMenuType.equals(MenuType.PROPERTIES)) { - this.buildMainPropertiesPanel(panelBuilder); - } else if (this.currentMenuType.equals(MenuType.REQUIREMENTS)) { - switch (this.challenge.getChallengeType()) { - case INVENTORY_TYPE -> this.buildInventoryRequirementsPanel(panelBuilder); - case ISLAND_TYPE -> this.buildIslandRequirementsPanel(panelBuilder); - case OTHER_TYPE -> this.buildOtherRequirementsPanel(panelBuilder); - case STATISTIC_TYPE -> this.buildStatisticRequirementsPanel(panelBuilder); - } - } else if (this.currentMenuType.equals(MenuType.REWARDS)) { - this.buildRewardsPanel(panelBuilder); - } - - panelBuilder.item(44, this.returnButton); - - // Every time when this GUI is build, save challenge - // This will ensure that all main things will be always stored - this.addon.getChallengesManager().saveChallenge(this.challenge); - // If for some reason challenge is not loaded, do it. - this.addon.getChallengesManager().loadChallenge(this.challenge, this.world, false, null, true); - - panelBuilder.build(); + PanelBuilder panelBuilder = new PanelBuilder().user(this.user).name(this.user + .getTranslation(Constants.TITLE + "edit-challenge", "[challenge]", this.challenge.getFriendlyName())); + + PanelUtils.fillBorder(panelBuilder); + + panelBuilder.item(2, this.createMenuButton(MenuType.PROPERTIES)); + panelBuilder.item(4, this.createMenuButton(MenuType.REQUIREMENTS)); + panelBuilder.item(6, this.createMenuButton(MenuType.REWARDS)); + + if (this.currentMenuType.equals(MenuType.PROPERTIES)) { + this.buildMainPropertiesPanel(panelBuilder); + } else if (this.currentMenuType.equals(MenuType.REQUIREMENTS)) { + switch (this.challenge.getChallengeType()) { + case INVENTORY_TYPE -> this.buildInventoryRequirementsPanel(panelBuilder); + case ISLAND_TYPE -> this.buildIslandRequirementsPanel(panelBuilder); + case OTHER_TYPE -> this.buildOtherRequirementsPanel(panelBuilder); + case STATISTIC_TYPE -> this.buildStatisticRequirementsPanel(panelBuilder); + } + } else if (this.currentMenuType.equals(MenuType.REWARDS)) { + this.buildRewardsPanel(panelBuilder); + } + + panelBuilder.item(44, this.returnButton); + + // Every time when this GUI is build, save challenge + // This will ensure that all main things will be always stored + this.addon.getChallengesManager().saveChallenge(this.challenge); + // If for some reason challenge is not loaded, do it. + this.addon.getChallengesManager().loadChallenge(this.challenge, this.world, false, null, true); + + panelBuilder.build(); } /** @@ -150,17 +151,18 @@ protected void build() { * @param panelBuilder PanelBuilder where icons must be added. */ private void buildMainPropertiesPanel(PanelBuilder panelBuilder) { - panelBuilder.listener(new IconChanger()); + panelBuilder.listener(new IconChanger()); - panelBuilder.item(10, this.createButton(Button.NAME)); - panelBuilder.item(16, this.createButton(Button.DEPLOYED)); + panelBuilder.item(10, this.createButton(Button.NAME)); + panelBuilder.item(13, this.createButton(Button.HIDE_REWARD_ITEMS)); + panelBuilder.item(16, this.createButton(Button.DEPLOYED)); - panelBuilder.item(19, this.createButton(Button.ICON)); - panelBuilder.item(22, this.createButton(Button.DESCRIPTION)); - panelBuilder.item(25, this.createButton(Button.ORDER)); + panelBuilder.item(19, this.createButton(Button.ICON)); + panelBuilder.item(22, this.createButton(Button.DESCRIPTION)); + panelBuilder.item(25, this.createButton(Button.ORDER)); - panelBuilder.item(28, this.createButton(Button.ENVIRONMENT)); - panelBuilder.item(31, this.createButton(Button.REMOVE_ON_COMPLETE)); + panelBuilder.item(28, this.createButton(Button.ENVIRONMENT)); + panelBuilder.item(31, this.createButton(Button.REMOVE_ON_COMPLETE)); } /** @@ -170,14 +172,17 @@ private void buildMainPropertiesPanel(PanelBuilder panelBuilder) { * @param panelBuilder PanelBuilder where icons must be added. */ private void buildIslandRequirementsPanel(PanelBuilder panelBuilder) { - panelBuilder.item(19, this.createRequirementButton(RequirementButton.REQUIRED_ENTITIES)); - panelBuilder.item(28, this.createRequirementButton(RequirementButton.REMOVE_ENTITIES)); + panelBuilder.item(19, this.createRequirementButton(RequirementButton.REQUIRED_ENTITIES)); + panelBuilder.item(20, this.createRequirementButton(RequirementButton.REQUIRED_ENTITYTAGS)); + panelBuilder.item(28, this.createRequirementButton(RequirementButton.REMOVE_ENTITIES)); - panelBuilder.item(21, this.createRequirementButton(RequirementButton.REQUIRED_BLOCKS)); - panelBuilder.item(30, this.createRequirementButton(RequirementButton.REMOVE_BLOCKS)); + panelBuilder.item(21, this.createRequirementButton(RequirementButton.REQUIRED_BLOCKS)); + panelBuilder.item(22, this.createRequirementButton(RequirementButton.REQUIRED_MATERIALTAGS)); + panelBuilder.item(30, this.createRequirementButton(RequirementButton.REMOVE_BLOCKS)); - panelBuilder.item(23, this.createRequirementButton(RequirementButton.SEARCH_RADIUS)); - panelBuilder.item(25, this.createRequirementButton(RequirementButton.REQUIRED_PERMISSIONS)); + + panelBuilder.item(23, this.createRequirementButton(RequirementButton.SEARCH_RADIUS)); + panelBuilder.item(25, this.createRequirementButton(RequirementButton.REQUIRED_PERMISSIONS)); } /** @@ -187,18 +192,18 @@ private void buildIslandRequirementsPanel(PanelBuilder panelBuilder) { * @param panelBuilder PanelBuilder where icons must be added. */ private void buildInventoryRequirementsPanel(PanelBuilder panelBuilder) { - panelBuilder.item(10, this.createRequirementButton(RequirementButton.REQUIRED_ITEMS)); - panelBuilder.item(19, this.createRequirementButton(RequirementButton.REMOVE_ITEMS)); + panelBuilder.item(10, this.createRequirementButton(RequirementButton.REQUIRED_ITEMS)); + panelBuilder.item(19, this.createRequirementButton(RequirementButton.REMOVE_ITEMS)); - if (!this.challenge.getRequirements().getRequiredItems().isEmpty()) { - panelBuilder.item(12, this.createRequirementButton(RequirementButton.ADD_IGNORED_META)); + if (!this.challenge.getRequirements().getRequiredItems().isEmpty()) { + panelBuilder.item(12, this.createRequirementButton(RequirementButton.ADD_IGNORED_META)); - if (!this.challenge.getRequirements().getIgnoreMetaData().isEmpty()) { - panelBuilder.item(21, this.createRequirementButton(RequirementButton.REMOVE_IGNORED_META)); - } - } + if (!this.challenge.getRequirements().getIgnoreMetaData().isEmpty()) { + panelBuilder.item(21, this.createRequirementButton(RequirementButton.REMOVE_IGNORED_META)); + } + } - panelBuilder.item(25, this.createRequirementButton(RequirementButton.REQUIRED_PERMISSIONS)); + panelBuilder.item(25, this.createRequirementButton(RequirementButton.REQUIRED_PERMISSIONS)); } /** @@ -208,15 +213,18 @@ private void buildInventoryRequirementsPanel(PanelBuilder panelBuilder) { * @param panelBuilder PanelBuilder where icons must be added. */ private void buildOtherRequirementsPanel(PanelBuilder panelBuilder) { - panelBuilder.item(10, this.createRequirementButton(RequirementButton.REQUIRED_EXPERIENCE)); - panelBuilder.item(19, this.createRequirementButton(RequirementButton.REMOVE_EXPERIENCE)); + panelBuilder.item(10, this.createRequirementButton(RequirementButton.REQUIRED_EXPERIENCE)); + panelBuilder.item(19, this.createRequirementButton(RequirementButton.REMOVE_EXPERIENCE)); + + panelBuilder.item(12, this.createRequirementButton(RequirementButton.REQUIRED_MONEY)); + panelBuilder.item(21, this.createRequirementButton(RequirementButton.REMOVE_MONEY)); - panelBuilder.item(12, this.createRequirementButton(RequirementButton.REQUIRED_MONEY)); - panelBuilder.item(21, this.createRequirementButton(RequirementButton.REMOVE_MONEY)); + panelBuilder.item(14, this.createRequirementButton(RequirementButton.REQUIRED_PAPI)); + panelBuilder.item(23, this.createRequirementButton(RequirementButton.REQUIRED_LEVEL)); - panelBuilder.item(23, this.createRequirementButton(RequirementButton.REQUIRED_LEVEL)); + panelBuilder.item(16, this.createRequirementButton(RequirementButton.REQUIRED_ADVANCEMENTS)); - panelBuilder.item(25, this.createRequirementButton(RequirementButton.REQUIRED_PERMISSIONS)); + panelBuilder.item(25, this.createRequirementButton(RequirementButton.REQUIRED_PERMISSIONS)); } /** @@ -226,24 +234,8 @@ private void buildOtherRequirementsPanel(PanelBuilder panelBuilder) { * @param panelBuilder PanelBuilder where icons must be added. */ private void buildStatisticRequirementsPanel(PanelBuilder panelBuilder) { - panelBuilder.item(10, this.createRequirementButton(RequirementButton.STATISTIC)); - panelBuilder.item(19, this.createRequirementButton(RequirementButton.REMOVE_STATISTIC)); - - panelBuilder.item(11, this.createRequirementButton(RequirementButton.STATISTIC_AMOUNT)); - - StatisticRequirements requirements = this.challenge.getRequirements(); - - if (requirements.getStatistic() != null) { - switch (requirements.getStatistic().getType()) { - case ITEM -> panelBuilder.item(13, this.createRequirementButton(RequirementButton.STATISTIC_ITEMS)); - case BLOCK -> panelBuilder.item(13, this.createRequirementButton(RequirementButton.STATISTIC_BLOCKS)); - case ENTITY -> panelBuilder.item(13, this.createRequirementButton(RequirementButton.STATISTIC_ENTITIES)); - default -> { - } - } - } - - panelBuilder.item(25, this.createRequirementButton(RequirementButton.REQUIRED_PERMISSIONS)); + // Give user the ability to add or remove statistics + panelBuilder.item(10, this.createRequirementButton(RequirementButton.REQUIRED_STATISTICS)); } /** @@ -252,34 +244,34 @@ private void buildStatisticRequirementsPanel(PanelBuilder panelBuilder) { * @param panelBuilder PanelBuilder where icons must be added. */ private void buildRewardsPanel(PanelBuilder panelBuilder) { - panelBuilder.item(10, this.createRewardButton(RewardButton.REWARD_TEXT)); - panelBuilder.item(19, this.createRewardButton(RewardButton.REWARD_COMMANDS)); + panelBuilder.item(10, this.createRewardButton(RewardButton.REWARD_TEXT)); + panelBuilder.item(19, this.createRewardButton(RewardButton.REWARD_COMMANDS)); - panelBuilder.item(11, this.createRewardButton(RewardButton.REWARD_ITEMS)); - panelBuilder.item(20, this.createRewardButton(RewardButton.REWARD_EXPERIENCE)); - panelBuilder.item(29, this.createRewardButton(RewardButton.REWARD_MONEY)); + panelBuilder.item(11, this.createRewardButton(RewardButton.REWARD_ITEMS)); + panelBuilder.item(20, this.createRewardButton(RewardButton.REWARD_EXPERIENCE)); + panelBuilder.item(29, this.createRewardButton(RewardButton.REWARD_MONEY)); - panelBuilder.item(22, this.createRewardButton(RewardButton.REPEATABLE)); + panelBuilder.item(22, this.createRewardButton(RewardButton.REPEATABLE)); - if (!this.challenge.getRewardItems().isEmpty() || !this.challenge.getRepeatItemReward().isEmpty()) { - panelBuilder.item(31, this.createRewardButton(RewardButton.ADD_IGNORED_META)); - } + if (!this.challenge.getRewardItems().isEmpty() || !this.challenge.getRepeatItemReward().isEmpty()) { + panelBuilder.item(31, this.createRewardButton(RewardButton.ADD_IGNORED_META)); + } - if (!this.challenge.getIgnoreRewardMetaData().isEmpty()) { - panelBuilder.item(32, this.createRewardButton(RewardButton.REMOVE_IGNORED_META)); - } + if (!this.challenge.getIgnoreRewardMetaData().isEmpty()) { + panelBuilder.item(32, this.createRewardButton(RewardButton.REMOVE_IGNORED_META)); + } - if (this.challenge.isRepeatable()) { - panelBuilder.item(13, this.createRewardButton(RewardButton.COOL_DOWN)); - panelBuilder.item(23, this.createRewardButton(RewardButton.REPEAT_COUNT)); + if (this.challenge.isRepeatable()) { + panelBuilder.item(13, this.createRewardButton(RewardButton.COOL_DOWN)); + panelBuilder.item(23, this.createRewardButton(RewardButton.REPEAT_COUNT)); - panelBuilder.item(15, this.createRewardButton(RewardButton.REPEAT_REWARD_TEXT)); - panelBuilder.item(24, this.createRewardButton(RewardButton.REPEAT_REWARD_COMMANDS)); + panelBuilder.item(15, this.createRewardButton(RewardButton.REPEAT_REWARD_TEXT)); + panelBuilder.item(24, this.createRewardButton(RewardButton.REPEAT_REWARD_COMMANDS)); - panelBuilder.item(16, this.createRewardButton(RewardButton.REPEAT_REWARD_ITEMS)); - panelBuilder.item(25, this.createRewardButton(RewardButton.REPEAT_REWARD_EXPERIENCE)); - panelBuilder.item(34, this.createRewardButton(RewardButton.REPEAT_REWARD_MONEY)); - } + panelBuilder.item(16, this.createRewardButton(RewardButton.REPEAT_REWARD_ITEMS)); + panelBuilder.item(25, this.createRewardButton(RewardButton.REPEAT_REWARD_EXPERIENCE)); + panelBuilder.item(34, this.createRewardButton(RewardButton.REPEAT_REWARD_MONEY)); + } } // --------------------------------------------------------------------- @@ -293,58 +285,57 @@ private void buildRewardsPanel(PanelBuilder panelBuilder) { * @return PanelItem that represents given menu type. */ private PanelItem createMenuButton(MenuType menuType) { - final String reference = Constants.BUTTON + menuType.name().toLowerCase() + "."; - - final String name = this.user.getTranslation(reference + "name"); - final List description = new ArrayList<>(3); - description.add(this.user.getTranslation(reference + "description")); - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-select")); - - ItemStack icon; - boolean glow; - PanelItem.ClickHandler clickHandler; - - switch (menuType) { - case PROPERTIES -> { - icon = new ItemStack(Material.CRAFTING_TABLE); - clickHandler = (panel, user, clickType, slot) -> { - this.currentMenuType = MenuType.PROPERTIES; - this.build(); - - return true; - }; - glow = this.currentMenuType.equals(MenuType.PROPERTIES); - } - case REQUIREMENTS -> { - icon = new ItemStack(Material.HOPPER); - clickHandler = (panel, user, clickType, slot) -> { - this.currentMenuType = MenuType.REQUIREMENTS; - this.build(); - - return true; - }; - glow = this.currentMenuType.equals(MenuType.REQUIREMENTS); - } - case REWARDS -> { - icon = new ItemStack(Material.DROPPER); - clickHandler = (panel, user, clickType, slot) -> { - this.currentMenuType = MenuType.REWARDS; - this.build(); - - return true; - }; - glow = this.currentMenuType.equals(MenuType.REWARDS); - } - default -> { - icon = new ItemStack(Material.PAPER); - clickHandler = null; - glow = false; - } - } - - return new PanelItemBuilder().icon(icon).name(name).description(description).glow(glow) - .clickHandler(clickHandler).build(); + final String reference = Constants.BUTTON + menuType.name().toLowerCase() + "."; + + final String name = this.user.getTranslation(reference + "name"); + final List description = new ArrayList<>(3); + description.add(this.user.getTranslation(reference + "description")); + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-select")); + + ItemStack icon; + boolean glow; + PanelItem.ClickHandler clickHandler; + + switch (menuType) { + case PROPERTIES -> { + icon = new ItemStack(Material.CRAFTING_TABLE); + clickHandler = (panel, user, clickType, slot) -> { + this.currentMenuType = MenuType.PROPERTIES; + this.build(); + + return true; + }; + glow = this.currentMenuType.equals(MenuType.PROPERTIES); + } + case REQUIREMENTS -> { + icon = new ItemStack(Material.HOPPER); + clickHandler = (panel, user, clickType, slot) -> { + this.currentMenuType = MenuType.REQUIREMENTS; + this.build(); + + return true; + }; + glow = this.currentMenuType.equals(MenuType.REQUIREMENTS); + } + case REWARDS -> { + icon = new ItemStack(Material.DROPPER); + clickHandler = (panel, user, clickType, slot) -> { + this.currentMenuType = MenuType.REWARDS; + this.build(); + + return true; + }; + glow = this.currentMenuType.equals(MenuType.REWARDS); + } + default -> { + icon = new ItemStack(Material.PAPER); + clickHandler = null; + glow = false; + } + } + return new PanelItemBuilder().icon(icon).name(name).description(description).glow(glow) + .clickHandler(clickHandler).build(); } /** @@ -354,204 +345,228 @@ private PanelItem createMenuButton(MenuType menuType) { * @return PanelItem that represents given button. */ private PanelItem createButton(Button button) { - final String reference = Constants.BUTTON + button.name().toLowerCase() + "."; - - final String name = this.user.getTranslation(reference + "name"); - final List description = new ArrayList<>(3); - description.add(this.user.getTranslation(reference + "description")); - - ItemStack icon; - boolean glow; - PanelItem.ClickHandler clickHandler; - - switch (button) { - case NAME -> { - description.add(this.user.getTranslation(reference + "value", Constants.PARAMETER_NAME, - this.challenge.getFriendlyName())); - - icon = new ItemStack(Material.NAME_TAG); - - clickHandler = (panel, user, clickType, i) -> { - // Create consumer that process description change - Consumer consumer = value -> { - if (value != null) { - this.challenge.setFriendlyName(value); - } - - this.build(); - }; - - // start conversation - ConversationUtils.createStringInput(consumer, user, - user.getTranslation(Constants.CONVERSATIONS + "write-name"), - user.getTranslation(Constants.CONVERSATIONS + "name-changed")); - - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); - } - case DEPLOYED -> { - description - .add(this.user.getTranslation(reference + (this.challenge.isDeployed() ? "enabled" : "disabled"))); - - icon = new ItemStack(Material.LEVER); - clickHandler = (panel, user, clickType, slot) -> { - if (this.challenge.isValid()) { - this.challenge.setDeployed(!this.challenge.isDeployed()); - } else { - Utils.sendMessage(this.user, this.world, Constants.CONVERSATIONS + "invalid-challenge", - Constants.PARAMETER_CHALLENGE, this.challenge.getFriendlyName()); - this.challenge.setDeployed(false); - } - - this.build(); - return true; - }; - glow = this.challenge.isDeployed(); - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-toggle")); - } - case ICON -> { - icon = this.challenge.getIcon(); - clickHandler = (panel, user, clickType, i) -> { - this.selectedButton = button; - this.build(); - return true; - }; - glow = this.selectedButton == button; - - if (this.selectedButton != button) { - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); - } else { - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-on-item")); - } - } - case DESCRIPTION -> { - icon = new ItemStack(Material.WRITTEN_BOOK); - - description.add(this.user.getTranslation(reference + "value")); - this.challenge.getDescription().forEach(line -> description.add(Util.translateColorCodes(line))); - - clickHandler = (panel, user, clickType, i) -> { - // Create consumer that process description change - Consumer> consumer = value -> { - if (value != null) { - this.challenge.setDescription(value); - } - - this.build(); - }; - - if (!this.challenge.getDescription().isEmpty() && clickType.isShiftClick()) { - // Reset to the empty value - consumer.accept(Collections.emptyList()); - } else { - // start conversation - ConversationUtils.createStringListInput(consumer, user, - user.getTranslation(Constants.CONVERSATIONS + "write-description"), - user.getTranslation(Constants.CONVERSATIONS + "description-changed")); - } - - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); - - if (!this.challenge.getDescription().isEmpty()) { - description.add(this.user.getTranslation(Constants.TIPS + "shift-click-to-reset")); - } - } - case ORDER -> { - description.add(this.user.getTranslation(reference + "value", Constants.PARAMETER_NUMBER, - String.valueOf(this.challenge.getOrder()))); - - icon = new ItemStack(Material.HOPPER, Math.max(1, this.challenge.getOrder())); - clickHandler = (panel, user, clickType, i) -> { - Consumer numberConsumer = number -> { - if (number != null) { - this.challenge.setOrder(number.intValue()); - } - - // reopen panel - this.build(); - }; - - ConversationUtils.createNumericInput(numberConsumer, this.user, - this.user.getTranslation(Constants.CONVERSATIONS + "input-number"), 0, 2000); - - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); - } - case ENVIRONMENT -> { - description.add(this.user.getTranslation( - this.challenge.getEnvironment().contains(World.Environment.NORMAL) ? reference + "enabled" - : reference + "disabled") - + Utils.prettifyObject(World.Environment.NORMAL, this.user)); - description.add(this.user.getTranslation( - this.challenge.getEnvironment().contains(World.Environment.NETHER) ? reference + "enabled" - : reference + "disabled") - + Utils.prettifyObject(World.Environment.NETHER, this.user)); - description.add(this.user.getTranslation( - this.challenge.getEnvironment().contains(World.Environment.THE_END) ? reference + "enabled" - : reference + "disabled") - + Utils.prettifyObject(World.Environment.THE_END, this.user)); - - icon = new ItemStack(Material.DROPPER); - clickHandler = (panel, user, clickType, slot) -> { - EnvironmentSelector.open(this.user, this.challenge.getEnvironment(), (status, value) -> { - if (status) { - this.challenge.setEnvironment(value); - } - - this.build(); - }); - - return true; - }; - glow = false; - } - case REMOVE_ON_COMPLETE -> { - description.add(this.user - .getTranslation(reference + (this.challenge.isRemoveWhenCompleted() ? "enabled" : "disabled"))); - - if (this.challenge.isRemoveWhenCompleted()) { - icon = new ItemStack(Material.LAVA_BUCKET); - } else { - icon = new ItemStack(Material.BUCKET); - } - - clickHandler = (panel, user, clickType, slot) -> { - this.challenge.setRemoveWhenCompleted(!this.challenge.isRemoveWhenCompleted()); - this.build(); - - return true; - }; - glow = this.challenge.isRemoveWhenCompleted(); - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-toggle")); - } - default -> { - icon = new ItemStack(Material.PAPER); - clickHandler = null; - glow = false; - } - } - - return new PanelItemBuilder().icon(icon).name(name).description(description).glow(glow) - .clickHandler(clickHandler).build(); + final String reference = Constants.BUTTON + button.name().toLowerCase() + "."; + + final String name = this.user.getTranslation(reference + "name"); + final List description = new ArrayList<>(3); + description.add(this.user.getTranslation(reference + "description")); + + ItemStack icon; + boolean glow; + PanelItem.ClickHandler clickHandler; + + switch (button) { + case NAME -> { + description.add(this.user.getTranslation(reference + "value", Constants.PARAMETER_NAME, + this.challenge.getFriendlyName())); + + icon = new ItemStack(Material.NAME_TAG); + + clickHandler = (panel, user, clickType, i) -> { + // Create consumer that process description change + Consumer consumer = value -> { + if (value != null) { + this.challenge.setFriendlyName(value); + } + + this.build(); + }; + + // start conversation + ConversationUtils.createStringInput(consumer, user, + user.getTranslation(Constants.CONVERSATIONS + "write-name"), + user.getTranslation(Constants.CONVERSATIONS + "name-changed")); + + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + } + case HIDE_REWARD_ITEMS -> { + description.add(this.user + .getTranslation(reference + (this.challenge.isHideRewardItems() ? "hide" : "show"))); + + icon = new ItemStack(Material.LEVER); + clickHandler = (panel, user, clickType, slot) -> { + if (this.challenge.isValid()) { + this.challenge.setHideRewardItems(!challenge.isHideRewardItems()); // Toggle + } else { + Utils.sendMessage(this.user, this.world, Constants.CONVERSATIONS + "invalid-challenge", + Constants.PARAMETER_CHALLENGE, this.challenge.getFriendlyName()); + this.challenge.setDeployed(false); + } + + this.build(); + return true; + }; + glow = this.challenge.isDeployed(); + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-toggle")); + + } + + case DEPLOYED -> { + description + .add(this.user.getTranslation(reference + (this.challenge.isDeployed() ? "enabled" : "disabled"))); + + icon = new ItemStack(Material.LEVER); + clickHandler = (panel, user, clickType, slot) -> { + if (this.challenge.isValid()) { + this.challenge.setDeployed(!this.challenge.isDeployed()); + } else { + Utils.sendMessage(this.user, this.world, Constants.CONVERSATIONS + "invalid-challenge", + Constants.PARAMETER_CHALLENGE, this.challenge.getFriendlyName()); + this.challenge.setDeployed(false); + } + + this.build(); + return true; + }; + glow = this.challenge.isDeployed(); + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-toggle")); + } + case ICON -> { + icon = this.challenge.getIcon(); + clickHandler = (panel, user, clickType, i) -> { + this.selectedButton = button; + this.build(); + return true; + }; + glow = this.selectedButton == button; + + if (this.selectedButton != button) { + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + } else { + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-on-item")); + } + } + case DESCRIPTION -> { + icon = new ItemStack(Material.WRITTEN_BOOK); + + description.add(this.user.getTranslation(reference + "value")); + this.challenge.getDescription().forEach(line -> description.add(Util.translateColorCodes(line))); + + clickHandler = (panel, user, clickType, i) -> { + // Create consumer that process description change + Consumer> consumer = value -> { + if (value != null) { + this.challenge.setDescription(value); + } + + this.build(); + }; + + if (!this.challenge.getDescription().isEmpty() && clickType.isShiftClick()) { + // Reset to the empty value + consumer.accept(Collections.emptyList()); + } else { + // start conversation + ConversationUtils.createStringListInput(consumer, user, + user.getTranslation(Constants.CONVERSATIONS + "write-description"), + user.getTranslation(Constants.CONVERSATIONS + "description-changed")); + } + + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + + if (!this.challenge.getDescription().isEmpty()) { + description.add(this.user.getTranslation(Constants.TIPS + "shift-click-to-reset")); + } + } + case ORDER -> { + description.add(this.user.getTranslation(reference + "value", Constants.PARAMETER_NUMBER, + String.valueOf(this.challenge.getOrder()))); + + icon = new ItemStack(Material.HOPPER, Math.max(1, this.challenge.getOrder())); + clickHandler = (panel, user, clickType, i) -> { + Consumer numberConsumer = number -> { + if (number != null) { + this.challenge.setOrder(number.intValue()); + } + + // reopen panel + this.build(); + }; + + ConversationUtils.createNumericInput(numberConsumer, this.user, + this.user.getTranslation(Constants.CONVERSATIONS + "input-number"), 0, 2000); + + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + } + case ENVIRONMENT -> { + description.add(this.user.getTranslation( + this.challenge.getEnvironment().contains(World.Environment.NORMAL) ? reference + "enabled" + : reference + "disabled") + + Utils.prettifyObject(World.Environment.NORMAL, this.user)); + description.add(this.user.getTranslation( + this.challenge.getEnvironment().contains(World.Environment.NETHER) ? reference + "enabled" + : reference + "disabled") + + Utils.prettifyObject(World.Environment.NETHER, this.user)); + description.add(this.user.getTranslation( + this.challenge.getEnvironment().contains(World.Environment.THE_END) ? reference + "enabled" + : reference + "disabled") + + Utils.prettifyObject(World.Environment.THE_END, this.user)); + + icon = new ItemStack(Material.DROPPER); + clickHandler = (panel, user, clickType, slot) -> { + EnvironmentSelector.open(this.user, this.challenge.getEnvironment(), (status, value) -> { + if (status) { + this.challenge.setEnvironment(value); + } + + this.build(); + }); + + return true; + }; + glow = false; + } + case REMOVE_ON_COMPLETE -> { + description.add(this.user + .getTranslation(reference + (this.challenge.isRemoveWhenCompleted() ? "enabled" : "disabled"))); + + if (this.challenge.isRemoveWhenCompleted()) { + icon = new ItemStack(Material.LAVA_BUCKET); + } else { + icon = new ItemStack(Material.BUCKET); + } + + clickHandler = (panel, user, clickType, slot) -> { + this.challenge.setRemoveWhenCompleted(!this.challenge.isRemoveWhenCompleted()); + this.build(); + + return true; + }; + glow = this.challenge.isRemoveWhenCompleted(); + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-toggle")); + } + default -> { + icon = new ItemStack(Material.PAPER); + clickHandler = null; + glow = false; + } + } + + return new PanelItemBuilder().icon(icon).name(name).description(description).glow(glow) + .clickHandler(clickHandler).build(); } /** @@ -561,79 +576,80 @@ private PanelItem createButton(Button button) { * @return PanelItem that represents given button. */ private PanelItem createRequirementButton(RequirementButton button) { - switch (button) { - case REQUIRED_PERMISSIONS -> { - String reference = Constants.BUTTON + button.name().toLowerCase() + "."; - - String name = this.user.getTranslation(reference + "name"); - final List description = new ArrayList<>(3); - description.add(this.user.getTranslation(reference + "description")); - - if (this.challenge.getRequirements().getRequiredPermissions().isEmpty()) { - description.add(this.user.getTranslation(reference + "none")); - } else { - description.add(this.user.getTranslation(reference + "title")); - - this.challenge.getRequirements().getRequiredPermissions().forEach(permission -> description - .add(this.user.getTranslation(reference + "permission", "[permission]", permission))); - } - - ItemStack icon = new ItemStack(Material.REDSTONE_LAMP); - - PanelItem.ClickHandler clickHandler = (panel, user, clickType, i) -> { - // Create consumer that process description change - Consumer> consumer = value -> { - if (value != null) { - this.challenge.getRequirements().setRequiredPermissions(new HashSet<>(value)); - } - - this.build(); - }; - - if (!this.challenge.getRequirements().getRequiredPermissions().isEmpty() && clickType.isShiftClick()) { - // Reset to the empty value - consumer.accept(Collections.emptyList()); - } else { - // start conversation - ConversationUtils.createStringListInput(consumer, user, - user.getTranslation(Constants.CONVERSATIONS + "write-permissions"), - user.getTranslation(Constants.CONVERSATIONS + "permissions-changed")); - } - - return true; - }; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); - - if (!this.challenge.getRequirements().getRequiredPermissions().isEmpty()) { - description.add(this.user.getTranslation(Constants.TIPS + "shift-click-to-reset")); - } - - return new PanelItemBuilder().icon(icon).name(name).description(description).clickHandler(clickHandler) - .build(); - } - // Buttons for Island Requirements - case REQUIRED_ENTITIES, REMOVE_ENTITIES, REQUIRED_BLOCKS, REMOVE_BLOCKS, SEARCH_RADIUS -> { - return this.createIslandRequirementButton(button); - } - // Buttons for Inventory Requirements - case REQUIRED_ITEMS, REMOVE_ITEMS, ADD_IGNORED_META, REMOVE_IGNORED_META -> { - return this.createInventoryRequirementButton(button); - } - // Buttons for Other Requirements - case REQUIRED_EXPERIENCE, REMOVE_EXPERIENCE, REQUIRED_LEVEL, REQUIRED_MONEY, REMOVE_MONEY -> { - return this.createOtherRequirementButton(button); - } - // Buttons for Statistic Requirements - case STATISTIC, STATISTIC_BLOCKS, STATISTIC_ITEMS, STATISTIC_ENTITIES, STATISTIC_AMOUNT, REMOVE_STATISTIC -> { - return this.createStatisticRequirementButton(button); - } - // Default behaviour. - default -> { - return PanelItem.empty(); - } - } + switch (button) { + case REQUIRED_PERMISSIONS -> { + String reference = Constants.BUTTON + button.name().toLowerCase() + "."; + + String name = this.user.getTranslation(reference + "name"); + final List description = new ArrayList<>(3); + description.add(this.user.getTranslation(reference + "description")); + + if (this.challenge.getRequirements().getRequiredPermissions().isEmpty()) { + description.add(this.user.getTranslation(reference + "none")); + } else { + description.add(this.user.getTranslation(reference + "title")); + + this.challenge.getRequirements().getRequiredPermissions().forEach(permission -> description + .add(this.user.getTranslation(reference + "permission", "[permission]", permission))); + } + + ItemStack icon = new ItemStack(Material.REDSTONE_LAMP); + + PanelItem.ClickHandler clickHandler = (panel, user, clickType, i) -> { + // Create consumer that process description change + Consumer> consumer = value -> { + if (value != null) { + this.challenge.getRequirements().setRequiredPermissions(new HashSet<>(value)); + } + + this.build(); + }; + + if (!this.challenge.getRequirements().getRequiredPermissions().isEmpty() && clickType.isShiftClick()) { + // Reset to the empty value + consumer.accept(Collections.emptyList()); + } else { + // start conversation + ConversationUtils.createStringListInput(consumer, user, + user.getTranslation(Constants.CONVERSATIONS + "write-permissions"), + user.getTranslation(Constants.CONVERSATIONS + "permissions-changed")); + } + + return true; + }; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + + if (!this.challenge.getRequirements().getRequiredPermissions().isEmpty()) { + description.add(this.user.getTranslation(Constants.TIPS + "shift-click-to-reset")); + } + return new PanelItemBuilder().icon(icon).name(name).description(description).clickHandler(clickHandler) + .build(); + } + // Buttons for Island Requirements + case REQUIRED_ENTITIES, REMOVE_ENTITIES, REQUIRED_BLOCKS, REMOVE_BLOCKS, SEARCH_RADIUS, + REQUIRED_MATERIALTAGS, REQUIRED_ENTITYTAGS -> { + return this.createIslandRequirementButton(button); + } + // Buttons for Inventory Requirements + case REQUIRED_ITEMS, REMOVE_ITEMS, ADD_IGNORED_META, REMOVE_IGNORED_META -> { + return this.createInventoryRequirementButton(button); + } + // Buttons for Other Requirements + case REQUIRED_EXPERIENCE, REMOVE_EXPERIENCE, REQUIRED_LEVEL, REQUIRED_MONEY, REMOVE_MONEY, REQUIRED_PAPI, + REQUIRED_ADVANCEMENTS -> { + return this.createOtherRequirementButton(button); + } + // Statistics + case REQUIRED_STATISTICS -> { + return this.createStatisticsRequirementButton(button); + } + // Default behaviour. + default -> { + return PanelItem.empty(); + } + } } /** @@ -643,127 +659,170 @@ private PanelItem createRequirementButton(RequirementButton button) { * @return PanelItem that represents given button. */ private PanelItem createIslandRequirementButton(RequirementButton button) { - final String reference = Constants.BUTTON + button.name().toLowerCase() + "."; - - final String name = this.user.getTranslation(reference + "name"); - final List description = new ArrayList<>(3); - description.add(this.user.getTranslation(reference + "description")); - - ItemStack icon; - boolean glow; - PanelItem.ClickHandler clickHandler; - - final IslandRequirements requirements = this.challenge.getRequirements(); - - switch (button) { - case REQUIRED_ENTITIES -> { - if (requirements.getRequiredEntities().isEmpty()) { - description.add(this.user.getTranslation(reference + "none")); - } else { - description.add(this.user.getTranslation(reference + "title")); - - requirements.getRequiredEntities().forEach( - (entity, count) -> description.add(this.user.getTranslation(reference + "list", "[entity]", - Utils.prettifyObject(entity, this.user), "[number]", String.valueOf(count)))); - } - - icon = new ItemStack(Material.CREEPER_HEAD); - clickHandler = (panel, user, clickType, slot) -> { - ManageEntitiesPanel.open(this, requirements.getRequiredEntities()); - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); - } - case REMOVE_ENTITIES -> { - description.add( - this.user.getTranslation(reference + (requirements.isRemoveEntities() ? "enabled" : "disabled"))); - - icon = new ItemStack(Material.LEVER); - clickHandler = (panel, user, clickType, slot) -> { - requirements.setRemoveEntities(!requirements.isRemoveEntities()); - this.build(); - return true; - }; - glow = requirements.isRemoveEntities(); - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-toggle")); - } - case REQUIRED_BLOCKS -> { - if (requirements.getRequiredBlocks().isEmpty()) { - description.add(this.user.getTranslation(reference + "none")); - } else { - description.add(this.user.getTranslation(reference + "title")); - - requirements.getRequiredBlocks() - .forEach((block, count) -> description.add(this.user.getTranslation(reference + "list", - "[block]", Utils.prettifyObject(block, this.user), "[number]", String.valueOf(count)))); - } - - icon = new ItemStack(Material.STONE); - clickHandler = (panel, user, clickType, slot) -> { - ManageBlocksPanel.open(this, requirements.getRequiredBlocks()); - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); - } - case REMOVE_BLOCKS -> { - description.add( - this.user.getTranslation(reference + (requirements.isRemoveBlocks() ? "enabled" : "disabled"))); - - icon = new ItemStack(Material.LEVER); - clickHandler = (panel, user, clickType, slot) -> { - requirements.setRemoveBlocks(!requirements.isRemoveBlocks()); - this.build(); - return true; - }; - glow = requirements.isRemoveBlocks(); - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-toggle")); - } - case SEARCH_RADIUS -> { - description.add(this.user.getTranslation(reference + "value", Constants.PARAMETER_NUMBER, - String.valueOf(requirements.getSearchRadius()))); - icon = new ItemStack(Material.COBBLESTONE_WALL); - clickHandler = (panel, user, clickType, i) -> { - Consumer numberConsumer = number -> { - if (number != null) { - requirements.setSearchRadius(number.intValue()); - } - - // reopen panel - this.build(); - }; - - int maxSearchDistance = this.addon.getPlugin().getIWM().getAddon(this.world) - .map(gameModeAddon -> gameModeAddon.getWorldSettings().getIslandDistance()).orElse(100); - - ConversationUtils.createNumericInput(numberConsumer, this.user, - this.user.getTranslation(Constants.CONVERSATIONS + "input-number"), 1, maxSearchDistance); - - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); - } - default -> { - icon = new ItemStack(Material.PAPER); - clickHandler = null; - glow = false; - } - } - - return new PanelItemBuilder().icon(icon).name(name).description(description).glow(glow) - .clickHandler(clickHandler).build(); + final String reference = Constants.BUTTON + button.name().toLowerCase() + "."; + + final String name = this.user.getTranslation(reference + "name"); + final List description = new ArrayList<>(3); + description.add(this.user.getTranslation(reference + "description")); + + ItemStack icon; + boolean glow; + PanelItem.ClickHandler clickHandler; + + final IslandRequirements requirements = this.challenge.getRequirements(); + + switch (button) { + case REQUIRED_ENTITIES -> { + if (requirements.getRequiredEntities().isEmpty()) { + description.add(this.user.getTranslation(reference + "none")); + } else { + description.add(this.user.getTranslation(reference + "title")); + + requirements.getRequiredEntities().forEach( + (entity, count) -> description.add(this.user.getTranslation(reference + "list", "[entity]", + Utils.prettifyObject(entity, this.user), "[number]", String.valueOf(count)))); + } + + icon = new ItemStack(Material.CREEPER_HEAD); + clickHandler = (panel, user, clickType, slot) -> { + ManageEntitiesPanel.open(this, requirements.getRequiredEntities()); + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + } + case REMOVE_ENTITIES -> { + description.add( + this.user.getTranslation(reference + (requirements.isRemoveEntities() ? "enabled" : "disabled"))); + + icon = new ItemStack(Material.LEVER); + clickHandler = (panel, user, clickType, slot) -> { + requirements.setRemoveEntities(!requirements.isRemoveEntities()); + this.build(); + return true; + }; + glow = requirements.isRemoveEntities(); + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-toggle")); + } + + case REQUIRED_MATERIALTAGS -> { + if (requirements.getRequiredMaterialTags().isEmpty()) { + description.add(this.user.getTranslation(reference + "none")); + } else { + description.add(this.user.getTranslation(reference + "title")); + // Add Material Tags only + requirements.getRequiredMaterialTags() + .forEach((block, count) -> description.add(this.user.getTranslation(reference + "list", "[tag]", + Utils.prettifyObject(block, this.user), "[number]", String.valueOf(count)))); + } + + icon = new ItemStack(Material.STONE_BRICKS); + clickHandler = (panel, user, clickType, slot) -> { + ManageBlockGroupsPanel.open(this, requirements.getRequiredMaterialTags()); + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + } + + case REQUIRED_ENTITYTAGS -> { + if (requirements.getRequiredEntityTypeTags().isEmpty()) { + description.add(this.user.getTranslation(reference + "none")); + } else { + description.add(this.user.getTranslation(reference + "title")); + // Add Material Tags only + requirements.getRequiredEntityTypeTags() + .forEach((block, count) -> description.add(this.user.getTranslation(reference + "list", "[tag]", + Utils.prettifyObject(block, this.user), "[number]", String.valueOf(count)))); + } + + icon = new ItemStack(Material.ZOMBIE_HEAD); + clickHandler = (panel, user, clickType, slot) -> { + ManageEntityGroupsPanel.open(this, requirements.getRequiredEntityTypeTags()); + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + } + + case REQUIRED_BLOCKS -> { + if (requirements.getRequiredBlocks().isEmpty()) { + description.add(this.user.getTranslation(reference + "none")); + } else { + description.add(this.user.getTranslation(reference + "title")); + requirements.getRequiredBlocks() + .forEach((block, count) -> description.add(this.user.getTranslation(reference + "list", + "[block]", Utils.prettifyObject(block, this.user), "[number]", String.valueOf(count)))); + } + + icon = new ItemStack(Material.STONE); + clickHandler = (panel, user, clickType, slot) -> { + ManageBlocksPanel.open(this, requirements.getRequiredBlocks()); + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + } + case REMOVE_BLOCKS -> { + description.add( + this.user.getTranslation(reference + (requirements.isRemoveBlocks() ? "enabled" : "disabled"))); + + icon = new ItemStack(Material.LEVER); + clickHandler = (panel, user, clickType, slot) -> { + requirements.setRemoveBlocks(!requirements.isRemoveBlocks()); + this.build(); + return true; + }; + glow = requirements.isRemoveBlocks(); + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-toggle")); + } + case SEARCH_RADIUS -> { + description.add(this.user.getTranslation(reference + "value", Constants.PARAMETER_NUMBER, + String.valueOf(requirements.getSearchRadius()))); + icon = new ItemStack(Material.COBBLESTONE_WALL); + clickHandler = (panel, user, clickType, i) -> { + Consumer numberConsumer = number -> { + if (number != null) { + requirements.setSearchRadius(number.intValue()); + } + + // reopen panel + this.build(); + }; + + int maxSearchDistance = this.addon.getPlugin().getIWM().getAddon(this.world) + .map(gameModeAddon -> gameModeAddon.getWorldSettings().getIslandDistance()).orElse(100); + + ConversationUtils.createNumericInput(numberConsumer, this.user, + this.user.getTranslation(Constants.CONVERSATIONS + "input-number"), 1, maxSearchDistance); + + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + } + default -> { + icon = new ItemStack(Material.PAPER); + clickHandler = null; + glow = false; + } + } + return new PanelItemBuilder().icon(icon).name(name).description(description).glow(glow) + .clickHandler(clickHandler).build(); } /** @@ -772,432 +831,370 @@ private PanelItem createIslandRequirementButton(RequirementButton button) { * @param button Button which panel item must be created. * @return PanelItem that represents given button. */ - private PanelItem createInventoryRequirementButton(RequirementButton button) { - final String reference = Constants.BUTTON + button.name().toLowerCase() + "."; - - final String name = this.user.getTranslation(reference + "name"); - final List description = new ArrayList<>(3); - description.add(this.user.getTranslation(reference + "description")); - - ItemStack icon; - boolean glow; - PanelItem.ClickHandler clickHandler; - - final InventoryRequirements requirements = this.challenge.getRequirements(); - - switch (button) { - case REQUIRED_ITEMS -> { - if (requirements.getRequiredItems().isEmpty()) { - description.add(this.user.getTranslation(reference + "none")); - } else { - description.add(this.user.getTranslation(reference + "title")); - - Utils.groupEqualItems(requirements.getRequiredItems(), requirements.getIgnoreMetaData()).stream() - .sorted(Comparator.comparing(ItemStack::getType)) - .forEach(itemStack -> description.add(this.user.getTranslationOrNothing(reference + "list", - "[number]", String.valueOf(itemStack.getAmount()), "[item]", - Utils.prettifyObject(itemStack, this.user)))); - } - - icon = new ItemStack(Material.CHEST); - clickHandler = (panel, user, clickType, slot) -> { - ItemSelector.open(this.user, requirements.getRequiredItems(), (status, value) -> { - if (status) { - requirements.setRequiredItems(value); - } - - this.build(); - }); - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); - } - case REMOVE_ITEMS -> { - description - .add(this.user.getTranslation(reference + (requirements.isTakeItems() ? "enabled" : "disabled"))); - - icon = new ItemStack(Material.LEVER); - clickHandler = (panel, user, clickType, slot) -> { - requirements.setTakeItems(!requirements.isTakeItems()); - this.build(); - return true; - }; - glow = requirements.isTakeItems(); - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-toggle")); - } - case ADD_IGNORED_META -> { - if (requirements.getIgnoreMetaData().isEmpty()) { - description.add(this.user.getTranslation(reference + "none")); - } else { - description.add(this.user.getTranslation(reference + "title")); - - requirements.getIgnoreMetaData().stream().sorted(Comparator.comparing(Material::name)) - .forEach(itemStack -> description.add(this.user.getTranslationOrNothing(reference + "list", - "[item]", Utils.prettifyObject(itemStack, this.user)))); - } - - icon = new ItemStack(Material.GREEN_SHULKER_BOX); - - clickHandler = (panel, user, clickType, slot) -> { - if (requirements.getRequiredItems().isEmpty()) { - // Do nothing if no requirements are set. - return true; - } - - // Allow choosing only from inventory items. - Set collection = Arrays.stream(Material.values()).collect(Collectors.toSet()); - requirements.getRequiredItems().stream().map(ItemStack::getType).forEach(collection::remove); - collection.addAll(requirements.getIgnoreMetaData()); - - if (Material.values().length == collection.size()) { - // If there are no items anymore, then do not allow opening gui. - return true; - } - - MultiBlockSelector.open(this.user, MultiBlockSelector.Mode.ANY, collection, (status, materials) -> { - if (status) { - materials.addAll(requirements.getIgnoreMetaData()); - requirements.setIgnoreMetaData(new HashSet<>(materials)); - } - - this.build(); - }); - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-add")); - } - case REMOVE_IGNORED_META -> { - icon = new ItemStack(Material.RED_SHULKER_BOX); - - clickHandler = (panel, user, clickType, slot) -> { - if (requirements.getIgnoreMetaData().isEmpty()) { - // Do nothing if no requirements are set. - return true; - } - - // Allow choosing only from inventory items. - Set collection = Arrays.stream(Material.values()).collect(Collectors.toSet()); - collection.removeAll(requirements.getIgnoreMetaData()); - - MultiBlockSelector.open(this.user, MultiBlockSelector.Mode.ANY, collection, (status, materials) -> { - if (status) { - requirements.getIgnoreMetaData().removeAll(materials); - } - - this.build(); - }); - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-remove")); - } - default -> { - icon = new ItemStack(Material.PAPER); - clickHandler = null; - glow = false; - } - } - - return new PanelItemBuilder().icon(icon).name(name).description(description).glow(glow) - .clickHandler(clickHandler).build(); + private PanelItem createStatisticsRequirementButton(RequirementButton button) { + final String reference = Constants.BUTTON + button.name().toLowerCase() + "."; + + final String name = this.user.getTranslation(reference + "name"); + final List description = new ArrayList<>(3); + description.add(this.user.getTranslation(reference + "description")); + + ItemStack icon; + boolean glow; + PanelItem.ClickHandler clickHandler; + + final StatisticRequirements requirements = this.challenge.getRequirements(); + switch (button) { + // Just one special statistic button right now + case REQUIRED_STATISTICS -> { + if (requirements.getRequiredStatistics().isEmpty()) { + description.add(this.user.getTranslation(reference + "none")); + } else { + description.add(this.user.getTranslation(reference + "title")); + + requirements.getRequiredStatistics().stream() + .sorted(Comparator.comparing(r -> r.statistic().getKey().getKey())) + // Just list the name of the statistic + .forEach(sr -> description.add(this.user.getTranslationOrNothing(reference + "list", "[name]", + Utils.prettifyObject(sr.statistic(), user)))); + } + + icon = new ItemStack(Material.CHEST); + clickHandler = (panel, user, clickType, slot) -> { + // Deal with adding and removing statistics in the MultiStatisticSelector class + ManageStatisticsPanel.open(this, requirements.getRequiredStatistics()); + return true; + }; + glow = false; + } + default -> { + // This should never need to be shown. Just for future expansion. + glow = false; + icon = new ItemStack(Material.PAPER); + clickHandler = null; + } + } + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + return new PanelItemBuilder().icon(icon).name(name).description(description).glow(glow) + .clickHandler(clickHandler).build(); } /** - * This method creates buttons for other requirements menu. + * This method creates buttons for inventory requirements menu. * * @param button Button which panel item must be created. * @return PanelItem that represents given button. */ - private PanelItem createOtherRequirementButton(RequirementButton button) { - final String reference = Constants.BUTTON + button.name().toLowerCase() + "."; - - final String name = this.user.getTranslation(reference + "name"); - final List description = new ArrayList<>(3); - description.add(this.user.getTranslation(reference + "description")); - - ItemStack icon; - boolean glow; - PanelItem.ClickHandler clickHandler; - - final OtherRequirements requirements = this.challenge.getRequirements(); - - switch (button) { - case REQUIRED_EXPERIENCE -> { - description.add(this.user.getTranslation(reference + "value", Constants.PARAMETER_NUMBER, - String.valueOf(requirements.getRequiredExperience()))); - icon = new ItemStack(Material.EXPERIENCE_BOTTLE); - clickHandler = (panel, user, clickType, i) -> { - Consumer numberConsumer = number -> { - if (number != null) { - requirements.setRequiredExperience(number.intValue()); - } - - // reopen panel - this.build(); - }; - ConversationUtils.createNumericInput(numberConsumer, this.user, - this.user.getTranslation(Constants.CONVERSATIONS + "input-number"), 0, Integer.MAX_VALUE); - - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); - } - case REMOVE_EXPERIENCE -> { - description.add( - this.user.getTranslation(reference + (requirements.isTakeExperience() ? "enabled" : "disabled"))); - - icon = new ItemStack(Material.LEVER); - clickHandler = (panel, user, clickType, slot) -> { - requirements.setTakeExperience(!requirements.isTakeExperience()); - this.build(); - return true; - }; - glow = requirements.isTakeExperience(); - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-toggle")); - } - case REQUIRED_LEVEL -> { - description.add(this.user.getTranslation(reference + "value", Constants.PARAMETER_NUMBER, - String.valueOf(requirements.getRequiredIslandLevel()))); - icon = new ItemStack(this.addon.isLevelProvided() ? Material.BEACON : Material.BARRIER); - clickHandler = (panel, user, clickType, i) -> { - Consumer numberConsumer = number -> { - if (number != null) { - requirements.setRequiredIslandLevel(number.longValue()); - } - - // reopen panel - this.build(); - }; - ConversationUtils.createNumericInput(numberConsumer, this.user, - this.user.getTranslation(Constants.CONVERSATIONS + "input-number"), 0, Integer.MAX_VALUE); - - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); - } - case REQUIRED_MONEY -> { - description.add(this.user.getTranslation(reference + "value", Constants.PARAMETER_NUMBER, - String.valueOf(requirements.getRequiredMoney()))); - icon = new ItemStack(this.addon.isEconomyProvided() ? Material.GOLD_INGOT : Material.BARRIER); - clickHandler = (panel, user, clickType, i) -> { - Consumer numberConsumer = number -> { - if (number != null) { - requirements.setRequiredMoney(number.doubleValue()); - } - - // reopen panel - this.build(); - }; - ConversationUtils.createNumericInput(numberConsumer, this.user, - this.user.getTranslation(Constants.CONVERSATIONS + "input-number"), 0, Double.MAX_VALUE); - - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); - } - case REMOVE_MONEY -> { - description - .add(this.user.getTranslation(reference + (requirements.isTakeMoney() ? "enabled" : "disabled"))); - - icon = new ItemStack(this.addon.isEconomyProvided() ? Material.LEVER : Material.BARRIER); - clickHandler = (panel, user, clickType, slot) -> { - requirements.setTakeMoney(!requirements.isTakeMoney()); - this.build(); - return true; - }; - glow = requirements.isTakeMoney(); - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-toggle")); - } - default -> { - icon = new ItemStack(Material.PAPER); - clickHandler = null; - glow = false; - } - } - - return new PanelItemBuilder().icon(icon).name(name).description(description).glow(glow) - .clickHandler(clickHandler).build(); + private PanelItem createInventoryRequirementButton(RequirementButton button) { + final String reference = Constants.BUTTON + button.name().toLowerCase() + "."; + + final String name = this.user.getTranslation(reference + "name"); + final List description = new ArrayList<>(3); + description.add(this.user.getTranslation(reference + "description")); + + ItemStack icon; + boolean glow; + PanelItem.ClickHandler clickHandler; + + final InventoryRequirements requirements = this.challenge.getRequirements(); + + switch (button) { + case REQUIRED_ITEMS -> { + if (requirements.getRequiredItems().isEmpty()) { + description.add(this.user.getTranslation(reference + "none")); + } else { + description.add(this.user.getTranslation(reference + "title")); + + Utils.groupEqualItems(requirements.getRequiredItems(), requirements.getIgnoreMetaData()).stream() + .sorted(Comparator.comparing(ItemStack::getType)) + .forEach(itemStack -> description.add(this.user.getTranslationOrNothing(reference + "list", + "[number]", String.valueOf(itemStack.getAmount()), "[item]", + Utils.prettifyObject(itemStack, this.user)))); + } + + icon = new ItemStack(Material.CHEST); + clickHandler = (panel, user, clickType, slot) -> { + ItemSelector.open(this.user, requirements.getRequiredItems(), (status, value) -> { + if (status) { + requirements.setRequiredItems(value); + } + + this.build(); + }); + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + } + case REMOVE_ITEMS -> { + description + .add(this.user.getTranslation(reference + (requirements.isTakeItems() ? "enabled" : "disabled"))); + + icon = new ItemStack(Material.LEVER); + clickHandler = (panel, user, clickType, slot) -> { + requirements.setTakeItems(!requirements.isTakeItems()); + this.build(); + return true; + }; + glow = requirements.isTakeItems(); + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-toggle")); + } + case ADD_IGNORED_META -> { + if (requirements.getIgnoreMetaData().isEmpty()) { + description.add(this.user.getTranslation(reference + "none")); + } else { + description.add(this.user.getTranslation(reference + "title")); + + requirements.getIgnoreMetaData().stream().sorted(Comparator.comparing(Material::name)) + .forEach(itemStack -> description.add(this.user.getTranslationOrNothing(reference + "list", + "[item]", Utils.prettifyObject(itemStack, this.user)))); + } + + icon = new ItemStack(Material.GREEN_SHULKER_BOX); + + clickHandler = (panel, user, clickType, slot) -> { + if (requirements.getRequiredItems().isEmpty()) { + // Do nothing if no requirements are set. + return true; + } + + // Allow choosing only from inventory items. + Set collection = Arrays.stream(Material.values()).collect(Collectors.toSet()); + requirements.getRequiredItems().stream().map(ItemStack::getType).forEach(collection::remove); + collection.addAll(requirements.getIgnoreMetaData()); + + if (Material.values().length == collection.size()) { + // If there are no items anymore, then do not allow opening gui. + return true; + } + + MultiBlockSelector.open(this.user, MultiBlockSelector.Mode.ANY, collection, (status, materials) -> { + if (status) { + materials.addAll(requirements.getIgnoreMetaData()); + requirements.setIgnoreMetaData(new HashSet<>(materials)); + } + + this.build(); + }); + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-add")); + } + case REMOVE_IGNORED_META -> { + icon = new ItemStack(Material.RED_SHULKER_BOX); + + clickHandler = (panel, user, clickType, slot) -> { + if (requirements.getIgnoreMetaData().isEmpty()) { + // Do nothing if no requirements are set. + return true; + } + + // Allow choosing only from inventory items. + Set collection = Arrays.stream(Material.values()).collect(Collectors.toSet()); + collection.removeAll(requirements.getIgnoreMetaData()); + + MultiBlockSelector.open(this.user, MultiBlockSelector.Mode.ANY, collection, (status, materials) -> { + if (status) { + requirements.getIgnoreMetaData().removeAll(materials); + } + + this.build(); + }); + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-remove")); + } + default -> { + icon = new ItemStack(Material.PAPER); + clickHandler = null; + glow = false; + } + } + return new PanelItemBuilder().icon(icon).name(name).description(description).glow(glow) + .clickHandler(clickHandler).build(); } /** - * Creates a button for statistic requirements. + * This method creates buttons for other requirements menu. * - * @param button Button that must be created. - * @return PanelItem button. + * @param button Button which panel item must be created. + * @return PanelItem that represents given button. */ - private PanelItem createStatisticRequirementButton(RequirementButton button) { - final String reference = Constants.BUTTON + button.name().toLowerCase() + "."; - - final String name = this.user.getTranslation(reference + "name"); - final List description = new ArrayList<>(3); - description.add(this.user.getTranslation(reference + "description")); - - ItemStack icon; - boolean glow; - PanelItem.ClickHandler clickHandler; - - final StatisticRequirements requirements = this.challenge.getRequirements(); - - switch (button) { - case STATISTIC -> { - description.add(this.user.getTranslation(reference + "value", "[statistic]", - Utils.prettifyObject(requirements.getStatistic(), this.user))); - - icon = new ItemStack(requirements.getStatistic() == null ? Material.BARRIER : Material.PAPER); - clickHandler = (panel, user, clickType, slot) -> { - StatisticSelector.open(this.user, (status, statistic) -> { - if (status) { - requirements.setStatistic(statistic); - requirements.setMaterial(null); - requirements.setEntity(null); - requirements.setAmount(0); - } - - this.build(); - }); - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); - } - case STATISTIC_AMOUNT -> { - description.add(this.user.getTranslation(reference + "value", Constants.PARAMETER_NUMBER, - String.valueOf(requirements.getAmount()))); - icon = new ItemStack(Material.CHEST); - clickHandler = (panel, user, clickType, i) -> { - Consumer numberConsumer = number -> { - if (number != null) { - requirements.setAmount(number.intValue()); - } - - // reopen panel - this.build(); - }; - ConversationUtils.createNumericInput(numberConsumer, this.user, - this.user.getTranslation(Constants.CONVERSATIONS + "input-number"), 0, Integer.MAX_VALUE); - - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); - } - case REMOVE_STATISTIC -> { - description.add( - this.user.getTranslation(reference + (requirements.isReduceStatistic() ? "enabled" : "disabled"))); - - icon = new ItemStack(Material.LEVER); - clickHandler = (panel, user, clickType, slot) -> { - requirements.setReduceStatistic(!requirements.isReduceStatistic()); - this.build(); - return true; - }; - glow = requirements.isReduceStatistic(); - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-toggle")); - } - case STATISTIC_BLOCKS -> { - description.add(this.user.getTranslation(reference + "value", "[block]", - Utils.prettifyObject(requirements.getMaterial(), this.user))); - - icon = requirements.getMaterial() == null ? new ItemStack(Material.BARRIER) - : new ItemStack(requirements.getMaterial()); - clickHandler = (panel, user, clickType, slot) -> { - SingleBlockSelector.open(this.user, SingleBlockSelector.Mode.BLOCKS, (status, block) -> { - if (status) { - requirements.setMaterial(block); - } - - this.build(); - }); - - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); - } - case STATISTIC_ITEMS -> { - description.add(this.user.getTranslation(reference + "value", "[item]", - Utils.prettifyObject(requirements.getMaterial(), this.user))); - - icon = requirements.getMaterial() == null ? new ItemStack(Material.BARRIER) - : new ItemStack(requirements.getMaterial()); - clickHandler = (panel, user, clickType, slot) -> { - SingleBlockSelector.open(this.user, SingleBlockSelector.Mode.ITEMS, (status, block) -> { - if (status) { - requirements.setMaterial(block); - } - - this.build(); - }); - - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); - } - case STATISTIC_ENTITIES -> { - description.add(this.user.getTranslation(reference + "value", "[entity]", - Utils.prettifyObject(requirements.getEntity(), this.user))); - - icon = requirements.getEntity() == null ? new ItemStack(Material.BARRIER) - : new ItemStack(PanelUtils.getEntityEgg(requirements.getEntity())); - clickHandler = (panel, user, clickType, slot) -> { - SingleEntitySelector.open(this.user, true, (status, entity) -> { - if (status) { - requirements.setEntity(entity); - } - - this.build(); - }); - - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); - } - default -> { - icon = new ItemStack(Material.PAPER); - clickHandler = null; - glow = false; - } - } - - return new PanelItemBuilder().icon(icon).name(name).description(description).glow(glow) - .clickHandler(clickHandler).build(); + private PanelItem createOtherRequirementButton(RequirementButton button) { + final String reference = Constants.BUTTON + button.name().toLowerCase() + "."; + + final String name = this.user.getTranslation(reference + "name"); + final List description = new ArrayList<>(3); + description.add(this.user.getTranslation(reference + "description")); + + ItemStack icon; + boolean glow; + PanelItem.ClickHandler clickHandler; + + final OtherRequirements requirements = this.challenge.getRequirements(); + + switch (button) { + case REQUIRED_EXPERIENCE -> { + description.add(this.user.getTranslation(reference + "value", Constants.PARAMETER_NUMBER, + String.valueOf(requirements.getRequiredExperience()))); + icon = new ItemStack(Material.EXPERIENCE_BOTTLE); + clickHandler = (panel, user, clickType, i) -> { + Consumer numberConsumer = number -> { + if (number != null) { + requirements.setRequiredExperience(number.intValue()); + } + + // reopen panel + this.build(); + }; + ConversationUtils.createNumericInput(numberConsumer, this.user, + this.user.getTranslation(Constants.CONVERSATIONS + "input-number"), 0, Integer.MAX_VALUE); + + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + } + case REMOVE_EXPERIENCE -> { + description.add( + this.user.getTranslation(reference + (requirements.isTakeExperience() ? "enabled" : "disabled"))); + + icon = new ItemStack(Material.LEVER); + clickHandler = (panel, user, clickType, slot) -> { + requirements.setTakeExperience(!requirements.isTakeExperience()); + this.build(); + return true; + }; + glow = requirements.isTakeExperience(); + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-toggle")); + } + case REQUIRED_LEVEL -> { + description.add(this.user.getTranslation(reference + "value", Constants.PARAMETER_NUMBER, + String.valueOf(requirements.getRequiredIslandLevel()))); + icon = new ItemStack(this.addon.isLevelProvided() ? Material.BEACON : Material.BARRIER); + clickHandler = (panel, user, clickType, i) -> { + Consumer numberConsumer = number -> { + if (number != null) { + requirements.setRequiredIslandLevel(number.longValue()); + } + + // reopen panel + this.build(); + }; + ConversationUtils.createNumericInput(numberConsumer, this.user, + this.user.getTranslation(Constants.CONVERSATIONS + "input-number"), 0, Integer.MAX_VALUE); + + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + } + case REQUIRED_PAPI -> { + if (!requirements.getPapiString().isEmpty()) { + description + .add(this.user.getTranslation(reference + "value", "[formula]", requirements.getPapiString())); + } + icon = new ItemStack( + this.addon.getPlugin().getHooks().getHook("PlaceholderAPI").isPresent() ? Material.PAPER + : Material.BARRIER); + clickHandler = (panel, user, clickType, i) -> { + Consumer stringConsumer = string -> { + if (string != null) { + requirements.setPapiString(string); + } + + // reopen panel + this.build(); + }; + ConversationUtils.createStringInput(stringConsumer, user, + this.user.getTranslation(Constants.CONVERSATIONS + "enter-formula"), ""); + + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + } + case REQUIRED_ADVANCEMENTS -> { + requirements.getAdvancements().forEach(adv -> description + .add(this.user.getTranslation(reference + "list", "[name]", adv.getDisplay().getTitle()))); + + icon = new ItemStack(Material.CYAN_BANNER); + clickHandler = (panel, user, clickType, i) -> { + // Deal with adding and removing statistics in the ManageAdvancementsPanel class + ManageAdvancementsPanel.open(this, requirements.getAdvancements()); + + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + } + case REQUIRED_MONEY -> { + description.add(this.user.getTranslation(reference + "value", Constants.PARAMETER_NUMBER, + String.valueOf(requirements.getRequiredMoney()))); + icon = new ItemStack(this.addon.isEconomyProvided() ? Material.GOLD_INGOT : Material.BARRIER); + clickHandler = (panel, user, clickType, i) -> { + Consumer numberConsumer = number -> { + if (number != null) { + requirements.setRequiredMoney(number.doubleValue()); + } + + // reopen panel + this.build(); + }; + ConversationUtils.createNumericInput(numberConsumer, this.user, + this.user.getTranslation(Constants.CONVERSATIONS + "input-number"), 0, Double.MAX_VALUE); + + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + } + case REMOVE_MONEY -> { + description + .add(this.user.getTranslation(reference + (requirements.isTakeMoney() ? "enabled" : "disabled"))); + + icon = new ItemStack(this.addon.isEconomyProvided() ? Material.LEVER : Material.BARRIER); + clickHandler = (panel, user, clickType, slot) -> { + requirements.setTakeMoney(!requirements.isTakeMoney()); + this.build(); + return true; + }; + glow = requirements.isTakeMoney(); + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-toggle")); + } + default -> { + icon = new ItemStack(Material.PAPER); + clickHandler = null; + glow = false; + } + } + return new PanelItemBuilder().icon(icon).name(name).description(description).glow(glow) + .clickHandler(clickHandler).build(); } /** @@ -1207,462 +1204,461 @@ private PanelItem createStatisticRequirementButton(RequirementButton button) { * @return PanelItem that represents given button. */ private PanelItem createRewardButton(RewardButton button) { - final String reference = Constants.BUTTON + button.name().toLowerCase() + "."; - - final String name = this.user.getTranslation(reference + "name"); - final List description = new ArrayList<>(3); - description.add(this.user.getTranslation(reference + "description")); - - ItemStack icon; - boolean glow; - PanelItem.ClickHandler clickHandler; - - switch (button) { - case REWARD_TEXT -> { - icon = new ItemStack(Material.WRITTEN_BOOK); - - description.add(this.user.getTranslation(reference + "value")); - description.add(Util.translateColorCodes(this.challenge.getRewardText())); - - clickHandler = (panel, user, clickType, i) -> { - // Create consumer that process description change - Consumer> consumer = value -> { - if (value != null) { - this.challenge.setRewardText(String.join("\n", value)); - } - - this.build(); - }; - - if (!this.challenge.getRewardText().isEmpty() && clickType.isShiftClick()) { - // Reset to the empty value - consumer.accept(Collections.emptyList()); - } else { - // start conversation - ConversationUtils.createStringListInput(consumer, user, - user.getTranslation(Constants.CONVERSATIONS + "write-reward-text"), - user.getTranslation(Constants.CONVERSATIONS + "reward-text-changed")); - } - - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); - - if (!this.challenge.getRewardText().isEmpty()) { - description.add(this.user.getTranslation(Constants.TIPS + "shift-click-to-reset")); - } - } - case REWARD_ITEMS -> { - - if (this.challenge.getRewardItems().isEmpty()) { - description.add(this.user.getTranslation(reference + "none")); - } else { - description.add(this.user.getTranslation(reference + "title")); - - Utils.groupEqualItems(this.challenge.getRewardItems(), this.challenge.getIgnoreRewardMetaData()) - .stream().sorted(Comparator.comparing(ItemStack::getType)) - .forEach(itemStack -> description.add(this.user.getTranslationOrNothing(reference + "list", - "[number]", String.valueOf(itemStack.getAmount()), "[item]", - Utils.prettifyObject(itemStack, this.user)))); - } - - icon = new ItemStack(Material.CHEST); - clickHandler = (panel, user, clickType, slot) -> { - ItemSelector.open(this.user, this.challenge.getRewardItems(), (status, value) -> { - if (status) { - this.challenge.setRewardItems(value); - } - - this.build(); - }); - - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); - } - case REWARD_EXPERIENCE -> { - description.add(this.user.getTranslation(reference + "value", Constants.PARAMETER_NUMBER, - String.valueOf(this.challenge.getRewardExperience()))); - icon = new ItemStack(Material.EXPERIENCE_BOTTLE); - clickHandler = (panel, user, clickType, i) -> { - Consumer numberConsumer = number -> { - if (number != null) { - this.challenge.setRewardExperience(number.intValue()); - } - - // reopen panel - this.build(); - }; - ConversationUtils.createNumericInput(numberConsumer, this.user, - this.user.getTranslation(Constants.CONVERSATIONS + "input-number"), 0, Integer.MAX_VALUE); - - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); - } - case REWARD_MONEY -> { - description.add(this.user.getTranslation(reference + "value", Constants.PARAMETER_NUMBER, - addon.getPlugin().getVault().map(v -> v.format(challenge.getRewardMoney())) - .orElse(String.valueOf(challenge.getRewardMoney())))); - icon = new ItemStack(this.addon.isEconomyProvided() ? Material.GOLD_INGOT : Material.BARRIER); - clickHandler = (panel, user, clickType, i) -> { - Consumer numberConsumer = number -> { - if (number != null) { - this.challenge.setRewardMoney(number.doubleValue()); - } - - // reopen panel - this.build(); - }; - ConversationUtils.createNumericInput(numberConsumer, this.user, - this.user.getTranslation(Constants.CONVERSATIONS + "input-number"), 0, Double.MAX_VALUE); - - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); - } - case REWARD_COMMANDS -> { - icon = new ItemStack(Material.COMMAND_BLOCK); - - description.add(this.user.getTranslation(reference + "value")); - description.addAll(this.challenge.getRewardCommands()); - - clickHandler = (panel, user, clickType, i) -> { - // Create consumer that process description change - Consumer> consumer = value -> { - if (value != null) { - this.challenge.setRewardCommands(value); - } - - this.build(); - }; - - if (!this.challenge.getRewardCommands().isEmpty() && clickType.isShiftClick()) { - // Reset to the empty value - consumer.accept(Collections.emptyList()); - } else { - // start conversation - ConversationUtils.createStringListInput(consumer, user, - user.getTranslation(Constants.CONVERSATIONS + "write-reward-commands"), - user.getTranslation(Constants.CONVERSATIONS + "reward-commands-changed")); - } - - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); - - if (!this.challenge.getRewardCommands().isEmpty()) { - description.add(this.user.getTranslation(Constants.TIPS + "shift-click-to-reset")); - } - } - case REPEATABLE -> { - description.add( - this.user.getTranslation(reference + (this.challenge.isRepeatable() ? "enabled" : "disabled"))); - - icon = new ItemStack(Material.LEVER); - clickHandler = (panel, user, clickType, slot) -> { - this.challenge.setRepeatable(!this.challenge.isRepeatable()); - this.build(); - return true; - }; - glow = this.challenge.isRepeatable(); - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-toggle")); - } - case REPEAT_COUNT -> { - description.add(this.user.getTranslation(reference + "value", Constants.PARAMETER_NUMBER, - String.valueOf(this.challenge.getMaxTimes()))); - icon = new ItemStack(Material.COBBLESTONE_WALL); - clickHandler = (panel, user, clickType, i) -> { - Consumer numberConsumer = number -> { - if (number != null) { - this.challenge.setMaxTimes(number.intValue()); - } - - // reopen panel - this.build(); - }; - ConversationUtils.createNumericInput(numberConsumer, this.user, - this.user.getTranslation(Constants.CONVERSATIONS + "input-number"), 0, Integer.MAX_VALUE); - - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); - } - case COOL_DOWN -> { - description.add(this.user.getTranslation(reference + "value", "[time]", - Utils.parseDuration(Duration.ofMillis(this.challenge.getTimeout()), this.user))); - icon = new ItemStack(Material.CLOCK); - clickHandler = (panel, user, clickType, i) -> { - Consumer numberConsumer = number -> { - if (number != null) { - this.challenge.setTimeout(number.longValue() * 1000); - } - - // reopen panel - this.build(); - }; - ConversationUtils.createNumericInput(numberConsumer, this.user, - this.user.getTranslation(Constants.CONVERSATIONS + "input-seconds"), 0, Integer.MAX_VALUE); - - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); - } - case REPEAT_REWARD_TEXT -> { - icon = new ItemStack(Material.WRITTEN_BOOK); - - description.add(this.user.getTranslation(reference + "value")); - description.add(Util.translateColorCodes(this.challenge.getRepeatRewardText())); - - clickHandler = (panel, user, clickType, i) -> { - // Create consumer that process description change - Consumer> consumer = value -> { - if (value != null) { - this.challenge.setRepeatRewardText(String.join("\n", value)); - } - - this.build(); - }; - - if (!this.challenge.getRepeatRewardText().isEmpty() && clickType.isShiftClick()) { - // Reset to the empty value - consumer.accept(Collections.emptyList()); - } else { - // start conversation - ConversationUtils.createStringListInput(consumer, user, - user.getTranslation(Constants.CONVERSATIONS + "write-repeat-reward-text"), - user.getTranslation(Constants.CONVERSATIONS + "repeat-reward-text-changed")); - } - - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); - - if (!this.challenge.getRepeatRewardText().isEmpty()) { - description.add(this.user.getTranslation(Constants.TIPS + "shift-click-to-reset")); - } - } - case REPEAT_REWARD_ITEMS -> { - - if (this.challenge.getRepeatItemReward().isEmpty()) { - description.add(this.user.getTranslation(reference + "none")); - } else { - description.add(this.user.getTranslation(reference + "title")); - - Utils.groupEqualItems(this.challenge.getRepeatItemReward(), this.challenge.getIgnoreRewardMetaData()) - .stream().sorted(Comparator.comparing(ItemStack::getType)) - .forEach(itemStack -> description.add(this.user.getTranslationOrNothing(reference + "list", - "[number]", String.valueOf(itemStack.getAmount()), "[item]", - Utils.prettifyObject(itemStack, this.user)))); - } - - icon = new ItemStack(Material.CHEST); - clickHandler = (panel, user, clickType, slot) -> { - ItemSelector.open(this.user, this.challenge.getRewardItems(), (status, value) -> { - if (status) { - this.challenge.setRepeatItemReward(value); - } - - this.build(); - }); - - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); - } - case REPEAT_REWARD_EXPERIENCE -> { - description.add(this.user.getTranslation(reference + "value", Constants.PARAMETER_NUMBER, - String.valueOf(this.challenge.getRepeatExperienceReward()))); - icon = new ItemStack(Material.EXPERIENCE_BOTTLE); - clickHandler = (panel, user, clickType, i) -> { - Consumer numberConsumer = number -> { - if (number != null) { - this.challenge.setRepeatExperienceReward(number.intValue()); - } - - // reopen panel - this.build(); - }; - ConversationUtils.createNumericInput(numberConsumer, this.user, - this.user.getTranslation(Constants.CONVERSATIONS + "input-number"), 0, Integer.MAX_VALUE); - - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); - } - case REPEAT_REWARD_MONEY -> { - description.add(this.user.getTranslation(reference + "value", Constants.PARAMETER_NUMBER, - String.valueOf(this.challenge.getRepeatMoneyReward()))); - icon = new ItemStack(this.addon.isEconomyProvided() ? Material.GOLD_NUGGET : Material.BARRIER); - clickHandler = (panel, user, clickType, i) -> { - Consumer numberConsumer = number -> { - if (number != null) { - this.challenge.setRepeatMoneyReward(number.doubleValue()); - } - - // reopen panel - this.build(); - }; - ConversationUtils.createNumericInput(numberConsumer, this.user, - this.user.getTranslation(Constants.CONVERSATIONS + "input-number"), 0, Double.MAX_VALUE); - - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); - } - case REPEAT_REWARD_COMMANDS -> { - icon = new ItemStack(Material.COMMAND_BLOCK); - - description.add(this.user.getTranslation(reference + "value")); - description.addAll(this.challenge.getRepeatRewardCommands()); - - clickHandler = (panel, user, clickType, i) -> { - // Create consumer that process description change - Consumer> consumer = value -> { - if (value != null) { - this.challenge.setRepeatRewardCommands(value); - } - - this.build(); - }; - - if (!this.challenge.getRepeatRewardCommands().isEmpty() && clickType.isShiftClick()) { - // Reset to the empty value - consumer.accept(Collections.emptyList()); - } else { - // start conversation - ConversationUtils.createStringListInput(consumer, user, - user.getTranslation(Constants.CONVERSATIONS + "write-repeat-reward-commands"), - user.getTranslation(Constants.CONVERSATIONS + "repeat-reward-commands-changed")); - } - - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); - - if (!this.challenge.getRepeatRewardCommands().isEmpty()) { - description.add(this.user.getTranslation(Constants.TIPS + "shift-click-to-reset")); - } - } - case ADD_IGNORED_META -> { - if (this.challenge.getIgnoreRewardMetaData().isEmpty()) { - description.add(this.user.getTranslation(reference + "none")); - } else { - description.add(this.user.getTranslation(reference + "title")); - - this.challenge.getIgnoreRewardMetaData().stream().sorted(Comparator.comparing(Material::name)) - .forEach(itemStack -> description.add(this.user.getTranslationOrNothing(reference + "list", - "[item]", Utils.prettifyObject(itemStack, this.user)))); - } - - icon = new ItemStack(Material.GREEN_SHULKER_BOX); - - clickHandler = (panel, user, clickType, slot) -> { - if (this.challenge.getRewardItems().isEmpty() && this.challenge.getRepeatItemReward().isEmpty()) { - // Do nothing if no requirements are set. - return true; - } - - // Allow choosing only from inventory items. - Set collection = Arrays.stream(Material.values()).collect(Collectors.toSet()); - this.challenge.getRewardItems().stream().map(ItemStack::getType).forEach(collection::remove); - this.challenge.getRepeatItemReward().stream().map(ItemStack::getType).forEach(collection::remove); - collection.addAll(this.challenge.getIgnoreRewardMetaData()); - - if (Material.values().length == collection.size()) { - // If there are no items anymore, then do not allow opening gui. - return true; - } - - MultiBlockSelector.open(this.user, MultiBlockSelector.Mode.ANY, collection, (status, materials) -> { - if (status) { - materials.addAll(this.challenge.getIgnoreRewardMetaData()); - this.challenge.setIgnoreRewardMetaData(new HashSet<>(materials)); - } - - this.build(); - }); - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-add")); - } - case REMOVE_IGNORED_META -> { - icon = new ItemStack(Material.RED_SHULKER_BOX); - - clickHandler = (panel, user, clickType, slot) -> { - if (this.challenge.getIgnoreRewardMetaData().isEmpty()) { - // Do nothing if no requirements are set. - return true; - } - - // Allow choosing only from inventory items. - Set collection = Arrays.stream(Material.values()).collect(Collectors.toSet()); - collection.removeAll(this.challenge.getIgnoreRewardMetaData()); - - MultiBlockSelector.open(this.user, MultiBlockSelector.Mode.ANY, collection, (status, materials) -> { - if (status) { - this.challenge.getIgnoreRewardMetaData().removeAll(materials); - } - - this.build(); - }); - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-remove")); - } - default -> { - icon = new ItemStack(Material.PAPER); - clickHandler = null; - glow = false; - } - } - - return new PanelItemBuilder().icon(icon).name(name).description(description).glow(glow) - .clickHandler(clickHandler).build(); + final String reference = Constants.BUTTON + button.name().toLowerCase() + "."; + + final String name = this.user.getTranslation(reference + "name"); + final List description = new ArrayList<>(3); + description.add(this.user.getTranslation(reference + "description")); + + ItemStack icon; + boolean glow; + PanelItem.ClickHandler clickHandler; + + switch (button) { + case REWARD_TEXT -> { + icon = new ItemStack(Material.WRITTEN_BOOK); + + description.add(this.user.getTranslation(reference + "value")); + description.add(Util.translateColorCodes(this.challenge.getRewardText())); + + clickHandler = (panel, user, clickType, i) -> { + // Create consumer that process description change + Consumer> consumer = value -> { + if (value != null) { + this.challenge.setRewardText(String.join("\n", value)); + } + + this.build(); + }; + + if (!this.challenge.getRewardText().isEmpty() && clickType.isShiftClick()) { + // Reset to the empty value + consumer.accept(Collections.emptyList()); + } else { + // start conversation + ConversationUtils.createStringListInput(consumer, user, + user.getTranslation(Constants.CONVERSATIONS + "write-reward-text"), + user.getTranslation(Constants.CONVERSATIONS + "reward-text-changed")); + } + + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + + if (!this.challenge.getRewardText().isEmpty()) { + description.add(this.user.getTranslation(Constants.TIPS + "shift-click-to-reset")); + } + } + case REWARD_ITEMS -> { + + if (this.challenge.getRewardItems().isEmpty()) { + description.add(this.user.getTranslation(reference + "none")); + } else { + description.add(this.user.getTranslation(reference + "title")); + + Utils.groupEqualItems(this.challenge.getRewardItems(), this.challenge.getIgnoreRewardMetaData()) + .stream().sorted(Comparator.comparing(ItemStack::getType)) + .forEach(itemStack -> description.add(this.user.getTranslationOrNothing(reference + "list", + "[number]", String.valueOf(itemStack.getAmount()), "[item]", + Utils.prettifyObject(itemStack, this.user)))); + } + + icon = new ItemStack(Material.CHEST); + clickHandler = (panel, user, clickType, slot) -> { + ItemSelector.open(this.user, this.challenge.getRewardItems(), (status, value) -> { + if (status) { + this.challenge.setRewardItems(value); + } + + this.build(); + }); + + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + } + case REWARD_EXPERIENCE -> { + description.add(this.user.getTranslation(reference + "value", Constants.PARAMETER_NUMBER, + String.valueOf(this.challenge.getRewardExperience()))); + icon = new ItemStack(Material.EXPERIENCE_BOTTLE); + clickHandler = (panel, user, clickType, i) -> { + Consumer numberConsumer = number -> { + if (number != null) { + this.challenge.setRewardExperience(number.intValue()); + } + + // reopen panel + this.build(); + }; + ConversationUtils.createNumericInput(numberConsumer, this.user, + this.user.getTranslation(Constants.CONVERSATIONS + "input-number"), 0, Integer.MAX_VALUE); + + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + } + case REWARD_MONEY -> { + description.add(this.user.getTranslation(reference + "value", Constants.PARAMETER_NUMBER, + addon.getPlugin().getVault().map(v -> v.format(challenge.getRewardMoney())) + .orElse(String.valueOf(challenge.getRewardMoney())))); + icon = new ItemStack(this.addon.isEconomyProvided() ? Material.GOLD_INGOT : Material.BARRIER); + clickHandler = (panel, user, clickType, i) -> { + Consumer numberConsumer = number -> { + if (number != null) { + this.challenge.setRewardMoney(number.doubleValue()); + } + + // reopen panel + this.build(); + }; + ConversationUtils.createNumericInput(numberConsumer, this.user, + this.user.getTranslation(Constants.CONVERSATIONS + "input-number"), 0, Double.MAX_VALUE); + + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + } + case REWARD_COMMANDS -> { + icon = new ItemStack(Material.COMMAND_BLOCK); + + description.add(this.user.getTranslation(reference + "value")); + description.addAll(this.challenge.getRewardCommands()); + + clickHandler = (panel, user, clickType, i) -> { + // Create consumer that process description change + Consumer> consumer = value -> { + if (value != null) { + this.challenge.setRewardCommands(value); + } + + this.build(); + }; + + if (!this.challenge.getRewardCommands().isEmpty() && clickType.isShiftClick()) { + // Reset to the empty value + consumer.accept(Collections.emptyList()); + } else { + // start conversation + ConversationUtils.createStringListInput(consumer, user, + user.getTranslation(Constants.CONVERSATIONS + "write-reward-commands"), + user.getTranslation(Constants.CONVERSATIONS + "reward-commands-changed")); + } + + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + + if (!this.challenge.getRewardCommands().isEmpty()) { + description.add(this.user.getTranslation(Constants.TIPS + "shift-click-to-reset")); + } + } + case REPEATABLE -> { + description.add( + this.user.getTranslation(reference + (this.challenge.isRepeatable() ? "enabled" : "disabled"))); + + icon = new ItemStack(Material.LEVER); + clickHandler = (panel, user, clickType, slot) -> { + this.challenge.setRepeatable(!this.challenge.isRepeatable()); + this.build(); + return true; + }; + glow = this.challenge.isRepeatable(); + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-toggle")); + } + case REPEAT_COUNT -> { + description.add(this.user.getTranslation(reference + "value", Constants.PARAMETER_NUMBER, + String.valueOf(this.challenge.getMaxTimes()))); + icon = new ItemStack(Material.COBBLESTONE_WALL); + clickHandler = (panel, user, clickType, i) -> { + Consumer numberConsumer = number -> { + if (number != null) { + this.challenge.setMaxTimes(number.intValue()); + } + + // reopen panel + this.build(); + }; + ConversationUtils.createNumericInput(numberConsumer, this.user, + this.user.getTranslation(Constants.CONVERSATIONS + "input-number"), 0, Integer.MAX_VALUE); + + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + } + case COOL_DOWN -> { + description.add(this.user.getTranslation(reference + "value", "[time]", + Utils.parseDuration(Duration.ofMillis(this.challenge.getTimeout()), this.user))); + icon = new ItemStack(Material.CLOCK); + clickHandler = (panel, user, clickType, i) -> { + Consumer numberConsumer = number -> { + if (number != null) { + this.challenge.setTimeout(number.longValue() * 1000); + } + + // reopen panel + this.build(); + }; + ConversationUtils.createNumericInput(numberConsumer, this.user, + this.user.getTranslation(Constants.CONVERSATIONS + "input-seconds"), 0, Integer.MAX_VALUE); + + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + } + case REPEAT_REWARD_TEXT -> { + icon = new ItemStack(Material.WRITTEN_BOOK); + + description.add(this.user.getTranslation(reference + "value")); + description.add(Util.translateColorCodes(this.challenge.getRepeatRewardText())); + + clickHandler = (panel, user, clickType, i) -> { + // Create consumer that process description change + Consumer> consumer = value -> { + if (value != null) { + this.challenge.setRepeatRewardText(String.join("\n", value)); + } + + this.build(); + }; + + if (!this.challenge.getRepeatRewardText().isEmpty() && clickType.isShiftClick()) { + // Reset to the empty value + consumer.accept(Collections.emptyList()); + } else { + // start conversation + ConversationUtils.createStringListInput(consumer, user, + user.getTranslation(Constants.CONVERSATIONS + "write-repeat-reward-text"), + user.getTranslation(Constants.CONVERSATIONS + "repeat-reward-text-changed")); + } + + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + + if (!this.challenge.getRepeatRewardText().isEmpty()) { + description.add(this.user.getTranslation(Constants.TIPS + "shift-click-to-reset")); + } + } + case REPEAT_REWARD_ITEMS -> { + + if (this.challenge.getRepeatItemReward().isEmpty()) { + description.add(this.user.getTranslation(reference + "none")); + } else { + description.add(this.user.getTranslation(reference + "title")); + + Utils.groupEqualItems(this.challenge.getRepeatItemReward(), this.challenge.getIgnoreRewardMetaData()) + .stream().sorted(Comparator.comparing(ItemStack::getType)) + .forEach(itemStack -> description.add(this.user.getTranslationOrNothing(reference + "list", + "[number]", String.valueOf(itemStack.getAmount()), "[item]", + Utils.prettifyObject(itemStack, this.user)))); + } + + icon = new ItemStack(Material.CHEST); + clickHandler = (panel, user, clickType, slot) -> { + ItemSelector.open(this.user, this.challenge.getRewardItems(), (status, value) -> { + if (status) { + this.challenge.setRepeatItemReward(value); + } + + this.build(); + }); + + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + } + case REPEAT_REWARD_EXPERIENCE -> { + description.add(this.user.getTranslation(reference + "value", Constants.PARAMETER_NUMBER, + String.valueOf(this.challenge.getRepeatExperienceReward()))); + icon = new ItemStack(Material.EXPERIENCE_BOTTLE); + clickHandler = (panel, user, clickType, i) -> { + Consumer numberConsumer = number -> { + if (number != null) { + this.challenge.setRepeatExperienceReward(number.intValue()); + } + + // reopen panel + this.build(); + }; + ConversationUtils.createNumericInput(numberConsumer, this.user, + this.user.getTranslation(Constants.CONVERSATIONS + "input-number"), 0, Integer.MAX_VALUE); + + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + } + case REPEAT_REWARD_MONEY -> { + description.add(this.user.getTranslation(reference + "value", Constants.PARAMETER_NUMBER, + String.valueOf(this.challenge.getRepeatMoneyReward()))); + icon = new ItemStack(this.addon.isEconomyProvided() ? Material.GOLD_NUGGET : Material.BARRIER); + clickHandler = (panel, user, clickType, i) -> { + Consumer numberConsumer = number -> { + if (number != null) { + this.challenge.setRepeatMoneyReward(number.doubleValue()); + } + + // reopen panel + this.build(); + }; + ConversationUtils.createNumericInput(numberConsumer, this.user, + this.user.getTranslation(Constants.CONVERSATIONS + "input-number"), 0, Double.MAX_VALUE); + + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + } + case REPEAT_REWARD_COMMANDS -> { + icon = new ItemStack(Material.COMMAND_BLOCK); + + description.add(this.user.getTranslation(reference + "value")); + description.addAll(this.challenge.getRepeatRewardCommands()); + + clickHandler = (panel, user, clickType, i) -> { + // Create consumer that process description change + Consumer> consumer = value -> { + if (value != null) { + this.challenge.setRepeatRewardCommands(value); + } + + this.build(); + }; + + if (!this.challenge.getRepeatRewardCommands().isEmpty() && clickType.isShiftClick()) { + // Reset to the empty value + consumer.accept(Collections.emptyList()); + } else { + // start conversation + ConversationUtils.createStringListInput(consumer, user, + user.getTranslation(Constants.CONVERSATIONS + "write-repeat-reward-commands"), + user.getTranslation(Constants.CONVERSATIONS + "repeat-reward-commands-changed")); + } + + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + + if (!this.challenge.getRepeatRewardCommands().isEmpty()) { + description.add(this.user.getTranslation(Constants.TIPS + "shift-click-to-reset")); + } + } + case ADD_IGNORED_META -> { + if (this.challenge.getIgnoreRewardMetaData().isEmpty()) { + description.add(this.user.getTranslation(reference + "none")); + } else { + description.add(this.user.getTranslation(reference + "title")); + + this.challenge.getIgnoreRewardMetaData().stream().sorted(Comparator.comparing(Material::name)) + .forEach(itemStack -> description.add(this.user.getTranslationOrNothing(reference + "list", + "[item]", Utils.prettifyObject(itemStack, this.user)))); + } + + icon = new ItemStack(Material.GREEN_SHULKER_BOX); + + clickHandler = (panel, user, clickType, slot) -> { + if (this.challenge.getRewardItems().isEmpty() && this.challenge.getRepeatItemReward().isEmpty()) { + // Do nothing if no requirements are set. + return true; + } + + // Allow choosing only from inventory items. + Set collection = Arrays.stream(Material.values()).collect(Collectors.toSet()); + this.challenge.getRewardItems().stream().map(ItemStack::getType).forEach(collection::remove); + this.challenge.getRepeatItemReward().stream().map(ItemStack::getType).forEach(collection::remove); + collection.addAll(this.challenge.getIgnoreRewardMetaData()); + + if (Material.values().length == collection.size()) { + // If there are no items anymore, then do not allow opening gui. + return true; + } + + MultiBlockSelector.open(this.user, MultiBlockSelector.Mode.ANY, collection, (status, materials) -> { + if (status) { + materials.addAll(this.challenge.getIgnoreRewardMetaData()); + this.challenge.setIgnoreRewardMetaData(new HashSet<>(materials)); + } + + this.build(); + }); + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-add")); + } + case REMOVE_IGNORED_META -> { + icon = new ItemStack(Material.RED_SHULKER_BOX); + + clickHandler = (panel, user, clickType, slot) -> { + if (this.challenge.getIgnoreRewardMetaData().isEmpty()) { + // Do nothing if no requirements are set. + return true; + } + + // Allow choosing only from inventory items. + Set collection = Arrays.stream(Material.values()).collect(Collectors.toSet()); + collection.removeAll(this.challenge.getIgnoreRewardMetaData()); + + MultiBlockSelector.open(this.user, MultiBlockSelector.Mode.ANY, collection, (status, materials) -> { + if (status) { + this.challenge.getIgnoreRewardMetaData().removeAll(materials); + } + + this.build(); + }); + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-remove")); + } + default -> { + icon = new ItemStack(Material.PAPER); + clickHandler = null; + glow = false; + } + } + return new PanelItemBuilder().icon(icon).name(name).description(description).glow(glow) + .clickHandler(clickHandler).build(); } // --------------------------------------------------------------------- @@ -1673,47 +1669,47 @@ private PanelItem createRewardButton(RewardButton button) { * This class allows changing icon for Generator Tier */ private class IconChanger implements PanelListener { - /** - * Process inventory click. If generator icon is selected and user clicks on - * item in his inventory, then change icon to the item from inventory. - * - * @param user the user - * @param event the event - */ - @Override - public void onInventoryClick(User user, InventoryClickEvent event) { - // Handle icon changing - if (EditChallengePanel.this.selectedButton != null && event.getCurrentItem() != null - && !event.getCurrentItem().getType().equals(Material.AIR) && event.getRawSlot() > 44) { - // set material and amount only. Other data should be removed. - - if (EditChallengePanel.this.selectedButton == Button.ICON) { - EditChallengePanel.this.challenge.setIcon(event.getCurrentItem().clone()); - // Deselect icon - EditChallengePanel.this.selectedButton = null; - // Rebuild icon - EditChallengePanel.this.build(); - } - } - } - - /** - * On inventory close. - * - * @param event the event - */ - @Override - public void onInventoryClose(InventoryCloseEvent event) { - // Do nothing - } - - /** - * Setup current listener. - */ - @Override - public void setup() { - // Do nothing - } + /** + * Process inventory click. If generator icon is selected and user clicks on + * item in his inventory, then change icon to the item from inventory. + * + * @param user the user + * @param event the event + */ + @Override + public void onInventoryClick(User user, InventoryClickEvent event) { + // Handle icon changing + if (EditChallengePanel.this.selectedButton != null && event.getCurrentItem() != null + && !event.getCurrentItem().getType().equals(Material.AIR) && event.getRawSlot() > 44) { + // set material and amount only. Other data should be removed. + + if (EditChallengePanel.this.selectedButton == Button.ICON) { + EditChallengePanel.this.challenge.setIcon(event.getCurrentItem().clone()); + // Deselect icon + EditChallengePanel.this.selectedButton = null; + // Rebuild icon + EditChallengePanel.this.build(); + } + } + } + + /** + * On inventory close. + * + * @param event the event + */ + @Override + public void onInventoryClose(InventoryCloseEvent event) { + // Do nothing + } + + /** + * Setup current listener. + */ + @Override + public void setup() { + // Do nothing + } } // --------------------------------------------------------------------- @@ -1724,37 +1720,39 @@ public void setup() { * Represents different types of menus */ private enum MenuType { - PROPERTIES, REQUIREMENTS, REWARDS + PROPERTIES, REQUIREMENTS, REWARDS } /** * Represents different buttons that could be in menus. */ private enum Button { - NAME, DEPLOYED, ICON, DESCRIPTION, ORDER, ENVIRONMENT, REMOVE_ON_COMPLETE, + NAME, DEPLOYED, ICON, DESCRIPTION, ORDER, ENVIRONMENT, REMOVE_ON_COMPLETE, HIDE_REWARD_ITEMS, } /** * Represents different rewards buttons that are used in menus. */ private enum RewardButton { - REWARD_TEXT, REWARD_ITEMS, REWARD_EXPERIENCE, REWARD_MONEY, REWARD_COMMANDS, + REWARD_TEXT, REWARD_ITEMS, REWARD_EXPERIENCE, REWARD_MONEY, REWARD_COMMANDS, - REPEATABLE, REPEAT_COUNT, COOL_DOWN, + REPEATABLE, REPEAT_COUNT, COOL_DOWN, - REPEAT_REWARD_TEXT, REPEAT_REWARD_ITEMS, REPEAT_REWARD_EXPERIENCE, REPEAT_REWARD_MONEY, REPEAT_REWARD_COMMANDS, + REPEAT_REWARD_TEXT, REPEAT_REWARD_ITEMS, REPEAT_REWARD_EXPERIENCE, REPEAT_REWARD_MONEY, REPEAT_REWARD_COMMANDS, - ADD_IGNORED_META, REMOVE_IGNORED_META, + ADD_IGNORED_META, REMOVE_IGNORED_META, } /** * Represents different requirement buttons that are used in menus. */ - private enum RequirementButton { - REQUIRED_ENTITIES, REMOVE_ENTITIES, REQUIRED_BLOCKS, REMOVE_BLOCKS, SEARCH_RADIUS, REQUIRED_PERMISSIONS, - REQUIRED_ITEMS, REMOVE_ITEMS, ADD_IGNORED_META, REMOVE_IGNORED_META, REQUIRED_EXPERIENCE, REMOVE_EXPERIENCE, - REQUIRED_LEVEL, REQUIRED_MONEY, REMOVE_MONEY, STATISTIC, STATISTIC_BLOCKS, STATISTIC_ITEMS, STATISTIC_ENTITIES, - STATISTIC_AMOUNT, REMOVE_STATISTIC, + public enum RequirementButton { + REQUIRED_ENTITIES, REMOVE_ENTITIES, REQUIRED_BLOCKS, REMOVE_BLOCKS, SEARCH_RADIUS, REQUIRED_PERMISSIONS, + REQUIRED_ITEMS, REMOVE_ITEMS, ADD_IGNORED_META, REMOVE_IGNORED_META, REQUIRED_EXPERIENCE, REMOVE_EXPERIENCE, + REQUIRED_LEVEL, REQUIRED_MONEY, REMOVE_MONEY, STATISTIC, STATISTIC_BLOCKS, STATISTIC_ITEMS, + STATISTIC_ENTITIES, + STATISTIC_AMOUNT, REMOVE_STATISTIC, REQUIRED_MATERIALTAGS, REQUIRED_ENTITYTAGS, REQUIRED_STATISTICS, + REMOVE_STATISTICS, REQUIRED_PAPI, REQUIRED_ADVANCEMENTS, } // --------------------------------------------------------------------- @@ -1772,4 +1770,5 @@ private enum RequirementButton { * Variable holds current active menu. */ private MenuType currentMenuType; + } diff --git a/src/main/java/world/bentobox/challenges/panel/admin/EditLevelPanel.java b/src/main/java/world/bentobox/challenges/panel/admin/EditLevelPanel.java index 07af626c..2ab24700 100644 --- a/src/main/java/world/bentobox/challenges/panel/admin/EditLevelPanel.java +++ b/src/main/java/world/bentobox/challenges/panel/admin/EditLevelPanel.java @@ -1,7 +1,15 @@ package world.bentobox.challenges.panel.admin; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -19,14 +27,14 @@ import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.util.Util; import world.bentobox.challenges.ChallengesAddon; -import world.bentobox.challenges.managers.ChallengesManager; import world.bentobox.challenges.database.object.Challenge; import world.bentobox.challenges.database.object.ChallengeLevel; +import world.bentobox.challenges.managers.ChallengesManager; import world.bentobox.challenges.panel.CommonPagedPanel; import world.bentobox.challenges.panel.CommonPanel; import world.bentobox.challenges.panel.ConversationUtils; -import world.bentobox.challenges.panel.util.ItemSelector; import world.bentobox.challenges.panel.util.ChallengeSelector; +import world.bentobox.challenges.panel.util.ItemSelector; import world.bentobox.challenges.panel.util.MultiBlockSelector; import world.bentobox.challenges.utils.Constants; import world.bentobox.challenges.utils.Utils; @@ -750,7 +758,7 @@ private PanelItem createButton(Button button) } case WAIVER_AMOUNT -> { description.add(this.user.getTranslation(reference + "value", - Constants.PARAMETER_NUMBER, String.valueOf(this.challengeLevel.getWaiverAmount()))); + Constants.PARAMETER_NUMBER, String.valueOf(this.challengeLevel.getWaiverAmount()))); icon = new ItemStack(Material.HOPPER, Math.max(1, this.challengeLevel.getWaiverAmount())); clickHandler = (panel, user, clickType, i) -> { diff --git a/src/main/java/world/bentobox/challenges/panel/admin/ListLevelsPanel.java b/src/main/java/world/bentobox/challenges/panel/admin/ListLevelsPanel.java index 9b344f53..9e688881 100644 --- a/src/main/java/world/bentobox/challenges/panel/admin/ListLevelsPanel.java +++ b/src/main/java/world/bentobox/challenges/panel/admin/ListLevelsPanel.java @@ -1,13 +1,13 @@ package world.bentobox.challenges.panel.admin; -import org.bukkit.Material; -import org.bukkit.World; - import java.util.List; import java.util.function.Consumer; import java.util.stream.Collectors; +import org.bukkit.Material; +import org.bukkit.World; + import lv.id.bonne.panelutils.PanelUtils; import world.bentobox.bentobox.api.panels.PanelItem; import world.bentobox.bentobox.api.panels.builders.PanelBuilder; diff --git a/src/main/java/world/bentobox/challenges/panel/admin/ManageAdvancementsPanel.java b/src/main/java/world/bentobox/challenges/panel/admin/ManageAdvancementsPanel.java new file mode 100644 index 00000000..c4715245 --- /dev/null +++ b/src/main/java/world/bentobox/challenges/panel/admin/ManageAdvancementsPanel.java @@ -0,0 +1,275 @@ +package world.bentobox.challenges.panel.admin; + + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.stream.Collectors; + +import org.bukkit.Material; +import org.bukkit.advancement.Advancement; +import org.bukkit.inventory.ItemStack; + +import lv.id.bonne.panelutils.PanelUtils; +import world.bentobox.bentobox.api.panels.PanelItem; +import world.bentobox.bentobox.api.panels.builders.PanelBuilder; +import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; +import world.bentobox.challenges.panel.CommonPagedPanel; +import world.bentobox.challenges.panel.CommonPanel; +import world.bentobox.challenges.panel.util.SingleAdvancementSelector; +import world.bentobox.challenges.utils.Constants; + + +/** + * This class allows to edit material that are in required material map. + */ +public class ManageAdvancementsPanel extends CommonPagedPanel +{ + + // --------------------------------------------------------------------- + // Section: Enums + // --------------------------------------------------------------------- + + /** + * Functional buttons in current GUI. + */ + private enum Button { + ADD_ADVANCEMENT, REMOVE_ADVANCEMENT + } + + // --------------------------------------------------------------------- + // Section: Variables + // --------------------------------------------------------------------- + + /** + * Contains selected advancements. + */ + private final Set selectedAdvancements; + + /** + * List of required advancements + */ + private final List advancementsList; + + /** + * Stores filtered items. + */ + private List filterElements; + + private ManageAdvancementsPanel(CommonPanel parentGUI, List advancementsList) + { + super(parentGUI); + this.advancementsList = advancementsList; + + // Sort tags by their ordinal value. + this.advancementsList.sort(Comparator.comparing(advancement -> advancement.getDisplay().getTitle())); + + this.selectedAdvancements = new HashSet<>(); + + // Init without filters applied. + this.filterElements = this.advancementsList; + } + + + /** + * Open the Challenges Admin GUI. + */ + public static void open(CommonPanel parentGUI, List advancementsList) { + new ManageAdvancementsPanel(parentGUI, advancementsList).build(); + } + + +// --------------------------------------------------------------------- +// Section: Methods +// --------------------------------------------------------------------- + + + /** + * This method is called when filter value is updated. + */ + @Override + protected void updateFilters() + { + if (this.searchString == null || this.searchString.isBlank()) + { + this.filterElements = this.advancementsList; + } + else + { + this.filterElements = this.advancementsList.stream(). + filter(element -> { + // If element name is set and name contains search field, then do not filter out. + return element.getDisplay().getTitle().toLowerCase(Locale.ENGLISH) + .contains(this.searchString.toLowerCase(Locale.ENGLISH)); + }). + distinct(). + collect(Collectors.toList()); + } + } + + + /** + * This method builds all necessary elements in GUI panel. + */ + @Override + protected void build() + { + PanelBuilder panelBuilder = new PanelBuilder().user(this.user). + name(this.user.getTranslation(Constants.TITLE + "manage-advancements")); + + // Create nice border. + PanelUtils.fillBorder(panelBuilder); + + panelBuilder.item(3, this.createButton(Button.ADD_ADVANCEMENT)); + panelBuilder.item(5, this.createButton(Button.REMOVE_ADVANCEMENT)); + // Fill the box with what is selected + this.populateElements(panelBuilder, this.filterElements); + + // Add return button. + panelBuilder.item(44, this.returnButton); + + panelBuilder.build(); + } + + + /** + * This method creates PanelItem button of requested type. + * @param button Button which must be created. + * @return new PanelItem with requested functionality. + */ + private PanelItem createButton(Button button) + { + final String reference = Constants.BUTTON + button.name().toLowerCase() + "."; + + final String name = this.user.getTranslation(reference + "name"); + final List description = new ArrayList<>(3); + description.add(this.user.getTranslation(reference + "description")); + + ItemStack icon; + PanelItem.ClickHandler clickHandler; + boolean glow; + + switch (button) + { + case ADD_ADVANCEMENT -> { + icon = new ItemStack(Material.BUCKET); + clickHandler = (panel, user1, clickType, slot) -> + { + SingleAdvancementSelector.open(this.user, (status, advancement) -> + { + if (status) + { + this.advancementsList.add(advancement); + } + + this.build(); + }); + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-add")); + } + case REMOVE_ADVANCEMENT -> { + + if (!this.selectedAdvancements.isEmpty()) + { + this.selectedAdvancements.forEach(adv -> description.add(adv.getDisplay().getTitle())); + } + + icon = new ItemStack(Material.LAVA_BUCKET); + + clickHandler = (panel, user1, clickType, slot) -> + { + if (!this.selectedAdvancements.isEmpty()) + { + this.advancementsList.removeAll(this.selectedAdvancements); + this.selectedAdvancements.clear(); + this.build(); + } + + return true; + }; + + glow = !this.selectedAdvancements.isEmpty(); + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-remove")); + } + default -> { + icon = new ItemStack(Material.PAPER); + clickHandler = null; + glow = false; + } + } + + return new PanelItemBuilder(). + icon(icon). + name(name). + description(description). + clickHandler(clickHandler). + glow(glow). + build(); + } + + + /** + * This method creates button for given stat. + * @param rec material which button must be created. + * @return new Button for material. + */ + @Override + protected PanelItem createElementButton(Advancement rec) + { + final String reference = Constants.BUTTON + "advancement_element."; + + List description = new ArrayList<>(); + + // Show everything about this advancement + description + .add(this.user.getTranslation(reference + "description", "[description]", + rec.getDisplay().getDescription())); + + if (this.selectedAdvancements.contains(rec)) + { + description.add(this.user.getTranslation(reference + "selected")); + } + + description.add(""); + + if (this.selectedAdvancements.contains(rec)) + { + description.add(this.user.getTranslation(Constants.TIPS + "right-click-to-deselect")); + } + else + { + description.add(this.user.getTranslation(Constants.TIPS + "right-click-to-select")); + } + + return new PanelItemBuilder(). + name(this.user.getTranslation(reference + "name", "[name]", rec.getDisplay().getTitle())) + .icon(rec.getDisplay().getIcon()). + description(description). + clickHandler((panel, user1, clickType, slot) -> { + // On right click change which entities are selected for deletion. + if (clickType.isRightClick()) + { + if (!this.selectedAdvancements.add(rec)) + { + // Remove material if it is already selected + this.selectedAdvancements.remove(rec); + } + + this.build(); + } + return true; + }). + glow(this.selectedAdvancements.contains(rec)). + build(); + } + +} diff --git a/src/main/java/world/bentobox/challenges/panel/admin/ManageBlockGroupsPanel.java b/src/main/java/world/bentobox/challenges/panel/admin/ManageBlockGroupsPanel.java new file mode 100644 index 00000000..025b2060 --- /dev/null +++ b/src/main/java/world/bentobox/challenges/panel/admin/ManageBlockGroupsPanel.java @@ -0,0 +1,320 @@ +package world.bentobox.challenges.panel.admin; + + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import org.bukkit.Material; +import org.bukkit.Registry; +import org.bukkit.Tag; +import org.bukkit.inventory.ItemStack; +import org.eclipse.jdt.annotation.Nullable; + +import lv.id.bonne.panelutils.PanelUtils; +import world.bentobox.bentobox.api.panels.PanelItem; +import world.bentobox.bentobox.api.panels.builders.PanelBuilder; +import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; +import world.bentobox.challenges.panel.CommonPagedPanel; +import world.bentobox.challenges.panel.CommonPanel; +import world.bentobox.challenges.panel.ConversationUtils; +import world.bentobox.challenges.panel.util.MultiMaterialTagsSelector; +import world.bentobox.challenges.utils.Constants; +import world.bentobox.challenges.utils.Utils; + + +/** + * This class allows to edit Block Groups that are in required. + */ +public class ManageBlockGroupsPanel extends CommonPagedPanel> +{ + + // --------------------------------------------------------------------- + // Section: Enums + // --------------------------------------------------------------------- + + /** + * Functional buttons in current GUI. + */ + private enum Button { + ADD_BLOCK_GROUP, REMOVE_BLOCK_GROUP + } + + // --------------------------------------------------------------------- + // Section: Variables + // --------------------------------------------------------------------- + + /** + * Contains selected materials. + */ + private final Set> selectedTags; + + /** + * List of materials to avoid order issues. + */ + private final List> materialList; + + /** + * List of required materials. + */ + private final Map, Integer> tagMap; + + /** + * Stores filtered items. + */ + private List> filterElements; + + private ManageBlockGroupsPanel(CommonPanel parentGUI, Map, Integer> map) + { + super(parentGUI); + this.tagMap = map; + this.materialList = new ArrayList<>(this.tagMap.keySet()); + + // Sort tags by their ordinal value. + this.materialList.sort(Comparator.comparing(tag -> tag.getKey().getKey())); + + this.selectedTags = new HashSet<>(); + + // Init without filters applied. + this.filterElements = this.materialList; + } + + + /** + * Open the Challenges Admin GUI. + */ + public static void open(CommonPanel parentGUI, Map, Integer> map) + { + new ManageBlockGroupsPanel(parentGUI, map).build(); + } + + +// --------------------------------------------------------------------- +// Section: Methods +// --------------------------------------------------------------------- + + + /** + * This method is called when filter value is updated. + */ + @Override + protected void updateFilters() + { + if (this.searchString == null || this.searchString.isBlank()) + { + this.filterElements = this.materialList; + } + else + { + this.filterElements = this.materialList.stream(). + filter(element -> { + // If element name is set and name contains search field, then do not filter out. + return element.getKey().getKey().toLowerCase(Locale.ENGLISH) + .contains(this.searchString.toLowerCase(Locale.ENGLISH)); + }). + distinct(). + collect(Collectors.toList()); + } + } + + + /** + * This method builds all necessary elements in GUI panel. + */ + @Override + protected void build() + { + PanelBuilder panelBuilder = new PanelBuilder().user(this.user). + name(this.user.getTranslation(Constants.TITLE + "manage-block-groups")); + + // Create nice border. + PanelUtils.fillBorder(panelBuilder); + + panelBuilder.item(3, this.createButton(Button.ADD_BLOCK_GROUP)); + panelBuilder.item(5, this.createButton(Button.REMOVE_BLOCK_GROUP)); + // Fill the box with what is selected + this.populateElements(panelBuilder, this.filterElements); + + // Add return button. + panelBuilder.item(44, this.returnButton); + + panelBuilder.build(); + } + + + /** + * This method creates PanelItem button of requested type. + * @param button Button which must be created. + * @return new PanelItem with requested functionality. + */ + private PanelItem createButton(Button button) + { + final String reference = Constants.BUTTON + button.name().toLowerCase() + "."; + + final String name = this.user.getTranslation(reference + "name"); + final List description = new ArrayList<>(3); + description.add(this.user.getTranslation(reference + "description")); + + ItemStack icon; + PanelItem.ClickHandler clickHandler; + boolean glow; + + switch (button) + { + case ADD_BLOCK_GROUP -> { + icon = new ItemStack(Material.BUCKET); + clickHandler = (panel, user1, clickType, slot) -> + { + MultiMaterialTagsSelector.open(this.user, MultiMaterialTagsSelector.Mode.BLOCKS, + new HashSet<>(this.materialList), + (status, materials) -> + { + if (status) + { + materials.forEach(material -> + { + this.tagMap.put(material, 1); + this.materialList.add(material); + }); + } + + this.build(); + }); + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-add")); + } + case REMOVE_BLOCK_GROUP -> { + + if (!this.selectedTags.isEmpty()) + { + description.add(this.user.getTranslation(reference + "title")); + this.selectedTags.forEach(material -> + description.add(this.user.getTranslation(reference + "material", + "[material]", Utils.prettifyObject(material, this.user)))); + } + + icon = new ItemStack(Material.LAVA_BUCKET); + + clickHandler = (panel, user1, clickType, slot) -> + { + if (!this.selectedTags.isEmpty()) + { + this.tagMap.keySet().removeAll(this.selectedTags); + this.materialList.removeAll(this.selectedTags); + this.selectedTags.clear(); + this.build(); + } + + return true; + }; + + glow = !this.selectedTags.isEmpty(); + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-remove")); + } + default -> { + icon = new ItemStack(Material.PAPER); + clickHandler = null; + glow = false; + } + } + + return new PanelItemBuilder(). + icon(icon). + name(name). + description(description). + clickHandler(clickHandler). + glow(glow). + build(); + } + + + /** + * This method creates button for given material. + * @param tag material which button must be created. + * @return new Button for material. + */ + @Override + protected PanelItem createElementButton(Tag tag) + { + final String reference = Constants.BUTTON + "block-group."; + + List description = new ArrayList<>(); + + if (this.selectedTags.contains(tag)) + { + description.add(this.user.getTranslation(reference + "selected")); + } + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "left-click-to-choose")); + + if (this.selectedTags.contains(tag)) + { + description.add(this.user.getTranslation(Constants.TIPS + "right-click-to-deselect")); + } + else + { + description.add(this.user.getTranslation(Constants.TIPS + "right-click-to-select")); + } + + return new PanelItemBuilder(). + name(this.user.getTranslation(reference + "name", "[id]", + Utils.prettifyObject(tag, this.user))). + icon(getIcon(tag, this.tagMap.get(tag))). + description(description). + clickHandler((panel, user1, clickType, slot) -> { + // On right click change which entities are selected for deletion. + if (clickType.isRightClick()) + { + if (!this.selectedTags.add(tag)) + { + // Remove material if it is already selected + this.selectedTags.remove(tag); + } + + this.build(); + } + else + { + Consumer numberConsumer = number -> { + if (number != null) + { + this.tagMap.put(tag, number.intValue()); + } + + // reopen panel + this.build(); + }; + + ConversationUtils.createNumericInput(numberConsumer, + this.user, + this.user.getTranslation(Constants.CONVERSATIONS + "input-number"), + 1, + Integer.MAX_VALUE); + } + return true; + }). + glow(this.selectedTags.contains(tag)). + build(); + } + + private @Nullable ItemStack getIcon(Tag materialTag, Integer quantity) { + Material m = MultiMaterialTagsSelector.ICONS.getOrDefault(materialTag, Registry.MATERIAL.stream() + .filter(materialTag::isTagged).filter(Material::isItem).findAny().orElse(Material.PAPER)); + return new ItemStack(m, quantity); + } + + +} diff --git a/src/main/java/world/bentobox/challenges/panel/admin/ManageBlocksPanel.java b/src/main/java/world/bentobox/challenges/panel/admin/ManageBlocksPanel.java index cd43adf7..f0ebbeb6 100644 --- a/src/main/java/world/bentobox/challenges/panel/admin/ManageBlocksPanel.java +++ b/src/main/java/world/bentobox/challenges/panel/admin/ManageBlocksPanel.java @@ -1,14 +1,9 @@ package world.bentobox.challenges.panel.admin; - import java.util.ArrayList; -import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; -import java.util.function.Consumer; -import java.util.stream.Collectors; import org.bukkit.Material; import org.bukkit.inventory.ItemStack; @@ -17,296 +12,182 @@ import world.bentobox.bentobox.api.panels.PanelItem; import world.bentobox.bentobox.api.panels.builders.PanelBuilder; import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; -import world.bentobox.challenges.panel.CommonPagedPanel; import world.bentobox.challenges.panel.CommonPanel; -import world.bentobox.challenges.panel.ConversationUtils; import world.bentobox.challenges.panel.util.MultiBlockSelector; import world.bentobox.challenges.utils.Constants; import world.bentobox.challenges.utils.Utils; - /** - * This class allows to edit material that are in required material map. + * The ManageBlocksPanel class provides a graphical interface for administrators + * to manage a collection of materials along with their required counts. + *

+ * It extends the abstract generic class {@code AbstractManageEnumPanel}, + * which provides common functionality for panels that handle paginated elements. + * This panel allows adding new materials using a multi-selection tool and + * removing selected materials. When a material is clicked (left or right), a + * numeric input conversation can be started to update the count associated with + * that material. + *

+ * + *

Usage: To display this panel, call the static {@link #open(CommonPanel, Map)} + * method with the parent panel and the map of materials to their counts.

*/ -public class ManageBlocksPanel extends CommonPagedPanel -{ - private ManageBlocksPanel(CommonPanel parentGUI, Map materialMap) - { - super(parentGUI); - this.materialMap = materialMap; - this.materialList = new ArrayList<>(this.materialMap.keySet()); - - // Sort materials by their ordinal value. - this.materialList.sort(Comparator.comparing(Enum::name)); - - this.selectedMaterials = new HashSet<>(); - - // Init without filters applied. - this.filterElements = this.materialList; - } - - - /** - * Open the Challenges Admin GUI. - */ - public static void open(CommonPanel parentGUI, Map materialMap) - { - new ManageBlocksPanel(parentGUI, materialMap).build(); - } - - -// --------------------------------------------------------------------- -// Section: Methods -// --------------------------------------------------------------------- - - - /** - * This method is called when filter value is updated. - */ - @Override - protected void updateFilters() - { - if (this.searchString == null || this.searchString.isBlank()) - { - this.filterElements = this.materialList; - } - else - { - this.filterElements = this.materialList.stream(). - filter(element -> { - // If element name is set and name contains search field, then do not filter out. - return element.name().toLowerCase().contains(this.searchString.toLowerCase()); - }). - distinct(). - collect(Collectors.toList()); - } - } - - - /** - * This method builds all necessary elements in GUI panel. - */ - @Override - protected void build() - { - PanelBuilder panelBuilder = new PanelBuilder().user(this.user). - name(this.user.getTranslation(Constants.TITLE + "manage-blocks")); - - // Create nice border. - PanelUtils.fillBorder(panelBuilder); - - panelBuilder.item(3, this.createButton(Button.ADD_BLOCK)); - panelBuilder.item(5, this.createButton(Button.REMOVE_BLOCK)); - - this.populateElements(panelBuilder, this.filterElements); - - // Add return button. - panelBuilder.item(44, this.returnButton); - - panelBuilder.build(); - } - - - /** - * This method creates PanelItem button of requested type. - * @param button Button which must be created. - * @return new PanelItem with requested functionality. - */ - private PanelItem createButton(Button button) - { - final String reference = Constants.BUTTON + button.name().toLowerCase() + "."; - - final String name = this.user.getTranslation(reference + "name"); - final List description = new ArrayList<>(3); - description.add(this.user.getTranslation(reference + "description")); - - ItemStack icon; - PanelItem.ClickHandler clickHandler; - boolean glow; - - switch (button) - { - case ADD_BLOCK -> { - icon = new ItemStack(Material.BUCKET); - clickHandler = (panel, user1, clickType, slot) -> - { - MultiBlockSelector.open(this.user, - MultiBlockSelector.Mode.BLOCKS, - new HashSet<>(this.materialList), - (status, materials) -> - { - if (status) - { - materials.forEach(material -> - { - this.materialMap.put(material, 1); - this.materialList.add(material); - }); - } - - this.build(); - }); - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-add")); - } - case REMOVE_BLOCK -> { - - if (!this.selectedMaterials.isEmpty()) - { - description.add(this.user.getTranslation(reference + "title")); - this.selectedMaterials.forEach(material -> - description.add(this.user.getTranslation(reference + "material", - "[material]", Utils.prettifyObject(material, this.user)))); - } - - icon = new ItemStack(Material.LAVA_BUCKET); - - clickHandler = (panel, user1, clickType, slot) -> - { - if (!this.selectedMaterials.isEmpty()) - { - this.materialMap.keySet().removeAll(this.selectedMaterials); - this.materialList.removeAll(this.selectedMaterials); - this.selectedMaterials.clear(); - this.build(); - } - - return true; - }; - - glow = !this.selectedMaterials.isEmpty(); - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-remove")); - } - default -> { - icon = new ItemStack(Material.PAPER); - clickHandler = null; - glow = false; - } - } - - return new PanelItemBuilder(). - icon(icon). - name(name). - description(description). - clickHandler(clickHandler). - glow(glow). - build(); - } - - - /** - * This method creates button for given material. - * @param material material which button must be created. - * @return new Button for material. - */ - @Override - protected PanelItem createElementButton(Material material) - { - final String reference = Constants.BUTTON + "material."; - - List description = new ArrayList<>(); - - if (this.selectedMaterials.contains(material)) - { - description.add(this.user.getTranslation(reference + "selected")); - } - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "left-click-to-choose")); - - if (this.selectedMaterials.contains(material)) - { - description.add(this.user.getTranslation(Constants.TIPS + "right-click-to-deselect")); - } - else - { - description.add(this.user.getTranslation(Constants.TIPS + "right-click-to-select")); - } - - return new PanelItemBuilder(). - name(this.user.getTranslation(reference + "name", "[material]", - Utils.prettifyObject(material, this.user))). - icon(PanelUtils.getMaterialItem(material, this.materialMap.get(material))). - description(description). - clickHandler((panel, user1, clickType, slot) -> { - // On right click change which entities are selected for deletion. - if (clickType.isRightClick()) - { - if (!this.selectedMaterials.add(material)) - { - // Remove material if it is already selected - this.selectedMaterials.remove(material); - } - - this.build(); - } - else - { - Consumer numberConsumer = number -> { - if (number != null) - { - this.materialMap.put(material, number.intValue()); - } - - // reopen panel - this.build(); - }; - - ConversationUtils.createNumericInput(numberConsumer, - this.user, - this.user.getTranslation(Constants.CONVERSATIONS + "input-number"), - 1, - Integer.MAX_VALUE); - } - return true; - }). - glow(this.selectedMaterials.contains(material)). - build(); - } - - -// --------------------------------------------------------------------- -// Section: Enums -// --------------------------------------------------------------------- - - - /** - * Functional buttons in current GUI. - */ - private enum Button - { - ADD_BLOCK, - REMOVE_BLOCK - } - - -// --------------------------------------------------------------------- -// Section: Variables -// --------------------------------------------------------------------- - - /** - * Contains selected materials. - */ - private final Set selectedMaterials; - - /** - * List of materials to avoid order issues. - */ - private final List materialList; - - /** - * List of required materials. - */ - private final Map materialMap; - - /** - * Stores filtered items. - */ - private List filterElements; +public class ManageBlocksPanel extends AbstractManageEnumPanel { + + /** + * Private constructor that initializes the ManageBlocksPanel with the provided + * material map. + * + * @param parentGUI The parent panel that spawns this panel. + * @param materialMap A map of Material objects to their required counts. + */ + private ManageBlocksPanel(CommonPanel parentGUI, Map materialMap) { + super(parentGUI, materialMap); + } + + /** + * Opens the Manage Blocks panel. + * + * @param parentGUI The parent panel that spawns this panel. + * @param materialMap A map of Material objects to their required counts. + */ + public static void open(CommonPanel parentGUI, Map materialMap) { + new ManageBlocksPanel(parentGUI, materialMap).build(); + } + + /** + * Provides the icon for a material element. + * + * @param material The material for which the icon is required. + * @param count The count to be displayed on the icon. + * @return An ItemStack representing the material icon. + */ + @Override + protected ItemStack getElementIcon(Material material, int count) { + return PanelUtils.getMaterialItem(material, count); + } + + /** + * Returns the translation prefix used for buttons related to materials. + * + * @return A string containing the translation prefix for material buttons. + */ + @Override + protected String getElementTranslationPrefix() { + return Constants.BUTTON + "material."; + } + + /** + * Returns the placeholder key used in translations for a material. + * + * @return The placeholder key for materials. + */ + @Override + protected String getElementPlaceholder() { + return "[material]"; + } + + /** + * Returns the translation key for the title of the panel. + * + * @return A string containing the translation key for the panel title. + */ + @Override + protected String getPanelTitleKey() { + return Constants.TITLE + "manage-blocks"; + } + + /** + * Adds functional buttons (e.g., Add and Remove) to the panel. + * + * @param panelBuilder The PanelBuilder used to construct the panel. + */ + @Override + protected void addFunctionalButtons(PanelBuilder panelBuilder) { + // Position 3: Button for adding new materials. + panelBuilder.item(3, createButton(Button.ADD_BLOCK)); + // Position 5: Button for removing selected materials. + panelBuilder.item(5, createButton(Button.REMOVE_BLOCK)); + } + + /** + * Creates a functional button based on the specified type. + * + * @param button The button type to create. + * @return A PanelItem representing the functional button. + */ + private PanelItem createButton(Button button) { + final String reference = Constants.BUTTON + button.name().toLowerCase() + "."; + final String name = this.user.getTranslation(reference + "name"); + final List description = new ArrayList<>(3); + description.add(this.user.getTranslation(reference + "description")); + + ItemStack icon; + PanelItem.ClickHandler clickHandler; + boolean glow; + + switch (button) { + case ADD_BLOCK -> { + icon = new ItemStack(Material.BUCKET); + clickHandler = (panel, user, clickType, slot) -> { + // Open a multi-selection tool to add new materials. + MultiBlockSelector.open(this.user, MultiBlockSelector.Mode.BLOCKS, new HashSet<>(this.itemList), + (status, materials) -> { + if (status) { + // For each selected material, add it to the map with a default count. + materials.forEach(material -> { + this.itemsMap.put(material, 1); + this.itemList.add(material); + }); + } + // Rebuild the panel to reflect changes. + this.build(); + }); + return true; + }; + glow = false; + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-add")); + } + case REMOVE_BLOCK -> { + if (!this.selectedItems.isEmpty()) { + // If any materials are selected, list them in the description. + description.add(this.user.getTranslation(reference + "title")); + this.selectedItems.forEach(material -> description.add(this.user.getTranslation(reference + "material", + "[material]", Utils.prettifyObject(material, this.user)))); + } + icon = new ItemStack(Material.LAVA_BUCKET); + clickHandler = (panel, user, clickType, slot) -> { + if (!this.selectedItems.isEmpty()) { + // Remove all selected materials from the map and list. + this.itemsMap.keySet().removeAll(this.selectedItems); + this.itemList.removeAll(this.selectedItems); + this.selectedItems.clear(); + // Rebuild the panel after removal. + this.build(); + } + return true; + }; + glow = !this.selectedItems.isEmpty(); + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-remove")); + } + default -> { + icon = new ItemStack(Material.PAPER); + clickHandler = null; + glow = false; + } + } + + return new PanelItemBuilder().icon(icon).name(name).description(description).clickHandler(clickHandler) + .glow(glow).build(); + } + + /** + * Enumeration of functional buttons in the Manage Blocks panel. + */ + private enum Button { + ADD_BLOCK, + REMOVE_BLOCK + } } diff --git a/src/main/java/world/bentobox/challenges/panel/admin/ManageEntitiesPanel.java b/src/main/java/world/bentobox/challenges/panel/admin/ManageEntitiesPanel.java index 951467b1..a1534d95 100644 --- a/src/main/java/world/bentobox/challenges/panel/admin/ManageEntitiesPanel.java +++ b/src/main/java/world/bentobox/challenges/panel/admin/ManageEntitiesPanel.java @@ -1,330 +1,198 @@ package world.bentobox.challenges.panel.admin; - import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; -import java.util.function.Consumer; -import java.util.stream.Collectors; import org.bukkit.Material; +import org.bukkit.Tag; import org.bukkit.entity.EntityType; import org.bukkit.inventory.ItemStack; -import lv.id.bonne.panelutils.PanelUtils; import world.bentobox.bentobox.api.panels.PanelItem; import world.bentobox.bentobox.api.panels.builders.PanelBuilder; import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; -import world.bentobox.challenges.panel.CommonPagedPanel; import world.bentobox.challenges.panel.CommonPanel; -import world.bentobox.challenges.panel.ConversationUtils; import world.bentobox.challenges.panel.util.MultiEntitySelector; +import world.bentobox.challenges.panel.util.SingleEntitySelector; import world.bentobox.challenges.utils.Constants; import world.bentobox.challenges.utils.Utils; - /** - * This class allows to edit entities that are in required entities map. + * The ManageEntitiesPanel class provides a graphical interface for administrators + * to manage a collection of entities (of type {@link EntityType}) along with their required counts. + *

+ * This panel enables adding new entities via a multi-selection tool, removing selected entities, + * and toggling the display between entity eggs and mob heads. It extends the generic + * {@code AbstractManageEnumPanel} to share common functionality with other paginated panels. + *

+ * + *

Usage: To display this panel, call the static {@link #open(CommonPanel, Map)} + * method with the parent panel and the map of entities to their counts.

*/ -public class ManageEntitiesPanel extends CommonPagedPanel -{ - private ManageEntitiesPanel(CommonPanel parentGUI, Map requiredEntities) - { - super(parentGUI); - this.requiredEntities = requiredEntities; - - this.entityList = new ArrayList<>(this.requiredEntities.keySet()); - this.entityList.sort(Comparator.comparing(Enum::name)); - - this.selectedEntities = new HashSet<>(EntityType.values().length); - this.filterElements = this.entityList; - } - - - /** - * Open the Challenges Admin GUI. - */ - public static void open(CommonPanel parentGUI, Map requiredEntities) - { - new ManageEntitiesPanel(parentGUI, requiredEntities).build(); - } - - -// --------------------------------------------------------------------- -// Section: Methods -// --------------------------------------------------------------------- - - - /** - * This method is called when filter value is updated. - */ - @Override - protected void updateFilters() - { - if (this.searchString == null || this.searchString.isBlank()) - { - this.filterElements = this.entityList; - } - else - { - this.filterElements = this.entityList.stream(). - filter(element -> { - // If element name is set and name contains search field, then do not filter out. - return element.name().toLowerCase().contains(this.searchString.toLowerCase()); - }). - distinct(). - collect(Collectors.toList()); - } - } - - - /** - * This method builds all necessary elements in GUI panel. - */ - @Override - protected void build() - { - PanelBuilder panelBuilder = new PanelBuilder().user(this.user). - name(this.user.getTranslation(Constants.TITLE + "manage-entities")); - - // create border - PanelUtils.fillBorder(panelBuilder); - - panelBuilder.item(3, this.createButton(Button.ADD_ENTITY)); - panelBuilder.item(5, this.createButton(Button.REMOVE_ENTITY)); - panelBuilder.item(8, this.createButton(Button.SWITCH_ENTITY)); - - this.populateElements(panelBuilder, this.filterElements); - - // Add return button. - panelBuilder.item(44, this.returnButton); - - panelBuilder.build(); - } - - - /** - * This method creates PanelItem button of requested type. - * @param button Button which must be created. - * @return new PanelItem with requested functionality. - */ - private PanelItem createButton(Button button) - { - final String reference = Constants.BUTTON + button.name().toLowerCase() + "."; - - final String name = this.user.getTranslation(reference + "name"); - final List description = new ArrayList<>(3); - description.add(this.user.getTranslation(reference + "description")); - - ItemStack icon; - PanelItem.ClickHandler clickHandler; - boolean glow; - - switch (button) - { - case ADD_ENTITY -> { - icon = new ItemStack(Material.BUCKET); - clickHandler = (panel, user1, clickType, slot) -> { - MultiEntitySelector.open(this.user, - this.asEggs, - MultiEntitySelector.Mode.ALIVE, - this.requiredEntities.keySet(), - (status, entities) -> { - if (status) - { - entities.forEach(entity -> { - this.requiredEntities.put(entity, 1); - this.entityList.add(entity); - }); - } - - this.build(); - }); - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-add")); - } - case REMOVE_ENTITY -> { - - if (!this.selectedEntities.isEmpty()) - { - description.add(this.user.getTranslation(reference + "title")); - this.selectedEntities.forEach(entity -> - description.add(this.user.getTranslation(reference + "entity", - "[entity]", Utils.prettifyObject(entity, this.user)))); - } - - icon = new ItemStack(Material.LAVA_BUCKET); - - clickHandler = (panel, user1, clickType, slot) -> - { - if (!this.selectedEntities.isEmpty()) - { - this.requiredEntities.keySet().removeAll(this.selectedEntities); - this.entityList.removeAll(this.selectedEntities); - this.selectedEntities.clear(); - this.build(); - } - - return true; - }; - - glow = !this.entityList.isEmpty(); - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-remove")); - } - case SWITCH_ENTITY -> { - icon = new ItemStack(this.asEggs ? Material.EGG : Material.PLAYER_HEAD); - - clickHandler = (panel, user1, clickType, slot) -> { - this.asEggs = !this.asEggs; - this.build(); - return true; - }; - glow = false; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-toggle")); - } - default -> { - icon = new ItemStack(Material.PAPER); - clickHandler = null; - glow = false; - } - } - - return new PanelItemBuilder(). - icon(icon). - name(name). - description(description). - clickHandler(clickHandler). - glow(glow). - build(); - } - - - /** - * This method creates button for given entity. - * @param entity Entity which button must be created. - * @return new Button for entity. - */ - @Override - protected PanelItem createElementButton(EntityType entity) - { - final String reference = Constants.BUTTON + "entity."; - - List description = new ArrayList<>(); - - if (this.selectedEntities.contains(entity)) - { - description.add(this.user.getTranslation(reference + "selected")); - } - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "left-click-to-choose")); - - if (this.selectedEntities.contains(entity)) - { - description.add(this.user.getTranslation(Constants.TIPS + "right-click-to-deselect")); - } - else - { - description.add(this.user.getTranslation(Constants.TIPS + "right-click-to-select")); - } - - return new PanelItemBuilder(). - name(this.user.getTranslation(reference + "name", "[entity]", - Utils.prettifyObject(entity, this.user))). - icon(this.asEggs ? - PanelUtils.getEntityEgg(entity, this.requiredEntities.get(entity)) : - PanelUtils.getEntityHead(entity, this.requiredEntities.get(entity))). - description(description). - clickHandler((panel, user1, clickType, slot) -> { - // On right click change which entities are selected for deletion. - if (clickType.isRightClick()) - { - if (!this.selectedEntities.add(entity)) - { - // Remove entity if it is already selected - this.selectedEntities.remove(entity); - } - - this.build(); - } - else - { - Consumer numberConsumer = number -> { - if (number != null) - { - this.requiredEntities.put(entity, number.intValue()); - } - - // reopen panel - this.build(); - }; - - ConversationUtils.createNumericInput(numberConsumer, - this.user, - this.user.getTranslation(Constants.CONVERSATIONS + "input-number"), - 1, - Integer.MAX_VALUE); - } - return true; - }). - glow(this.selectedEntities.contains(entity)). - build(); - } - - -// --------------------------------------------------------------------- -// Section: Enums -// --------------------------------------------------------------------- - - - /** - * Functional buttons in current GUI. - */ - private enum Button - { - ADD_ENTITY, - REMOVE_ENTITY, - SWITCH_ENTITY - } - - -// --------------------------------------------------------------------- -// Section: Variables -// --------------------------------------------------------------------- - - /** - * List with entities to avoid list irregularities. - */ - private final List entityList; - - /** - * Set with entities that are selected. - */ - private final Set selectedEntities; - - /** - * Map that contains all entities and their cound. - */ - private final Map requiredEntities; - - /** - * Boolean indicate if entities should be displayed as eggs or mob heads. - */ - private boolean asEggs; - - /** - * Stores filtered items. - */ - private List filterElements; +public class ManageEntitiesPanel extends AbstractManageEnumPanel { + + /** + * Private constructor that initializes the ManageEntitiesPanel with the provided + * entities map. + * + * @param parentGUI The parent panel that spawns this panel. + * @param requiredEntities A map of EntityType objects to their required counts. + */ + private ManageEntitiesPanel(CommonPanel parentGUI, Map requiredEntities) { + super(parentGUI, requiredEntities); + } + + /** + * Opens the Manage Entities panel. + * + * @param parentGUI The parent panel that spawns this panel. + * @param requiredEntities A map of EntityType objects to their required counts. + */ + public static void open(CommonPanel parentGUI, Map requiredEntities) { + new ManageEntitiesPanel(parentGUI, requiredEntities).build(); + } + + /** + * Provides the icon for an entity element. + *

+ * Depending on the {@code asEggs} flag, this method returns either the egg icon or the mob head icon. + *

+ * + * @param entity The entity for which the icon is required. + * @param count The count to be displayed on the icon. + * @return An ItemStack representing the entity icon. + */ + @Override + protected ItemStack getElementIcon(EntityType entity, int count) { + if (Tag.ENTITY_TYPES_CAN_TURN_IN_BOATS.isTagged(entity) && count > 1) { + return new ItemStack(Material.OAK_PLANKS, count); // Boats cannot be stacked + } + ItemStack icon = SingleEntitySelector.getIcon(entity); + icon.setAmount(count); + return icon; + } + + /** + * Returns the translation prefix used for buttons related to entities. + * + * @return A string containing the translation prefix for entity buttons. + */ + @Override + protected String getElementTranslationPrefix() { + return Constants.BUTTON + "entity."; + } + + /** + * Returns the placeholder key used in translations for an entity. + * + * @return The placeholder key for entities. + */ + @Override + protected String getElementPlaceholder() { + return "[entity]"; + } + + /** + * Returns the translation key for the title of the panel. + * + * @return A string containing the translation key for the panel title. + */ + @Override + protected String getPanelTitleKey() { + return Constants.TITLE + "manage-entities"; + } + + /** + * Adds functional buttons (e.g., Add, Remove, and Switch Display Mode) to the panel. + * + * @param panelBuilder The PanelBuilder used to construct the panel. + */ + @Override + protected void addFunctionalButtons(PanelBuilder panelBuilder) { + // Position 3: Button for adding new entities. + panelBuilder.item(3, createButton(Button.ADD_ENTITY)); + // Position 5: Button for removing selected entities. + panelBuilder.item(5, createButton(Button.REMOVE_ENTITY)); + } + + /** + * Creates a functional button based on the specified action. + * + * @param button The button type to create. + * @return A PanelItem representing the functional button. + */ + private PanelItem createButton(Button button) { + final String reference = Constants.BUTTON + button.name().toLowerCase() + "."; + final String name = this.user.getTranslation(reference + "name"); + final List description = new ArrayList<>(3); + description.add(this.user.getTranslation(reference + "description")); + + ItemStack icon; + PanelItem.ClickHandler clickHandler; + boolean glow; + + switch (button) { + case ADD_ENTITY -> { + icon = new ItemStack(Material.BUCKET); + clickHandler = (panel, user, clickType, slot) -> { + // Open a multi-selection tool to add new entities. + MultiEntitySelector.open(this.user, MultiEntitySelector.Mode.ALIVE, this.itemsMap.keySet(), + (status, entities) -> { + if (status) { + // For each selected entity, add it to the map with a default count. + entities.forEach(entity -> { + this.itemsMap.put(entity, 1); + this.itemList.add(entity); + }); + } + // Rebuild the panel to reflect changes. + this.build(); + }); + return true; + }; + glow = false; + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-add")); + } + case REMOVE_ENTITY -> { + if (!this.selectedItems.isEmpty()) { + // If any entities are selected, list them in the description. + description.add(this.user.getTranslation(reference + "title")); + this.selectedItems.forEach(entity -> description.add(this.user.getTranslation(reference + "entity", + "[entity]", Utils.prettifyObject(entity, this.user)))); + } + icon = new ItemStack(Material.LAVA_BUCKET); + clickHandler = (panel, user, clickType, slot) -> { + if (!this.selectedItems.isEmpty()) { + // Remove all selected entities from the map and list. + this.itemsMap.keySet().removeAll(this.selectedItems); + this.itemList.removeAll(this.selectedItems); + this.selectedItems.clear(); + // Rebuild the panel after removal. + this.build(); + } + return true; + }; + glow = !this.selectedItems.isEmpty(); + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-remove")); + } + default -> { + icon = new ItemStack(Material.PAPER); + clickHandler = null; + glow = false; + } + } + + return new PanelItemBuilder().icon(icon).name(name).description(description).clickHandler(clickHandler) + .glow(glow).build(); + } + + /** + * Enumeration of functional buttons in the Manage Entities panel. + */ + private enum Button { + ADD_ENTITY, REMOVE_ENTITY, SWITCH_ENTITY + } } diff --git a/src/main/java/world/bentobox/challenges/panel/admin/ManageEntityGroupsPanel.java b/src/main/java/world/bentobox/challenges/panel/admin/ManageEntityGroupsPanel.java new file mode 100644 index 00000000..a8c8cf0e --- /dev/null +++ b/src/main/java/world/bentobox/challenges/panel/admin/ManageEntityGroupsPanel.java @@ -0,0 +1,330 @@ +package world.bentobox.challenges.panel.admin; + + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import org.bukkit.Material; +import org.bukkit.Registry; +import org.bukkit.Tag; +import org.bukkit.entity.EntityType; +import org.bukkit.inventory.ItemStack; +import org.eclipse.jdt.annotation.Nullable; + +import lv.id.bonne.panelutils.PanelUtils; +import world.bentobox.bentobox.api.panels.PanelItem; +import world.bentobox.bentobox.api.panels.builders.PanelBuilder; +import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; +import world.bentobox.challenges.panel.CommonPagedPanel; +import world.bentobox.challenges.panel.CommonPanel; +import world.bentobox.challenges.panel.ConversationUtils; +import world.bentobox.challenges.panel.util.MultiEntityTypeTagsSelector; +import world.bentobox.challenges.panel.util.SingleEntitySelector; +import world.bentobox.challenges.panel.util.UnifiedMultiSelector.Mode; +import world.bentobox.challenges.utils.Constants; +import world.bentobox.challenges.utils.Utils; + + +/** + * This class allows to edit material that are in required material map. + */ +public class ManageEntityGroupsPanel extends CommonPagedPanel> +{ + + // --------------------------------------------------------------------- + // Section: Enums + // --------------------------------------------------------------------- + + /** + * Functional buttons in current GUI. + */ + private enum Button { + ADD_ENTITY_GROUP, REMOVE_ENTITY_GROUP + } + + // --------------------------------------------------------------------- + // Section: Variables + // --------------------------------------------------------------------- + + /** + * Contains selected materials. + */ + private final Set> selectedTags; + + /** + * List of materials to avoid order issues. + */ + private final List> materialList; + + /** + * List of required materials. + */ + private final Map, Integer> tagMap; + + /** + * Stores filtered items. + */ + private List> filterElements; + + private ManageEntityGroupsPanel(CommonPanel parentGUI, Map, Integer> map) + { + super(parentGUI); + this.tagMap = map; + this.materialList = new ArrayList<>(this.tagMap.keySet()); + + // Sort tags by their ordinal value. + this.materialList.sort(Comparator.comparing(tag -> tag.getKey().getKey())); + + this.selectedTags = new HashSet<>(); + + // Init without filters applied. + this.filterElements = this.materialList; + } + + + /** + * Open the Challenges Admin GUI. + */ + public static void open(CommonPanel parentGUI, Map, Integer> map) + { + new ManageEntityGroupsPanel(parentGUI, map).build(); + } + + +// --------------------------------------------------------------------- +// Section: Methods +// --------------------------------------------------------------------- + + + /** + * This method is called when filter value is updated. + */ + @Override + protected void updateFilters() + { + if (this.searchString == null || this.searchString.isBlank()) + { + this.filterElements = this.materialList; + } + else + { + this.filterElements = this.materialList.stream(). + filter(element -> { + // If element name is set and name contains search field, then do not filter out. + return element.getKey().getKey().toLowerCase(Locale.ENGLISH) + .contains(this.searchString.toLowerCase(Locale.ENGLISH)); + }). + distinct(). + collect(Collectors.toList()); + } + } + + + /** + * This method builds all necessary elements in GUI panel. + */ + @Override + protected void build() + { + PanelBuilder panelBuilder = new PanelBuilder().user(this.user). + name(this.user.getTranslation(Constants.TITLE + "manage-entity-groups")); + + // Create nice border. + PanelUtils.fillBorder(panelBuilder); + + panelBuilder.item(3, this.createButton(Button.ADD_ENTITY_GROUP)); + panelBuilder.item(5, this.createButton(Button.REMOVE_ENTITY_GROUP)); + // Fill the box with what is selected + this.populateElements(panelBuilder, this.filterElements); + + // Add return button. + panelBuilder.item(44, this.returnButton); + + panelBuilder.build(); + } + + + /** + * This method creates PanelItem button of requested type. + * @param button Button which must be created. + * @return new PanelItem with requested functionality. + */ + private PanelItem createButton(Button button) + { + final String reference = Constants.BUTTON + button.name().toLowerCase() + "."; + + final String name = this.user.getTranslation(reference + "name"); + final List description = new ArrayList<>(3); + description.add(this.user.getTranslation(reference + "description")); + + ItemStack icon; + PanelItem.ClickHandler clickHandler; + boolean glow; + + switch (button) + { + case ADD_ENTITY_GROUP -> { + icon = new ItemStack(Material.BUCKET); + clickHandler = (panel, user1, clickType, slot) -> + { + MultiEntityTypeTagsSelector.open(this.user, Mode.ENTITY_TYPE, + new HashSet<>(this.materialList), + (status, materials) -> + { + if (status) + { + materials.forEach(material -> + { + this.tagMap.put(material, 1); + this.materialList.add(material); + }); + } + + this.build(); + }); + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-add")); + } + case REMOVE_ENTITY_GROUP -> { + + if (!this.selectedTags.isEmpty()) + { + description.add(this.user.getTranslation(reference + "title")); + this.selectedTags.forEach(material -> + description.add(this.user.getTranslation(reference + "entity", "[tag]", + Utils.prettifyObject(material, this.user)))); + } + + icon = new ItemStack(Material.LAVA_BUCKET); + + clickHandler = (panel, user1, clickType, slot) -> + { + if (!this.selectedTags.isEmpty()) + { + this.tagMap.keySet().removeAll(this.selectedTags); + this.materialList.removeAll(this.selectedTags); + this.selectedTags.clear(); + this.build(); + } + + return true; + }; + + glow = !this.selectedTags.isEmpty(); + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-remove")); + } + default -> { + icon = new ItemStack(Material.PAPER); + clickHandler = null; + glow = false; + } + } + + return new PanelItemBuilder(). + icon(icon). + name(name). + description(description). + clickHandler(clickHandler). + glow(glow). + build(); + } + + + /** + * This method creates button for given material. + * @param tag material which button must be created. + * @return new Button for material. + */ + @Override + protected PanelItem createElementButton(Tag tag) + { + final String reference = Constants.BUTTON + "entity-group."; + + List description = new ArrayList<>(); + + if (this.selectedTags.contains(tag)) + { + description.add(this.user.getTranslation(reference + "selected")); + } + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "left-click-to-choose")); + + if (this.selectedTags.contains(tag)) + { + description.add(this.user.getTranslation(Constants.TIPS + "right-click-to-deselect")); + } + else + { + description.add(this.user.getTranslation(Constants.TIPS + "right-click-to-select")); + } + + return new PanelItemBuilder(). + name(this.user.getTranslation(reference + "name", "[id]", + Utils.prettifyObject(tag, this.user))). + icon(getIcon(tag, this.tagMap.get(tag))). + description(description). + clickHandler((panel, user1, clickType, slot) -> { + // On right click change which entities are selected for deletion. + if (clickType.isRightClick()) + { + if (!this.selectedTags.add(tag)) + { + // Remove material if it is already selected + this.selectedTags.remove(tag); + } + + this.build(); + } + else + { + Consumer numberConsumer = number -> { + if (number != null) + { + this.tagMap.put(tag, number.intValue()); + } + + // reopen panel + this.build(); + }; + + ConversationUtils.createNumericInput(numberConsumer, + this.user, + this.user.getTranslation(Constants.CONVERSATIONS + "input-number"), + 1, + Integer.MAX_VALUE); + } + return true; + }). + glow(this.selectedTags.contains(tag)). + build(); + } + + private @Nullable ItemStack getIcon(Tag entityTag, Integer quantity) { + if (entityTag.getKey().getKey().contains("boat")) { + return new ItemStack(Material.OAK_PLANKS, quantity); // Boats cannot be stacked + } + EntityType entType = Registry.ENTITY_TYPE.stream() + .filter(entityTag::isTagged).findAny().orElse(null); + if (entType == null) { + return new ItemStack(Material.PAPER, quantity); + } + return SingleEntitySelector.getIcon(entType); + + } + + +} diff --git a/src/main/java/world/bentobox/challenges/panel/admin/ManageStatisticsPanel.java b/src/main/java/world/bentobox/challenges/panel/admin/ManageStatisticsPanel.java new file mode 100644 index 00000000..7d2a40a4 --- /dev/null +++ b/src/main/java/world/bentobox/challenges/panel/admin/ManageStatisticsPanel.java @@ -0,0 +1,613 @@ +package world.bentobox.challenges.panel.admin; + + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import org.bukkit.Material; +import org.bukkit.Statistic; +import org.bukkit.inventory.ItemStack; + +import lv.id.bonne.panelutils.PanelUtils; +import world.bentobox.bentobox.api.panels.Panel; +import world.bentobox.bentobox.api.panels.PanelItem; +import world.bentobox.bentobox.api.panels.builders.PanelBuilder; +import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; +import world.bentobox.challenges.database.object.requirements.StatisticRequirements.StatisticRec; +import world.bentobox.challenges.panel.CommonPagedPanel; +import world.bentobox.challenges.panel.CommonPanel; +import world.bentobox.challenges.panel.ConversationUtils; +import world.bentobox.challenges.panel.admin.EditChallengePanel.RequirementButton; +import world.bentobox.challenges.panel.util.SingleBlockSelector; +import world.bentobox.challenges.panel.util.SingleEntitySelector; +import world.bentobox.challenges.panel.util.StatisticSelector; +import world.bentobox.challenges.utils.Constants; +import world.bentobox.challenges.utils.Utils; + + +/** + * This class allows to edit material that are in required material map. + */ +public class ManageStatisticsPanel extends CommonPagedPanel +{ + + // --------------------------------------------------------------------- + // Section: Enums + // --------------------------------------------------------------------- + + /** + * Functional buttons in current GUI. + */ + private enum Button { + ADD_STATISTIC, REMOVE_STATISTIC + } + + // --------------------------------------------------------------------- + // Section: Variables + // --------------------------------------------------------------------- + + /** + * Contains selected stats. + */ + private final Set selectedStats; + + /** + * List of required statistics + */ + private final List statisticsList; + + /** + * Stores filtered items. + */ + private List filterElements; + + private ManageStatisticsPanel(CommonPanel parentGUI, List statisticsList) + { + super(parentGUI); + this.statisticsList = statisticsList; + + // Sort tags by their ordinal value. + this.statisticsList.sort(Comparator.comparing(tag -> tag.statistic().getKey().getKey())); + + this.selectedStats = new HashSet<>(); + + // Init without filters applied. + this.filterElements = this.statisticsList; + } + + + /** + * Open the Challenges Admin GUI. + */ + public static void open(CommonPanel parentGUI, List statisticsList) { + new ManageStatisticsPanel(parentGUI, statisticsList).build(); + } + + +// --------------------------------------------------------------------- +// Section: Methods +// --------------------------------------------------------------------- + + + /** + * This method is called when filter value is updated. + */ + @Override + protected void updateFilters() + { + if (this.searchString == null || this.searchString.isBlank()) + { + this.filterElements = this.statisticsList; + } + else + { + this.filterElements = this.statisticsList.stream(). + filter(element -> { + // If element name is set and name contains search field, then do not filter out. + return element.statistic().getKey().getKey().toLowerCase(Locale.ENGLISH) + .contains(this.searchString.toLowerCase(Locale.ENGLISH)); + }). + distinct(). + collect(Collectors.toList()); + } + } + + + /** + * This method builds all necessary elements in GUI panel. + */ + @Override + protected void build() + { + PanelBuilder panelBuilder = new PanelBuilder().user(this.user). + name(this.user.getTranslation(Constants.TITLE + "manage-statistics")); + + // Create nice border. + PanelUtils.fillBorder(panelBuilder); + + panelBuilder.item(3, this.createButton(Button.ADD_STATISTIC)); + panelBuilder.item(5, this.createButton(Button.REMOVE_STATISTIC)); + // Fill the box with what is selected + this.populateElements(panelBuilder, this.filterElements); + + // Add return button. + panelBuilder.item(44, this.returnButton); + + panelBuilder.build(); + } + + + /** + * This method creates PanelItem button of requested type. + * @param button Button which must be created. + * @return new PanelItem with requested functionality. + */ + private PanelItem createButton(Button button) + { + final String reference = Constants.BUTTON + button.name().toLowerCase() + "."; + + final String name = this.user.getTranslation(reference + "name"); + final List description = new ArrayList<>(3); + description.add(this.user.getTranslation(reference + "description")); + + ItemStack icon; + PanelItem.ClickHandler clickHandler; + boolean glow; + + switch (button) + { + case ADD_STATISTIC -> { + icon = new ItemStack(Material.BUCKET); + clickHandler = (panel, user1, clickType, slot) -> + { + StatisticSelector.open(this.user, (status, statistic) -> + { + if (status) + { + StatisticRec newItem = new StatisticRec(statistic, null, null, 0, false); + this.statisticsList.add(newItem); + + } + + this.build(); + }); + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-add")); + } + case REMOVE_STATISTIC -> { + + if (!this.selectedStats.isEmpty()) + { + description.add(this.user.getTranslation(reference + "title")); + this.selectedStats.forEach(stat -> + description.add(this.user.getTranslation(reference + "statistic_element", "[statistic]", + Utils.prettifyObject(stat.statistic(), this.user)))); + } + + icon = new ItemStack(Material.LAVA_BUCKET); + + clickHandler = (panel, user1, clickType, slot) -> + { + if (!this.selectedStats.isEmpty()) + { + this.statisticsList.removeAll(this.selectedStats); + this.selectedStats.clear(); + this.build(); + } + + return true; + }; + + glow = !this.selectedStats.isEmpty(); + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-remove")); + } + default -> { + icon = new ItemStack(Material.PAPER); + clickHandler = null; + glow = false; + } + } + + return new PanelItemBuilder(). + icon(icon). + name(name). + description(description). + clickHandler(clickHandler). + glow(glow). + build(); + } + + + /** + * This method creates button for given stat. + * @param rec material which button must be created. + * @return new Button for material. + */ + @Override + protected PanelItem createElementButton(StatisticRec rec) + { + final String reference = Constants.BUTTON + "statistic_element."; + + List description = new ArrayList<>(); + + // Show everything about this statistic + // Type is not shown to the user, just used to decide what to show + switch (rec.statistic().getType()) { + case BLOCK: + description.add(this.user.getTranslation(reference + "block", "[block]", + Utils.prettifyObject(rec.material(), this.user))); + break; + case ENTITY: + description.add(this.user.getTranslation(reference + "entity", "[entity]", + Utils.prettifyObject(rec.entity(), this.user))); + break; + case ITEM: + description.add(this.user.getTranslation(reference + "item", "[item]", + Utils.prettifyObject(rec.material(), this.user))); + break; + default: + break; + } + // Amount + description.add(this.user.getTranslation(reference + "amount", Constants.PARAMETER_NUMBER, + String.valueOf(Objects.requireNonNullElse(rec.amount(), 0)))); + // Removal + description.add(this.user.getTranslation(reference + "remove.name", "[value]", + this.user.getTranslation(reference + "remove.value." + + (Objects.requireNonNullElse(rec.reduceStatistic(), false) ? "enabled" : "disabled")))); + + if (this.selectedStats.contains(rec)) + { + description.add(this.user.getTranslation(reference + "selected")); + } + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "left-click-to-choose")); + + if (this.selectedStats.contains(rec)) + { + description.add(this.user.getTranslation(Constants.TIPS + "right-click-to-deselect")); + } + else + { + description.add(this.user.getTranslation(Constants.TIPS + "right-click-to-select")); + } + + return new PanelItemBuilder(). + name(this.user.getTranslation(reference + "name", "[statistic]", + Utils.prettifyObject(rec.statistic(), this.user))) + .icon(getStatisticIcon(rec.statistic())). + description(description). + clickHandler((panel, user1, clickType, slot) -> { + // On right click change which entities are selected for deletion. + if (clickType.isRightClick()) + { + if (!this.selectedStats.add(rec)) + { + // Remove material if it is already selected + this.selectedStats.remove(rec); + } + + this.build(); + } + else + { + // Left click + this.buildStatisticPanel(rec); + } + return true; + }). + glow(this.selectedStats.contains(rec)). + build(); + } + + /** + * Get an icon for a Statistic. Hand selected! + * @param stat Statistic + * @return ItemStack icon + */ + public static ItemStack getStatisticIcon(Statistic stat) { + return switch (stat) { + case ANIMALS_BRED -> new ItemStack(Material.WHEAT); + case ARMOR_CLEANED -> new ItemStack(Material.LEATHER_CHESTPLATE); + case AVIATE_ONE_CM -> new ItemStack(Material.ELYTRA); + case BANNER_CLEANED -> new ItemStack(Material.RED_BANNER); + case BEACON_INTERACTION -> new ItemStack(Material.BEACON); + case BELL_RING -> new ItemStack(Material.BELL); + case BOAT_ONE_CM -> new ItemStack(Material.OAK_BOAT); + case BREAK_ITEM -> new ItemStack(Material.AMETHYST_SHARD); + case BREWINGSTAND_INTERACTION -> new ItemStack(Material.BREWING_STAND); + case CAKE_SLICES_EATEN -> new ItemStack(Material.CAKE); + case CAULDRON_FILLED -> new ItemStack(Material.CAULDRON); + case CAULDRON_USED -> new ItemStack(Material.CAULDRON); + case CHEST_OPENED -> new ItemStack(Material.CHEST); + case CLEAN_SHULKER_BOX -> new ItemStack(Material.SHULKER_BOX); + case CLIMB_ONE_CM -> new ItemStack(Material.LADDER); + case CRAFTING_TABLE_INTERACTION -> new ItemStack(Material.CRAFTING_TABLE); + case CRAFT_ITEM -> new ItemStack(Material.CRAFTING_TABLE); + case CROUCH_ONE_CM -> new ItemStack(Material.PAPER); + case DAMAGE_ABSORBED -> new ItemStack(Material.IRON_SWORD); + case DAMAGE_BLOCKED_BY_SHIELD -> new ItemStack(Material.SHIELD); + case DAMAGE_DEALT -> new ItemStack(Material.DIAMOND_SWORD); + case DAMAGE_DEALT_ABSORBED -> new ItemStack(Material.GOLDEN_SWORD); + case DAMAGE_DEALT_RESISTED -> new ItemStack(Material.WOODEN_SWORD); + case DAMAGE_RESISTED -> new ItemStack(Material.IRON_HELMET); + case DAMAGE_TAKEN -> new ItemStack(Material.IRON_LEGGINGS); + case DEATHS -> new ItemStack(Material.OBSIDIAN); + case DISPENSER_INSPECTED -> new ItemStack(Material.DISPENSER); + case DROP -> new ItemStack(Material.PAPER); + case DROPPER_INSPECTED -> new ItemStack(Material.DROPPER); + case DROP_COUNT -> new ItemStack(Material.PAPER); + case ENDERCHEST_OPENED -> new ItemStack(Material.ENDER_CHEST); + case ENTITY_KILLED_BY -> new ItemStack(Material.BOW); + case FALL_ONE_CM -> new ItemStack(Material.PAPER); + case FISH_CAUGHT -> new ItemStack(Material.FISHING_ROD); + case FLOWER_POTTED -> new ItemStack(Material.FLOWER_POT); + case FLY_ONE_CM -> new ItemStack(Material.ELYTRA); + case FURNACE_INTERACTION -> new ItemStack(Material.FURNACE); + case HOPPER_INSPECTED -> new ItemStack(Material.HOPPER); + case HORSE_ONE_CM -> new ItemStack(Material.IRON_HORSE_ARMOR); + case INTERACT_WITH_ANVIL -> new ItemStack(Material.ANVIL); + case INTERACT_WITH_BLAST_FURNACE -> new ItemStack(Material.BLAST_FURNACE); + case INTERACT_WITH_CAMPFIRE -> new ItemStack(Material.CAMPFIRE); + case INTERACT_WITH_CARTOGRAPHY_TABLE -> new ItemStack(Material.CARTOGRAPHY_TABLE); + case INTERACT_WITH_GRINDSTONE -> new ItemStack(Material.GRINDSTONE); + case INTERACT_WITH_LECTERN -> new ItemStack(Material.LECTERN); + case INTERACT_WITH_LOOM -> new ItemStack(Material.LOOM); + case INTERACT_WITH_SMITHING_TABLE -> new ItemStack(Material.SMITHING_TABLE); + case INTERACT_WITH_SMOKER -> new ItemStack(Material.SMOKER); + case INTERACT_WITH_STONECUTTER -> new ItemStack(Material.STONECUTTER); + case ITEM_ENCHANTED -> new ItemStack(Material.ENCHANTED_GOLDEN_APPLE); + case JUMP -> new ItemStack(Material.RABBIT_FOOT); + case KILL_ENTITY -> new ItemStack(Material.DIAMOND_SWORD); + case LEAVE_GAME -> new ItemStack(Material.PAPER); + case MINECART_ONE_CM -> new ItemStack(Material.MINECART); + case MINE_BLOCK -> new ItemStack(Material.STONE); + case MOB_KILLS -> new ItemStack(Material.PAPER); + case NOTEBLOCK_PLAYED -> new ItemStack(Material.NOTE_BLOCK); + case NOTEBLOCK_TUNED -> new ItemStack(Material.NOTE_BLOCK); + case OPEN_BARREL -> new ItemStack(Material.BARREL); + case PICKUP -> new ItemStack(Material.PAPER); + case PIG_ONE_CM -> new ItemStack(Material.PIG_SPAWN_EGG); + case PLAYER_KILLS -> new ItemStack(Material.PLAYER_HEAD); + case PLAY_ONE_MINUTE -> new ItemStack(Material.CLOCK); + case RAID_TRIGGER -> new ItemStack(Material.OMINOUS_BOTTLE); + case RAID_WIN -> new ItemStack(Material.GOLD_INGOT); + case RECORD_PLAYED -> new ItemStack(Material.MUSIC_DISC_BLOCKS); + case SHULKER_BOX_OPENED -> new ItemStack(Material.SHULKER_BOX); + case SLEEP_IN_BED -> new ItemStack(Material.RED_BED); + case SNEAK_TIME -> new ItemStack(Material.PAPER); + case SPRINT_ONE_CM -> new ItemStack(Material.PAPER); + case STRIDER_ONE_CM -> new ItemStack(Material.STRIDER_SPAWN_EGG); + case SWIM_ONE_CM -> new ItemStack(Material.WATER_BUCKET); + case TALKED_TO_VILLAGER -> new ItemStack(Material.EMERALD); + case TARGET_HIT -> new ItemStack(Material.TARGET); + case TIME_SINCE_DEATH -> new ItemStack(Material.CLOCK); + case TIME_SINCE_REST -> new ItemStack(Material.CLOCK); + case TOTAL_WORLD_TIME -> new ItemStack(Material.CLOCK); + case TRADED_WITH_VILLAGER -> new ItemStack(Material.EMERALD); + case TRAPPED_CHEST_TRIGGERED -> new ItemStack(Material.TRAPPED_CHEST); + case USE_ITEM -> new ItemStack(Material.STICK); + case WALK_ONE_CM -> new ItemStack(Material.LEATHER_BOOTS); + case WALK_ON_WATER_ONE_CM -> new ItemStack(Material.LILY_PAD); + case WALK_UNDER_WATER_ONE_CM -> new ItemStack(Material.WATER_BUCKET); + default -> new ItemStack(Material.PAPER); + + }; + } + + private Panel buildStatisticPanel(StatisticRec req) { + + PanelBuilder panelBuilder = new PanelBuilder().user(this.user); + + panelBuilder.name(this.user.getTranslation(Constants.TITLE + "statistic-selector")); + + PanelUtils.fillBorder(panelBuilder); + + panelBuilder.item(10, this.createStatisticRequirementButton(RequirementButton.STATISTIC, req)); + panelBuilder.item(19, this.createStatisticRequirementButton(RequirementButton.REMOVE_STATISTIC, req)); + + panelBuilder.item(11, this.createStatisticRequirementButton(RequirementButton.STATISTIC_AMOUNT, req)); + switch (req.statistic().getType()) { + case BLOCK: + panelBuilder.item(13, this.createStatisticRequirementButton(RequirementButton.STATISTIC_BLOCKS, req)); + break; + case ENTITY: + panelBuilder.item(13, this.createStatisticRequirementButton(RequirementButton.STATISTIC_ENTITIES, req)); + break; + case ITEM: + panelBuilder.item(13, this.createStatisticRequirementButton(RequirementButton.STATISTIC_ITEMS, req)); + break; + case UNTYPED: + break; + default: + break; + + } + panelBuilder.item(44, this.returnButton); + return panelBuilder.build(); + } + + /** + * Creates a button for statistic requirements. + * + * @param button Button that must be created. + * @param req + * @return PanelItem button. + */ + private PanelItem createStatisticRequirementButton(RequirementButton button, StatisticRec req) { + + final String reference = Constants.BUTTON + button.name().toLowerCase(Locale.ENGLISH) + "."; + final String name = this.user.getTranslation(reference + "name"); + final List description = new ArrayList<>(3); + description.add(this.user.getTranslation(reference + "description")); + + ItemStack icon; + boolean glow; + PanelItem.ClickHandler clickHandler; + + switch (button) { + case STATISTIC -> { + description.add(this.user.getTranslation(reference + "value", "[statistic]", + Utils.prettifyObject(req.statistic(), this.user))); + + icon = new ItemStack(req.statistic() == null ? Material.BARRIER : Material.PAPER); + clickHandler = (panel, user, clickType, slot) -> { + StatisticSelector.open(this.user, (status, statistic) -> { + if (status) { + // Replace the old with the new + statisticsList.removeIf(sr -> sr.equals(req)); + statisticsList.add(new StatisticRec(statistic, null, null, 0, false)); + } + this.build(); + }); + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + } + case STATISTIC_AMOUNT -> { + description.add(this.user.getTranslation(reference + "value", Constants.PARAMETER_NUMBER, + String.valueOf(req.amount()))); + icon = new ItemStack(Material.CHEST); + clickHandler = (panel, user, clickType, i) -> { + Consumer numberConsumer = number -> { + if (number != null) { + // Replace the old with the new + statisticsList.removeIf(sr -> sr.equals(req)); + statisticsList.add(new StatisticRec(req.statistic(), req.entity(), req.material(), + number.intValue(), req.reduceStatistic())); + } + + // reopen panel + this.build(); + }; + ConversationUtils.createNumericInput(numberConsumer, this.user, + this.user.getTranslation(Constants.CONVERSATIONS + "input-number"), 0, Integer.MAX_VALUE); + + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + } + case REMOVE_STATISTIC -> { + description.add(this.user.getTranslation(reference + (req.reduceStatistic() ? "enabled" : "disabled"))); + + icon = new ItemStack(Material.LEVER); + clickHandler = (panel, user, clickType, slot) -> { + // Replace the old with the new + statisticsList.removeIf(sr -> sr.equals(req)); + statisticsList.add(new StatisticRec(req.statistic(), req.entity(), req.material(), req.amount(), + !req.reduceStatistic())); + + this.build(); + return true; + }; + glow = req.reduceStatistic(); + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-toggle")); + } + case STATISTIC_BLOCKS -> { + description.add(this.user.getTranslation(reference + "value", "[block]", + Utils.prettifyObject(req.material(), this.user))); + + icon = req.material() == null ? new ItemStack(Material.BARRIER) : new ItemStack(req.material()); + clickHandler = (panel, user, clickType, slot) -> { + SingleBlockSelector.open(this.user, SingleBlockSelector.Mode.BLOCKS, (status, block) -> { + if (status) { + // Replace the old with the new + statisticsList.removeIf(sr -> sr.equals(req)); + statisticsList.add(new StatisticRec(req.statistic(), req.entity(), block, req.amount(), + req.reduceStatistic())); + + } + + this.build(); + }); + + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + } + case STATISTIC_ITEMS -> { + description.add(this.user.getTranslation(reference + "value", "[item]", + Utils.prettifyObject(req.material(), this.user))); + + icon = req.material() == null ? new ItemStack(Material.BARRIER) : new ItemStack(req.material()); + clickHandler = (panel, user, clickType, slot) -> { + SingleBlockSelector.open(this.user, SingleBlockSelector.Mode.ITEMS, (status, block) -> { + if (status) { + // Replace the old with the new + statisticsList.removeIf(sr -> sr.equals(req)); + statisticsList.add(new StatisticRec(req.statistic(), req.entity(), block, req.amount(), + req.reduceStatistic())); + } + + this.build(); + }); + + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + } + case STATISTIC_ENTITIES -> { + description.add(this.user.getTranslation(reference + "value", "[entity]", + Utils.prettifyObject(req.entity(), this.user))); + + icon = req.entity() == null ? new ItemStack(Material.BARRIER) + : new ItemStack(PanelUtils.getEntityEgg(req.entity())); + clickHandler = (panel, user, clickType, slot) -> { + SingleEntitySelector.open(this.user, (status, entity) -> { + if (status) { + // Replace the old with the new + statisticsList.removeIf(sr -> sr.equals(req)); + statisticsList.add(new StatisticRec(req.statistic(), entity, req.material(), req.amount(), + req.reduceStatistic())); + } + + this.build(); + }); + + return true; + }; + glow = false; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-change")); + } + default -> { + icon = new ItemStack(Material.PAPER); + clickHandler = null; + glow = false; + } + } + return new PanelItemBuilder().icon(icon).name(name).description(description).glow(glow) + .clickHandler(clickHandler).build(); + } + + +} diff --git a/src/main/java/world/bentobox/challenges/panel/user/ChallengesPanel.java b/src/main/java/world/bentobox/challenges/panel/user/ChallengesPanel.java index 4095dc57..7b8b7e1d 100644 --- a/src/main/java/world/bentobox/challenges/panel/user/ChallengesPanel.java +++ b/src/main/java/world/bentobox/challenges/panel/user/ChallengesPanel.java @@ -7,17 +7,18 @@ package world.bentobox.challenges.panel.user; -import org.bukkit.World; -import org.bukkit.event.inventory.ClickType; -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 org.bukkit.World; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.inventory.ItemStack; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; + import world.bentobox.bentobox.api.panels.PanelItem; import world.bentobox.bentobox.api.panels.TemplatedPanel; import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; diff --git a/src/main/java/world/bentobox/challenges/panel/user/GameModePanel.java b/src/main/java/world/bentobox/challenges/panel/user/GameModePanel.java index c8ed4778..1b3b9b0b 100644 --- a/src/main/java/world/bentobox/challenges/panel/user/GameModePanel.java +++ b/src/main/java/world/bentobox/challenges/panel/user/GameModePanel.java @@ -7,15 +7,16 @@ 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 org.bukkit.World; +import org.bukkit.inventory.ItemStack; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; + import world.bentobox.bentobox.api.addons.GameModeAddon; import world.bentobox.bentobox.api.panels.PanelItem; import world.bentobox.bentobox.api.panels.TemplatedPanel; diff --git a/src/main/java/world/bentobox/challenges/panel/user/MultiplePanel.java b/src/main/java/world/bentobox/challenges/panel/user/MultiplePanel.java index 011f66ae..c951cc09 100644 --- a/src/main/java/world/bentobox/challenges/panel/user/MultiplePanel.java +++ b/src/main/java/world/bentobox/challenges/panel/user/MultiplePanel.java @@ -7,15 +7,15 @@ 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 org.bukkit.inventory.ItemStack; +import org.eclipse.jdt.annotation.NonNull; + import world.bentobox.bentobox.api.panels.PanelItem; import world.bentobox.bentobox.api.panels.TemplatedPanel; import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; diff --git a/src/main/java/world/bentobox/challenges/panel/util/ChallengeTypeSelector.java b/src/main/java/world/bentobox/challenges/panel/util/ChallengeTypeSelector.java index 024f4411..389c5fde 100644 --- a/src/main/java/world/bentobox/challenges/panel/util/ChallengeTypeSelector.java +++ b/src/main/java/world/bentobox/challenges/panel/util/ChallengeTypeSelector.java @@ -20,7 +20,11 @@ import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; import world.bentobox.bentobox.api.user.User; import world.bentobox.challenges.database.object.Challenge; -import world.bentobox.challenges.database.object.requirements.*; +import world.bentobox.challenges.database.object.requirements.InventoryRequirements; +import world.bentobox.challenges.database.object.requirements.IslandRequirements; +import world.bentobox.challenges.database.object.requirements.OtherRequirements; +import world.bentobox.challenges.database.object.requirements.Requirements; +import world.bentobox.challenges.database.object.requirements.StatisticRequirements; import world.bentobox.challenges.utils.Constants; diff --git a/src/main/java/world/bentobox/challenges/panel/util/MultiBlockSelector.java b/src/main/java/world/bentobox/challenges/panel/util/MultiBlockSelector.java index e847c97f..00bb833d 100644 --- a/src/main/java/world/bentobox/challenges/panel/util/MultiBlockSelector.java +++ b/src/main/java/world/bentobox/challenges/panel/util/MultiBlockSelector.java @@ -1,7 +1,11 @@ package world.bentobox.challenges.panel.util; - -import java.util.*; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import java.util.function.BiConsumer; import java.util.stream.Collectors; @@ -9,286 +13,109 @@ import org.bukkit.inventory.ItemStack; import lv.id.bonne.panelutils.PanelUtils; -import world.bentobox.bentobox.api.panels.PanelItem; -import world.bentobox.bentobox.api.panels.builders.PanelBuilder; -import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; import world.bentobox.bentobox.api.user.User; -import world.bentobox.challenges.utils.Constants; import world.bentobox.challenges.utils.Utils; - /** - * This class contains all necessary things that allows to select single block from all ingame blocks. Selected - * block will be returned via BiConsumer. + * This class provides a multi-selector GUI for selecting blocks (i.e. Materials). + * It extends the unified multi-selector base class and provides the type-specific + * implementations required for block selection. */ -public class MultiBlockSelector extends PagedSelector -{ - private MultiBlockSelector(User user, Mode mode, Set excluded, BiConsumer> consumer) - { - super(user); - this.consumer = consumer; - - // Current GUI cannot display air blocks. It crashes with null-pointer - excluded.add(Material.AIR); - excluded.add(Material.CAVE_AIR); - excluded.add(Material.VOID_AIR); - - // Piston head and moving piston is not necessary. useless. - excluded.add(Material.PISTON_HEAD); - excluded.add(Material.MOVING_PISTON); - - // Barrier cannot be accessible to user. - excluded.add(Material.BARRIER); - - this.selectedElements = new HashSet<>(); - - this.elements = Arrays.stream(Material.values()). - filter(material -> !excluded.contains(material)). - filter(material -> { - switch (mode) - { - case BLOCKS -> { - return material.isBlock(); - } - case ITEMS -> { - return material.isItem(); - } - default -> { - return true; - } - } - }). - // Sort by name - sorted(Comparator.comparing(Material::name)). - collect(Collectors.toList()); - // Init without filters applied. - this.filterElements = this.elements; - } - - - /** - * This method opens GUI that allows to select challenge type. - * - * @param user User who opens GUI. - * @param consumer Consumer that allows to get clicked type. - */ - public static void open(User user, Mode mode, Set excluded, BiConsumer> consumer) - { - new MultiBlockSelector(user, mode, excluded, consumer).build(); - } - - - /** - * This method opens GUI that allows to select challenge type. - * - * @param user User who opens GUI. - * @param consumer Consumer that allows to get clicked type. - */ - public static void open(User user, BiConsumer> consumer) - { - new MultiBlockSelector(user, Mode.ANY, new HashSet<>(), consumer).build(); - } - - -// --------------------------------------------------------------------- -// Section: Methods -// --------------------------------------------------------------------- - - - /** - * This method builds all necessary elements in GUI panel. - */ - @Override - protected void build() - { - PanelBuilder panelBuilder = new PanelBuilder().user(this.user); - panelBuilder.name(this.user.getTranslation(Constants.TITLE + "block-selector")); - - PanelUtils.fillBorder(panelBuilder, Material.BLUE_STAINED_GLASS_PANE); - - this.populateElements(panelBuilder, this.filterElements); - - panelBuilder.item(3, this.createButton(Button.ACCEPT_SELECTED)); - panelBuilder.item(5, this.createButton(Button.CANCEL)); - - panelBuilder.build(); - } - - - /** - * This method is called when filter value is updated. - */ - @Override - protected void updateFilters() - { - if (this.searchString == null || this.searchString.isBlank()) - { - this.filterElements = this.elements; - } - else - { - this.filterElements = this.elements.stream(). - filter(element -> { - // If element name is set and name contains search field, then do not filter out. - return element.name().toLowerCase().contains(this.searchString.toLowerCase()); - }). - distinct(). - collect(Collectors.toList()); - } - } - - - /** - * This method creates PanelItem button of requested type. - * @param button Button which must be created. - * @return new PanelItem with requested functionality. - */ - private PanelItem createButton(Button button) - { - final String reference = Constants.BUTTON + button.name().toLowerCase() + "."; - - final String name = this.user.getTranslation(reference + "name"); - final List description = new ArrayList<>(3); - description.add(this.user.getTranslation(reference + "description")); - - ItemStack icon; - PanelItem.ClickHandler clickHandler; - - switch (button) - { - case ACCEPT_SELECTED -> { - if (!this.selectedElements.isEmpty()) - { - description.add(this.user.getTranslation(reference + "title")); - this.selectedElements.forEach(material -> - description.add(this.user.getTranslation(reference + "element", - "[element]", Utils.prettifyObject(material, this.user)))); - } - - icon = new ItemStack(Material.COMMAND_BLOCK); - clickHandler = (panel, user1, clickType, slot) -> - { - this.consumer.accept(true, this.selectedElements); - return true; - }; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-save")); - } - case CANCEL -> { - - icon = new ItemStack(Material.IRON_DOOR); - - clickHandler = (panel, user1, clickType, slot) -> - { - this.consumer.accept(false, null); - return true; - }; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-cancel")); - } - default -> { - icon = new ItemStack(Material.PAPER); - clickHandler = null; - } - } - - return new PanelItemBuilder(). - icon(icon). - name(name). - description(description). - clickHandler(clickHandler). - build(); - } - - - /** - * This method creates button for given material. - * @param material material which button must be created. - * @return new Button for material. - */ - @Override - protected PanelItem createElementButton(Material material) - { - final String reference = Constants.BUTTON + "material."; - - List description = new ArrayList<>(); - description.add(this.user.getTranslation(reference + "description", - "[id]", material.name())); - - if (this.selectedElements.contains(material)) - { - description.add(this.user.getTranslation(reference + "selected")); - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-deselect")); - } - else - { - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-select")); - } - - return new PanelItemBuilder(). - name(this.user.getTranslation(reference + "name", "[material]", - Utils.prettifyObject(material, this.user))). - icon(PanelUtils.getMaterialItem(material)). - description(description). - clickHandler((panel, user1, clickType, slot) -> { - // On right click change which entities are selected for deletion. - if (!this.selectedElements.add(material)) - { - // Remove material if it is already selected - this.selectedElements.remove(material); - } - - this.build(); - return true; - }). - glow(this.selectedElements.contains(material)). - build(); - } - - - /** - * Functional buttons in current GUI. - */ - private enum Button - { - ACCEPT_SELECTED, - CANCEL - } - - - public enum Mode - { - BLOCKS, - ITEMS, - ANY - } - - -// --------------------------------------------------------------------- -// Section: Variables -// --------------------------------------------------------------------- - - /** - * List with elements that will be displayed in current GUI. - */ - private final List elements; - - /** - * Set that contains selected materials. - */ - private final Set selectedElements; - - /** - * This variable stores consumer. - */ - private final BiConsumer> consumer; - - /** - * Stores filtered items. - */ - private List filterElements; +public class MultiBlockSelector extends UnifiedMultiSelector { + + + private final Set excluded; + + /** + * Private constructor. + * + * @param user the user opening the selector + * @param mode the mode indicating whether to show only blocks, only items, or any + * @param excluded a set of Materials to exclude from the list + * @param consumer the callback to be invoked when the user confirms or cancels + */ + private MultiBlockSelector(User user, Mode mode, Set excluded, + BiConsumer> consumer) { + super(user, mode, consumer); + + if (excluded == null) { + excluded = new HashSet<>(); + } + this.excluded = excluded; + // Add default exclusions + this.excluded.add(Material.AIR); + this.excluded.add(Material.CAVE_AIR); + this.excluded.add(Material.VOID_AIR); + this.excluded.add(Material.PISTON_HEAD); + this.excluded.add(Material.MOVING_PISTON); + this.excluded.add(Material.BARRIER); + } + + /** + * Opens the MultiBlockSelector GUI with a specified mode and exclusions. + * + * @param user the user who opens the GUI + * @param mode the mode for filtering (BLOCKS, ITEMS, or ANY) + * @param excluded a set of Materials to exclude + * @param consumer a callback to receive the result + */ + public static void open(User user, Mode mode, Set excluded, + BiConsumer> consumer) { + new MultiBlockSelector(user, mode, excluded, consumer).build(); + } + + /** + * Opens the MultiBlockSelector GUI with default mode (ANY) and no exclusions. + * + * @param user the user who opens the GUI + * @param consumer a callback to receive the result + */ + public static void open(User user, BiConsumer> consumer) { + new MultiBlockSelector(user, Mode.ANY, new HashSet<>(), consumer).build(); + } + + @Override + protected List getElements() { + return Arrays.stream(Material.values()).filter(material -> excluded == null || !excluded.contains(material)) + .filter(material -> { + switch (mode) { + case BLOCKS: + return material.isBlock(); + case ITEMS: + return material.isItem(); + default: + return true; + } + }).sorted(Comparator.comparing(Material::name)).collect(Collectors.toList()); + } + + @Override + protected String getTitleKey() { + return "block-selector"; + } + + @Override + protected String getElementKeyPrefix() { + return "material."; + } + + @Override + protected ItemStack getIcon(Material element) { + return PanelUtils.getMaterialItem(element); + } + + @Override + protected String getElementDisplayName(Material element) { + return Utils.prettifyObject(element, this.user); + } + + @Override + protected String elementToString(Material element) { + return element.name(); + } + + @Override + protected String getElementPlaceholder() { + return "[material]"; + } } diff --git a/src/main/java/world/bentobox/challenges/panel/util/MultiEntitySelector.java b/src/main/java/world/bentobox/challenges/panel/util/MultiEntitySelector.java index fefe9927..7df90a56 100644 --- a/src/main/java/world/bentobox/challenges/panel/util/MultiEntitySelector.java +++ b/src/main/java/world/bentobox/challenges/panel/util/MultiEntitySelector.java @@ -1,282 +1,119 @@ package world.bentobox.challenges.panel.util; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; -import org.bukkit.Material; import org.bukkit.entity.EntityType; import org.bukkit.inventory.ItemStack; -import java.util.*; -import java.util.function.BiConsumer; -import java.util.stream.Collectors; -import lv.id.bonne.panelutils.PanelUtils; -import world.bentobox.bentobox.api.panels.PanelItem; -import world.bentobox.bentobox.api.panels.builders.PanelBuilder; -import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; import world.bentobox.bentobox.api.user.User; -import world.bentobox.challenges.utils.Constants; import world.bentobox.challenges.utils.Utils; - /** - * This class contains all necessary things that allows to select single block from all ingame blocks. Selected - * block will be returned via BiConsumer. + * This class provides a multi-selector GUI for selecting entities. + * It extends the unified multi-selector base class and supplies the + * type-specific implementations required for entity selection. */ -public class MultiEntitySelector extends PagedSelector -{ - private MultiEntitySelector(User user, boolean asEgg, Mode mode, Set excluded, BiConsumer> consumer) - { - super(user); - - this.consumer = consumer; - this.asEgg = asEgg; - this.selectedElements = new HashSet<>(); - - this.elements = Arrays.stream(EntityType.values()). - filter(entity -> !excluded.contains(entity)). - filter(entity -> { - if (mode == Mode.ALIVE) - { - return entity.isAlive(); - } - else - { - return true; - } - }). - // Sort by name - sorted(Comparator.comparing(EntityType::name)). - collect(Collectors.toList()); - // Init without filters applied. - this.filterElements = this.elements; - } - - - /** - * This method opens GUI that allows to select challenge type. - * - * @param user User who opens GUI. - * @param consumer Consumer that allows to get clicked type. - */ - public static void open(User user, boolean asEgg, Mode mode, Set excluded, BiConsumer> consumer) - { - new MultiEntitySelector(user, asEgg, mode, excluded, consumer).build(); - } - - - /** - * This method opens GUI that allows to select challenge type. - * - * @param user User who opens GUI. - * @param consumer Consumer that allows to get clicked type. - */ - public static void open(User user, boolean asEgg, BiConsumer> consumer) - { - new MultiEntitySelector(user, asEgg, Mode.ANY, new HashSet<>(), consumer).build(); - } - - -// --------------------------------------------------------------------- -// Section: Methods -// --------------------------------------------------------------------- - - - /** - * This method builds all necessary elements in GUI panel. - */ - protected void build() - { - PanelBuilder panelBuilder = new PanelBuilder().user(this.user); - panelBuilder.name(this.user.getTranslation(Constants.TITLE + "entity-selector")); - - PanelUtils.fillBorder(panelBuilder, Material.BLUE_STAINED_GLASS_PANE); - - this.populateElements(panelBuilder, this.filterElements); - - panelBuilder.item(3, this.createButton(Button.ACCEPT_SELECTED)); - panelBuilder.item(5, this.createButton(Button.CANCEL)); - - panelBuilder.build(); - } - - - /** - * This method is called when filter value is updated. - */ - @Override - protected void updateFilters() - { - if (this.searchString == null || this.searchString.isBlank()) - { - this.filterElements = this.elements; - } - else - { - this.filterElements = this.elements.stream(). - filter(element -> { - // If element name is set and name contains search field, then do not filter out. - return element.name().toLowerCase().contains(this.searchString.toLowerCase()); - }). - distinct(). - collect(Collectors.toList()); - } - } - - - /** - * This method creates PanelItem button of requested type. - * @param button Button which must be created. - * @return new PanelItem with requested functionality. - */ - private PanelItem createButton(Button button) - { - final String reference = Constants.BUTTON + button.name().toLowerCase() + "."; - - final String name = this.user.getTranslation(reference + "name"); - final List description = new ArrayList<>(3); - description.add(this.user.getTranslation(reference + "description")); - - ItemStack icon; - PanelItem.ClickHandler clickHandler; - - switch (button) - { - case ACCEPT_SELECTED -> { - if (!this.selectedElements.isEmpty()) - { - description.add(this.user.getTranslation(reference + "title")); - this.selectedElements.forEach(material -> - description.add(this.user.getTranslation(reference + "element", - "[element]", Utils.prettifyObject(material, this.user)))); - } - - icon = new ItemStack(Material.COMMAND_BLOCK); - clickHandler = (panel, user1, clickType, slot) -> - { - this.consumer.accept(true, this.selectedElements); - return true; - }; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-save")); - } - case CANCEL -> { - - icon = new ItemStack(Material.IRON_DOOR); - - clickHandler = (panel, user1, clickType, slot) -> - { - this.consumer.accept(false, null); - return true; - }; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-cancel")); - } - default -> { - icon = new ItemStack(Material.PAPER); - clickHandler = null; - } - } - - return new PanelItemBuilder(). - icon(icon). - name(name). - description(description). - clickHandler(clickHandler). - build(); - } - - - /** - * This method creates button for given entity. - * @param entity entity which button must be created. - * @return new Button for entity. - */ - @Override - protected PanelItem createElementButton(EntityType entity) - { - final String reference = Constants.BUTTON + "entity."; - - List description = new ArrayList<>(); - description.add(this.user.getTranslation(reference + "description", - "[id]", entity.name())); - - if (this.selectedElements.contains(entity)) - { - description.add(this.user.getTranslation(reference + "selected")); - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-deselect")); - } - else - { - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-select")); - } - - return new PanelItemBuilder(). - name(this.user.getTranslation(reference + "name", "[entity]", - Utils.prettifyObject(entity, this.user))). - icon(this.asEgg ? PanelUtils.getEntityEgg(entity) : PanelUtils.getEntityHead(entity)). - description(description). - clickHandler((panel, user1, clickType, slot) -> { - // On right click change which entities are selected for deletion. - if (!this.selectedElements.add(entity)) - { - // Remove entity if it is already selected - this.selectedElements.remove(entity); - } - - this.build(); - return true; - }). - glow(this.selectedElements.contains(entity)). - build(); - } - - - /** - * Functional buttons in current GUI. - */ - private enum Button - { - ACCEPT_SELECTED, - CANCEL - } - - - public enum Mode - { - ALIVE, - ANY - } - - -// --------------------------------------------------------------------- -// Section: Variables -// --------------------------------------------------------------------- - - /** - * List with elements that will be displayed in current GUI. - */ - private final List elements; - - /** - * Set that contains selected materials. - */ - private final Set selectedElements; - - /** - * This variable stores consumer. - */ - private final BiConsumer> consumer; - - /** - * Indicates that entity must be displayed as egg. - */ - private final boolean asEgg; - - /** - * Stores filtered items. - */ - private List filterElements; +public class MultiEntitySelector extends UnifiedMultiSelector { + + private final Set excluded; + + /** + * Private constructor. + * + * @param user the user opening the selector + * @param mode determines whether to show only living entities (ALIVE) or all (ANY) + * @param excluded a set of EntityType values to exclude + * @param consumer the callback to be invoked when the user confirms or cancels + */ + private MultiEntitySelector(User user, Mode mode, Set excluded, + java.util.function.BiConsumer> consumer) { + super(user, mode, consumer); + this.excluded = excluded; + } + + /** + * Opens the MultiEntitySelector GUI with the specified parameters. + * + * @param user the user who opens the GUI + * @param mode the filtering mode (ALIVE or ANY) + * @param excluded a set of EntityType values to exclude from the list + * @param consumer a callback to receive the result + */ + public static void open(User user, Mode mode, Set excluded, + java.util.function.BiConsumer> consumer) { + new MultiEntitySelector(user, mode, excluded, consumer).build(); + } + + /** + * Opens the MultiEntitySelector GUI with default parameters (mode ANY and no exclusions). + * + * @param user the user who opens the GUI + * @param consumer a callback to receive the result + */ + public static void open(User user, + java.util.function.BiConsumer> consumer) { + new MultiEntitySelector(user, Mode.ANY, new HashSet<>(), consumer).build(); + } + + /** + * Returns the list of EntityType values to display, applying the specified exclusions and mode. + */ + @Override + protected List getElements() { + return Arrays.stream(EntityType.values()).filter(entity -> excluded == null || !excluded.contains(entity)) + .filter(entity -> !SingleEntitySelector.NON_ENTITIES.contains(entity)) + .sorted(Comparator.comparing(EntityType::name)).collect(Collectors.toList()); + } + + /** + * Returns the title key used to form the GUI title. + */ + @Override + protected String getTitleKey() { + return "entity-selector"; + } + + /** + * Returns the translation key prefix for element buttons. + */ + @Override + protected String getElementKeyPrefix() { + return "entity."; + } + + /** + * Returns the icon for the given EntityType. + * If asEgg is true, an entity spawn egg is returned; otherwise, the entity head is returned. + */ + @Override + protected ItemStack getIcon(EntityType element) { + return SingleEntitySelector.getIcon(element); + } + + /** + * Returns the display name for the given EntityType. + */ + @Override + protected String getElementDisplayName(EntityType element) { + return Utils.prettifyObject(element, this.user); + } + + /** + * Returns a string representation of the given EntityType used for filtering. + */ + @Override + protected String elementToString(EntityType element) { + return element.name(); + } + + @Override + protected String getElementPlaceholder() { + return "[entity]"; + } } diff --git a/src/main/java/world/bentobox/challenges/panel/util/MultiEntityTypeTagsSelector.java b/src/main/java/world/bentobox/challenges/panel/util/MultiEntityTypeTagsSelector.java new file mode 100644 index 00000000..e9191870 --- /dev/null +++ b/src/main/java/world/bentobox/challenges/panel/util/MultiEntityTypeTagsSelector.java @@ -0,0 +1,196 @@ +package world.bentobox.challenges.panel.util; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.function.BiConsumer; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.Registry; +import org.bukkit.Tag; +import org.bukkit.entity.EntityType; +import org.bukkit.inventory.ItemStack; + +import world.bentobox.bentobox.api.user.User; +import world.bentobox.challenges.utils.Utils; + +/** + * This class provides a multi-selector GUI for selecting entity-type tags. + * It extends the unified multi-selector base class (UnifiedMultiSelector) and supplies + * all tag-specific implementations such as retrieving the tag list, filtering unwanted tags, + * and choosing an appropriate icon. + * + * @see UnifiedMultiSelector + */ +public class MultiEntityTypeTagsSelector extends UnifiedMultiSelector> { + + private final Set> excluded; + + /** + * Private constructor. + * + * @param user the user opening the selector + * @param mode the mode (ENTITY_TYPE or ANY) that might influence filtering behavior + * @param excluded a set of tags to be excluded from display + * @param consumer a callback to receive the selected tags (or cancellation) + */ + private MultiEntityTypeTagsSelector(User user, Mode mode, Set> excluded, + BiConsumer>> consumer) { + super(user, mode, consumer); + this.excluded = excluded; + } + + /** + * Opens the entity-type tag selector GUI with the specified mode and exclusions. + * + * @param user the user who opens the GUI + * @param mode filtering mode (ENTITY_TYPE or ANY) + * @param excluded a set of tags to exclude + * @param consumer a callback to receive the result + */ + public static void open(User user, Mode mode, Set> excluded, + BiConsumer>> consumer) { + new MultiEntityTypeTagsSelector(user, mode, excluded, consumer).build(); + } + + /** + * Opens the entity-type tag selector GUI with default parameters (mode ANY and no exclusions). + * + * @param user the user who opens the GUI + * @param consumer a callback to receive the result + */ + public static void open(User user, + BiConsumer>> consumer) { + new MultiEntityTypeTagsSelector(user, Mode.ANY, new HashSet<>(), consumer).build(); + } + + /** + * Retrieves the list of available entity-type tags. + *

+ * This method uses Bukkit’s tag API to get all tags for "entity_types" (of type EntityType), + * sorts them by their key, then removes any that are deemed irrelevant based on their key name + * (for example, tags containing "AXOLOTL", "IMMUNE", etc.) and any tags specified in the excluded set. + *

+ * + * @return a sorted and filtered list of Tag<EntityType> + */ + @Override + protected List> getElements() { + List> tagList = new ArrayList<>(); + Iterable> iterable = Bukkit.getTags("entity_types", EntityType.class); + iterable.forEach(tagList::add); + tagList.sort(Comparator.comparing(tag -> tag.getKey().getKey())); + + // Remove irrelevant tags based on key contents. + tagList.removeIf(t -> t.getKey().getKey().toUpperCase(Locale.ENGLISH).contains("AXOLOTL")); + tagList.removeIf(t -> t.getKey().getKey().toUpperCase(Locale.ENGLISH).contains("IMMUNE")); + tagList.removeIf(t -> t.getKey().getKey().toUpperCase(Locale.ENGLISH).contains("IGNORES")); + tagList.removeIf(t -> t.getKey().getKey().toUpperCase(Locale.ENGLISH).contains("FRIEND")); + tagList.removeIf(t -> t.getKey().getKey().toUpperCase(Locale.ENGLISH).contains("SENSITIVE")); + tagList.removeIf(t -> t.getKey().getKey().toUpperCase(Locale.ENGLISH).contains("PROJECTILE")); + + // Remove specific known tags. + tagList.remove(org.bukkit.Tag.ENTITY_TYPES_ARROWS); + tagList.remove(org.bukkit.Tag.ENTITY_TYPES_BEEHIVE_INHABITORS); + tagList.remove(org.bukkit.Tag.ENTITY_TYPES_CAN_TURN_IN_BOATS); + tagList.remove(org.bukkit.Tag.ENTITY_TYPES_DISMOUNTS_UNDERWATER); + tagList.remove(org.bukkit.Tag.ENTITY_TYPES_FALL_DAMAGE_IMMUNE); + tagList.remove(org.bukkit.Tag.ENTITY_TYPES_FREEZE_HURTS_EXTRA_TYPES); + tagList.remove(org.bukkit.Tag.ENTITY_TYPES_INVERTED_HEALING_AND_HARM); + tagList.remove(org.bukkit.Tag.ENTITY_TYPES_NO_ANGER_FROM_WIND_CHARGE); + tagList.remove(org.bukkit.Tag.ENTITY_TYPES_NON_CONTROLLING_RIDER); + tagList.remove(org.bukkit.Tag.ENTITY_TYPES_NOT_SCARY_FOR_PUFFERFISH); + tagList.remove(org.bukkit.Tag.ENTITY_TYPES_FROG_FOOD); + + // Remove any tags specified in the excluded set. + if (excluded != null) { + for (Tag ex : excluded) { + tagList.removeIf(tag -> tag.equals(ex)); + } + } + return tagList; + } + + /** + * Returns the title key used to build the GUI title. + * + * @return "entity-selector" + */ + @Override + protected String getTitleKey() { + return "entity-selector"; + } + + /** + * Returns the translation key prefix for individual element buttons. + * + * @return "entity-group." + */ + @Override + protected String getElementKeyPrefix() { + return "entity-group."; + } + + /** + * Returns the icon for the given entity-type tag. + *

+ * If the tag’s key contains "boat", an oak boat icon is used. Otherwise, the method attempts + * to find any EntityType that is tagged by this tag and constructs a spawn egg material name + * (e.g. "CREEPER_SPAWN_EGG"). If no matching material is found, a PAPER icon is returned. + *

+ * + * @param element the Tag<EntityType> for which to determine the icon + * @return an ItemStack representing the icon + */ + @Override + protected ItemStack getIcon(Tag element) { + Material iconMaterial; + if (element.getKey().getKey().contains("boat")) { + iconMaterial = Material.OAK_BOAT; + } else { + EntityType entType = Registry.ENTITY_TYPE.stream().filter(element::isTagged).findAny().orElse(null); + if (entType != null) { + String eggName = entType.getKey().getKey().toUpperCase(Locale.ENGLISH) + "_SPAWN_EGG"; + try { + iconMaterial = Material.valueOf(eggName); + } catch (Exception e) { + iconMaterial = Material.PAPER; + } + } else { + iconMaterial = Material.PAPER; + } + } + return new ItemStack(iconMaterial); + } + + /** + * Returns the display name for the given tag. + * + * @param element the Tag<EntityType> + * @return a pretty-printed string for display + */ + @Override + protected String getElementDisplayName(Tag element) { + return Utils.prettifyObject(element, this.user); + } + + /** + * Returns a string representation of the tag used for filtering. + * + * @param element the Tag<EntityType> + * @return the tag’s key (as a string) + */ + @Override + protected String elementToString(Tag element) { + return element.getKey().getKey(); + } + + @Override + protected String getElementPlaceholder() { + return "[id]"; + } +} diff --git a/src/main/java/world/bentobox/challenges/panel/util/MultiMaterialTagsSelector.java b/src/main/java/world/bentobox/challenges/panel/util/MultiMaterialTagsSelector.java new file mode 100644 index 00000000..335921b1 --- /dev/null +++ b/src/main/java/world/bentobox/challenges/panel/util/MultiMaterialTagsSelector.java @@ -0,0 +1,225 @@ +package world.bentobox.challenges.panel.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.Registry; +import org.bukkit.Tag; +import org.bukkit.inventory.ItemStack; + +import world.bentobox.bentobox.api.user.User; +import world.bentobox.challenges.utils.Utils; + +/** + * This class provides a multi-selector GUI for selecting material tags. + * It extends the unified multi-selector base class and supplies the tag‑specific + * implementations such as retrieving the tag list, filtering unwanted tags, + * selecting an appropriate icon, and generating display names. + * + *

+ * Two static open methods are provided: one that accepts a filtering mode and an excluded set, + * and one that uses default parameters. + *

+ * + * @see UnifiedMultiSelector + */ +public class MultiMaterialTagsSelector extends UnifiedMultiSelector> { + + /** + * A map of specific tags to custom icon materials. + */ + public static final Map, Material> ICONS = Map.of( + Tag.AIR, Material.BARRIER, Tag.FIRE, Material.TORCH, Tag.CANDLE_CAKES, Material.CAKE, Tag.PORTALS, + Material.MAGENTA_STAINED_GLASS_PANE, Tag.WALL_HANGING_SIGNS, Material.ACACIA_SIGN, Tag.WALL_SIGNS, + Material.OAK_SIGN, + Tag.WALL_CORALS, Material.BUBBLE_CORAL_FAN, Tag.CAVE_VINES, Material.VINE + ); + + private final Set> excluded; + + /** + * Private constructor. + * + * @param user the user opening the selector + * @param mode filtering mode (BLOCKS, ITEMS, or ANY) + * @param excluded a set of tags to exclude from display + * @param consumer the callback to receive the selected tags or cancellation + */ + private MultiMaterialTagsSelector(User user, Mode mode, Set> excluded, + BiConsumer>> consumer) { + super(user, Mode.ANY, consumer); + this.excluded = excluded; + } + + /** + * Opens the material tag selector GUI with a specified mode and exclusions. + * + * @param user the user who opens the GUI + * @param mode the filtering mode (BLOCKS, ITEMS, or ANY) + * @param excluded a set of tags to exclude + * @param consumer a callback to receive the result + */ + public static void open(User user, Mode mode, Set> excluded, + BiConsumer>> consumer) { + new MultiMaterialTagsSelector(user, mode, excluded, consumer).build(); + } + + /** + * Opens the material tag selector GUI with default parameters (mode ANY and no exclusions). + * + * @param user the user who opens the GUI + * @param consumer a callback to receive the result + */ + public static void open(User user, BiConsumer>> consumer) { + new MultiMaterialTagsSelector(user, Mode.ANY, new HashSet<>(), consumer).build(); + } + + /** + * Retrieves the list of available material tags. + * + *

+ * This method obtains tags using Bukkit’s tag API for the "blocks" category, + * sorts them by their key name, applies several removeIf filters to eliminate irrelevant tags, + * and then removes any tags provided in the excluded set. + *

+ * + * @return a sorted and filtered list of Tag<Material> + */ + @Override + protected List> getElements() { + List> list = new ArrayList<>(); + Iterable> iterable = Bukkit.getTags("blocks", Material.class); + iterable.forEach(list::add); + list.sort(Comparator.comparing(tag -> tag.getKey().getKey())); + + // Remove irrelevant tags based on their key. + list.removeIf(t -> t.getKey().getKey().toUpperCase(Locale.ENGLISH).contains("SPAWNABLE")); + list.removeIf(t -> t.getKey().getKey().toUpperCase(Locale.ENGLISH).contains("PLACE")); + list.removeIf(t -> t.getKey().getKey().toUpperCase(Locale.ENGLISH).contains("TEMPT")); + list.removeIf(t -> t.getKey().getKey().toUpperCase(Locale.ENGLISH).contains("_ON")); + list.removeIf(t -> t.getKey().getKey().toUpperCase(Locale.ENGLISH).contains("BASE")); + list.removeIf(t -> t.getKey().getKey().toUpperCase(Locale.ENGLISH).contains("SOUND_BLOCKS")); + list.removeIf(t -> t.getKey().getKey().toUpperCase(Locale.ENGLISH).contains("DRAGON")); + list.removeIf(t -> t.getKey().getKey().toUpperCase(Locale.ENGLISH).contains("VALID")); + list.removeIf(t -> t.getKey().getKey().toUpperCase(Locale.ENGLISH).contains("INCORRECT")); + list.removeIf(t -> t.getKey().getKey().toUpperCase(Locale.ENGLISH).contains("INFINIBURN")); + list.removeIf(t -> t.getKey().getKey().toUpperCase(Locale.ENGLISH).contains("MINEABLE")); + list.removeIf(t -> t.getKey().getKey().toUpperCase(Locale.ENGLISH).contains("TOOL")); + list.removeIf(t -> t.getKey().getKey().toUpperCase(Locale.ENGLISH).contains("SNIFFER")); + list.removeIf(t -> t.getKey().getKey().toUpperCase(Locale.ENGLISH).contains("OVERRIDE")); + list.removeIf(t -> t.getKey().getKey().toUpperCase(Locale.ENGLISH).contains("OVERWORLD")); + + // Remove specific known tags. + list.remove(Tag.BLOCKS_WIND_CHARGE_EXPLOSIONS); + list.remove(Tag.CONVERTABLE_TO_MUD); + list.remove(Tag.DAMPENS_VIBRATIONS); + list.remove(Tag.DOES_NOT_BLOCK_HOPPERS); + list.remove(Tag.ENCHANTMENT_POWER_PROVIDER); + list.remove(Tag.ENCHANTMENT_POWER_TRANSMITTER); + list.remove(Tag.ENDERMAN_HOLDABLE); + list.remove(Tag.FEATURES_CANNOT_REPLACE); + list.remove(Tag.FALL_DAMAGE_RESETTING); + list.remove(Tag.FROG_PREFER_JUMP_TO); + list.remove(Tag.MAINTAINS_FARMLAND); + list.remove(Tag.MANGROVE_LOGS_CAN_GROW_THROUGH); + list.remove(Tag.MANGROVE_ROOTS_CAN_GROW_THROUGH); + list.remove(Tag.BEE_GROWABLES); + list.remove(Tag.MOB_INTERACTABLE_DOORS); + list.remove(Tag.HOGLIN_REPELLENTS); + list.remove(Tag.PIGLIN_REPELLENTS); + list.remove(Tag.SNAPS_GOAT_HORN); + list.remove(Tag.SOUL_SPEED_BLOCKS); + list.remove(Tag.STRIDER_WARM_BLOCKS); + list.remove(Tag.SWORD_EFFICIENT); + list.remove(Tag.UNSTABLE_BOTTOM_CENTER); + list.remove(Tag.COMPLETES_FIND_TREE_TUTORIAL); + list.remove(Tag.GUARDED_BY_PIGLINS); + list.remove(Tag.IMPERMEABLE); + list.remove(Tag.PREVENT_MOB_SPAWNING_INSIDE); + list.remove(Tag.SMELTS_TO_GLASS); + list.remove(Tag.WITHER_IMMUNE); + + // Remove any tags specified in the excluded set. + if (excluded != null) { + for (Tag ex : excluded) { + list.removeIf(tag -> tag.equals(ex)); + } + } + return list; + } + + /** + * Returns the title key used for the GUI. + * + * @return "block-selector" + */ + @Override + protected String getTitleKey() { + return "block-selector"; + } + + /** + * Returns the translation key prefix for individual element buttons. + * + * @return "block-group." + */ + @Override + protected String getElementKeyPrefix() { + return "block-group."; + } + + /** + * Returns the icon for the given material tag. + * + *

+ * This method first checks the ICONS map; if a mapping exists for the tag, that material is used. + * Otherwise, it searches through the Bukkit material registry for any material tagged by the given tag + * that is also an item. If none is found, it falls back to PAPER. + *

+ * + * @param element the Tag<Material> for which to determine the icon + * @return an ItemStack representing the icon + */ + @Override + protected ItemStack getIcon(Tag element) { + Material iconMaterial = ICONS.getOrDefault(element, Registry.MATERIAL.stream().filter(element::isTagged) + .filter(Material::isItem).findAny().orElse(Material.PAPER)); + return new ItemStack(iconMaterial); + } + + /** + * Returns the display name for the given material tag. + * + * @param element the Tag<Material> + * @return a pretty-printed string for display + */ + @Override + protected String getElementDisplayName(Tag element) { + return Utils.prettifyObject(element, this.user); + } + + /** + * Returns a string representation of the tag used for filtering. + * + * @param element the Tag<Material> + * @return the tag's key (as a string) + */ + @Override + protected String elementToString(Tag element) { + return element.getKey().getKey(); + } + + @Override + protected String getElementPlaceholder() { + return "[id]"; + } +} diff --git a/src/main/java/world/bentobox/challenges/panel/util/PagedSelector.java b/src/main/java/world/bentobox/challenges/panel/util/PagedSelector.java index 290fdf2c..04b3d9b8 100644 --- a/src/main/java/world/bentobox/challenges/panel/util/PagedSelector.java +++ b/src/main/java/world/bentobox/challenges/panel/util/PagedSelector.java @@ -7,12 +7,13 @@ package world.bentobox.challenges.panel.util; -import org.bukkit.Material; -import org.bukkit.inventory.ItemStack; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + import world.bentobox.bentobox.api.panels.PanelItem; import world.bentobox.bentobox.api.panels.builders.PanelBuilder; import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; diff --git a/src/main/java/world/bentobox/challenges/panel/util/SingleAdvancementSelector.java b/src/main/java/world/bentobox/challenges/panel/util/SingleAdvancementSelector.java new file mode 100644 index 00000000..e8868034 --- /dev/null +++ b/src/main/java/world/bentobox/challenges/panel/util/SingleAdvancementSelector.java @@ -0,0 +1,213 @@ +package world.bentobox.challenges.panel.util; + + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.advancement.Advancement; +import org.bukkit.inventory.ItemStack; + +import lv.id.bonne.panelutils.PanelUtils; +import world.bentobox.bentobox.api.panels.PanelItem; +import world.bentobox.bentobox.api.panels.builders.PanelBuilder; +import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.challenges.utils.Constants; + + +/** + * This GUI allows to select single entity and return it via Consumer. + */ +public class SingleAdvancementSelector extends PagedSelector +{ + /** + * Instantiates a new Single advancement selector. + * + * @param user the user + * @param mode the mode + * @param excluded the excluded + * @param consumer the consumer + */ + private SingleAdvancementSelector(User user, Mode mode, Set excluded, + BiConsumer consumer) + { + super(user); + this.elements = new ArrayList(); + this.consumer = consumer; + Bukkit.advancementIterator().forEachRemaining(elements::add); + elements.removeIf(a -> a.getDisplay() == null); // Remove any that don't get displayed + elements.sort(Comparator.comparing(advancement -> advancement.getDisplay().getTitle())); + // Init without filters applied. + this.filterElements = this.elements; + } + + + /** + * This method opens GUI that allows to select challenge type. + * + * @param user User who opens GUI. + * @param consumer Consumer that allows to get clicked type. + */ + public static void open(User user, Mode mode, Set excluded, BiConsumer consumer) + { + new SingleAdvancementSelector(user, mode, excluded, consumer).build(); + } + + + /** + * This method opens GUI that allows to select challenge type. + * + * @param user User who opens GUI. + * @param consumer Consumer that allows to get clicked type. + */ + public static void open(User user, BiConsumer consumer) + { + new SingleAdvancementSelector(user, Mode.ANY, new HashSet<>(), consumer).build(); + } + + + /** + * This method opens GUI that allows to select challenge type. + * + * @param user User who opens GUI. + * @param consumer Consumer that allows to get clicked type. + */ + public static void open(User user, Mode mode, BiConsumer consumer) + { + new SingleAdvancementSelector(user, mode, new HashSet<>(), consumer).build(); + } + + + // --------------------------------------------------------------------- + // Section: Methods + // --------------------------------------------------------------------- + + + /** + * This method builds + */ + @Override + protected void build() { + PanelBuilder panelBuilder = new PanelBuilder().user(this.user); + panelBuilder.name(this.user.getTranslation(Constants.TITLE + "advancement-selector")); + + PanelUtils.fillBorder(panelBuilder, Material.BLUE_STAINED_GLASS_PANE); + + this.populateElements(panelBuilder, this.filterElements); + + panelBuilder.item(4, this.createButton()); + + panelBuilder.build(); + } + + + /** + * This method is called when filter value is updated. + */ + @Override + protected void updateFilters() { + if (this.searchString == null || this.searchString.isBlank()) { + this.filterElements = this.elements; + } else { + this.filterElements = this.elements.stream().filter(element -> { + // If element name is set and name contains search field, then do not filter out. + return element.getDisplay().getTitle().toLowerCase().contains(this.searchString.toLowerCase()); + }).distinct().collect(Collectors.toList()); + } + } + + + /** + * This method builds PanelItem for given entity. + * @param entity Entity which PanelItem must be created. + * @return new PanelItem for given Entity. + */ + @Override + protected PanelItem createElementButton(Advancement advancement) { + final String reference = Constants.BUTTON + "advancement."; + List description = new ArrayList<>(); + description.add(this.user.getTranslation(reference + "description", "[description]", + advancement.getDisplay().getDescription())); + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-choose")); + + + return new PanelItemBuilder() + .name(this.user.getTranslation(reference + "name", "[name]", + advancement.getDisplay().getTitle())) + .icon(getIcon(advancement)).description(description) + .clickHandler((panel, user1, clickType, slot) -> { + this.consumer.accept(true, advancement); + return true; + }).build(); + } + + /** + * Get an ItemStack icon for any entity type, or PAPER if it's not really known + * @param et entity type + * @return ItemStack + */ + public static ItemStack getIcon(Advancement advancement) { + return advancement.getDisplay().getIcon(); + } + + /** + * This method creates PanelItem button of requested type. + * @return new PanelItem with requested functionality. + */ + private PanelItem createButton() { + final String reference = Constants.BUTTON + "cancel."; + + final String name = this.user.getTranslation(reference + "name"); + final List description = new ArrayList<>(3); + description.add(this.user.getTranslation(reference + "description")); + + ItemStack icon = new ItemStack(Material.IRON_DOOR); + PanelItem.ClickHandler clickHandler = (panel, user1, clickType, slot) -> { + this.consumer.accept(false, null); + return true; + }; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-cancel")); + + return new PanelItemBuilder().icon(icon).name(name).description(description).clickHandler(clickHandler).build(); + } + + + // --------------------------------------------------------------------- + // Section: Mode + // --------------------------------------------------------------------- + + + public enum Mode { + ANY + } + + + // --------------------------------------------------------------------- + // Section: Variables + // --------------------------------------------------------------------- + + /** + * List with elements that will be displayed in current GUI. + */ + private final List elements; + + /** + * This variable stores consumer. + */ + private final BiConsumer consumer; + + /** + * Stores filtered items. + */ + private List filterElements; +} diff --git a/src/main/java/world/bentobox/challenges/panel/util/SingleBlockSelector.java b/src/main/java/world/bentobox/challenges/panel/util/SingleBlockSelector.java index d9fae37d..03cde66f 100644 --- a/src/main/java/world/bentobox/challenges/panel/util/SingleBlockSelector.java +++ b/src/main/java/world/bentobox/challenges/panel/util/SingleBlockSelector.java @@ -1,12 +1,18 @@ package world.bentobox.challenges.panel.util; -import org.bukkit.Material; -import org.bukkit.inventory.ItemStack; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import java.util.function.BiConsumer; import java.util.stream.Collectors; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + import lv.id.bonne.panelutils.PanelUtils; import world.bentobox.bentobox.api.panels.PanelItem; import world.bentobox.bentobox.api.panels.builders.PanelBuilder; diff --git a/src/main/java/world/bentobox/challenges/panel/util/SingleEntitySelector.java b/src/main/java/world/bentobox/challenges/panel/util/SingleEntitySelector.java index b0219aa5..aa2355ac 100644 --- a/src/main/java/world/bentobox/challenges/panel/util/SingleEntitySelector.java +++ b/src/main/java/world/bentobox/challenges/panel/util/SingleEntitySelector.java @@ -1,7 +1,13 @@ package world.bentobox.challenges.panel.util; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; import java.util.function.BiConsumer; import java.util.stream.Collectors; @@ -23,216 +29,231 @@ */ public class SingleEntitySelector extends PagedSelector { - /** - * Instantiates a new Single entity selector. - * - * @param user the user - * @param asEggs the boolean - * @param mode the mode - * @param excluded the excluded - * @param consumer the consumer - */ - private SingleEntitySelector(User user, boolean asEggs, Mode mode, Set excluded, BiConsumer consumer) - { - super(user); - this.consumer = consumer; - this.asEggs = asEggs; - this.elements = Arrays.stream(EntityType.values()). - filter(entity -> !excluded.contains(entity)). - filter(entity -> { - if (mode == Mode.ALIVE) - { - return entity.isAlive(); - } - else - { - return true; - } - }). - // Sort by names - sorted(Comparator.comparing(EntityType::name)). - collect(Collectors.toList()); - // Init without filters applied. - this.filterElements = this.elements; - } - - - /** - * This method opens GUI that allows to select challenge type. - * - * @param user User who opens GUI. - * @param consumer Consumer that allows to get clicked type. - */ - public static void open(User user, boolean asEggs, Mode mode, Set excluded, BiConsumer consumer) - { - new SingleEntitySelector(user, asEggs, mode, excluded, consumer).build(); - } - - - /** - * This method opens GUI that allows to select challenge type. - * - * @param user User who opens GUI. - * @param consumer Consumer that allows to get clicked type. - */ - public static void open(User user, boolean asEggs, BiConsumer consumer) - { - new SingleEntitySelector(user, asEggs, Mode.ANY, new HashSet<>(), consumer).build(); - } - - - /** - * This method opens GUI that allows to select challenge type. - * - * @param user User who opens GUI. - * @param consumer Consumer that allows to get clicked type. - */ - public static void open(User user, boolean asEggs, Mode mode, BiConsumer consumer) - { - new SingleEntitySelector(user, asEggs, mode, new HashSet<>(), consumer).build(); - } - - -// --------------------------------------------------------------------- -// Section: Methods -// --------------------------------------------------------------------- - - - /** - * This method builds - */ - @Override - protected void build() - { - PanelBuilder panelBuilder = new PanelBuilder().user(this.user); - panelBuilder.name(this.user.getTranslation(Constants.TITLE + "entity-selector")); - - PanelUtils.fillBorder(panelBuilder, Material.BLUE_STAINED_GLASS_PANE); - - this.populateElements(panelBuilder, this.filterElements); - - panelBuilder.item(4, this.createButton()); - - panelBuilder.build(); - } - - - /** - * This method is called when filter value is updated. - */ - @Override - protected void updateFilters() - { - if (this.searchString == null || this.searchString.isBlank()) - { - this.filterElements = this.elements; - } - else - { - this.filterElements = this.elements.stream(). - filter(element -> { - // If element name is set and name contains search field, then do not filter out. - return element.name().toLowerCase().contains(this.searchString.toLowerCase()); - }). - distinct(). - collect(Collectors.toList()); - } - } - - - /** - * This method builds PanelItem for given entity. - * @param entity Entity which PanelItem must be created. - * @return new PanelItem for given Entity. - */ - @Override - protected PanelItem createElementButton(EntityType entity) - { - final String reference = Constants.BUTTON + "entity."; - - List description = new ArrayList<>(); - description.add(this.user.getTranslation(reference + "description", - "[id]", entity.name())); - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-choose")); - - return new PanelItemBuilder(). - name(this.user.getTranslation(reference + "name", "[entity]", - Utils.prettifyObject(entity, this.user))). - icon(this.asEggs ? PanelUtils.getEntityEgg(entity) : PanelUtils.getEntityHead(entity)). - description(description). - clickHandler((panel, user1, clickType, slot) -> { - this.consumer.accept(true, entity); - return true; - }). - build(); - } - - - /** - * This method creates PanelItem button of requested type. - * @return new PanelItem with requested functionality. - */ - private PanelItem createButton() - { - final String reference = Constants.BUTTON + "cancel."; - - final String name = this.user.getTranslation(reference + "name"); - final List description = new ArrayList<>(3); - description.add(this.user.getTranslation(reference + "description")); - - ItemStack icon = new ItemStack(Material.IRON_DOOR); - PanelItem.ClickHandler clickHandler = (panel, user1, clickType, slot) -> - { - this.consumer.accept(false, null); - return true; - }; - - description.add(""); - description.add(this.user.getTranslation(Constants.TIPS + "click-to-cancel")); - - return new PanelItemBuilder(). - icon(icon). - name(name). - description(description). - clickHandler(clickHandler). - build(); - } - - -// --------------------------------------------------------------------- -// Section: Mode -// --------------------------------------------------------------------- - - - public enum Mode - { - ALIVE, - ANY - } - - -// --------------------------------------------------------------------- -// Section: Variables -// --------------------------------------------------------------------- - - /** - * List with elements that will be displayed in current GUI. - */ - private final List elements; - - /** - * Indicates if entities are displayed as eggs or heads. - */ - private final boolean asEggs; - - /** - * This variable stores consumer. - */ - private final BiConsumer consumer; - - /** - * Stores filtered items. - */ - private List filterElements; + + /** + * Non entities that really cannot be used + */ + public static final List NON_ENTITIES = List.of(EntityType.UNKNOWN, EntityType.BLOCK_DISPLAY, + EntityType.ITEM_DISPLAY, + EntityType.TEXT_DISPLAY, EntityType.FALLING_BLOCK, EntityType.FIREBALL, EntityType.FISHING_BOBBER, + EntityType.GIANT, EntityType.ILLUSIONER, EntityType.INTERACTION, EntityType.LIGHTNING_BOLT, + EntityType.LLAMA_SPIT, EntityType.MARKER, EntityType.SHULKER_BULLET, EntityType.SMALL_FIREBALL, + EntityType.DRAGON_FIREBALL, EntityType.EVOKER_FANGS, EntityType.BREEZE_WIND_CHARGE, + EntityType.AREA_EFFECT_CLOUD); + + /** + * Instantiates a new Single entity selector. + * + * @param user the user + * @param asEggs the boolean + * @param mode the mode + * @param excluded the excluded + * @param consumer the consumer + */ + private SingleEntitySelector(User user, Mode mode, Set excluded, + BiConsumer consumer) + { + super(user); + this.consumer = consumer; + this.elements = Arrays.stream(EntityType.values()).filter(entity -> !excluded.contains(entity)) + .filter(entity -> !NON_ENTITIES.contains(entity)) + .filter(entity -> { + if (mode == Mode.ALIVE) { + return entity.isAlive(); + } else { + return true; + } + }). + // Sort by names + sorted(Comparator.comparing(EntityType::name)).collect(Collectors.toList()); + // Init without filters applied. + this.filterElements = this.elements; + } + + + /** + * This method opens GUI that allows to select challenge type. + * + * @param user User who opens GUI. + * @param consumer Consumer that allows to get clicked type. + */ + public static void open(User user, Mode mode, Set excluded, BiConsumer consumer) + { + new SingleEntitySelector(user, mode, excluded, consumer).build(); + } + + + /** + * This method opens GUI that allows to select challenge type. + * + * @param user User who opens GUI. + * @param consumer Consumer that allows to get clicked type. + */ + public static void open(User user, BiConsumer consumer) + { + new SingleEntitySelector(user, Mode.ANY, new HashSet<>(), consumer).build(); + } + + + /** + * This method opens GUI that allows to select challenge type. + * + * @param user User who opens GUI. + * @param consumer Consumer that allows to get clicked type. + */ + public static void open(User user, Mode mode, BiConsumer consumer) + { + new SingleEntitySelector(user, mode, new HashSet<>(), consumer).build(); + } + + + // --------------------------------------------------------------------- + // Section: Methods + // --------------------------------------------------------------------- + + + /** + * This method builds + */ + @Override + protected void build() { + PanelBuilder panelBuilder = new PanelBuilder().user(this.user); + panelBuilder.name(this.user.getTranslation(Constants.TITLE + "entity-selector")); + + PanelUtils.fillBorder(panelBuilder, Material.BLUE_STAINED_GLASS_PANE); + + this.populateElements(panelBuilder, this.filterElements); + + panelBuilder.item(4, this.createButton()); + + panelBuilder.build(); + } + + + /** + * This method is called when filter value is updated. + */ + @Override + protected void updateFilters() { + if (this.searchString == null || this.searchString.isBlank()) { + this.filterElements = this.elements; + } else { + this.filterElements = this.elements.stream().filter(element -> { + // If element name is set and name contains search field, then do not filter out. + return element.name().toLowerCase().contains(this.searchString.toLowerCase()); + }).distinct().collect(Collectors.toList()); + } + } + + + /** + * This method builds PanelItem for given entity. + * @param entity Entity which PanelItem must be created. + * @return new PanelItem for given Entity. + */ + @Override + protected PanelItem createElementButton(EntityType entity) { + final String reference = Constants.BUTTON + "entity."; + + List description = new ArrayList<>(); + description.add(this.user.getTranslation(reference + "description", "[id]", entity.name())); + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-choose")); + + + return new PanelItemBuilder() + .name(this.user.getTranslation(reference + "name", "[entity]", Utils.prettifyObject(entity, this.user))) + .icon(getIcon(entity)).description(description) + .clickHandler((panel, user1, clickType, slot) -> { + this.consumer.accept(true, entity); + return true; + }).build(); + } + + /** + * Get an ItemStack icon for any entity type, or PAPER if it's not really known + * @param et entity type + * @return ItemStack + */ + public static ItemStack getIcon(EntityType et) { + // Check for Materials like boats that are named the same as their entitytype. + Material m = Material.getMaterial(et.getKey().getKey().toUpperCase(Locale.ENGLISH)); + if (m != null) { + return new ItemStack(m); + } + // Try to get the spawn egg for the given entity by using the naming convention. + String spawnEggName = et.name() + "_SPAWN_EGG"; + try { + Material spawnEgg = Material.valueOf(spawnEggName); + // If found, return an ItemStack of the spawn egg. + return new ItemStack(spawnEgg); + } catch (IllegalArgumentException ex) { + // No spawn egg material exists for this entity type. + } + // Fallback + return switch (et) { + case EYE_OF_ENDER -> new ItemStack(Material.ENDER_EYE); + case LEASH_KNOT -> new ItemStack(Material.LEAD); + case OMINOUS_ITEM_SPAWNER -> new ItemStack(Material.TRIAL_SPAWNER); + case PLAYER -> new ItemStack(Material.PLAYER_HEAD); + case SPAWNER_MINECART -> new ItemStack(Material.MINECART); + case TRADER_LLAMA -> new ItemStack(Material.LLAMA_SPAWN_EGG); + case WITHER_SKULL -> new ItemStack(Material.WITHER_SKELETON_SKULL); + default -> new ItemStack(Material.PAPER); + + }; + } + + /** + * This method creates PanelItem button of requested type. + * @return new PanelItem with requested functionality. + */ + private PanelItem createButton() { + final String reference = Constants.BUTTON + "cancel."; + + final String name = this.user.getTranslation(reference + "name"); + final List description = new ArrayList<>(3); + description.add(this.user.getTranslation(reference + "description")); + + ItemStack icon = new ItemStack(Material.IRON_DOOR); + PanelItem.ClickHandler clickHandler = (panel, user1, clickType, slot) -> { + this.consumer.accept(false, null); + return true; + }; + + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-cancel")); + + return new PanelItemBuilder().icon(icon).name(name).description(description).clickHandler(clickHandler).build(); + } + + + // --------------------------------------------------------------------- + // Section: Mode + // --------------------------------------------------------------------- + + + public enum Mode { + ALIVE, ANY + } + + + // --------------------------------------------------------------------- + // Section: Variables + // --------------------------------------------------------------------- + + /** + * List with elements that will be displayed in current GUI. + */ + private final List elements; + + /** + * This variable stores consumer. + */ + private final BiConsumer consumer; + + /** + * Stores filtered items. + */ + private List filterElements; } diff --git a/src/main/java/world/bentobox/challenges/panel/util/StatisticSelector.java b/src/main/java/world/bentobox/challenges/panel/util/StatisticSelector.java index f6d2aedd..2fcf957f 100644 --- a/src/main/java/world/bentobox/challenges/panel/util/StatisticSelector.java +++ b/src/main/java/world/bentobox/challenges/panel/util/StatisticSelector.java @@ -1,18 +1,23 @@ package world.bentobox.challenges.panel.util; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; + import org.bukkit.Material; import org.bukkit.Statistic; import org.bukkit.inventory.ItemStack; -import java.util.*; -import java.util.function.BiConsumer; -import java.util.stream.Collectors; import lv.id.bonne.panelutils.PanelUtils; import world.bentobox.bentobox.api.panels.PanelItem; import world.bentobox.bentobox.api.panels.builders.PanelBuilder; import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; import world.bentobox.bentobox.api.user.User; +import world.bentobox.challenges.panel.admin.ManageStatisticsPanel; import world.bentobox.challenges.utils.Constants; import world.bentobox.challenges.utils.Utils; @@ -128,13 +133,11 @@ protected PanelItem createElementButton(Statistic statistic) return new PanelItemBuilder(). name(this.user.getTranslation(reference + "name", "[statistic]", Utils.prettifyObject(statistic, this.user))). - icon(Material.PAPER). - description(description). - clickHandler((panel, user1, clickType, slot) -> { - this.consumer.accept(true, statistic); - return true; - }). - build(); + icon(ManageStatisticsPanel.getStatisticIcon(statistic)).description(description) + .clickHandler((panel, user1, clickType, slot) -> { + this.consumer.accept(true, statistic); + return true; + }).build(); } diff --git a/src/main/java/world/bentobox/challenges/panel/util/UnifiedMultiSelector.java b/src/main/java/world/bentobox/challenges/panel/util/UnifiedMultiSelector.java new file mode 100644 index 00000000..bd505f36 --- /dev/null +++ b/src/main/java/world/bentobox/challenges/panel/util/UnifiedMultiSelector.java @@ -0,0 +1,206 @@ +package world.bentobox.challenges.panel.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +import lv.id.bonne.panelutils.PanelUtils; +import world.bentobox.bentobox.api.panels.PanelItem; +import world.bentobox.bentobox.api.panels.builders.PanelBuilder; +import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.challenges.utils.Constants; + +/** + * Unified abstract class for multi‐selector GUIs. + *

+ * This class provides the common logic for building the GUI, filtering the list, + * and creating the functional buttons. Subclasses must supply the list of available elements + * and type‐specific details such as how to obtain an element’s display name, icon, and + * string representation for filtering. + *

+ * + * @param The type of element shown in the GUI (e.g. Material, EntityType, or Tag<Material>, etc.) + */ +public abstract class UnifiedMultiSelector extends PagedSelector { + + protected final Mode mode; + + public enum Mode { + ALIVE, BLOCKS, ITEMS, ANY, ENTITY_TYPE + } + + protected final List elements; + protected final Set selectedElements; + protected final BiConsumer> consumer; + protected List filterElements; + + protected UnifiedMultiSelector(User user, Mode mode, BiConsumer> consumer) { + this(user, mode, null, consumer); + } + + protected UnifiedMultiSelector(User user, Mode mode, List elements, + BiConsumer> consumer) { + super(user); + this.mode = mode; + this.consumer = consumer; + this.selectedElements = new HashSet<>(); + this.elements = (elements != null) ? elements : getElements(); // Use provided elements or get them from subclass + this.elements.sort(Comparator.comparing(this::elementToString)); + this.filterElements = this.elements; + } + + + /** + * Subclasses must return the complete list of available elements. + */ + protected abstract List getElements(); + + /** + * Returns the title key (to be appended to Constants.TITLE) + * for this selector (for example, "entity-selector" or "block-selector"). + */ + protected abstract String getTitleKey(); + + /** + * Returns the translation key prefix used for element buttons + * (for example, "entity." or "material."). + */ + protected abstract String getElementKeyPrefix(); + + /** + * Returns the placeholder used for element buttons + * (for example, "[entity]", or "[material]"). + */ + protected abstract String getElementPlaceholder(); + + /** + * Returns the icon for the given element. + */ + protected abstract ItemStack getIcon(T element); + + /** + * Returns the display name for the given element. + * (For instance, by calling Utils.prettifyObject(element, user)). + */ + protected abstract String getElementDisplayName(T element); + + /** + * Returns a string representation of the element used for filtering. + * (For enums you might simply return element.name().) + */ + protected abstract String elementToString(T element); + + @Override + protected void build() { + PanelBuilder panelBuilder = new PanelBuilder().user(this.user); + panelBuilder.name(this.user.getTranslation(Constants.TITLE + getTitleKey())); + + PanelUtils.fillBorder(panelBuilder, Material.BLUE_STAINED_GLASS_PANE); + + // Populate the GUI with the filtered list. + this.populateElements(panelBuilder, this.filterElements); + + // Add functional buttons. + panelBuilder.item(3, createButton(Button.ACCEPT_SELECTED)); + panelBuilder.item(5, createButton(Button.CANCEL)); + + panelBuilder.build(); + } + + @Override + protected void updateFilters() { + if (this.searchString == null || this.searchString.isBlank()) { + this.filterElements = this.elements; + } else { + this.filterElements = this.elements.stream() + .filter(element -> elementToString(element).toLowerCase(Locale.ENGLISH) + .contains(this.searchString.toLowerCase(Locale.ENGLISH))) + .distinct().collect(Collectors.toList()); + } + } + + private PanelItem createButton(Button button) { + final String reference = Constants.BUTTON + button.name().toLowerCase() + "."; + final String name = this.user.getTranslation(reference + "name"); + final List description = new ArrayList<>(); + description.add(this.user.getTranslation(reference + "description")); + + ItemStack icon; + PanelItem.ClickHandler clickHandler; + + switch (button) { + case ACCEPT_SELECTED -> { + if (!this.selectedElements.isEmpty()) { + description.add(this.user.getTranslation(reference + "title")); + for (T element : this.selectedElements) { + description.add(this.user.getTranslation(reference + "element", "[element]", + getElementDisplayName(element))); + } + } + icon = new ItemStack(Material.COMMAND_BLOCK); + clickHandler = (panel, user1, clickType, slot) -> { + this.consumer.accept(true, this.selectedElements); + return true; + }; + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-save")); + } + case CANCEL -> { + icon = new ItemStack(Material.IRON_DOOR); + clickHandler = (panel, user1, clickType, slot) -> { + this.consumer.accept(false, null); + return true; + }; + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-cancel")); + } + default -> { + icon = new ItemStack(Material.PAPER); + clickHandler = null; + } + } + + return new PanelItemBuilder().icon(icon).name(name).description(description).clickHandler(clickHandler).build(); + } + + @Override + protected PanelItem createElementButton(T element) { + final String reference = Constants.BUTTON + getElementKeyPrefix(); + List description = new ArrayList<>(); + description.add(this.user.getTranslation(reference + "description", "[id]", elementToString(element))); + + if (this.selectedElements.contains(element)) { + description.add(this.user.getTranslation(reference + "selected")); + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-deselect")); + } else { + description.add(""); + description.add(this.user.getTranslation(Constants.TIPS + "click-to-select")); + } + return new PanelItemBuilder() + .name(this.user.getTranslation(reference + "name", getElementPlaceholder(), + getElementDisplayName(element))) + .icon(getIcon(element)).description(description).clickHandler((panel, user1, clickType, slot) -> { + // Toggle the selection state. + if (!this.selectedElements.add(element)) { + this.selectedElements.remove(element); + } + this.build(); + return true; + }).glow(this.selectedElements.contains(element)).build(); + } + + protected enum Button { + ACCEPT_SELECTED, CANCEL + } +} diff --git a/src/main/java/world/bentobox/challenges/tasks/TryToComplete.java b/src/main/java/world/bentobox/challenges/tasks/TryToComplete.java index 971923a5..8bde71c7 100644 --- a/src/main/java/world/bentobox/challenges/tasks/TryToComplete.java +++ b/src/main/java/world/bentobox/challenges/tasks/TryToComplete.java @@ -2,25 +2,32 @@ -import com.google.common.collect.UnmodifiableIterator; -import java.time.*; +import java.time.Duration; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.EnumMap; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Objects; import java.util.PriorityQueue; import java.util.Queue; import java.util.UUID; +import java.util.function.BiPredicate; import java.util.stream.Collectors; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.GameMode; +import org.bukkit.Keyed; +import org.bukkit.Location; import org.bukkit.Material; +import org.bukkit.Tag; import org.bukkit.World; +import org.bukkit.advancement.AdvancementProgress; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.entity.Entity; @@ -29,19 +36,24 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.util.BoundingBox; +import com.google.common.collect.UnmodifiableIterator; + +import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.localization.TextVariables; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.util.Util; import world.bentobox.challenges.ChallengesAddon; -import world.bentobox.challenges.managers.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.database.object.requirements.CheckPapi; import world.bentobox.challenges.database.object.requirements.InventoryRequirements; import world.bentobox.challenges.database.object.requirements.IslandRequirements; import world.bentobox.challenges.database.object.requirements.OtherRequirements; import world.bentobox.challenges.database.object.requirements.StatisticRequirements; +import world.bentobox.challenges.database.object.requirements.StatisticRequirements.StatisticRec; +import world.bentobox.challenges.managers.ChallengesManager; import world.bentobox.challenges.utils.Constants; import world.bentobox.challenges.utils.Utils; @@ -184,8 +196,7 @@ public static boolean complete(ChallengesAddon addon, int maxTimes) { return new TryToComplete(addon, user, challenge, world, topLabel, permissionPrefix). - build(maxTimes). - meetsRequirements; + build(maxTimes).meetsRequirements; } @@ -266,16 +277,14 @@ ChallengeResult build(int maxTimes) if (result.getFactor() == 1) { Utils.sendMessage(this.user, - this.world, - Constants.MESSAGES + "you-completed-challenge", - Constants.PARAMETER_VALUE, this.challenge.getFriendlyName()); + this.world, Constants.MESSAGES + "you-completed-challenge", Constants.PARAMETER_VALUE, + this.challenge.getFriendlyName()); } if (this.addon.getChallengesSettings().isBroadcastMessages()) { Bukkit.getOnlinePlayers().stream(). - map(User::getInstance). - forEach(user -> Utils.sendMessage(user, + map(User::getInstance).forEach(user -> Utils.sendMessage(user, this.world, Constants.MESSAGES + "name-has-completed-challenge", Constants.PARAMETER_NAME, this.user.getName(), @@ -286,11 +295,11 @@ ChallengeResult build(int maxTimes) if (this.addon.getChallengesSettings().isShowCompletionTitle()) { this.user.getPlayer().sendTitle( - this.parseChallenge(this.user.getTranslation("challenges.titles.challenge-title"), this.challenge), - this.parseChallenge(this.user.getTranslation("challenges.titles.challenge-subtitle"), this.challenge), - 10, - this.addon.getChallengesSettings().getTitleShowtime(), - 20); + this.parseChallenge(this.user.getTranslation("challenges.titles.challenge-title"), + this.challenge), + this.parseChallenge(this.user.getTranslation("challenges.titles.challenge-subtitle"), + this.challenge), + 10, this.addon.getChallengesSettings().getTitleShowtime(), 20); } } @@ -315,7 +324,7 @@ ChallengeResult build(int maxTimes) if (this.addon.isEconomyProvided()) { this.addon.getEconomyProvider().deposit(this.user, - this.challenge.getRepeatMoneyReward() * rewardFactor); + this.challenge.getRepeatMoneyReward() * rewardFactor); } // Experience Repeat Reward @@ -331,17 +340,14 @@ ChallengeResult build(int maxTimes) if (result.getFactor() > 1) { Utils.sendMessage(this.user, - this.world, - Constants.MESSAGES + "you-repeated-challenge-multiple", - Constants.PARAMETER_VALUE, this.challenge.getFriendlyName(), - "[count]", Integer.toString(result.getFactor())); + this.world, Constants.MESSAGES + "you-repeated-challenge-multiple", Constants.PARAMETER_VALUE, + this.challenge.getFriendlyName(), "[count]", Integer.toString(result.getFactor())); } else { Utils.sendMessage(this.user, - this.world, - Constants.MESSAGES + "you-repeated-challenge", - Constants.PARAMETER_VALUE, this.challenge.getFriendlyName()); + this.world, Constants.MESSAGES + "you-repeated-challenge", Constants.PARAMETER_VALUE, + this.challenge.getFriendlyName()); } } @@ -380,15 +386,13 @@ ChallengeResult build(int maxTimes) this.runCommands(level.getRewardCommands()); Utils.sendMessage(this.user, - this.world, - Constants.MESSAGES + "you-completed-level", - Constants.PARAMETER_VALUE, level.getFriendlyName()); + this.world, Constants.MESSAGES + "you-completed-level", Constants.PARAMETER_VALUE, + level.getFriendlyName()); if (this.addon.getChallengesSettings().isBroadcastMessages()) { Bukkit.getOnlinePlayers().stream(). - map(User::getInstance). - forEach(user -> Utils.sendMessage(user, + map(User::getInstance).forEach(user -> Utils.sendMessage(user, this.world, Constants.MESSAGES + "name-has-completed-level", Constants.PARAMETER_NAME, this.user.getName(), @@ -401,11 +405,9 @@ ChallengeResult build(int maxTimes) if (this.addon.getChallengesSettings().isShowCompletionTitle()) { this.user.getPlayer().sendTitle( - this.parseLevel(this.user.getTranslation("challenges.titles.level-title"), level), - this.parseLevel(this.user.getTranslation("challenges.titles.level-subtitle"), level), - 10, - this.addon.getChallengesSettings().getTitleShowtime(), - 20); + this.parseLevel(this.user.getTranslation("challenges.titles.level-title"), level), + this.parseLevel(this.user.getTranslation("challenges.titles.level-subtitle"), level), + 10, this.addon.getChallengesSettings().getTitleShowtime(), 20); } } } @@ -423,239 +425,190 @@ private void fullFillRequirements(ChallengeResult result) { switch (this.challenge.getChallengeType()) { - case ISLAND_TYPE -> { - IslandRequirements requirements = this.challenge.getRequirements(); + case ISLAND_TYPE -> { + IslandRequirements requirements = this.challenge.getRequirements(); - if (result.meetsRequirements && + if (result.meetsRequirements && requirements.isRemoveEntities() && !requirements.getRequiredEntities().isEmpty()) - { - this.removeEntities(result.entities, result.getFactor()); - } + { + this.removeEntities(result.entities, result.getFactor()); + } - if (result.meetsRequirements && + if (result.meetsRequirements && requirements.isRemoveBlocks() && !requirements.getRequiredBlocks().isEmpty()) - { - this.removeBlocks(result.blocks, result.getFactor()); - } + { + this.removeBlocks(result.blocks, result.getFactor()); } - case INVENTORY_TYPE -> { - // If remove items, then remove them - if (this.getInventoryRequirements().isTakeItems()) - { - int sumEverything = result.requiredItems.stream(). + } + case INVENTORY_TYPE -> { + // If remove items, then remove them + if (this.getInventoryRequirements().isTakeItems()) { + int sumEverything = result.requiredItems.stream(). mapToInt(itemStack -> itemStack.getAmount() * result.getFactor()). sum(); - Map removedItems = + Map removedItems = this.removeItems(result.requiredItems, result.getFactor()); - int removedAmount = removedItems.values().stream().mapToInt(num -> num).sum(); + int removedAmount = removedItems.values().stream().mapToInt(num -> num).sum(); - // Something is not removed. - if (sumEverything != removedAmount) - { - Utils.sendMessage(this.user, + // Something is not removed. + if (sumEverything != removedAmount) { + Utils.sendMessage(this.user, this.world, Constants.ERRORS + "cannot-remove-items"); - result.removedItems = removedItems; - result.meetsRequirements = false; - } + result.removedItems = removedItems; + result.meetsRequirements = false; } } - case OTHER_TYPE -> { - OtherRequirements requirements = this.challenge.getRequirements(); + } + case OTHER_TYPE -> { + OtherRequirements requirements = this.challenge.getRequirements(); - if (this.addon.isEconomyProvided() && requirements.isTakeMoney()) - { - this.addon.getEconomyProvider().withdraw(this.user, requirements.getRequiredMoney()); - } + if (this.addon.isEconomyProvided() && requirements.isTakeMoney()) { + this.addon.getEconomyProvider().withdraw(this.user, requirements.getRequiredMoney()); + } - if (requirements.isTakeExperience() && - this.user.getPlayer().getGameMode() != GameMode.CREATIVE) - { - // Cannot take anything from creative game mode. - this.user.getPlayer().setTotalExperience( + if (requirements.isTakeExperience() && this.user.getPlayer().getGameMode() != GameMode.CREATIVE) { + // Cannot take anything from creative game mode. + this.user.getPlayer().setTotalExperience( this.user.getPlayer().getTotalExperience() - requirements.getRequiredExperience()); - } } - case STATISTIC_TYPE -> { - StatisticRequirements requirements = this.challenge.getRequirements(); - - if (requirements.isReduceStatistic() && requirements.getStatistic() != null) - { - int removeAmount = result.getFactor() * requirements.getAmount(); + } + case STATISTIC_TYPE -> { + StatisticRequirements requirements = this.challenge.getRequirements(); + for (StatisticRec s : requirements.getRequiredStatistics()) { + if (s.reduceStatistic() && s.statistic() != null) { + int removeAmount = result.getFactor() * s.amount(); // Start to remove from player who called the completion. - switch (requirements.getStatistic().getType()) + switch (s.statistic().getType()) { - case UNTYPED -> { - int statistic = this.user.getPlayer().getStatistic(requirements.getStatistic()); - - if (removeAmount >= statistic) - { - this.user.getPlayer().setStatistic(requirements.getStatistic(), 0); - removeAmount -= statistic; - } - else - { - this.user.getPlayer().setStatistic(requirements.getStatistic(), statistic - removeAmount); - removeAmount = 0; - } + case UNTYPED -> { + int statistic = this.user.getPlayer().getStatistic(s.statistic()); + + if (removeAmount >= statistic) { + this.user.getPlayer().setStatistic(s.statistic(), 0); + removeAmount -= statistic; + } else { + this.user.getPlayer().setStatistic(s.statistic(), statistic - removeAmount); + removeAmount = 0; } - case ITEM, BLOCK -> { - if (requirements.getMaterial() == null) - { - // Just a sanity check. Material cannot be null at this point of code. - removeAmount = 0; - } - else - { - int statistic = this.user.getPlayer().getStatistic(requirements.getStatistic(), - requirements.getMaterial()); - - if (removeAmount >= statistic) - { - this.user.getPlayer() - .setStatistic(requirements.getStatistic(), requirements.getMaterial(), 0); - removeAmount -= statistic; - } - else - { - this.user.getPlayer().setStatistic(requirements.getStatistic(), - requirements.getMaterial(), + } + case ITEM, BLOCK -> { + if (s.material() == null) { + // Just a sanity check. Material cannot be null at this point of code. + removeAmount = 0; + } else { + int statistic = this.user.getPlayer().getStatistic(s.statistic(), s.material()); + + if (removeAmount >= statistic) { + this.user.getPlayer().setStatistic(s.statistic(), s.material(), 0); + removeAmount -= statistic; + } else { + this.user.getPlayer().setStatistic(s.statistic(), s.material(), statistic - removeAmount); - removeAmount = 0; - } + removeAmount = 0; } } - case ENTITY -> { - if (requirements.getEntity() == null) - { - // Just a sanity check. Entity cannot be null at this point of code. + } + case ENTITY -> { + if (s.entity() == null) { + // Just a sanity check. Entity cannot be null at this point of code. + removeAmount = 0; + } else { + int statistic = this.user.getPlayer().getStatistic(s.statistic(), s.entity()); + + if (removeAmount >= statistic) { + this.user.getPlayer().setStatistic(s.statistic(), s.entity(), 0); + removeAmount -= statistic; + } else { + this.user.getPlayer().setStatistic(s.statistic(), s.entity(), statistic - removeAmount); removeAmount = 0; } - else - { - int statistic = this.user.getPlayer().getStatistic(requirements.getStatistic(), - requirements.getEntity()); - - if (removeAmount >= statistic) - { - this.user.getPlayer().setStatistic(requirements.getStatistic(), requirements.getEntity(), 0); - removeAmount -= statistic; - } - else - { - this.user.getPlayer().setStatistic(requirements.getStatistic(), - requirements.getEntity(), - statistic - removeAmount); - removeAmount = 0; - } - } } } + } // If challenges are in sync with all island members, then punish others too. if (this.addon.getChallengesSettings().isStoreAsIslandData()) { Island island = this.addon.getIslands().getIsland(this.world, this.user); - if (island == null) - { + if (island == null) { // hmm return; } - for (UnmodifiableIterator iterator = island.getMemberSet().iterator(); - iterator.hasNext() && removeAmount > 0; ) - { + for (UnmodifiableIterator iterator = island.getMemberSet().iterator(); iterator.hasNext() + && removeAmount > 0;) { Player player = Bukkit.getPlayer(iterator.next()); - if (player == null || player == this.user.getPlayer()) - { + if (player == null || player == this.user.getPlayer()) { // cannot punish null or player who already was punished. continue; } - - switch (Objects.requireNonNull(requirements.getStatistic()).getType()) - { - case UNTYPED -> { - int statistic = player.getStatistic(requirements.getStatistic()); - - if (removeAmount >= statistic) - { + + switch (Objects.requireNonNull(s.statistic()).getType()) { + case UNTYPED -> { + int statistic = player.getStatistic(s.statistic()); + + if (removeAmount >= statistic) + { + removeAmount -= statistic; + player.setStatistic(s.statistic(), 0); + } + else + { + player.setStatistic(s.statistic(), statistic - removeAmount); + removeAmount = 0; + } + } + case ITEM, BLOCK -> { + if (s.material() == null) { + // Just a sanity check. Entity cannot be null at this point of code. + removeAmount = 0; + } else { + int statistic = player.getStatistic(s.statistic(), s.material()); + + if (removeAmount >= statistic) { removeAmount -= statistic; - player.setStatistic(requirements.getStatistic(), 0); - } - else - { - player.setStatistic(requirements.getStatistic(), statistic - removeAmount); + player.setStatistic(s.statistic(), s.material(), 0); + } else { + player.setStatistic(s.statistic(), s.material(), statistic - removeAmount); removeAmount = 0; } } - case ITEM, BLOCK -> { - if (requirements.getMaterial() == null) - { - // Just a sanity check. Entity cannot be null at this point of code. - removeAmount = 0; - } - else - { - int statistic = player.getStatistic(requirements.getStatistic(), - requirements.getMaterial()); - - if (removeAmount >= statistic) - { - removeAmount -= statistic; - player.setStatistic(requirements.getStatistic(), - requirements.getMaterial(), - 0); - } - else - { - player.setStatistic(requirements.getStatistic(), - requirements.getMaterial(), - statistic - removeAmount); - removeAmount = 0; - } - } + } + case ENTITY -> { + if (s.entity() == null) + { + // Just a sanity check. Entity cannot be null at this point of code. + removeAmount = 0; } - case ENTITY -> { - if (requirements.getEntity() == null) - { - // Just a sanity check. Entity cannot be null at this point of code. + else + { + int statistic = player.getStatistic(s.statistic(), s.entity()); + + if (removeAmount >= statistic) { + removeAmount -= statistic; + player.setStatistic(s.statistic(), s.entity(), 0); + } else { + player.setStatistic(s.statistic(), s.entity(), statistic - removeAmount); removeAmount = 0; } - else - { - int statistic = player.getStatistic(requirements.getStatistic(), - requirements.getEntity()); - - if (removeAmount >= statistic) - { - removeAmount -= statistic; - player.setStatistic(requirements.getStatistic(), - requirements.getEntity(), - 0); - } - else - { - player.setStatistic(requirements.getStatistic(), - requirements.getEntity(), - statistic - removeAmount); - removeAmount = 0; - } - } } } + } } } } } } + } } @@ -667,7 +620,7 @@ private void fullFillRequirements(ChallengeResult result) private ChallengeResult checkIfCanCompleteChallenge(int maxTimes) { ChallengeResult result; - + ChallengeType type = this.challenge.getChallengeType(); // Check the world if (!this.challenge.isDeployed()) @@ -681,14 +634,14 @@ else if (maxTimes < 1) result = EMPTY_RESULT; } else if (Util.getWorld(this.world) != Util.getWorld(this.user.getWorld()) || - !this.challenge.matchGameMode(Utils.getGameMode(this.world))) + !this.challenge.matchGameMode(Utils.getGameMode(this.world))) { Utils.sendMessage(this.user, this.world, "general.errors.wrong-world"); result = EMPTY_RESULT; } // Player is not on island else if (this.user.getLocation() == null || - ChallengesAddon.CHALLENGES_WORLD_PROTECTION.isSetForWorld(this.world) && + ChallengesAddon.CHALLENGES_WORLD_PROTECTION.isSetForWorld(this.world) && !this.addon.getIslands().locationIsOnIsland(this.user.getPlayer(), this.user.getLocation())) { Utils.sendMessage(this.user, this.world, Constants.MESSAGES + "not-on-island"); @@ -696,22 +649,21 @@ else if (this.user.getLocation() == null || } // Check player permission else if (!this.addon.getIslands().getIslandAt(this.user.getLocation()). - map(i -> i.isAllowed(this.user, ChallengesAddon.CHALLENGES_ISLAND_PROTECTION)). - orElse(false)) + map(i -> i.isAllowed(this.user, ChallengesAddon.CHALLENGES_ISLAND_PROTECTION)).orElse(false)) { Utils.sendMessage(this.user, this.world, Constants.MESSAGES + "no-rank"); result = EMPTY_RESULT; } // Check if user has unlocked challenges level. else if (!this.challenge.getLevel().equals(ChallengesManager.FREE) && - !this.manager.isLevelUnlocked(this.user, this.world, this.manager.getLevel(this.challenge.getLevel()))) + !this.manager.isLevelUnlocked(this.user, this.world, this.manager.getLevel(this.challenge.getLevel()))) { Utils.sendMessage(this.user, this.world, Constants.ERRORS + "challenge-level-not-available"); result = EMPTY_RESULT; } // Check max times else if (this.challenge.isRepeatable() && this.challenge.getMaxTimes() > 0 && - this.manager.getChallengeTimes(this.user, this.world, this.challenge) >= this.challenge.getMaxTimes()) + this.manager.getChallengeTimes(this.user, this.world, this.challenge) >= this.challenge.getMaxTimes()) { Utils.sendMessage(this.user, this.world, Constants.ERRORS + "not-repeatable"); result = EMPTY_RESULT; @@ -726,16 +678,16 @@ else if (!this.challenge.isRepeatable() && this.manager.isChallengeComplete(this else if (this.manager.isBreachingTimeOut(this.user, this.world, this.challenge)) { long missing = this.manager.getLastCompletionDate(this.user, this.world, challenge) + - this.challenge.getTimeout() - System.currentTimeMillis(); + this.challenge.getTimeout() - System.currentTimeMillis(); Utils.sendMessage(this.user, this.world, Constants.ERRORS + "timeout", - "[timeout]", Utils.parseDuration(Duration.ofMillis(this.challenge.getTimeout()), this.user), - "[wait-time]", Utils.parseDuration(Duration.ofMillis(missing), this.user)); + "[timeout]", Utils.parseDuration(Duration.ofMillis(this.challenge.getTimeout()), this.user), + "[wait-time]", Utils.parseDuration(Duration.ofMillis(missing), this.user)); result = EMPTY_RESULT; } // Check environment else if (!this.challenge.getEnvironment().isEmpty() && - !this.challenge.getEnvironment().contains(this.user.getWorld().getEnvironment())) + !this.challenge.getEnvironment().contains(this.user.getWorld().getEnvironment())) { Utils.sendMessage(this.user, this.world, Constants.ERRORS + "wrong-environment"); result = EMPTY_RESULT; @@ -784,7 +736,8 @@ else if (type.equals(ChallengeType.STATISTIC_TYPE)) private boolean checkPermissions() { return this.challenge.getRequirements().getRequiredPermissions().isEmpty() || - this.challenge.getRequirements().getRequiredPermissions().stream().allMatch(s -> this.user.hasPermission(s)); + this.challenge.getRequirements().getRequiredPermissions().stream() + .allMatch(s -> this.user.hasPermission(s)); } @@ -838,10 +791,11 @@ private void runCommands(List commands) String alert = "Running command '" + cmd + "' as " + this.user.getName(); this.addon.getLogger().info(alert); cmd = cmd.substring(6). - replaceAll(Constants.ESC + Constants.PARAMETER_PLAYER, this.user.getName()). - replaceAll(Constants.ESC + Constants.PARAMETER_OWNER, owner). - replaceAll(Constants.ESC + Constants.PARAMETER_NAME, island == null || island.getName() == null ? "" : island.getName()). - trim(); + replaceAll(Constants.ESC + Constants.PARAMETER_PLAYER, this.user.getName()) + .replaceAll(Constants.ESC + Constants.PARAMETER_OWNER, owner) + .replaceAll(Constants.ESC + Constants.PARAMETER_NAME, + island == null || island.getName() == null ? "" : island.getName()) + .trim(); try { if (!user.performCommand(cmd)) @@ -862,9 +816,10 @@ private void runCommands(List commands) try { cmd = cmd.replaceAll(Constants.ESC + Constants.PARAMETER_PLAYER, this.user.getName()). - replaceAll(Constants.ESC + Constants.PARAMETER_OWNER, owner). - replaceAll(Constants.ESC + Constants.PARAMETER_NAME, island == null || island.getName() == null ? "" : island.getName()). - trim(); + replaceAll(Constants.ESC + Constants.PARAMETER_OWNER, owner) + .replaceAll(Constants.ESC + Constants.PARAMETER_NAME, + island == null || island.getName() == null ? "" : island.getName()) + .trim(); if (!this.addon.getServer().dispatchCommand(this.addon.getServer().getConsoleSender(), cmd)) { @@ -914,7 +869,7 @@ private ChallengeResult checkInventory(int maxTimes) if (this.user.getPlayer().getGameMode() != GameMode.CREATIVE) { requiredItems = Utils.groupEqualItems(this.getInventoryRequirements().getRequiredItems(), - this.getInventoryRequirements().getIgnoreMetaData()); + this.getInventoryRequirements().getIgnoreMetaData()); // Check if all required items are in players inventory. for (ItemStack required : requiredItems) @@ -924,25 +879,20 @@ private ChallengeResult checkInventory(int maxTimes) if (this.getInventoryRequirements().getIgnoreMetaData().contains(required.getType())) { numInInventory = Arrays.stream(this.user.getInventory().getContents()). - filter(Objects::nonNull). - filter(i -> i.getType().equals(required.getType())). - mapToInt(ItemStack::getAmount). - sum(); + filter(Objects::nonNull).filter(i -> i.getType().equals(required.getType())) + .mapToInt(ItemStack::getAmount).sum(); } else { numInInventory = Arrays.stream(this.user.getInventory().getContents()). - filter(Objects::nonNull). - filter(i -> i.isSimilar(required)). - mapToInt(ItemStack::getAmount). - sum(); + filter(Objects::nonNull).filter(i -> i.isSimilar(required)).mapToInt(ItemStack::getAmount) + .sum(); } if (numInInventory < required.getAmount()) { Utils.sendMessage(this.user, this.world, Constants.ERRORS + "not-enough-items", - "[items]", - Utils.prettifyObject(required, this.user)); + "[items]", Utils.prettifyObject(required, this.user)); return EMPTY_RESULT; } @@ -987,17 +937,14 @@ else if (this.getInventoryRequirements().getIgnoreMetaData().contains(required.g { // Use collecting method that ignores item meta. itemsInInventory = Arrays.stream(user.getInventory().getContents()). - filter(Objects::nonNull). - filter(i -> i.getType().equals(required.getType())). - collect(Collectors.toList()); + filter(Objects::nonNull).filter(i -> i.getType().equals(required.getType())) + .collect(Collectors.toList()); } else { // Use collecting method that compares item meta. itemsInInventory = Arrays.stream(user.getInventory().getContents()). - filter(Objects::nonNull). - filter(i -> i.isSimilar(required)). - collect(Collectors.toList()); + filter(Objects::nonNull).filter(i -> i.isSimilar(required)).collect(Collectors.toList()); } for (ItemStack itemStack : itemsInInventory) @@ -1102,8 +1049,7 @@ private ChallengeResult checkSurrounding(int factor) // Protection code. Do not allow to select too large region for completing challenge. if (boundingBox.getWidthX() > distance * 2 + 3 || - boundingBox.getWidthZ() > distance * 2 + 3 || - boundingBox.getHeight() > distance * 2 + 3) + boundingBox.getWidthZ() > distance * 2 + 3 || boundingBox.getHeight() > distance * 2 + 3) { this.addon.logError("BoundingBox is larger than SearchRadius. " + " | BoundingBox: " + boundingBox + @@ -1118,15 +1064,118 @@ private ChallengeResult checkSurrounding(int factor) ChallengeResult result = this.searchForEntities(requirements.getRequiredEntities(), factor, boundingBox); + // For Material tags + if (result.isMeetsRequirements() && !requirements.getRequiredMaterialTags().isEmpty()) { + result = searchForTags(requirements.getRequiredMaterialTags(), factor, boundingBox, Tag::isTagged, + (world, x, y, z) -> world.getBlockAt(x, y, z).getType()); + } + + // For EntityType tags + if (result.isMeetsRequirements() && !requirements.getRequiredEntityTypeTags().isEmpty()) { + result = searchForTags(requirements.getRequiredEntityTypeTags(), factor, boundingBox, Tag::isTagged, + (world, x, y, z) -> { + Collection entities = world.getNearbyEntities(new Location(world, x, y, z), 1, 1, 1); + return entities.isEmpty() ? null : entities.iterator().next().getType(); + }); + } if (result.isMeetsRequirements() && !requirements.getRequiredBlocks().isEmpty()) { // Search for items only if entities found result = this.searchForBlocks(requirements.getRequiredBlocks(), result.getFactor(), boundingBox); } + return result; } + /** + * Generic method to search for required tags in given challenge boundingBox. + * @param The type parameter for the Tag (must extend Keyed) + * @param requiredMap Required Tag Map + * @param factor Requirement multiplier + * @param boundingBox Bounding box of island challenge + * @param typeChecker Function to check if an element matches the tag + * @param elementGetter Function to get the element at a specific location + * @return ChallengeResult + */ + private ChallengeResult searchForTags(Map, Integer> requiredMap, int factor, + BoundingBox boundingBox, BiPredicate, T> typeChecker, LocationElementGetter elementGetter) { + + if (requiredMap.isEmpty()) { + return new ChallengeResult().setMeetsRequirements().setCompleteFactor(factor); + } + + Map, Integer> tags = new HashMap<>(requiredMap); + Map, Integer> tagsFound = new HashMap<>(requiredMap.size()); + + Queue blockQueue = new PriorityQueue<>((o1, o2) -> { + if (this.user.getLocation() != null) { + return Double.compare(o1.getLocation().distance(this.user.getLocation()), + o2.getLocation().distance(this.user.getLocation())); + } else { + return 0; + } + }); + + 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++) { + T element = elementGetter.getElement(this.user.getWorld(), x, y, z); + if (element == null) + continue; + + for (Entry, Integer> en : requiredMap.entrySet()) { + if (typeChecker.test(en.getKey(), element)) { + Block block = this.user.getWorld().getBlockAt(x, y, z); + blockQueue.add(block); + tagsFound.putIfAbsent(en.getKey(), 1); + tagsFound.computeIfPresent(en.getKey(), (k, v) -> v + 1); + // Remove one + tags.computeIfPresent(en.getKey(), (k, v) -> v - 1); + // Remove any that have an amount of 0 + tags.entrySet().removeIf(e -> e.getValue() <= 0); + + if (tags.isEmpty() && factor == 1) { + return new ChallengeResult().setMeetsRequirements().setCompleteFactor(factor) + .setBlockQueue(blockQueue); + } + } + } + } + } + } + + if (tags.isEmpty()) { + if (factor > 1) { + // Calculate minimal completion count + for (Map.Entry, Integer> entry : tagsFound.entrySet()) { + factor = Math.min(factor, entry.getValue() / requiredMap.get(entry.getKey())); + } + } + + tagsFound.clear(); + return new ChallengeResult().setMeetsRequirements().setCompleteFactor(factor).setBlockQueue(blockQueue); + } + + Utils.sendMessage(this.user, this.world, Constants.ERRORS + "not-close-enough", Constants.PARAMETER_NUMBER, + String.valueOf(this.getIslandRequirements().getSearchRadius())); + + tags.forEach((k, v) -> Utils.sendMessage(this.user, this.world, Constants.ERRORS + "you-still-need", "[amount]", + String.valueOf(v), "[item]", Utils.prettifyObject(k, this.user))); + + // kick garbage collector + tags.clear(); + tagsFound.clear(); + blockQueue.clear(); + + return EMPTY_RESULT; + } + + // Interface to get elements at a specific location + @FunctionalInterface + private interface LocationElementGetter { + T getElement(World world, int x, int y, int z); + } /** * This method search required blocks in given challenge boundingBox. @@ -1135,39 +1184,29 @@ private ChallengeResult checkSurrounding(int factor) * @param boundingBox Bounding box of island challenge * @return ChallengeResult */ - private ChallengeResult searchForBlocks(Map requiredMap, int factor, BoundingBox boundingBox) - { - if (requiredMap.isEmpty()) - { + private ChallengeResult searchForBlocks(Map requiredMap, int factor, BoundingBox boundingBox) { + if (requiredMap.isEmpty()) { 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. + // This queue will contain only blocks with the required type ordered by distance till player. Queue blockFromWorld = new PriorityQueue<>((o1, o2) -> { - if (o1.getType().equals(o2.getType()) && this.user.getLocation() != null) - { + if (o1.getType().equals(o2.getType()) && this.user.getLocation() != null) { return Double.compare(o1.getLocation().distance(this.user.getLocation()), - o2.getLocation().distance(this.user.getLocation())); - } - else - { + 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++) - { + 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++) { Block block = this.user.getWorld().getBlockAt(x, y, z); - if (requiredMap.containsKey(block.getType())) - { + if (requiredMap.containsKey(block.getType())) { blockFromWorld.add(block); blocksFound.putIfAbsent(block.getType(), 1); @@ -1178,26 +1217,22 @@ private ChallengeResult searchForBlocks(Map requiredMap, int // Remove any that have an amount of 0 blocks.entrySet().removeIf(en -> en.getValue() <= 0); - if (blocks.isEmpty() && factor == 1) - { + 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); + return new ChallengeResult().setMeetsRequirements().setCompleteFactor(factor) + .setBlockQueue(blockFromWorld); } } } } } - if (blocks.isEmpty()) - { - if (factor > 1) - { + if (blocks.isEmpty()) { + if (factor > 1) { // Calculate minimal completion count. - for (Map.Entry entry : blocksFound.entrySet()) - { - factor = Math.min(factor, - entry.getValue() / requiredMap.get(entry.getKey())); + for (Map.Entry entry : blocksFound.entrySet()) { + factor = Math.min(factor, entry.getValue() / requiredMap.get(entry.getKey())); } } @@ -1207,16 +1242,11 @@ private ChallengeResult searchForBlocks(Map requiredMap, int return new ChallengeResult().setMeetsRequirements().setCompleteFactor(factor).setBlockQueue(blockFromWorld); } - Utils.sendMessage(this.user, - this.world, - Constants.ERRORS + "not-close-enough", - Constants.PARAMETER_NUMBER, String.valueOf(this.getIslandRequirements().getSearchRadius())); + Utils.sendMessage(this.user, this.world, Constants.ERRORS + "not-close-enough", Constants.PARAMETER_NUMBER, + String.valueOf(this.getIslandRequirements().getSearchRadius())); - blocks.forEach((k, v) -> Utils.sendMessage(this.user, - this.world, - Constants.ERRORS + "you-still-need", - "[amount]", String.valueOf(v), - "[item]", Utils.prettifyObject(k, this.user))); + blocks.forEach((k, v) -> Utils.sendMessage(this.user, this.world, Constants.ERRORS + "you-still-need", + "[amount]", String.valueOf(v), "[item]", Utils.prettifyObject(k, this.user))); // kick garbage collector @@ -1235,12 +1265,9 @@ private ChallengeResult searchForBlocks(Map requiredMap, int * @param boundingBox Bounding box of island challenge * @return ChallengeResult */ - private ChallengeResult searchForEntities(Map requiredMap, - int factor, - BoundingBox boundingBox) - { - if (requiredMap.isEmpty()) - { + private ChallengeResult searchForEntities(Map requiredMap, int factor, + BoundingBox boundingBox) { + if (requiredMap.isEmpty()) { return new ChallengeResult().setMeetsRequirements().setCompleteFactor(factor); } @@ -1250,12 +1277,10 @@ private ChallengeResult searchForEntities(Map requiredMap, // Create queue that contains all required entities ordered by distance till player. Queue entityQueue = new PriorityQueue<>((o1, o2) -> { - if (o1.getType().equals(o2.getType()) && this.user.getLocation() != null) - { + if (o1.getType().equals(o2.getType()) && this.user.getLocation() != null) { return Double.compare(o1.getLocation().distance(this.user.getLocation()), - o2.getLocation().distance(this.user.getLocation())); - } - else + o2.getLocation().distance(this.user.getLocation())); + } else { return o1.getType().compareTo(o2.getType()); } @@ -1276,16 +1301,14 @@ private ChallengeResult searchForEntities(Map requiredMap, } }); - if (minimalRequirements.isEmpty()) - { + if (minimalRequirements.isEmpty()) { if (factor > 1) { // Calculate minimal completion count. for (Map.Entry entry : entitiesFound.entrySet()) { - factor = Math.min(factor, - entry.getValue() / requiredMap.get(entry.getKey())); + factor = Math.min(factor, entry.getValue() / requiredMap.get(entry.getKey())); } } @@ -1295,12 +1318,9 @@ private ChallengeResult searchForEntities(Map requiredMap, return new ChallengeResult().setMeetsRequirements().setCompleteFactor(factor).setEntityQueue(entityQueue); } - minimalRequirements.forEach((reqEnt, amount) -> - Utils.sendMessage(this.user, - this.world, - Constants.ERRORS + "you-still-need", - "[amount]", String.valueOf(amount), - "[item]", Utils.prettifyObject(reqEnt, this.user))); + minimalRequirements.forEach( + (reqEnt, amount) -> Utils.sendMessage(this.user, this.world, Constants.ERRORS + "you-still-need", + "[amount]", String.valueOf(amount), "[item]", Utils.prettifyObject(reqEnt, this.user))); // Kick garbage collector entitiesFound.clear(); @@ -1316,8 +1336,7 @@ private ChallengeResult searchForEntities(Map requiredMap, * @param blockQueue Queue with blocks that could be removed * @param factor requirement factor for each block type. */ - private void removeBlocks(Queue blockQueue, int factor) - { + private void removeBlocks(Queue blockQueue, int factor) { Map blocks = new EnumMap<>(this.getIslandRequirements().getRequiredBlocks()); // Increase required blocks by factor. @@ -1340,23 +1359,22 @@ private void removeBlocks(Queue blockQueue, int factor) * @param entityQueue Queue with entities that could be removed * @param factor requirement factor for each entity type. */ - private void removeEntities(Queue entityQueue, int factor) - { - Map entities = this.getIslandRequirements().getRequiredEntities().isEmpty() ? - new EnumMap<>(EntityType.class) : new EnumMap<>(this.getIslandRequirements().getRequiredEntities()); - - // Increase required entities by factor. - entities.entrySet().forEach(entry -> entry.setValue(entry.getValue() * factor)); - - // 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); - entity.remove(); - } - }); + private void removeEntities(Queue entityQueue, int factor) { + Map entities = this.getIslandRequirements().getRequiredEntities().isEmpty() + ? new EnumMap<>(EntityType.class) + : new EnumMap<>(this.getIslandRequirements().getRequiredEntities()); + + // Increase required entities by factor. + entities.entrySet().forEach(entry -> entry.setValue(entry.getValue() * factor)); + + // 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); + entity.remove(); + } + }); } @@ -1370,71 +1388,65 @@ private void removeEntities(Queue entityQueue, int factor) * It returns ChallengeResult. * @param factor - times that user wanted to complete */ - private ChallengeResult checkOthers(int factor) - { - if (factor <= 0) - { + private ChallengeResult checkOthers(int factor) { + if (factor <= 0) { return EMPTY_RESULT; } OtherRequirements requirements = this.getOtherRequirements(); - if (!this.addon.isLevelProvided() && requirements.getRequiredIslandLevel() != 0) - { + if (!this.addon.isLevelProvided() && requirements.getRequiredIslandLevel() != 0) { Utils.sendMessage(this.user, this.world, Constants.ERRORS + "missing-addon"); - } - else if (!this.addon.isEconomyProvided() && - requirements.getRequiredMoney() != 0) - { + } else if (!this.addon.isEconomyProvided() && requirements.getRequiredMoney() != 0) { Utils.sendMessage(this.user, this.world, Constants.ERRORS + "missing-addon"); - } - else if (this.addon.isEconomyProvided() && requirements.getRequiredMoney() < 0) - { + } else if (this.addon.isEconomyProvided() && requirements.getRequiredMoney() < 0) { Utils.sendMessage(this.user, this.world, Constants.ERRORS + "incorrect"); - } - else if (this.addon.isEconomyProvided() && - !this.addon.getEconomyProvider().has(this.user, requirements.getRequiredMoney())) - { - Utils.sendMessage(this.user, - this.world, - Constants.ERRORS + "not-enough-money", - Constants.PARAMETER_VALUE, Double.toString(requirements.getRequiredMoney())); - } - else if (requirements.getRequiredExperience() < 0) - { + } else if (this.addon.isEconomyProvided() + && !this.addon.getEconomyProvider().has(this.user, requirements.getRequiredMoney())) { + Utils.sendMessage(this.user, this.world, Constants.ERRORS + "not-enough-money", Constants.PARAMETER_VALUE, + Double.toString(requirements.getRequiredMoney())); + } else if (requirements.getRequiredExperience() < 0) { Utils.sendMessage(this.user, this.world, Constants.ERRORS + "incorrect"); - } - else if (this.user.getPlayer().getTotalExperience() < requirements.getRequiredExperience() && - this.user.getPlayer().getGameMode() != GameMode.CREATIVE) - { + } else if (this.user.getPlayer().getTotalExperience() < requirements.getRequiredExperience() + && this.user.getPlayer().getGameMode() != GameMode.CREATIVE) { // Players in creative gamemode has infinite amount of EXP. + Utils.sendMessage(this.user, this.world, Constants.ERRORS + "not-enough-experience", + Constants.PARAMETER_VALUE, Integer.toString(requirements.getRequiredExperience())); + } else if (this.addon.isLevelProvided() && this.addon.getLevelAddon().getIslandLevel(this.world, + this.user.getUniqueId()) < requirements.getRequiredIslandLevel()) { Utils.sendMessage(this.user, - this.world, - Constants.ERRORS + "not-enough-experience", - Constants.PARAMETER_VALUE, - Integer.toString(requirements.getRequiredExperience())); - } - else if (this.addon.isLevelProvided() && - this.addon.getLevelAddon().getIslandLevel(this.world, this.user.getUniqueId()) < requirements.getRequiredIslandLevel()) - { - Utils.sendMessage(this.user, - this.world, - Constants.ERRORS + "island-level", - TextVariables.NUMBER, - String.valueOf(requirements.getRequiredIslandLevel())); + this.world, Constants.ERRORS + "island-level", TextVariables.NUMBER, + String.valueOf(requirements.getRequiredIslandLevel())); + } else if (this.addon.getPlugin().getHooks().getHook("PlaceholderAPI").isPresent() + && !requirements.getPapiString().isEmpty() + && !CheckPapi.evaluate(user.getPlayer(), requirements.getPapiString())) { + Utils.sendMessage(this.user, this.world, Constants.ERRORS + "incorrect"); + if (!requirements.getPapiString().isEmpty()) { + addon.log("FYI:.Challenge failed for " + user.getName() + ". PAPI formula: " + + requirements.getPapiString() + " = " + + CheckPapi.evaluate(user.getPlayer(), requirements.getPapiString())); + } + } else if (!requirements.getAdvancements().stream().map(user.getPlayer()::getAdvancementProgress) + .allMatch(AdvancementProgress::isDone)) { + Utils.sendMessage(this.user, this.world, Constants.ERRORS + "incorrect"); + user.sendMessage("challenges.gui.buttons.required_advancements.title"); + requirements.getAdvancements().stream().filter(ad -> !user.getPlayer().getAdvancementProgress(ad).isDone()) + .forEach(ad -> Utils.sendMessage(this.user, this.world, + "challenges.gui.buttons.advancement_element.name", "[name]", + ad.getDisplay().getTitle())); } else { // calculate factor - if (this.addon.isEconomyProvided() && requirements.isTakeMoney()) - { - factor = Math.min(factor, (int) (this.addon.getEconomyProvider().getBalance(this.user) / requirements.getRequiredMoney())); + if (this.addon.isEconomyProvided() && requirements.isTakeMoney()) { + factor = Math.min(factor, (int) (this.addon.getEconomyProvider().getBalance(this.user) + / requirements.getRequiredMoney())); } - if (requirements.getRequiredExperience() > 0 && requirements.isTakeExperience()) - { - factor = Math.min(factor, this.user.getPlayer().getTotalExperience() / requirements.getRequiredExperience()); + if (requirements.getRequiredExperience() > 0 && requirements.isTakeExperience()) { + factor = Math.min(factor, + this.user.getPlayer().getTotalExperience() / requirements.getRequiredExperience()); } return new ChallengeResult().setMeetsRequirements().setCompleteFactor(factor); @@ -1443,21 +1455,17 @@ else if (this.addon.isLevelProvided() && return EMPTY_RESULT; } - // --------------------------------------------------------------------- // Section: Statistic Challenge // --------------------------------------------------------------------- - /** * Checks if a statistic challenge can be completed or not * It returns ChallengeResult. * @param factor - times that user wanted to complete */ - private ChallengeResult checkStatistic(int factor) - { - if (factor <= 0) - { + private ChallengeResult checkStatistic(int factor) { + if (factor <= 0) { return EMPTY_RESULT; } @@ -1465,83 +1473,75 @@ private ChallengeResult checkStatistic(int factor) int currentValue; - if (requirements.getStatistic() == null) - { + if (requirements.getRequiredStatistics().isEmpty()) { // Sanity check. return EMPTY_RESULT; } + List cr = new ArrayList<>(); + // Check all requirements + for (StatisticRec s : requirements.getRequiredStatistics()) { - switch (Objects.requireNonNull(requirements.getStatistic()).getType()) - { - case UNTYPED -> currentValue = - this.manager.getStatisticData(this.user, this.world, requirements.getStatistic()); - case ITEM, BLOCK -> currentValue = - this.manager.getStatisticData(this.user, this.world, requirements.getStatistic(), requirements.getMaterial()); - case ENTITY -> currentValue = - this.manager.getStatisticData(this.user, this.world, requirements.getStatistic(), requirements.getEntity()); + switch (Objects.requireNonNull(s.statistic()).getType()) + { + case UNTYPED -> currentValue = this.manager.getStatisticData(this.user, this.world, s.statistic()); + case ITEM, BLOCK -> + currentValue = this.manager.getStatisticData(this.user, this.world, s.statistic(), s.material()); + case ENTITY -> + currentValue = this.manager.getStatisticData(this.user, this.world, s.statistic(), s.entity()); default -> currentValue = 0; - } + } - if (currentValue < requirements.getAmount()) - { - switch (Objects.requireNonNull(requirements.getStatistic()).getType()) - { + if (currentValue < s.amount()) { + switch (Objects.requireNonNull(s.statistic()).getType()) { case ITEM, BLOCK -> { - Utils.sendMessage(this.user, - this.world, - Constants.ERRORS + "requirement-not-met-material", - TextVariables.NUMBER, String.valueOf(requirements.getAmount()), - "[statistic]", Utils.prettifyObject(requirements.getStatistic(), this.user), - "[material]", Utils.prettifyObject(requirements.getMaterial(), this.user), - Constants.PARAMETER_VALUE, String.valueOf(currentValue)); + Utils.sendMessage(this.user, this.world, Constants.ERRORS + "requirement-not-met-material", + TextVariables.NUMBER, String.valueOf(s.amount()), "[statistic]", + Utils.prettifyObject(s.statistic(), this.user), "[material]", + Utils.prettifyObject(s.material(), this.user), Constants.PARAMETER_VALUE, + String.valueOf(currentValue)); } case ENTITY -> { - Utils.sendMessage(this.user, - this.world, - Constants.ERRORS + "requirement-not-met-entity", - TextVariables.NUMBER, String.valueOf(requirements.getAmount()), - "[statistic]", Utils.prettifyObject(requirements.getStatistic(), this.user), - "[entity]", Utils.prettifyObject(requirements.getEntity(), this.user), - Constants.PARAMETER_VALUE, String.valueOf(currentValue)); + Utils.sendMessage(this.user, this.world, Constants.ERRORS + "requirement-not-met-entity", + TextVariables.NUMBER, String.valueOf(s.amount()), "[statistic]", + Utils.prettifyObject(s.statistic(), this.user), "[entity]", + Utils.prettifyObject(s.entity(), this.user), Constants.PARAMETER_VALUE, + String.valueOf(currentValue)); } default -> { - Utils.sendMessage(this.user, - this.world, - Constants.ERRORS + "requirement-not-met", - TextVariables.NUMBER, String.valueOf(requirements.getAmount()), - "[statistic]", Utils.prettifyObject(requirements.getStatistic(), this.user), - Constants.PARAMETER_VALUE, String.valueOf(currentValue)); + Utils.sendMessage(this.user, this.world, Constants.ERRORS + "requirement-not-met", + TextVariables.NUMBER, String.valueOf(s.amount()), "[statistic]", + Utils.prettifyObject(s.statistic(), this.user), Constants.PARAMETER_VALUE, + String.valueOf(currentValue)); } + } + } else { + factor = s.amount() == 0 ? factor : Math.min(factor, currentValue / s.amount()); + // Store result + cr.add(new ChallengeResult().setMeetsRequirements().setCompleteFactor(factor)); } } - else - { - factor = requirements.getAmount() == 0 ? factor : Math.min(factor, currentValue / requirements.getAmount()); - - return new ChallengeResult().setMeetsRequirements().setCompleteFactor(factor); + // Check results -- there must be some and all must pass + if (!cr.isEmpty() && cr.stream().allMatch(result -> result.meetsRequirements)) { + // Return any of them, because they pass + return cr.getFirst(); } - return EMPTY_RESULT; } - // --------------------------------------------------------------------- // Section: Title parsings // --------------------------------------------------------------------- - /** * This method pareses input message by replacing all challenge variables in [] with their values. * @param inputMessage inputMessage string * @param challenge Challenge from which these values should be taken * @return new String that replaces [VALUE] with correct value from challenge. */ - private String parseChallenge(String inputMessage, Challenge challenge) - { + private String parseChallenge(String inputMessage, Challenge challenge) { String outputMessage = inputMessage; - if (inputMessage.contains("[") && inputMessage.contains("]")) - { + if (inputMessage.contains("[") && inputMessage.contains("]")) { outputMessage = outputMessage.replace("[friendlyName]", challenge.getFriendlyName()); ChallengeLevel level = challenge.getLevel().isEmpty() ? null : this.manager.getLevel(challenge.getLevel()); @@ -1560,8 +1560,7 @@ private String parseChallenge(String inputMessage, Challenge challenge) * @param level level from which these values should be taken * @return new String that replaces [VALUE] with correct value from level. */ - private String parseLevel(String inputMessage, ChallengeLevel level) - { + private String parseLevel(String inputMessage, ChallengeLevel level) { String outputMessage = inputMessage; if (inputMessage.contains("[") && inputMessage.contains("]")) @@ -1573,7 +1572,6 @@ private String parseLevel(String inputMessage, ChallengeLevel level) return ChatColor.translateAlternateColorCodes('&', outputMessage); } - // --------------------------------------------------------------------- // Section: Simple getter methods // --------------------------------------------------------------------- @@ -1583,8 +1581,7 @@ private String parseLevel(String inputMessage, ChallengeLevel level) * This is simple cast method. Easier access to IslandRequirements. * @return Island Requirements */ - private IslandRequirements getIslandRequirements() - { + private IslandRequirements getIslandRequirements() { return this.challenge.getRequirements(); } @@ -1593,8 +1590,7 @@ private IslandRequirements getIslandRequirements() * This is simple cast method. Easier access to InventoryRequirements. * @return Inventory Requirements */ - private InventoryRequirements getInventoryRequirements() - { + private InventoryRequirements getInventoryRequirements() { return this.challenge.getRequirements(); } @@ -1603,8 +1599,7 @@ private InventoryRequirements getInventoryRequirements() * This is simple cast method. Easier access to OtherRequirements. * @return Other Requirements */ - private OtherRequirements getOtherRequirements() - { + private OtherRequirements getOtherRequirements() { return this.challenge.getRequirements(); } @@ -1619,8 +1614,7 @@ private OtherRequirements getOtherRequirements() * * @author tastybento */ - static class ChallengeResult - { + static class ChallengeResult { /** * This method sets that challenge meets all requirements at least once. * @return Current object. @@ -1637,8 +1631,7 @@ ChallengeResult setMeetsRequirements() * @param completed boolean that indicate that challenge has been already completed. * @return Current object. */ - ChallengeResult setCompleted(boolean completed) - { + ChallengeResult setCompleted(boolean completed) { this.completed = completed; return this; } @@ -1715,8 +1708,7 @@ boolean wasCompleted() * This method returns how many times challenge can be completed. * @return completion count. */ - int getFactor() - { + int getFactor() { return this.factor; } @@ -1725,8 +1717,7 @@ int getFactor() * This method returns if challenge requirements has been met at least once. * @return value of meets requirements variable. */ - boolean isMeetsRequirements() - { + boolean isMeetsRequirements() { return this.meetsRequirements; } diff --git a/src/main/java/world/bentobox/challenges/utils/Utils.java b/src/main/java/world/bentobox/challenges/utils/Utils.java index 120dc9b4..f2e8a810 100644 --- a/src/main/java/world/bentobox/challenges/utils/Utils.java +++ b/src/main/java/world/bentobox/challenges/utils/Utils.java @@ -11,6 +11,7 @@ import org.bukkit.ChatColor; import org.bukkit.Material; import org.bukkit.Statistic; +import org.bukkit.Tag; import org.bukkit.World; import org.bukkit.enchantments.Enchantment; import org.bukkit.entity.EntityType; @@ -284,6 +285,48 @@ public static String prettifyDescription(World.Environment object, User user) return ""; } + /** + * Prettify the Tag object for user. + * @param object a tag, like ALL_HANGING_SIGNS + * @param user user + * @return prettified tag + */ + public static String prettifyObject(@Nullable Tag object, User user) { + // Nothing to translate + if (object == null) { + return ""; + } + String translation = user.getTranslationOrNothing( + Constants.MATERIALS + object.getKey().getKey().toLowerCase(Locale.ENGLISH) + ".name"); + String any = user.getTranslationOrNothing(Constants.MATERIALS + "any"); + // Prettify and remove last s + String tag = any + Util.prettifyText(object.getKey().getKey()).replaceAll("s$", ""); + + return translation.isEmpty() ? tag : translation; + } + + /** + * Prettify object + * @param class that extends Enum + * @param object that extends Enum + * @param user use who will see the text + * @return string of pretty text for user + */ + public static > String prettifyObject(@Nullable T object, User user) { + if (object == null) { + return ""; + } + if (object instanceof Material m) { + return prettifyMaterial(m, user); + } + // Build a translation key using the enum name. + String translation = user + .getTranslationOrNothing(Constants.MATERIALS + object.name().toLowerCase(Locale.ENGLISH) + ".name"); + String any = user.getTranslationOrNothing(Constants.MATERIALS + "any"); + // Use the enum's name and prettify it (for example, convert ALL_HANGING_SIGNS to "All Hanging Sign") + String tag = any + Util.prettifyText(object.name()).replaceAll("s$", ""); + return translation.isEmpty() ? tag : translation; + } /** * Prettify Material object for user. @@ -291,7 +334,7 @@ public static String prettifyDescription(World.Environment object, User user) * @param user User who will see the object. * @return Prettified string for Material. */ - public static String prettifyObject(@Nullable Material object, User user) + public static String prettifyMaterial(@Nullable Material object, User user) { // Nothing to translate if (object == null) diff --git a/src/main/resources/addon.yml b/src/main/resources/addon.yml index 8cd6b891..8c70bcde 100755 --- a/src/main/resources/addon.yml +++ b/src/main/resources/addon.yml @@ -1,7 +1,7 @@ name: Challenges main: world.bentobox.challenges.ChallengesAddon version: ${version}${build.number} -api-version: 2.7.1 +api-version: 3.2.4 repository: 'BentoBoxWorld/Challenges' metrics: true @@ -9,7 +9,7 @@ authors: - tastybento - BONNe -softdepend: AcidIsland, BSkyBlock, CaveBlock, SkyGrid, Level +softdepend: AcidIsland, BSkyBlock, CaveBlock, SkyGrid, Level, Poseidon, Boxed permissions: addon.admin.challenges: diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index fe517be4..197e5c44 100755 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -8,6 +8,13 @@ meta: - BONNe challenges: + materials: + any: "Any " + all_hanging_signs: + name: "Any Hanging Sign" + all_signs: + name: "Any Sign" + commands: admin: main: @@ -17,33 +24,33 @@ challenges: description: "Reload challenges from the database" parameters: '' show: - description: 'Prints all challenges in the chat which exist in this world.' + description: "Displays all challenges in the chat that exist in this world." parameters: '' complete: - description: 'Complete a challenge for a player.' + description: "Completes a challenge for a player." parameters: ' ' reset: - description: 'Reset a challenge for a player. If "challenge_id" is set to "all", then it will reset all challenges.' + description: "Resets a challenge for a player. If \"challenge_id\" is set to \"all\", then all challenges will be reset." parameters: ' ' migrate: - description: 'Migrate current game world challenges data to 0.8.0 storage format.' + description: "Migrates the current game world’s challenge data to the 0.8.0 storage format." parameters: '' user: main: - description: 'Open Challenges GUI.' + description: "Opens the Challenges GUI." parameters: '' complete: - description: 'Complete challenge.' + description: "Completes a challenge." parameters: ' [count]' gui: titles: # The title for the Main GUI player-gui: "&0&l Challenges Menu" # The title for the Main GUI - gamemode-gui: "&0&l Select GameMode" + gamemode-gui: "&0&l Select Game Mode" # The title for the Multiple Completion GUI multiple-gui: "&0&l How many times?" - # GUI titles below is visible just for Admins. + # GUI titles below are visible just for Admins. admin-gui: "&0&l Challenges Admin Menu" edit-challenge: "&0&l Edit [challenge]" edit-level: "&0&l Edit [level]" @@ -53,7 +60,12 @@ challenges: choose-player: "&0&l Choose Player" library: "&0&l Library" manage-blocks: "&0&l Manage Blocks" + manage-block-groups: "&0&l Manage Block Groups" + manage-entity-groups: "&0&l Manage Entity Groups" manage-entities: "&0&l Manage Entities" + manage-statistics: "&0&l Manage Statistics" + manage-advancements: "&0&l Manage Advancements" + advancement-selector: "&0&l Advancement Selector" type-selector: "&0&l Challenge Type Selector" item-selector: "&0&l Item Selector" block-selector: "&0&l Block Selector" @@ -72,23 +84,23 @@ challenges: return: name: "&f&l Return" description: |- - &7 Return to previous menu - &7 or exit GUI + &7 Return to the previous menu + &7 or exit the GUI # Button that is used in multi-page GUIs which allows to return to previous page. previous: name: "&f&l Previous Page" description: |- - &7 Switch to &e [number] &7 page + &7 Switch to page &e [number] # Button that is used in multi-page GUIs which allows to go to next page. next: name: "&f&l Next Page" description: |- - &7 Switch to &e [number] &7 page + &7 Switch to page &e [number] # Button that allows to reduce number reduce: name: "&f&l Reduce" description: |- - &7 Reduce by &e [number] + &7 Decrease by &e [number] # Button that allows to increase number increase: name: "&f&l Increase" @@ -99,24 +111,23 @@ challenges: name: "&f&l Complete" description: |- &7 Complete challenge &e [number] - &7 time(-s) + &7 time(s) # Button that allows to quit the current gui. quit: name: "&f&l Quit" description: |- - &7 Exit from the GUI. + &7 Exit the GUI. # All following buttons are mainly for Admin GUI. complete_user_challenges: name: "&f&l Complete User Challenge(-s)" description: |- - &7 Allows to choose user and - &7 complete challenge(-s) for - &7 him + &7 Allows you to select a user and + &7 complete challenge(s) for them. reset_user_challenges: name: "&f&l Reset User Challenges" description: |- - &7 Allows to choose user and - &7 reset his challenges + &7 Allows you to select a user and + &7 reset their challenges. add_challenge: name: "&f&l Create Challenge" description: |- @@ -130,63 +141,63 @@ challenges: edit_challenge: name: "&f&l Edit Challenge" description: |- - &7 Allows to choose and edit + &7 Allows you to select and edit &7 a challenge. edit_level: name: "&f&l Edit Level" description: |- - &7 Allows to choose and edit + &7 Allows you to select and edit &7 a level. delete_challenge: name: "&f&l Delete Challenge" description: |- - &7 Allows to choose and delete + &7 Allows you to select and delete &7 a challenge. delete_level: name: "&f&l Delete Level" description: |- - &7 Allows to choose and delete + &7 Allows you to select and delete &7 a level. edit_settings: name: "&f&l Settings" description: |- - &7 Allows to view and edit - &7 an addon settings. + &7 Allows you to view and edit + &7 the add-on settings. complete_wipe: name: "&f&l Complete Wipe" description: |- - &7 Completely clears challenges - &7 addon database, including + &7 Completely clears the challenges + &7 add-on database, including &7 user data. challenge_wipe: name: "&f&l Challenge Wipe" description: |- - &7 Completely clears challenges - &7 and levels from database. + &7 Completely clears the challenges + &7 and levels from the database. user_wipe: name: "&f&l User Wipe" description: |- &7 Completely clears user - &7 data from database. + &7 data from the database. library: name: "&f&l Library" description: |- - &7 Opens a public + &7 Opens the public &7 challenges library. import_database: name: "&f&l Import Database" description: |- - &7 Allows to import exported + &7 Allows you to import an exported &7 challenges database. import_template: name: "&f&l Import Template" description: |- - &7 Allows to import template - &7 file with challenges. + &7 Allows you to import a template + &7 file containing challenges. export_challenges: name: "&f&l Export Challenges" description: |- - &7 Allows to export database + &7 Allows you to export the database &7 to a local file. properties: name: "&f&l Properties" @@ -195,358 +206,389 @@ challenges: requirements: name: "&f&l Requirements" description: |- - &7 View requirements properties. + &7 View the requirements properties. rewards: name: "&f&l Rewards" description: |- - &7 View rewards properties. + &7 View the rewards properties. + hide_reward_items: + name: "&f&l Hide Reward Items" + description: |- + &7 Toggle whether reward items + &7 are shown or not in GUI. + hide: "&2 Hide" + show: "&c Show" deployed: name: "&f&l Deployment" description: |- - &7 Toggle if challenge is - &7 deployed and users can - &7 complete it. + &7 Toggle whether the challenge is + &7 deployed and can be completed + &7 by users. enabled: "&2 Enabled" disabled: "&c Disabled" name: name: "&f&l Name" description: |- - &7 Allows to change + &7 Allows you to change &7 the display name. value: "&7 Currently: &r [name]" remove_on_complete: name: "&f&l Hide After Completion" description: |- - &7 Toggle if challenge should - &7 hidden from player after + &7 Toggle whether the challenge should + &7 be hidden from the player after &7 it is completed. enabled: "&2 Enabled" disabled: "&c Disabled" description: name: "&f&l Description" description: |- - &7 The specific description - &7 for the challenge. The color - &7 codes must be applied to it. + &7 Specifies the challenge's description. + &7 Color codes must be applied to it. value: "&7 Current description:" environment: name: "&f&l Dimension" description: |- - &7 Allows to limit in which - &7 dimension the challenge - &7 can be completed. + &7 Allows you to restrict the challenge + &7 to a specific dimension. enabled: "&2" disabled: "&c" order: name: "&f&l Order" description: |- - &7 Allows to change order of + &7 Allows you to change the order of &7 objects. - &7 Objects with equal numbers + &7 Objects with the same number &7 will be ordered by their - &7 unique id names. + &7 unique ID names. value: "&7 Current order: &e [number]" icon: name: "&f&l Icon" description: |- - &7 Allows to change icon + &7 Allows you to change the icon &7 for this challenge. locked_icon: name: "&f&l Locked Icon" description: |- - &7 Allows to change locked + &7 Allows you to change the locked &7 level icon. required_permissions: name: "&f&l Required Permissions" description: |- - &7 Allows to change required - &7 permissions for this - &7 challenge to be completable. + &7 Allows you to change the required + &7 permissions for this challenge to + &7 be completed. title: "&7 Permissions: " permission: " &8 - [permission]" - none: "&7 Permissions are not set." + none: "&7 No permissions are set." remove_entities: name: "&f&l Remove Entities" description: |- - &7 Allows to toggle if - &7 required entities will - &7 be removed from world - &7 after completing the - &7 challenge. + &7 Toggle whether required entities will + &7 be removed from the world after + &7 completing the challenge. enabled: "&2 Enabled" disabled: "&c Disabled" required_entities: name: "&f&l Required Entities" description: |- - &7 Allows to change required - &7 entities for this - &7 challenge to be completable. + &7 Allows you to change the required + &7 entities for this challenge to be + &7 completed. title: "&7 Entities: " list: " &8 - [number] x [entity]" - none: "&7 Entities are not added." + none: "&7 No entities have been added." remove_blocks: name: "&f&l Remove Blocks" description: |- - &7 Allows to toggle if - &7 required blocks will - &7 be removed from world - &7 after completing the - &7 challenge. + &7 Toggle whether required blocks will + &7 be removed from the world after + &7 completing the challenge. enabled: "&2 Enabled" disabled: "&c Disabled" required_blocks: name: "&f&l Required Blocks" description: |- - &7 Allows to change required - &7 blocks for this - &7 challenge to be completable. + &7 Allows you to change the required + &7 blocks for this challenge to be + &7 completed. title: "&7 Blocks: " list: " &8 - [number] x [block]" - none: "&7 Blocks are not added." + none: "&7 No blocks have been added." + required_statistics: + name: "&f&l Required Statistics" + description: |- + &7 Allows you to change the required + &7 statistics for this challenge to be + &7 completed. + title: "&7 Statistics: " + list: " &8 - [name]" + none: "&7 No statistics have been added." + required_advancements: + name: "&f&l Required Advancements" + description: |- + &7 Allows you to change the required + &7 advancements for this challenge to + &7 be completed. + title: "&7 Advancements: " + list: " &8 - [name]" + none: "&7 No advancements have been added." search_radius: name: "&f&l Search Radius" description: |- - &7 Allows to change the radius - &7 around player from which - &7 blocks and/or entities are - &7 detected. + &7 Allows you to change the radius + &7 around the player from which + &7 blocks and/or entities are detected. value: "&7 Current distance: &e [number]" remove_items: name: "&f&l Remove Items" description: |- - &7 Allows to toggle if - &7 required items will - &7 be removed from inventory - &7 after completing the - &7 challenge. + &7 Toggle whether required items will + &7 be removed from the inventory after + &7 completing the challenge. enabled: "&2 Enabled" disabled: "&c Disabled" required_items: name: "&f&l Required Items" description: |- - &7 Allows to change required - &7 items for this - &7 challenge to be completable. + &7 Allows you to change the required + &7 items for this challenge to be completed. title: "&7 Items: " list: " &8 - [number] x [item]" - none: "&7 Items are not added." + none: "&7 No items have been added." add_ignored_meta: name: "&f&l Add Ignore Metadata" description: |- - &7 Allows to add which - &7 items should ignore - &7 any meta-data that - &7 is assigned to them. + &7 Allows you to specify which items + &7 should ignore any metadata + &7 assigned to them. title: "&7 Items: " list: " &8 - [number] x [item]" - none: "&7 Items are not added." + none: "&7 No items have been added." remove_ignored_meta: name: "&f&l Remove Ignore Metadata" description: |- - &7 Allows to remove which - &7 items should ignore - &7 any meta-data that - &7 is assigned to them. + &7 Removes items that ignore metadata + &7 assigned to them. remove_experience: name: "&f&l Remove Experience" description: |- - &7 Allows to toggle if - &7 required experience will - &7 be removed from player - &7 after completing the - &7 challenge. + &7 Toggle whether the required + &7 experience will be removed + &7 from the player after completing + &7 the challenge. enabled: "&2 Enabled" disabled: "&c Disabled" required_experience: name: "&f&l Required Experience" description: |- - &7 Allows to change the - &7 required experience for - &7 the player. + &7 Allows you to change the required + &7 experience for the player. value: "&7 Current experience: &e [number]" required_level: name: "&f&l Required Island Level" description: |- - &7 Allows to change the + &7 Allows you to change the &7 required island level &7 for the challenge. value: "&7 Current level: &e [number]" + required_materialtags: + name: "&f&l Required Block Groups" + description: |- + &7 Allows you to change the required + &7 block groups for this challenge to + &7 be completed. + title: "&7 Block group: " + list: " &8 - [number] x [tag]" + none: "&7 No block groups have been added." + required_entitytags: + name: "&f&l Required Entity Groups" + description: |- + &7 Allows you to change the required + &7 entity groups for this challenge to + &7 be completed. + title: "&7 Entity group: " + list: " &8 - [number] x [tag]" + none: "&7 No entity groups have been added." remove_money: name: "&f&l Remove Money" description: |- - &7 Allows to toggle if - &7 required money will - &7 be removed from player - &7 account after completing - &7 the challenge. + &7 Toggle whether the required + &7 money will be removed from + &7 the player's account after + &7 completing the challenge. enabled: "&2 Enabled" disabled: "&c Disabled" required_money: name: "&f&l Required Money" description: |- - &7 Allows to change the - &7 required money on player + &7 Allows you to change the required + &7 money on the player's &7 account for the challenge. value: "&7 Current value: &e [number]" + required_papi: + name: "&f&l Required PAPI" + description: |- + &7 Checks a formula that can + &7 include PAPI placeholders + &7 and math and logical elements. + value: "&7 Formula: &e [formula]" statistic: name: "&f&l Statistic" description: |- - &7 Allows to change the - &7 statistic type that is - &7 checked in this challenge. + &7 Allows you to change the statistic + &7 type that is checked for + &7 this challenge. value: "&7 Current value: &e [statistic]" statistic_amount: name: "&f&l Target Value" description: |- - &7 Allows to change the - &7 statistic target value + &7 Allows you to change the target + &7 statistic value &7 that must be met. value: "&7 Current value: &e [number]" + add_statistic: + name: "&f&l Add Statistic" + description: |- + &7 Allows you to add a new + &7 statistic to the list. remove_statistic: - name: "&f&l Reduce Statistic" + name: "&f&l Remove Statistic" description: |- - &7 Allows to toggle if - &7 statistic value will - &7 be reduced after completing - &7 the challenge. + &7 Allows you to remove + &7 selected statistics + &7 from the list. + enabled: "&2 Enabled" + disabled: "&c Disabled" + advancement: + name: "&f&l [name]" + description: "[description]" + add_advancement: + name: "&f&l Add Advancement" + description: |- + &7 Allows you to add a new + &7 advancement to the list. + remove_advancement: + name: "&f&l Remove Advancement" + description: |- + &7 Allows you to remove + &7 selected advancements + &7 from the list. enabled: "&2 Enabled" disabled: "&c Disabled" statistic_blocks: name: "&f&l Target Block" description: |- - &7 Allows to change the - &7 statistic target block. + &7 Allows you to change the target + &7 block for the statistic. value: "&7 Current block: &e [block]" statistic_items: name: "&f&l Target Item" description: |- - &7 Allows to change the - &7 statistic target item. + &7 Allows you to change the target + &7 item for the statistic. value: "&7 Current item: &e [item]" statistic_entities: name: "&f&l Target Entity" description: |- - &7 Allows to change the - &7 statistic target entity. + &7 Allows you to change the target + &7 entity for the statistic. value: "&7 Current entity: &e [entity]" reward_text: name: "&f&l Reward Text" description: |- - &7 The specific reward text. - &7 The color codes must be - &7 applied to it. + &7 Specifies the reward text. + &7 Color codes must be applied. value: "&7 Current text:" repeat_reward_text: name: "&f&l Repeat Reward Text" description: |- - &7 The specific repeat reward text - &7 for the challenge. The color - &7 codes must be applied to it. + &7 Specifies the repeat reward + &7 text for the challenge. + &7 Color codes must be applied. value: "&7 Current text:" reward_items: name: "&f&l Reward Items" description: |- - &7 Allows to change reward - &7 items. + &7 Allows you to change the reward items. title: "&7 Items: " list: " &8 - [number] x [item]" - none: "&7 Items are not added." + none: "&7 No items have been added." repeat_reward_items: name: "&f&l Repeat Reward Items" description: |- - &7 Allows to change repeat - &7 reward items for this - &7 challenge. + &7 Allows you to change the repeat + &7 reward items for this challenge. title: "&7 Items: " list: " &8 - [number] x [item]" - none: "&7 Items are not added." + none: "&7 No items have been added." reward_experience: name: "&f&l Reward Experience" description: |- - &7 Allows to change the - &7 reward experience for - &7 the player. + &7 Allows you to change the reward + & experience for the player. value: "&7 Reward experience: &e [number]" repeat_reward_experience: name: "&f&l Repeat Reward Experience" description: |- - &7 Allows to change the - &7 repeat reward experience - &7 for the player. + &7 Allows you to change the repeat + &7 reward experience for the player. value: "&7 Reward experience: &e [number]" reward_money: name: "&f&l Reward Money" description: |- - &7 Allows to change the - &7 reward money. + &7 Allows you to change the reward money. value: "&7 Current value: &e [number]" repeat_reward_money: name: "&f&l Repeat Reward Money" description: |- - &7 Allows to change the - &7 repeat reward money - &7 for the challenge. + &7 Allows you to change the repeat + &7 reward money for the challenge. value: "&7 Current value: &e [number]" reward_commands: name: "&f&l Reward Commands" description: |- - &7 The specific reward commands. + &7 Specifies the reward commands. &8 Tip: - &8 The command does not requires - &8 writing first `/` as it will - &8 be applied automatically. - &8 By default commands will be - &8 executed by server. However - &8 adding `[SELF]` at the start - &8 will allow command to be - &8 executed by player. It also - &8 supports one placeholder - &8 `[player]` that will be - &8 replaced with a player name - &8 who completed challenge. + &8 The command does not require a leading `/` as it is + &8 applied automatically. + &8 By default, commands are executed by the server. + &8 However, adding `[SELF]` at the beginning will cause + &8 the command to be executed by the player. + &8 It also supports the placeholder `[player]` which will be + &8 replaced with the name of the player who completed the challenge. value: "&7 Current commands:" repeat_reward_commands: name: "&f&l Repeat Reward Commands" description: |- - &7 The specific repeat reward - &7 commands for the challenge. + &7 Specifies the repeat reward commands for the challenge. &8 Tip: - &8 The command does not requires - &8 writing first `/` as it will - &8 be applied automatically. - &8 By default commands will be - &8 executed by server. However - &8 adding `[SELF]` at the start - &8 will allow command to be - &8 executed by player. It also - &8 supports one placeholder - &8 `[player]` that will be - &8 replaced with a player name - &8 who completed challenge. + &8 The command does not require a leading `/` as it is + &8 applied automatically. + &8 By default, commands are executed by the server. + &8 However, adding `[SELF]` at the beginning will cause + &8 the command to be executed by the player. + &8 It also supports the placeholder `[player]` which will be + &8 replaced with the name of the player who completed the challenge. value: "&7 Current commands:" repeatable: name: "&f&l Repeatable" description: |- - &7 Allows to toggle if - &7 the challenge is - &7 repeatable. + &7 Toggle whether the challenge is repeatable. enabled: "&2 Enabled" disabled: "&c Disabled" repeat_count: name: "&f&l Repeat Count" description: |- - &7 Allows to change the - &7 number of repeats + &7 Allows you to change the number of repeats &7 for the challenge. value: "&7 Current value: &e [number]" cool_down: name: "&f&l Cool Down" description: |- - &7 Allows to change the - &7 cool down seconds that - &7 must be waited between - &7 repeatable challenge - &7 completions. + &7 Allows you to change the cooldown + &7 period (in seconds) that must elapse + &7 between repeatable challenge completions. value: "&7 Current value: &e [time]" challenges: name: "&f&l Challenges" @@ -556,128 +598,117 @@ challenges: waiver_amount: name: "&f&l Waiver Amount" description: |- - &7 Allows to set a number - &7 of challenges that can - &7 be left uncompleted for - &7 unlocking next level. + &7 Allows you to set the number of challenges + &7 that can be left uncompleted to unlock + &7 the next level. value: "&7 Current value: &e [number]" add_challenges: name: "&f&l Add Challenge(-s)" description: |- - &7 Allows to select and - &7 adds challenges to the - &7 level. + &7 Allows you to select and add + &7 challenges to the level. remove_challenges: name: "&f&l Remove Challenge(-s)" description: |- - &7 Allows to select and - &7 remove challenges to the - &7 level. + &7 Allows you to select and remove + &7 challenges from the level. reset_on_new: name: "&f&l Reset On New" description: |- - &7 Allows to toggle if - &7 challenges should be - &7 reset when user leaves - &7 island or creates a new - &7 island. + &7 Toggle whether challenges should + & be reset when a user leaves + &7 their island or creates a new one. enabled: "&2 Enabled" disabled: "&c Disabled" broadcast: name: "&f&l Broadcast" description: |- - &7 Broadcasts challenge and - &7 level first time completion - &7 to everyone. + &7 Broadcasts the first-time completion + &7 of a challenge or level to everyone. enabled: "&2 Enabled" disabled: "&c Disabled" remove_completed: name: "&f&l Hide Completed" description: |- - &7 Hides compleated challenges + &7 Hides completed challenges &7 from the menu. enabled: "&2 Enabled" disabled: "&c Disabled" glow_completed: name: "&f&l Glow Completed" description: |- - &7 Adds enchantment glow - &7 to the completed challenges. + &7 Applies an enchantment glow + &7 to completed challenges. enabled: "&2 Enabled" disabled: "&c Disabled" store_history: name: "&f&l Store History" description: |- - &7 Stores internal history - &7 when each challenge is - &7 completed. - &7 Currently viewable only - &7 in the database. + &7 Stores internal history each time + &7 a challenge is completed. + &7 Currently viewable only in the + &7 database. enabled: "&2 Enabled" disabled: "&c Disabled" data_per_island: name: "&f&l Store Per Island" description: |- - &7 Stores the completed - &7 challenges per island. - &7 Progress will be shared - &7 with all players in team. + &7 Stores completed challenges + &7 on a per-island basis. + &7 Progress will be shared + &7 with all team members. enabled: "&2 Enabled" disabled: "&c Disabled" show_title: name: "&f&l Show Title" description: |- - &7 Shows title when a - &7 challenge or level - &7 is completed. + &7 Displays a title when a challenge + &7 or level is completed. enabled: "&2 Enabled" disabled: "&c Disabled" gamemode_gui: name: "&f&l GameMode Selection GUI" description: |- - &7 Enables single GUI that - &7 is available via /challenges - &7 command. - &c Requires server restart. + &7 Enables a single GUI accessible + &7 via the /challenges command. + &7 (Requires server restart.) enabled: "&2 Enabled" disabled: "&c Disabled" locked_level_icon: name: "&f&l Default Locked Level Icon" description: |- - &7 Default icon for all locked - &7 levels. Each level can change - &7 this icon. + &7 Default icon for all locked levels. + &7 Each level can override this icon. purge_history: name: "&f&l History Lifetime" description: |- - &7 Number of days how long - &7 history data is stored + &7 Specifies the number of days + &7 that history data is stored &7 in user data. - &7 0 means that data will + &7 A value of 0 means the data will &7 not be removed. value: "&7 Current value: &e [number]" title_showtime: name: "&f&l Title Showtime" description: |- - &7 Number of ticks that title - &7 will be showed to the player. + &7 Number of ticks the title will + & be displayed to the player. value: "&7 Current value: &e [number]" active_world_list: name: "&f&l Show Only Active World" description: |- - &7 If GameMode Selection GUI - &7 is enabled, this can switch - &7 if GUI shows GameMode selection - &7 or challenges for current world. - &c Requires server restart. + &7 If the GameMode Selection GUI is + &7 enabled, this setting determines + &7 whether the GUI displays the + &7 GameMode selection or challenges + &7 for the current world. + &7 (Requires server restart.) enabled: "&2 Enabled" disabled: "&c Disabled" visibility_mode: name: "&f&l Visibility Mode" description: |- - &7 Visibility Mode for - &7 challenges that are - &7 hiddend. + &7 Visibility mode for challenges that are hidden. enabled: "&2" disabled: "&c" visible: "Show visible challenges" @@ -686,16 +717,14 @@ challenges: include_undeployed: name: "&f&l Include Undeployed Challenges" description: |- - &7 Indicates if undeployed - &7 challenges should be - &7 counted towards level - &7 completion. + &7 Indicates whether undeployed challenges + &7 should be counted towards level completion. enabled: "&2 Enabled" disabled: "&c Disabled" download: name: "&f&l Download Libraries" description: |- - &7 Manually update available + &7 Manually updates the available &7 challenges libraries. enabled: "&2 With cache clear" disabled: "&c Without cache clear" @@ -706,13 +735,13 @@ challenges: members: "&7 Island Members:" member: "&8 - [name]" no-island: |- - &c Player does not have + &c This player does not have &c an island. player_list: name: "&f&l Choose User List" description: |- &7 Choose which user list - &7 should be showed. + &7 should be displayed. enabled: "&2" disabled: "&c" online: "Online Players" @@ -721,66 +750,99 @@ challenges: add_block: name: "&f&l Add Block" description: |- - &7 Allows to add a new + &7 Allows you to add a new &7 block to the list. + add_block_group: + name: "&f&l Add Block Group" + description: |- + &7 Allows you to add a new + &7 block group to the list. remove_block: name: "&f&l Remove Block" description: |- - &7 Allows to remove + &7 Allows you to remove &7 selected blocks - &7 from lists. + &7 from the list. title: "&7 Selected Materials:" material: "&8 - [material]" + remove_block_group: + name: "&f&l Remove Block Group" + description: |- + &7 Allows you to remove + &7 selected block groups + &7 from the list. + title: "&7 Selected Block Groups:" + material: "&8 - [material]" material: name: "&f&l [material]" description: |- &7 Material ID: [id] selected: "&2 Selected" + block-group: + name: "&f&l [id]" + description: "" + selected: "&2 Selected" add_entity: name: "&f&l Add Entity" description: |- - &7 Allows to add a new + &7 Allows you to add a new &7 entity to the list. + add_entity_group: + name: "&f&l Add Entity Group" + description: |- + &7 Allows you to add a new + &7 entity group to the list. switch_entity: name: "&f&l Switch Eggs" description: |- - &7 Allows to switch from - &7 eggs to the mob heads. + &7 Allows you to switch from + &7 eggs to mob heads. remove_entity: name: "&f&l Remove Entity" description: |- - &7 Allows to remove + &7 Allows you to remove &7 selected entities - &7 from lists. + &7 from the list. title: "&7 Selected Entities:" entity: "&8 - [entity]" + remove_entity_group: + name: "&f&l Remove Entity Group" + description: |- + &7 Allows you to remove + &7 selected entity groups + &7 from the list. + title: "&7 Selected Entity Groups:" + entity: "&8 - [tag]" entity: name: "&f&l [entity]" description: |- &7 Entity ID: [id] selected: "&2 Selected" + entity-group: + name: "&f&l [id]" + description: "" + selected: "&2 Selected" inventory_type: name: "&f&l Inventory Type" description: |- - &7 Challenge that checks - &7 items in player inventory + &7 Challenge that checks items + &7 in the player's inventory. island_type: name: "&f&l Island Type" description: |- - &7 Challenge that checks - &7 blocks or entities around - &7 player. + &7 Challenge that checks blocks + &7 or entities around the player. other_type: name: "&f&l Other Type" description: |- - &7 Challenge that uses - &7 plugins or addons things, - &7 like level and money. + &7 Challenge that utilizes plugins + &7 or add-on features, + &7 such as level and money. statistic_type: name: "&f&l Statistic Type" description: |- - &7 Challenge that checks - &7 player statistic data. + &7 Challenge that checks the + &7 player's statistic data. save: name: "&f&l Save" description: |- @@ -794,28 +856,41 @@ challenges: accept_selected: name: "&f&l Accept Selected" description: |- - &7 Returns selected elements - &7 and opens previous GUI. + &7 Returns the selected elements + &7 and reopens the previous GUI. title: "&7 Selected: " element: "&8 - [element]" + advancement_element: + name: "&f&l [name]" + description: "[description]" + selected: "&2 Selected" statistic_element: name: "&f&l [statistic]" + amount: "&7 Target Value: &e [number]" + remove: + name: "&7 Reduce Statistic: [value]" + value: + enabled: "&c Enabled" + disabled: "&2 Disabled" + block: "&7 Target Block: &e [block]" + item: "&7 Target Item: &e [item]" + entity: "&7 Target Entity: &e [entity]" description: "[description]" + selected: "&2 Selected" environment_element: name: "&f&l [environment]" description: "[description]" search: name: "&f&l Search" description: |- - &7 Allows to search an - &7 element with input - &7 text value. + &7 Allows you to search for an + &7 element using a text input value. search: "&b Value: [value]" tips: click-to-select: "&e Click &7 to select." click-to-choose: "&e Click &7 to choose." click-to-complete: "&e Click &7 to complete." - right-click-multiple-open: "&e Right Click &7 to chose completion count." + right-click-multiple-open: "&e Right Click &7 to choose a completion count." shift-left-click-to-complete-all: "&e Shift Click &7 to complete all." left-click-to-accept: "&e Left Click &7 to complete." right-click-to-write: "&e Right Click &7 to write." @@ -830,7 +905,7 @@ challenges: click-to-export: "&e Click &7 to export." click-to-create: "&e Click &7 to create." left-click-to-open: "&e Left Click &7 to open." - right-click-to-reset-all: "&e Right Click &7 to wipe all." + right-click-to-reset-all: "&e Right Click &7 to reset all." click-to-toggle: "&e Click &7 to toggle." click-to-change: "&e Click &7 to change." shift-click-to-reset: "&e Shift Click &7 to reset." @@ -850,7 +925,7 @@ challenges: click-to-save: "&e Click &7 to save." click-to-deselect: "&e Click &7 to deselect." click-on-item: |- - &e Click &7 on item in + &e Click &7 on an item in &7 your inventory. left-click-to-edit: "&e Left Click &7 to edit." right-click-to-clear: "&e Right Click &7 to clear." @@ -872,7 +947,7 @@ challenges: # Status message for completed unrepeatable challenge completed: "&2&l Completed" # Status message that contains number of completions for unlimited repeatable challenge - completed-times: "&2 Completed &7&l [number] &r&2 time(-s)" + completed-times: "&2 Completed &7&l [number] &r&2 time(s)" # Status message that contains number of completions from max available for repeatable challenge completed-times-of: "&2 Completed &7&l [number] &r&2 out of &7&l [max] &r&2 times" # Status message that indicates that max completion count reached for repeatable challenge @@ -882,9 +957,9 @@ challenges: lore: |- [timeout] [wait-time] - # Text message that shows challenges timeout. + # Text message that shows challenge timeout. timeout: "&7&l Cool down: &r&7 [time]" - # Text message that shows challenges wait time if it is larger than 0. + # Text message that shows challenge wait time if it is larger than 0. wait-time: "&c&l Available after: &r&c [time]" # Text message that replaces days if number > 1 in-days: "[number] d " @@ -907,7 +982,7 @@ challenges: # Message that will be added after environment-title-multiple. environment-list: " &7 - &e [environment]" # Message that will replace [permissions] placeholder if there is just a single permission. - permission-single: "&c Requires [permission] permission" + permission-single: "&c Requires [permission] permission." # Message that will replace [permissions] placeholder if there are multiple permissions. permissions-title: "&c Requires permissions: " # Message that will be added after permissions-title-multiple. @@ -920,19 +995,19 @@ challenges: [search-radius] [warning-block] [warning-entity] - # Title that will be used if there are defined blocks in island challenge + # Title that will be used if there are defined blocks in an island challenge blocks-title: "&7&l Required Blocks:" # Listing of blocks that are required on the island. block-value: " &7 - &e [material]" blocks-value: " &7 - &e [number] x [material]" - # Title that will be used if there are defined entities in island challenge + # Title that will be used if there are defined entities in an island challenge entities-title: "&7&l Required Entities:" # Listing of entities that are required on the island. entity-value: " &7 - &e [entity]" entities-value: " &7 - &e [number] x [entity]" # Search radius for the blocks/entities - search-radius: "&7 Not further than &e [number] &7 meters" - # Waning about block/entity removing + search-radius: "&7 No further than &e [number] &7 meters" + # Warning about block/entity removal warning-block: "&e Blocks will be &c removed" warning-entity: "&e Entities will be &c removed" # Message that will generate for inventory type requirements and replace [type-requirements] @@ -940,13 +1015,13 @@ challenges: lore: |- [items] [warning] - # Title that will be used if there are list of items for challenge + # Title that will be used if there is a list of items for the challenge item-title: "&7&l Required Items:" - # Listing of an item that are required multiple times. + # Listing of an item that is required once or multiple times. item-value: " &7 - &e [item]" items-value: " &7 - &e [number] x [item]" # Warning that items will be removed - warning: "&e Item(-s) will be &c removed" + warning: "&e Item(s) will be &c removed" # Message that will generate for other type requirements and replace [type-requirements] other: lore: |- @@ -965,22 +1040,21 @@ challenges: money-warning: "&e Money will be &c removed" # Text for required island level level: "&7&l Required island level: &r&e [number]" - # Message that will generate for statistic type requirements and replace [type-requirements] + # Message that will generate for statistic type requirements and replace [type-requirement] statistic: lore: |- [statistic] [warning] # Type of statistic for multiple target counter. Target may be entity or material/block multiple-target: "&7&l [statistic]: &r&e [number] x [target]" - # Type of statistic for single target. Target may be entity or material/block + # Type of statistic for a single target. Target may be entity or material/block single-target: "&7&l [statistic]: &r&e [target]" - # Type of statistic without entity/block target + # Type of statistic without an entity/block target statistic: "&7&l [statistic] &r&e [number]" - # Warning that statistic will be removed + # Warning that statistic will be reduced warning: "&e Statistic data will be &c reduced" # Contains a text generated inside [rewards] lore rewards: - # [text] comes from challenge.rewardText and challenge.repeatRewardText lore: |- &7&l Rewards: [text] @@ -988,9 +1062,9 @@ challenges: [experience] [money] [commands] - # Title that will be used if there are list of items for rewards + # Title that will be used if there is a list of items for rewards item-title: "&7 Items:" - # Listing of an item that are rewards multiple times. + # Listing of an item that is rewarded multiple times. item-value: " &7 - &e [item]" items-value: " &7 - &e [number] x [item]" # Text for reward experience @@ -1008,27 +1082,25 @@ challenges: [status] [waiver] [rewards] - # Status is either challengeLevel.unlockMessage or current status of the level + # Status is either challengeLevel.unlockMessage or the current status of the level status: - # Status message for completed unrepeatable challenge + # Status message for a completed unrepeatable challenge completed: "&2&l Completed" - # Status message that contains number of completed challenges from all challenges + # Status message that contains the number of completed challenges out of all challenges completed-challenges-of: |- - &2 Completed &7&l [number] &r&2 out of - &7&l [max] &r&2 challenges. - # Status message for locked level + &2 Completed &7&l [number] &r&2 out of &7&l [max] &r&2 challenges. + # Status message for a locked level locked: "&c&l Locked" - # Status message for locked level that will show missing challenge count. + # Status message for a locked level that will show the missing challenge count. missing-challenges: |- - &7 [number] more challenges must be + &7 [number] more challenges need to be &7 completed to unlock this level. - # Contains a text for waiver amount to unlock next level + # Contains text for the waiver amount to unlock the next level waiver: |- - &7&l [number] challenge(-s) &r&7 can be - &7 skipped to unlock next level. - # Contains a text generated inside [rewards] lore + &7&l [number] challenge(s) can be + &7 skipped to unlock the next level. + # Contains text generated inside [rewards] lore rewards: - # [text] comes from challengeLevel.rewardText lore: |- &7&l Rewards: [text] @@ -1036,185 +1108,127 @@ challenges: [experience] [money] [commands] - # Title that will be used if there are list of items for rewards item-title: "&7 Items:" - # Listing of an item that are rewards single time. item-value: " &7 - &e [item]" items-value: " &7 - &e [number] x [item]" - # Text for reward experience experience: "&7 Experience: &r&e [number]" - # Text for reward money money: "&7 Money: &r&e [number]" - # Title for commands listing: commands-title: "&7 Commands:" - # Command listing element command: " &7 - &e [command]" # This part generates description for the Library Entry library: - author: '&7 by &e [author]' - version: '&7 Made with Challenges &e [version]' - lang: '&7 Language: &e [lang]' - gamemode: '&7 Primary for &e [gamemode]' + author: "&7 by &e [author]" + version: "&7 Made with Challenges &e [version]" + lang: "&7 Language: &e [lang]" + gamemode: "&7 Primary for &e [gamemode]" conversations: - # Prefix for messages that are send from server. prefix: "&l&6 [BentoBox]: &r" - # List of strings that are valid for confirming input. (separated with ,) confirm-string: "true, on, yes, confirm, y, valid, correct" - # List of strings that are valid for denying input. (separated with ,) deny-string: "false, off, no, deny, n, invalid, incorrect" - # String that allows to cancel conversation. (can be only one) cancel-string: "cancel" - # List of strings that allows to exit conversation. (separated with ,) exit-string: "cancel, exit, quit" - # Message that is send to user when conversation is cancelled. cancelled: "&c Conversation cancelled!" - # Message that appears when admin clicks on number editing button. - input-number: "&e Please enter a number in chat." - # Message that appears when admin clicks on seconds editing button. - input-seconds: "&e Please enter a seconds in chat." - # Error message that is showed if user input a value that is not a number. + input-number: "&e Please enter a number in the chat." + input-seconds: "&e Please enter a number of seconds in the chat." numeric-only: "&c The given [value] is not a number!" - # Error message that is showed if user input a number that is smaller or larger that allowed. - not-valid-value: "&c The given number [value] is not valid. It must be larger than [min] and smaller than [max]!" - # Message that confirms user data removing. - user-data-removed: "&a All user data for [gamemode] is cleared from the database." - # Message that asks confirmation for user data removing. - confirm-user-data-deletion: "&e Please confirm that you want to clear user database for [gamemode]." - # Message that confirms user data removing. - challenge-data-removed: "&a All challenges data for [gamemode] is cleared from the database." - # Message that asks confirmation for user data removing. - confirm-challenge-data-deletion: "&e Please confirm that you want to clear challenges database for [gamemode]." - # Message that confirms user data removing. - all-data-removed: "&a All addon data for [gamemode] is cleared from the database." - # Message that asks confirmation for user data removing. - confirm-all-data-deletion: "&e Please confirm that you want to clear addon data for [gamemode]." - # Message that asks user to write a name - write-name: "&e Please write a name in the chat." - # Message that confirms new object creation. - new-object-created: "&a New object for [gamemode] is created." - # Error message that sends that object cannot be created with a given name - object-already-exists: "&c Object &7 [id] &c already exists. Choose different name." - # Error message that sends information that challenge cannot be deployed. - invalid-challenge: "&c Challenge [challenge] contains invalid data. It cannot be deployed!" - # Message that confirms name changing - name-changed: "&a Success, the name was updated." - # Message that appears after clicking - write-description: "&e Please enter a new description in chat and 'quit' on a line by itself to finish." - # Message that appears after successful description change. - description-changed: "&a Success, the description was updated." - # Message that appears when admin clicks on permission editing button. - write-permissions: "&e Please enter the required permissions, one per line in chat, and 'quit' on a line by itself to finish." - # Message that appears after successful permission updating. - permissions-changed: "&a Success, challenge permissions were updated." - # Message that appears after clicking - write-reward-text: "&e Please enter a new reward text in chat and 'quit' on a line by itself to finish." - # Message that appears after successful reward-text change. - reward-text-changed: "&a Success, the reward text was updated." - # Message that appears after clicking - write-repeat-reward-text: "&e Please enter a new repeat reward text in chat and 'quit' on a line by itself to finish." - # Message that appears after successful repeat-reward-text change. - repeat-reward-text-changed: "&a Success, the repeat reward text was updated." - # Message that appears after clicking - write-reward-commands: "&e Please enter a new reward command per line in chat and 'quit' on a line by itself to finish." - # Message that appears after successful commands-text change. - reward-commands-changed: "&a Success, the reward commands was updated." - # Message that appears after clicking - write-repeat-reward-commands: "&e Please enter a new repeat reward command per line in chat and 'quit' on a line by itself to finish." - # Message that appears after successful repeat-commands-text change. - repeat-reward-commands-changed: "&a Success, the repeat reward commands was updated." - # Message that confirms user data removing. - challenge-removed: "&a Challenges [challenge] for [gamemode] is removed from the database." - # Message that asks confirmation for user data removing. - confirm-challenge-deletion: "&e Please confirm that you want to remove [challenge] for [gamemode] from database." - # Message that confirms user data removing. - level-removed: "&a Level [level] for [gamemode] is removed from the database." - # Message that asks confirmation for user data removing. - confirm-level-deletion: "&e Please confirm that you want to remove [level] for [gamemode] from database." - # Message that appears when user clicks on library installation. - start-downloading: "&a Starting to download and import Challenges Library." - # Message that appears when writing multiline text. + not-valid-value: "&c The given number [value] is not valid. It must be greater than [min] and less than [max]!" + user-data-removed: "&a All user data for [gamemode] has been cleared from the database." + confirm-user-data-deletion: "&e Please confirm that you want to clear the user database for [gamemode]." + challenge-data-removed: "&a All challenge data for [gamemode] has been cleared from the database." + confirm-challenge-data-deletion: "&e Please confirm that you want to clear the challenges database for [gamemode]." + all-data-removed: "&a All add-on data for [gamemode] has been cleared from the database." + confirm-all-data-deletion: "&e Please confirm that you want to clear the add-on data for [gamemode]." + write-name: "&e Please enter a name in the chat." + new-object-created: "&a A new object for [gamemode] has been created." + object-already-exists: "&c Object &7 [id] &c already exists. Please choose a different name." + invalid-challenge: "&c Challenge [challenge] contains invalid data and cannot be deployed!" + name-changed: "&a Success: the name has been updated." + write-description: "&e Please enter a new description in the chat. Type 'quit' on a new line to finish." + description-changed: "&a Success: the description has been updated." + write-permissions: "&e Please enter the required permissions, one per line, in the chat. Type 'quit' on a new line to finish." + permissions-changed: "&a Success: the challenge permissions have been updated." + write-reward-text: "&e Please enter a new reward text in the chat. Type 'quit' on a new line to finish." + reward-text-changed: "&a Success: the reward text has been updated." + write-repeat-reward-text: "&e Please enter a new repeat reward text in the chat. Type 'quit' on a new line to finish." + repeat-reward-text-changed: "&a Success: the repeat reward text has been updated." + write-reward-commands: "&e Please enter a new reward command on each line in the chat. Type 'quit' on a new line to finish." + reward-commands-changed: "&a Success: the reward commands have been updated." + write-repeat-reward-commands: "&e Please enter a new repeat reward command on each line in the chat. Type 'quit' on a new line to finish." + repeat-reward-commands-changed: "&a Success: the repeat reward commands have been updated." + challenge-removed: "&a Challenge [challenge] for [gamemode] has been removed from the database." + confirm-challenge-deletion: "&e Please confirm that you want to remove [challenge] for [gamemode] from the database." + level-removed: "&a Level [level] for [gamemode] has been removed from the database." + confirm-level-deletion: "&e Please confirm that you want to remove [level] for [gamemode] from the database." + start-downloading: "&a Starting to download and import the Challenges Library." written-text: "&a Input Text:" - # Message that appears after importing library data into database. - confirm-data-replacement: "&e Please confirm that you want to replace your current challenges with new one." - # Message that appears after successful data importing - new-challenges-imported: "&a Success, new Challenges for [gamemode] were imported." - # Message that appears after admin clicks on database exporting button. - exported-file-name: "&e Please enter a file name for the exported database file. (write 'cancel' to exit)" - # Message that appears after successful database exporting to file. - database-export-completed: "&a Success, the database export for [world] is completed. File [file] generated." - # Message that appears if input file name is already taken. - file-name-exist: "&c File with name '[id]' exists. Cannot overwrite." - # Message that asks for search value input. - write-search: "&e Please write a search value. (write 'cancel' to exit)" - # Message that appears after updating search value. + confirm-data-replacement: "&e Please confirm that you want to replace your current challenges with the new ones." + new-challenges-imported: "&a Success: new challenges for [gamemode] have been imported." + exported-file-name: "&e Please enter a file name for the exported database file. (Type 'cancel' to exit)" + database-export-completed: "&a Success: the database export for [world] is complete. File [file] generated." + file-name-exist: "&c A file named '[id]' already exists. Cannot overwrite." + write-search: "&e Please enter a search value. (Type 'cancel' to exit)" search-updated: "&a Search value updated." + enter-formula: "&a Enter a formula that uses PAPI placeholders and symbols =,<>,<+,>=, ==, !=, AND, OR only.\n&a Example: &7 %my_lifetime_count% >= 1000 AND %island_level% >= 100" titles: - # Title and subtitle may contain variables in [] that will be replaced with a proper message from the challenge object. - # [friendlyName] will be replaced with challenge friendly name. - # [level] will be replaced with level friendly name. - # [rewardText] will be replaced with the challenge reward text. - challenge-title: 'Successfully completed' - challenge-subtitle: '[friendlyName]' - # Title and subtitle may contain variables in [] that will be replaced with a proper message from the level object. - # [friendlyName] will be replaced with level friendly name. - # [rewardText] will be replaced with the level reward text. - level-title: 'Successfully completed' - level-subtitle: '[friendlyName]' + challenge-title: "Success!" + challenge-subtitle: "[friendlyName]" + level-title: "Success!" + level-subtitle: "[friendlyName]" messages: - completed: '&2 You completed challenge [name] for [player]!' - already-completed: '&2 This challenge was already completed!' - reset: '&2 You reset challenge [name] for [player]!' - reset-all: '&2 All [player] challenges were reset!' - not-completed: '&2 This challenge is not completed yet!' - migrate-start: '&2 Start migrating challenges addon data.' - migrate-end: '&2 Challenges addon data updated to new format.' - migrate-not: '&2 All data is valid.' - start-downloading: '&5 Starting to download and import Challenges Library.' - you-completed-challenge: '&2 You completed the [value] &r &2 challenge!' - you-repeated-challenge: '&2 You repeated the [value] &r &2 challenge!' - you-repeated-challenge-multiple: '&2 You repeated the [value] &r &2 challenge [count] times!' - you-completed-level: '&2 You completed the [value] &r &2 level!' - name-has-completed-challenge: '&5 [name] has completed the [value] &r &5 challenge!' - name-has-completed-level: '&5 [name] has completed the [value] &r &5 level!' - load-skipping: '"[value]" already exists - skipping' - load-overwriting: 'Overwriting "[value]"' - load-add: 'Adding new object: [value]' + completed: "&2 You completed the challenge [name] for [player]!" + already-completed: "&2 This challenge has already been completed!" + reset: "&2 You have reset the challenge [name] for [player]!" + reset-all: "&2 All challenges for [player] have been reset!" + not-completed: "&2 This challenge has not been completed yet!" + migrate-start: "&2 Starting migration of challenges add-on data." + migrate-end: "&2 Challenges add-on data has been updated to the new format." + migrate-not: "&2 All data is valid." + start-downloading: "&5 Starting to download and import the Challenges Library." + you-completed-challenge: "&2 You completed the [value] &r &2 challenge!" + you-repeated-challenge: "&2 You repeated the [value] &r &2 challenge!" + you-repeated-challenge-multiple: "&2 You repeated the [value] &r &2 challenge [count] times!" + you-completed-level: "&2 You completed the [value] &r &2 level!" + name-has-completed-challenge: "&5 [name] has completed the [value] &r &5 challenge!" + name-has-completed-level: "&5 [name] has completed the [value] &r &5 level!" + load-skipping: "\"[value]\" already exists - skipping" + load-overwriting: "Overwriting \"[value]\"" + load-add: "Adding new object: [value]" errors: - no-name: '&c Missing challenge name' - unknown-challenge: '&c Unknown challenge' + no-name: "&c Missing challenge name" + unknown-challenge: "&c Unknown challenge" not-valid-integer: |- - &c Given integer "[value]" is not valid! - Value should be between [min] and [max]. - not-deployed: '&c Challenge is not deployed!' - not-on-island: '&c You must be on your island to do that!' - challenge-level-not-available: '&c You have not unlocked the required level to complete this challenge.' - not-repeatable: '&c This challenge is not repeatable!' - wrong-environment: '&c You are in the wrong environment!' - not-enough-items: '&c You do not have enough [items] to complete this challenge!' - not-close-enough: '&c You must be standing within [number] blocks of all required items.' - you-still-need: '&c You still need [amount] x [item]' - missing-addon: '&c Cannot complete challenge: Required addon or plugin is missing.' - incorrect: '&c Cannot complete challenge: Requirements are incorrect.' - not-enough-money: '&c It is necessary to have [value] on your account to complete the challenge.' - not-enough-experience: '&c It is necessary to have [value] EXP to complete this challenge.' - island-level: '&c Your island must be level [number] or greater to complete this challenge!' - no-load: '&c Error: Could not load [file]. Error [message]' - load-error: '&c Error: Cannot load [value].' - no-rank: "&c You do not have rank that is high enough to do that." - cannot-remove-items: '&c Some items cannot be removed from your inventory!' - exist-challenges-or-levels: '&c Challenges already exist in your world. Cannot proceed!' - no-challenges: '&c Challenges are not implemented in this world yet!' - no-challenges-admin: '&c Challenges are not implemented in this world yet! Use &5 /[command] &c to add them!' - missing-arguments: '&c Command is missing arguments.' + &c The given integer "[value]" is not valid! + It should be between [min] and [max]. + not-deployed: "&c The challenge is not deployed!" + not-on-island: "&c You must be on your island to do that!" + challenge-level-not-available: "&c You have not unlocked the required level to complete this challenge." + not-repeatable: "&c This challenge is not repeatable!" + wrong-environment: "&c You are in the wrong environment!" + not-enough-items: "&c You do not have enough [items] to complete this challenge!" + not-close-enough: "&c You must be standing within [number] blocks of all required items." + you-still-need: "&c You still need [amount] x [item]" + missing-addon: "&c Cannot complete the challenge: a required add-on or plugin is missing." + incorrect: "&c Cannot complete the challenge: the requirements are not met." + not-enough-money: "&c You must have [value] in your account to complete the challenge." + not-enough-experience: "&c You must have [value] EXP to complete this challenge." + island-level: "&c Your island must be at least level [number] to complete this challenge!" + no-load: "&c Error: Could not load [file]. Error: [message]." + load-error: "&c Error: Cannot load [value]." + no-rank: "&c You do not have a high enough rank to do that." + cannot-remove-items: "&c Some items cannot be removed from your inventory!" + exist-challenges-or-levels: "&c Challenges already exist in your world. Cannot proceed!" + no-challenges: "&c Challenges are not implemented in this world yet!" + no-challenges-admin: "&c Challenges are not implemented in this world yet! Use &5 /[command] &c to add them!" + missing-arguments: "&c The command is missing arguments." no-multiple-permission: "&c You do not have permission to complete this challenge multiple times at once." - invalid-level: "&c Level [level] contains invalid data. It will not be loaded from database!" - invalid-challenge: "&c Challenge [challenge] contains invalid data. It will not be loaded from database!" - no-library-entries: "&c Cannot find any library entries. Nothing to show." - not-hooked: "&c Challenges Addon could not find any GameMode." - timeout: "&c This challenge requires to wait [timeout] between completions. You must wait [wait-time] till complete it again." - requirement-not-met: "&c This challenge requires [statistic] to have [number]. You have only [value]. " - requirement-not-met-entity: "&c This challenge requires [statistic] [entity] to have [number]. You have only [value]. " - requirement-not-met-material: "&c This challenge requires [statistic] [material] to have [number]. You have only [value]. " + invalid-level: "&c Level [level] contains invalid data and will not be loaded from the database!" + invalid-challenge: "&c Challenge [challenge] contains invalid data and will not be loaded from the database!" + no-library-entries: "&c No library entries found. Nothing to show." + not-hooked: "&c The Challenges add-on could not find any GameMode." + timeout: "&c This challenge requires a wait time of [timeout] between completions. You must wait [wait-time] before completing it again." + requirement-not-met: "&c This challenge requires [statistic] of [number]. You only have [value]." + requirement-not-met-entity: "&c This challenge requires [statistic] for [entity] to be [number]. You only have [value]." + requirement-not-met-material: "&c This challenge requires [statistic] for [material] to be [number]. You only have [value]." # # Showcase for manual material translation # materials: # # Names should be lowercase. @@ -1306,7 +1320,7 @@ protection: description: "&5 &o Toggle who can\n&5 &o complete challenges" name: "Challenges protection" CHALLENGES_WORLD_PROTECTION: - description: "&5 &o Enable/disable\n&5 &o requirement for players to\n&5 &o be on their island to\n&5 &o complete a challenge." + description: "&5 &o Enable/disable\n&5 &o the requirement for players to\n&5 &o be on their island to\n&5 &o complete a challenge." name: "Challenges Island limitation" - hint: "No challenges outside island" + hint: "No challenges outside the island" version: 12 diff --git a/src/test/java/world/bentobox/challenges/database/object/requirements/CheckPapiTest.java b/src/test/java/world/bentobox/challenges/database/object/requirements/CheckPapiTest.java new file mode 100644 index 00000000..b0013913 --- /dev/null +++ b/src/test/java/world/bentobox/challenges/database/object/requirements/CheckPapiTest.java @@ -0,0 +1,102 @@ +package world.bentobox.challenges.database.object.requirements; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import org.bukkit.entity.Player; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import me.clip.placeholderapi.PlaceholderAPI; + +@RunWith(PowerMockRunner.class) +@PrepareForTest(PlaceholderAPI.class) +public class CheckPapiTest { + + @Mock + private Player player; + + @Before + public void setUp() { + PowerMockito.mockStatic(PlaceholderAPI.class, Mockito.RETURNS_MOCKS); + // Return back the input string + when(PlaceholderAPI.setPlaceholders(eq(player), anyString())) + .thenAnswer((Answer) invocation -> invocation.getArgument(1, String.class)); + } + + @Test + public void testNumericEquality() { + // Using numeric equality comparisons. + assertTrue(CheckPapi.evaluate(player, "40 == 40")); + assertFalse(CheckPapi.evaluate(player, "40 == 50")); + assertTrue(CheckPapi.evaluate(player, "100 = 100")); + assertFalse(CheckPapi.evaluate(player, "100 = 101")); + } + + @Test + public void testNumericComparison() { + assertTrue(CheckPapi.evaluate(player, "40 > 20")); + assertFalse(CheckPapi.evaluate(player, "20 > 40")); + assertTrue(CheckPapi.evaluate(player, "20 < 40")); + assertFalse(CheckPapi.evaluate(player, "40 < 20")); + assertTrue(CheckPapi.evaluate(player, "30 <= 30")); + assertFalse(CheckPapi.evaluate(player, "31 <= 30")); + assertTrue(CheckPapi.evaluate(player, "30 >= 30")); + assertFalse(CheckPapi.evaluate(player, "29 >= 30")); + // Extra tokens beyond a valid expression. + assertTrue(CheckPapi.evaluate(player, "40 > 20 extra")); + + } + + @Test + public void testStringEquality() { + // String comparisons with multi-word operands. + assertTrue(CheckPapi.evaluate(player, "john smith == john smith")); + assertFalse(CheckPapi.evaluate(player, "john smith == jane doe")); + // Using inequality operators. + assertTrue(CheckPapi.evaluate(player, "john smith <> jane doe")); + assertFalse(CheckPapi.evaluate(player, "john smith <> john smith")); + } + + @Test + public void testStringLexicographicalComparison() { + // Lexicographical comparison using string compareTo semantics. + assertTrue(CheckPapi.evaluate(player, "apple < banana")); + assertTrue(CheckPapi.evaluate(player, "banana > apple")); + assertTrue(CheckPapi.evaluate(player, "cat >= cat")); + assertTrue(CheckPapi.evaluate(player, "cat <= cat")); + } + + @Test + public void testMultipleConditionsAndOr() { + // AND has higher precedence than OR. + // "john smith == john smith AND 40 > 20" should be true. + assertTrue(CheckPapi.evaluate(player, "john smith == john smith AND 40 > 20")); + // "john smith == jane doe OR 40 > 20" should be true because second condition is true. + assertTrue(CheckPapi.evaluate(player, "john smith == jane doe OR 40 > 20")); + // "john smith == jane doe AND 40 > 20" should be false because first condition fails. + assertFalse(CheckPapi.evaluate(player, "john smith == jane doe AND 40 > 20")); + // Mixed AND and OR: AND is evaluated first. + // Equivalent to: (john smith == jane doe) OR ((40 > 20) AND (10 < 20)) + assertTrue(CheckPapi.evaluate(player, "john smith == jane doe OR 40 > 20 AND 10 < 20")); + } + + @Test + public void testInvalidFormula() { + // Missing operator between operands. + assertFalse(CheckPapi.evaluate(player, "40 40")); + // Incomplete condition. + assertFalse(CheckPapi.evaluate(player, "40 >")); + } + +} diff --git a/src/test/java/world/bentobox/challenges/utils/UtilsTest.java b/src/test/java/world/bentobox/challenges/utils/UtilsTest.java index b8a3c99f..e355241e 100644 --- a/src/test/java/world/bentobox/challenges/utils/UtilsTest.java +++ b/src/test/java/world/bentobox/challenges/utils/UtilsTest.java @@ -10,7 +10,11 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; import org.bukkit.Bukkit; import org.bukkit.Material;