diff --git a/src/main/java/world/bentobox/challenges/database/object/ChallengesPlayerData.java b/src/main/java/world/bentobox/challenges/database/object/ChallengesPlayerData.java index c1de447..8f5e55f 100644 --- a/src/main/java/world/bentobox/challenges/database/object/ChallengesPlayerData.java +++ b/src/main/java/world/bentobox/challenges/database/object/ChallengesPlayerData.java @@ -251,13 +251,25 @@ public class ChallengesPlayerData implements DataObject * @param challengeName - unique challenge name * @param times - the number of times to set */ - public void setChallengeTimes(@NonNull String challengeName, @NonNull int times) + public void setChallengeTimes(@NonNull String challengeName, int times) { challengeStatus.put(challengeName, times); challengesTimestamp.put(challengeName, System.currentTimeMillis()); } + /** + * Gets last completion time. + * + * @param challengeName the unique id + * @return the last completion time + */ + public long getLastCompletionTime(@NonNull String challengeName) + { + return this.challengesTimestamp.getOrDefault(challengeName, 0L); + } + + /** * Check if a challenge has been done * diff --git a/src/main/java/world/bentobox/challenges/managers/ChallengesManager.java b/src/main/java/world/bentobox/challenges/managers/ChallengesManager.java index eb07ee1..33aa402 100644 --- a/src/main/java/world/bentobox/challenges/managers/ChallengesManager.java +++ b/src/main/java/world/bentobox/challenges/managers/ChallengesManager.java @@ -1397,6 +1397,43 @@ public class ChallengesManager } + /** + * This method returns if given user breached timeout for given challenge. + * @param user - User that must be checked. + * @param world - World where challenge operates. + * @param challenge - Challenge that must be checked. + * @return True, if challenge is breached timeout, otherwise - false. + */ + public boolean isBreachingTimeOut(User user, World world, Challenge challenge) + { + if (challenge.getTimeout() <= 0) + { + // Challenge does not have a timeout. + return false; + } + + return System.currentTimeMillis() < + this.getLastCompletionDate(user, world, challenge) + challenge.getTimeout(); + } + + + /** + * Gets last completion date for given challenge. + * + * @param user the user + * @param world the world + * @param challenge the challenge + * @return the last completion date + */ + public long getLastCompletionDate(User user, World world, Challenge challenge) + { + String userId = this.getDataUniqueID(user, Util.getWorld(world)); + this.addPlayerData(userId); + + return this.playerCacheData.get(userId).getLastCompletionTime(challenge.getUniqueId()); + } + + /** * This method sets given challenge as completed. * @param user - Targeted user. diff --git a/src/main/java/world/bentobox/challenges/panel/CommonPanel.java b/src/main/java/world/bentobox/challenges/panel/CommonPanel.java index 939649e..337f740 100644 --- a/src/main/java/world/bentobox/challenges/panel/CommonPanel.java +++ b/src/main/java/world/bentobox/challenges/panel/CommonPanel.java @@ -13,6 +13,7 @@ import org.bukkit.inventory.ItemStack; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; +import java.time.*; import java.util.*; import java.util.stream.Collectors; @@ -159,13 +160,17 @@ public abstract class CommonPanel String requirements = isCompletedAll ? "" : this.generateRequirements(challenge, target); // Get rewards in single string String rewards = isCompletedAll ? "" : this.generateRewards(challenge, isCompletedOnce); + // Get coolDown in singe string + String coolDown = isCompletedAll || challenge.getTimeout() <= 0 ? "" : + this.generateCoolDown(challenge, target); if (!description.replaceAll("(?m)^[ \\t]*\\r?\\n", "").isEmpty()) { String returnString = this.user.getTranslationOrNothing(reference + "lore", "[requirements]", requirements, "[rewards]", rewards, - "[status]", status); + "[status]", status, + "[cooldown]", coolDown); // remove empty lines from the generated text. List collect = @@ -191,7 +196,8 @@ public abstract class CommonPanel Constants.PARAMETER_DESCRIPTION, description, "[requirements]", requirements, "[rewards]", rewards, - "[status]", status); + "[status]", status, + "[cooldown]", coolDown); // Remove empty lines and returns as a list. @@ -202,6 +208,43 @@ public abstract class CommonPanel } + /** + * Generate cool down string. + * + * @param challenge the challenge + * @param target the target + * @return the string + */ + private String generateCoolDown(Challenge challenge, @Nullable User target) + { + final String reference = Constants.DESCRIPTIONS + "challenge.cooldown."; + + String coolDown; + + if (target != null && this.manager.isBreachingTimeOut(target, this.world, challenge)) + { + long missing = this.manager.getLastCompletionDate(this.user, this.world, challenge) + + challenge.getTimeout() - System.currentTimeMillis(); + + coolDown = this.user.getTranslation(reference + "wait-time", + "[time]", + Utils.parseDuration(Duration.ofMillis(missing), this.user)); + } + else + { + coolDown = ""; + } + + String timeout = this.user.getTranslation(reference + "timeout", + "[time]", + Utils.parseDuration(Duration.ofMillis(challenge.getTimeout()), this.user)); + + return this.user.getTranslation(reference + "lore", + "[timeout]", timeout, + "[wait-time]", coolDown); + } + + /** * This method generate requirements description for given challenge. * @param challenge Challenge which requirements must be generated. 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 4e0494b..723cb2f 100644 --- a/src/main/java/world/bentobox/challenges/panel/user/ChallengesPanel.java +++ b/src/main/java/world/bentobox/challenges/panel/user/ChallengesPanel.java @@ -303,6 +303,12 @@ public class ChallengesPanel extends CommonPanel this.createChallengeButton(template, challenge).getItem()); } } + else if (challenge.isRepeatable() && challenge.getTimeout() > 0) + { + // Update timeout after clicking. + panel.getInventory().setItem(i, + this.createChallengeButton(template, challenge).getItem()); + } break; case "COMPLETE_MAX": if (challenge.isRepeatable()) @@ -328,6 +334,12 @@ public class ChallengesPanel extends CommonPanel this.createChallengeButton(template, challenge).getItem()); } } + else if (challenge.getTimeout() > 0) + { + // Update timeout after clicking. + panel.getInventory().setItem(i, + this.createChallengeButton(template, challenge).getItem()); + } } break; case "MULTIPLE_PANEL": diff --git a/src/main/java/world/bentobox/challenges/tasks/TryToComplete.java b/src/main/java/world/bentobox/challenges/tasks/TryToComplete.java index 31a64d4..4536e08 100644 --- a/src/main/java/world/bentobox/challenges/tasks/TryToComplete.java +++ b/src/main/java/world/bentobox/challenges/tasks/TryToComplete.java @@ -3,6 +3,8 @@ package world.bentobox.challenges.tasks; import com.google.common.collect.UnmodifiableIterator; +import java.time.*; +import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Collections; import java.util.EnumMap; @@ -228,7 +230,8 @@ public class TryToComplete int maxTimes) { return new TryToComplete(addon, user, challenge, world, topLabel, permissionPrefix). - build(maxTimes).meetsRequirements; + build(maxTimes). + meetsRequirements; } @@ -713,6 +716,17 @@ public class TryToComplete Utils.sendMessage(this.user, this.user.getTranslation("challenges.errors.not-repeatable")); result = EMPTY_RESULT; } + // Check if timeout is not broken + 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(); + + Utils.sendMessage(this.user, this.user.getTranslation("challenges.errors.timeout", + "[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())) @@ -776,7 +790,7 @@ public class TryToComplete */ private int getAvailableCompletionTimes(int vantedTimes) { - if (!this.challenge.isRepeatable()) + if (!this.challenge.isRepeatable() || this.challenge.getTimeout() > 0) { // Challenge is not repeatable vantedTimes = 1; @@ -1513,7 +1527,7 @@ public class TryToComplete * * @author tastybento */ - class ChallengeResult + static class ChallengeResult { /** * This method sets that challenge meets all requirements at least once. diff --git a/src/main/java/world/bentobox/challenges/utils/Utils.java b/src/main/java/world/bentobox/challenges/utils/Utils.java index 728924c..b4cd99e 100644 --- a/src/main/java/world/bentobox/challenges/utils/Utils.java +++ b/src/main/java/world/bentobox/challenges/utils/Utils.java @@ -1,6 +1,7 @@ package world.bentobox.challenges.utils; +import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -878,4 +879,43 @@ public class Utils "[type]", prettifyObject(itemType, user), "[meta]", meta); } + + + /** + * This method parses duration to a readable format. + * @param duration that needs to be parsed. + * @return parsed duration string. + */ + public static String parseDuration(Duration duration, User user) + { + final String reference = Constants.DESCRIPTIONS + "challenge.cooldown."; + + String returnString = ""; + + if (duration.toDays() > 0) + { + returnString += user.getTranslationOrNothing(reference + "in-days", + Constants.PARAMETER_NUMBER, String.valueOf(duration.toDays())); + } + + if (duration.toHoursPart() > 0) + { + returnString += user.getTranslationOrNothing(reference + "in-hours", + Constants.PARAMETER_NUMBER, String.valueOf(duration.toHoursPart())); + } + + if (duration.toMinutesPart() > 0) + { + returnString += user.getTranslationOrNothing(reference + "in-minutes", + Constants.PARAMETER_NUMBER, String.valueOf(duration.toMinutesPart())); + } + + if (duration.toSecondsPart() > 0 || returnString.isBlank()) + { + returnString += user.getTranslationOrNothing(reference + "in-seconds", + Constants.PARAMETER_NUMBER, String.valueOf(duration.toSecondsPart())); + } + + return returnString; + } } diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index ecaa945..d5e904f 100755 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -829,6 +829,7 @@ challenges: lore: |- [description] [status] + [cooldown] [requirements] [rewards] # Contains a text generated inside [status] lore @@ -841,6 +842,23 @@ challenges: 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 completed-times-reached: "&2&l Completed all &7 [max] &2 times" + # Contains a text generated inside [cooldown] lore + cooldown: + lore: |- + [timeout] + [wait-time] + # Text message that shows challenges timeout. + timeout: "&7&l Cool down: &r&7 [time]" + # Text message that shows challenges 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, " + # Text message that replaces hours if number > 1 + in-hours: "[number] h, " + # Text message that replaces minutes if number > 1 + in-minutes: "[number] min, " + # Text message that replaces seconds if number > 1 + in-seconds: "[number] s" # Contains a text generated inside [requirements] lore requirements: lore: |- @@ -1156,6 +1174,7 @@ challenges: 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." # # Showcase for manual material translation # materials: # # Names should be lowercase. diff --git a/src/main/resources/locales/lv.yml b/src/main/resources/locales/lv.yml index 71e5e98..9067b46 100644 --- a/src/main/resources/locales/lv.yml +++ b/src/main/resources/locales/lv.yml @@ -849,6 +849,7 @@ challenges: lore: |- [description] [status] + [cooldown] [requirements] [rewards] # Contains a text generated inside [status] lore @@ -861,6 +862,23 @@ challenges: completed-times-of: "&2 Izpildīts &7&l [number] &r&2 no &7&l [max] &r&2 reizēm" # Status message that indicates that max completion count reached for repeatable challenge completed-times-reached: "&2&l Izpildīts visas &7 [max] &2 reizes" + # Contains a text generated inside [cooldown] lore + cooldown: + lore: |- + [timeout] + [wait-time] + # Text message that shows challenges timeout. + timeout: "&7&l Taimauts: &r&7 [time]" + # Text message that shows challenges wait time if it is larger than 0. + wait-time: "&c&l Jānogaida: &r&c [time]" + # Text message that replaces days if number > 1 + in-days: "[number] d, " + # Text message that replaces hours if number > 1 + in-hours: "[number] h, " + # Text message that replaces minutes if number > 1 + in-minutes: "[number] min, " + # Text message that replaces seconds if number > 1 + in-seconds: "[number] s" # Contains a text generated inside [requirements] lore requirements: lore: |- @@ -1180,6 +1198,7 @@ challenges: invalid-challenge: "&c Uzdevums [challenge] satur nekorektus datus. Tas var tikt ielādēts nekorekti!" no-library-entries: "&c Nevar atrast bibliotēkas ierakstus. Nav ko rādīt." not-hooked: "&c Uzdevumu Papildinājumam neizdevās atrast Spēles Režīmu." + timeout: "&c Šim uzdevumam ir uzstādīts [timeout] taimauts starp izpildēm. Tev vēl ir jāgaida [wait-time], lai varētu pildīt uzdevumu." # # Showcase for manual material translation # materials: # # Names should be lowercase.