mirror of
https://github.com/BentoBoxWorld/Challenges.git
synced 2025-02-21 23:01:40 +01:00
Added a PAPI formula option to Other challenge types.
This commit is contained in:
parent
90bb3d68b7
commit
fb782c23cd
12
pom.xml
12
pom.xml
@ -117,6 +117,11 @@
|
||||
<id>minecraft-repo</id>
|
||||
<url>https://libraries.minecraft.net/</url>
|
||||
</repository>
|
||||
<!-- Placeholder API -->
|
||||
<repository>
|
||||
<id>placeholderapi</id>
|
||||
<url>https://repo.extendedclip.com/releases/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
@ -198,6 +203,13 @@
|
||||
<artifactId>commons-math3</artifactId>
|
||||
<version>3.6.1</version>
|
||||
</dependency>
|
||||
<!-- Placeholder API -->
|
||||
<dependency>
|
||||
<groupId>me.clip</groupId>
|
||||
<artifactId>placeholderapi</artifactId>
|
||||
<version>2.11.6</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
@ -0,0 +1,183 @@
|
||||
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;
|
||||
|
||||
public class CheckPapi {
|
||||
|
||||
/**
|
||||
* Evaluates the formula by first replacing PAPI placeholders (using the provided Player)
|
||||
* and then evaluating the resulting expression. The expression is expected to be a series
|
||||
* of numeric comparisons (using =, <>, <=, >=, <, >) joined by Boolean operators AND and OR.
|
||||
*
|
||||
* For example:
|
||||
* "%aoneblock_my_island_lifetime_count% >= 1000 AND %Level_aoneblock_island_level% >= 100"
|
||||
*
|
||||
* If any placeholder evaluates to a non-numeric value or the formula is malformed, false is returned.
|
||||
*
|
||||
* @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 using the provided Player.
|
||||
String parsedFormula = PlaceholderAPI.setPlaceholders(player, formula);
|
||||
|
||||
// Tokenize the parsed formula (tokens are assumed to be separated by whitespace).
|
||||
List<String> tokens = tokenize(parsedFormula);
|
||||
if (tokens.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
Parser parser = new Parser(tokens);
|
||||
boolean result = parser.parseExpression();
|
||||
// If there are leftover tokens, the expression is malformed.
|
||||
if (parser.hasNext()) {
|
||||
return false;
|
||||
}
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
// Any error in parsing or evaluation results in false.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits the given string into tokens using whitespace as the delimiter.
|
||||
*
|
||||
* @param s the string to tokenize.
|
||||
* @return a list of tokens.
|
||||
*/
|
||||
private static List<String> tokenize(String s) {
|
||||
return new ArrayList<>(Arrays.asList(s.split("\\s+")));
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple recursive descent parser that evaluates expressions according to the following grammar:
|
||||
*
|
||||
* <pre>
|
||||
* Expression -> Term { OR Term }
|
||||
* Term -> Factor { AND Factor }
|
||||
* Factor -> operand operator operand
|
||||
* </pre>
|
||||
*
|
||||
* A Factor is expected to be a numeric condition in the form:
|
||||
* number operator number
|
||||
* where operator is one of: =, <>, <=, >=, <, or >.
|
||||
*/
|
||||
private static class Parser {
|
||||
private final List<String> tokens;
|
||||
private int pos = 0;
|
||||
|
||||
public Parser(List<String> tokens) {
|
||||
this.tokens = tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there are more tokens to process.
|
||||
*/
|
||||
public boolean hasNext() {
|
||||
return pos < tokens.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next token without advancing.
|
||||
*/
|
||||
public String peek() {
|
||||
return tokens.get(pos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next token and advances the position.
|
||||
*/
|
||||
public String next() {
|
||||
return tokens.get(pos++);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an Expression:
|
||||
* Expression -> Term { OR Term }
|
||||
*/
|
||||
public boolean parseExpression() {
|
||||
boolean value = parseTerm();
|
||||
while (hasNext() && peek().equalsIgnoreCase("OR")) {
|
||||
next(); // consume "OR"
|
||||
boolean termValue = parseTerm();
|
||||
value = value || termValue;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a Term:
|
||||
* Term -> Factor { AND Factor }
|
||||
*/
|
||||
public boolean parseTerm() {
|
||||
boolean value = parseFactor();
|
||||
while (hasNext() && peek().equalsIgnoreCase("AND")) {
|
||||
next(); // consume "AND"
|
||||
boolean factorValue = parseFactor();
|
||||
value = value && factorValue;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a Factor, which is a single condition in the form:
|
||||
* operand operator operand
|
||||
*
|
||||
* For example: "1234 >= 1000"
|
||||
*
|
||||
* @return the boolean result of the condition.
|
||||
*/
|
||||
public boolean parseFactor() {
|
||||
// There must be at least three tokens remaining.
|
||||
if (pos + 2 >= tokens.size()) {
|
||||
throw new RuntimeException("Incomplete condition");
|
||||
}
|
||||
|
||||
String leftOperand = next();
|
||||
String operator = next();
|
||||
String rightOperand = next();
|
||||
|
||||
// Validate operator.
|
||||
if (!operator.equals("=") && !operator.equals("<>") && !operator.equals("<=") && !operator.equals(">=")
|
||||
&& !operator.equals("<") && !operator.equals(">")) {
|
||||
throw new RuntimeException("Invalid operator: " + operator);
|
||||
}
|
||||
|
||||
double leftVal, rightVal;
|
||||
try {
|
||||
leftVal = Double.parseDouble(leftOperand);
|
||||
rightVal = Double.parseDouble(rightOperand);
|
||||
} catch (NumberFormatException e) {
|
||||
// If either operand is not numeric, return false.
|
||||
return false;
|
||||
}
|
||||
// Evaluate the condition.
|
||||
switch (operator) {
|
||||
case "=":
|
||||
return Double.compare(leftVal, rightVal) == 0;
|
||||
case "<>":
|
||||
return Double.compare(leftVal, rightVal) != 0;
|
||||
case "<=":
|
||||
return leftVal <= rightVal;
|
||||
case ">=":
|
||||
return leftVal >= rightVal;
|
||||
case "<":
|
||||
return leftVal < rightVal;
|
||||
case ">":
|
||||
return leftVal > rightVal;
|
||||
default:
|
||||
// This case is never reached.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -139,6 +139,20 @@ public class OtherRequirements extends Requirements
|
||||
{
|
||||
this.requiredIslandLevel = requiredIslandLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the papiString
|
||||
*/
|
||||
public String getPapiString() {
|
||||
return papiString == null ? "" : papiString;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param papiString the papiString to set
|
||||
*/
|
||||
public void setPapiString(String papiString) {
|
||||
this.papiString = papiString;
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
@ -162,6 +176,7 @@ public class OtherRequirements extends Requirements
|
||||
clone.setRequiredMoney(this.requiredMoney);
|
||||
clone.setTakeMoney(this.takeMoney);
|
||||
clone.setRequiredIslandLevel(this.requiredIslandLevel);
|
||||
clone.setPapiString(this.papiString);
|
||||
|
||||
return clone;
|
||||
}
|
||||
@ -201,4 +216,12 @@ public class OtherRequirements extends Requirements
|
||||
*/
|
||||
@Expose
|
||||
private long requiredIslandLevel;
|
||||
|
||||
/**
|
||||
* Formulas that include math symbols and PAPI placeholders
|
||||
*/
|
||||
@Expose
|
||||
private String papiString;
|
||||
|
||||
|
||||
}
|
||||
|
@ -215,6 +215,7 @@ public class EditChallengePanel extends CommonPanel {
|
||||
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(25, this.createRequirementButton(RequirementButton.REQUIRED_PERMISSIONS));
|
||||
@ -630,7 +631,7 @@ public class EditChallengePanel extends CommonPanel {
|
||||
return this.createInventoryRequirementButton(button);
|
||||
}
|
||||
// Buttons for Other Requirements
|
||||
case REQUIRED_EXPERIENCE, REMOVE_EXPERIENCE, REQUIRED_LEVEL, REQUIRED_MONEY, REMOVE_MONEY -> {
|
||||
case REQUIRED_EXPERIENCE, REMOVE_EXPERIENCE, REQUIRED_LEVEL, REQUIRED_MONEY, REMOVE_MONEY, REQUIRED_PAPI -> {
|
||||
return this.createOtherRequirementButton(button);
|
||||
}
|
||||
// Statistics
|
||||
@ -1098,6 +1099,33 @@ public class EditChallengePanel extends CommonPanel {
|
||||
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<String> 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_MONEY -> {
|
||||
description.add(this.user.getTranslation(reference + "value", Constants.PARAMETER_NUMBER,
|
||||
String.valueOf(requirements.getRequiredMoney())));
|
||||
@ -1701,7 +1729,7 @@ public class EditChallengePanel extends CommonPanel {
|
||||
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,
|
||||
REMOVE_STATISTICS, REQUIRED_PAPI,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
@ -37,6 +37,7 @@ 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;
|
||||
@ -45,6 +46,7 @@ import world.bentobox.challenges.ChallengesAddon;
|
||||
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;
|
||||
@ -530,91 +532,79 @@ public class TryToComplete
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// hmm
|
||||
return;
|
||||
}
|
||||
|
||||
for (UnmodifiableIterator<UUID> iterator = island.getMemberSet().iterator(); iterator.hasNext()
|
||||
&& removeAmount > 0;)
|
||||
// If challenges are in sync with all island members, then punish others too.
|
||||
if (this.addon.getChallengesSettings().isStoreAsIslandData())
|
||||
{
|
||||
Player player = Bukkit.getPlayer(iterator.next());
|
||||
Island island = this.addon.getIslands().getIsland(this.world, this.user);
|
||||
|
||||
if (player == null || player == this.user.getPlayer()) {
|
||||
// cannot punish null or player who already was punished.
|
||||
continue;
|
||||
if (island == null) {
|
||||
// hmm
|
||||
return;
|
||||
}
|
||||
|
||||
switch (Objects.requireNonNull(s.statistic()).getType()) {
|
||||
case UNTYPED -> {
|
||||
int statistic = player.getStatistic(s.statistic());
|
||||
for (UnmodifiableIterator<UUID> iterator = island.getMemberSet().iterator(); iterator.hasNext()
|
||||
&& removeAmount > 0;) {
|
||||
Player player = Bukkit.getPlayer(iterator.next());
|
||||
|
||||
if (removeAmount >= statistic)
|
||||
{
|
||||
removeAmount -= statistic;
|
||||
player.setStatistic(s.statistic(), 0);
|
||||
if (player == null || player == this.user.getPlayer()) {
|
||||
// cannot punish null or player who already was punished.
|
||||
continue;
|
||||
}
|
||||
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());
|
||||
|
||||
switch (Objects.requireNonNull(s.statistic()).getType()) {
|
||||
case UNTYPED -> {
|
||||
int statistic = player.getStatistic(s.statistic());
|
||||
|
||||
if (removeAmount >= statistic)
|
||||
{
|
||||
removeAmount -= statistic;
|
||||
player.setStatistic(s.statistic(), s.material(), 0);
|
||||
player.setStatistic(s.statistic(), 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
player.setStatistic(s.statistic(), s.material(),
|
||||
statistic - removeAmount);
|
||||
player.setStatistic(s.statistic(), 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;
|
||||
}
|
||||
else
|
||||
{
|
||||
int statistic = player.getStatistic(s.statistic(), s.entity());
|
||||
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)
|
||||
if (removeAmount >= statistic) {
|
||||
removeAmount -= statistic;
|
||||
player.setStatistic(s.statistic(), s.material(), 0);
|
||||
} else {
|
||||
player.setStatistic(s.statistic(), s.material(), statistic - removeAmount);
|
||||
removeAmount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
case ENTITY -> {
|
||||
if (s.entity() == null)
|
||||
{
|
||||
removeAmount -= statistic;
|
||||
player.setStatistic(s.statistic(), s.entity(), 0);
|
||||
// Just a sanity check. Entity cannot be null at this point of code.
|
||||
removeAmount = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
player.setStatistic(s.statistic(), s.entity(),
|
||||
statistic - removeAmount);
|
||||
removeAmount = 0;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1426,6 +1416,15 @@ public class TryToComplete
|
||||
Utils.sendMessage(this.user,
|
||||
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
|
||||
{
|
||||
@ -1451,7 +1450,6 @@ public class TryToComplete
|
||||
// Section: Statistic Challenge
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
|
||||
/**
|
||||
* Checks if a statistic challenge can be completed or not
|
||||
* It returns ChallengeResult.
|
||||
|
@ -415,6 +415,13 @@ challenges:
|
||||
&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: |-
|
||||
@ -1130,6 +1137,7 @@ challenges:
|
||||
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 number placeholders and symbols =,<>,<+,>=, AND, OR only.\n&a Example: &7 %aoneblock_my_island_lifetime_count% >= 1000 AND %Level_aoneblock_island_level% >= 100"
|
||||
titles:
|
||||
challenge-title: "Success!"
|
||||
challenge-subtitle: "[friendlyName]"
|
||||
|
Loading…
Reference in New Issue
Block a user