diff --git a/src/main/java/net/knarcraft/blacksmith/config/GlobalSetting.java b/src/main/java/net/knarcraft/blacksmith/config/GlobalSetting.java index 34c9c85..15c7d48 100644 --- a/src/main/java/net/knarcraft/blacksmith/config/GlobalSetting.java +++ b/src/main/java/net/knarcraft/blacksmith/config/GlobalSetting.java @@ -31,7 +31,12 @@ public enum GlobalSetting { /** * Whether the cost should increase for damage taken, as opposed to increase for durability present */ - NATURAL_COST("global.useNaturalCost", SettingValueType.BOOLEAN, true, "useNaturalCost"); + NATURAL_COST("global.useNaturalCost", SettingValueType.BOOLEAN, true, "useNaturalCost"), + + /** + * Whether to show exact time when displaying the wait time for a reforging or the cool-down + */ + SHOW_EXACT_TIME("global.showExactTime", SettingValueType.BOOLEAN, false, "showExactTime"); private final String path; private final String parent; diff --git a/src/main/java/net/knarcraft/blacksmith/config/GlobalSettings.java b/src/main/java/net/knarcraft/blacksmith/config/GlobalSettings.java index cf2ed84..74c5c61 100644 --- a/src/main/java/net/knarcraft/blacksmith/config/GlobalSettings.java +++ b/src/main/java/net/knarcraft/blacksmith/config/GlobalSettings.java @@ -180,6 +180,15 @@ public class GlobalSettings { return asBoolean(GlobalSetting.NATURAL_COST); } + /** + * Gets whether to show exact time for reforging wait-time, and for wait-time between sessions + * + * @return

Whether to show exact time

+ */ + public boolean getShowExactTime() { + return asBoolean(GlobalSetting.SHOW_EXACT_TIME); + } + /** * Gets the base price for the given material * diff --git a/src/main/java/net/knarcraft/blacksmith/config/NPCSetting.java b/src/main/java/net/knarcraft/blacksmith/config/NPCSetting.java index b92538a..cb4f2f8 100644 --- a/src/main/java/net/knarcraft/blacksmith/config/NPCSetting.java +++ b/src/main/java/net/knarcraft/blacksmith/config/NPCSetting.java @@ -70,13 +70,14 @@ public enum NPCSetting { * The message displayed when the blacksmith is already reforging something for the player */ BUSY_WITH_REFORGE_MESSAGE("messages.busyReforgeMessage", SettingValueType.STRING, - "&cI'm working on it. Be patient!", "busyReforgeMessage"), + "&cI'm working on it. Be patient! I'll finish {time}!", "busyReforgeMessage"), /** * The message displayed if the player has to wait for the cool-down to expire */ COOL_DOWN_UNEXPIRED_MESSAGE("messages.coolDownUnexpiredMessage", SettingValueType.STRING, - "&cYou've already had your chance! Give me a break!", "coolDownUnexpiredMessage"), + "&cYou've already had your chance! Give me a break! I'll be ready {time}!", + "coolDownUnexpiredMessage"), /** * The message displayed when displaying the cost of reforging the held item to the player diff --git a/src/main/java/net/knarcraft/blacksmith/formatting/TimeFormatter.java b/src/main/java/net/knarcraft/blacksmith/formatting/TimeFormatter.java new file mode 100644 index 0000000..6050cb3 --- /dev/null +++ b/src/main/java/net/knarcraft/blacksmith/formatting/TimeFormatter.java @@ -0,0 +1,126 @@ +package net.knarcraft.blacksmith.formatting; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static net.knarcraft.blacksmith.formatting.StringFormatter.replacePlaceholder; + +/** + * A utility for formatting a string specifying an amount of time + */ +public final class TimeFormatter { + + private static Map timeUnits; + private static List sortedUnits; + + private TimeFormatter() { + + } + + /** + * Formats the given number of remaining seconds to the appropriate string + * + * @param exact

Whether to display the exact number of seconds, or some unclear expression

+ * @param seconds

The number of seconds remaining

+ * @return

The time formatted correctly

+ */ + public static String formatTime(boolean exact, int seconds) { + if (exact) { + return getDurationString(seconds); + } else { + return formatUnclearTime(seconds); + } + } + + /** + * Gets a time format using vague wording to specify a known interval + * + * @param seconds

The number of seconds to format

+ * @return

Text describing approximate time remaining

+ */ + private static String formatUnclearTime(int seconds) { + List intervals = Arrays.stream(TimeInterval.values()).sorted().toList(); + for (TimeInterval interval : intervals) { + if (seconds < interval.getIntervalMax()) { + //Use the set message, or use the default + //TODO: Check for commas in the message. If present, split on the comma and choose a random expression + String text = Translator.getTranslatedMessage(TranslatableMessage.valueOf(interval.name())); + if (text != null && !text.trim().isEmpty()) { + return text; + } else { + return interval.getDefaultText(); + } + } + } + return TimeInterval.INTERVAL_MORE_THAN_5_MINUTES.getDefaultText(); + } + + /** + * Gets the string used for displaying this sign's duration + * + * @return

The string used for displaying this sign's duration

+ */ + public static String getDurationString(int duration) { + if (duration == 0) { + return Translator.getTranslatedMessage(TranslatableMessage.UNIT_NOW); + } else { + if (sortedUnits == null) { + initializeUnits(); + } + for (Double unit : sortedUnits) { + if (duration / unit >= 1) { + double units = round(duration / unit); + return formatDurationString(units, timeUnits.get(unit)[units == 1 ? 0 : 1], + (units * 10) % 10 == 0); + } + } + return formatDurationString(duration, TranslatableMessage.UNIT_SECONDS, false); + } + } + + /** + * Rounds a number to its last two digits + * + * @param number

The number to round

+ * @return

The rounded number

+ */ + private static double round(double number) { + return Math.round(number * 100.0) / 100.0; + } + + /** + * Formats a duration string + * + * @param duration

The duration to display

+ * @param translatableMessage

The time unit to display

+ * @param castToInt

Whether to cast the duration to an int

+ * @return

The formatted duration string

+ */ + private static String formatDurationString(double duration, TranslatableMessage translatableMessage, boolean castToInt) { + String durationFormat = Translator.getTranslatedMessage(TranslatableMessage.DURATION_FORMAT); + durationFormat = replacePlaceholder(durationFormat, "{unit}", + Translator.getTranslatedMessage(translatableMessage)); + return replacePlaceholder(durationFormat, "{duration}", castToInt ? String.valueOf((int) duration) : + String.valueOf(duration)); + } + + /** + * Initializes the mapping of available time units for formatting permission sign duration + */ + private static void initializeUnits() { + double minute = 60; + + timeUnits = new HashMap<>(); + timeUnits.put(minute, new TranslatableMessage[]{TranslatableMessage.UNIT_MINUTE, TranslatableMessage.UNIT_MINUTES}); + timeUnits.put(1D, new TranslatableMessage[]{TranslatableMessage.UNIT_SECOND, TranslatableMessage.UNIT_SECONDS}); + + sortedUnits = new ArrayList<>(timeUnits.keySet()); + Collections.sort(sortedUnits); + Collections.reverse(sortedUnits); + } + +} diff --git a/src/main/java/net/knarcraft/blacksmith/formatting/TimeInterval.java b/src/main/java/net/knarcraft/blacksmith/formatting/TimeInterval.java new file mode 100644 index 0000000..d198fb0 --- /dev/null +++ b/src/main/java/net/knarcraft/blacksmith/formatting/TimeInterval.java @@ -0,0 +1,65 @@ +package net.knarcraft.blacksmith.formatting; + +/** + * The important intervals when not using exact wait times + */ +public enum TimeInterval { + + /** + * Less than 10 seconds left + */ + INTERVAL_LESS_THAN_10_SECONDS("momentarily", 10), + + /** + * Less than 30 seconds left + */ + INTERVAL_LESS_THAN_30_SECONDS("in a little while", 30), + + /** + * Less than 1 minute left + */ + INTERVAL_LESS_THAN_1_MINUTE("in a while", 60), + + /** + * Less than 5 minutes left + */ + INTERVAL_LESS_THAN_5_MINUTES("after some time", 300), + + /** + * More than 5 minutes left + */ + INTERVAL_MORE_THAN_5_MINUTES("in quite a while", Integer.MAX_VALUE); + + private final String defaultText; + private final int maxSeconds; + + /** + * Instantiates a new time interval + * + * @param defaultText

The default text used to describe the time interval

+ * @param maxSeconds

The maximum number of seconds to fall within this interval

+ */ + TimeInterval(String defaultText, int maxSeconds) { + this.defaultText = defaultText; + this.maxSeconds = maxSeconds; + } + + /** + * Gets the default text to display for this interval + * + * @return

The default text to display for this interval

+ */ + public String getDefaultText() { + return this.defaultText; + } + + /** + * Gets the maximum number of seconds before exceeding this interval + * + * @return

The max seconds of this interval

+ */ + public int getIntervalMax() { + return maxSeconds; + } + +} diff --git a/src/main/java/net/knarcraft/blacksmith/formatting/TranslatableMessage.java b/src/main/java/net/knarcraft/blacksmith/formatting/TranslatableMessage.java index 04f0180..760e732 100644 --- a/src/main/java/net/knarcraft/blacksmith/formatting/TranslatableMessage.java +++ b/src/main/java/net/knarcraft/blacksmith/formatting/TranslatableMessage.java @@ -10,25 +10,155 @@ import static net.knarcraft.blacksmith.formatting.StringFormatter.replacePlaceho */ public enum TranslatableMessage { + /** + * The message displayed when a configuration value has been successfully changed + */ VALUE_CHANGED, + + /** + * The message displayed when a configuration value for one material or enchantment has been successfully changed + */ VALUE_FOR_ITEM_CHANGED, + + /** + * The message displayed when showing the current value of a configuration option + */ CURRENT_VALUE, + + /** + * The message displayed when showing the current value of a configuration option for one material or enchantment + */ CURRENT_VALUE_FOR_ITEM, + + /** + * The translation of enchantment + */ ITEM_TYPE_ENCHANTMENT, + + /** + * The translation of material + */ ITEM_TYPE_MATERIAL, + + /** + * The message displayed when showing the "raw" value of messages + */ RAW_VALUE, + + /** + * The message displayed when trying to change a blacksmith value before selecting a blacksmith + */ NO_NPC_SELECTED, + + /** + * The message displayed if trying to change the default value of reforge-able item using commands + */ DEFAULT_REFORGE_ABLE_ITEMS_UNCHANGEABLE, + + /** + * The message displayed if a string list is required, but something else is given + */ INPUT_STRING_LIST_REQUIRED, + + /** + * The message displayed if a value between 0 and 100 is required, but something else is given + */ INPUT_PERCENTAGE_REQUIRED, + + /** + * The message displayed if a string is required, but something else is given + */ INPUT_STRING_REQUIRED, + + /** + * The message displayed if a positive double is required, but something else is given + */ INPUT_POSITIVE_DOUBLE_REQUIRED, + + /** + * The message displayed if a positive integer is required, but something else is given + */ INPUT_POSITIVE_INTEGER_REQUIRED, + + /** + * The message displayed if a player is missing the required permission for an action + */ PERMISSION_DENIED, + + /** + * The message displayed if this plugin is successfully reloaded + */ PLUGIN_RELOADED, + + /** + * The message displayed if a filter is specified which isn't supported by the specified preset + */ INVALID_FILTER_FOR_PRESET, + + /** + * The message displayed if an invalid preset or an invalid filter is specified + */ INVALID_PRESET_OR_FILTER, - PRESET_MATERIALS; + + /** + * The message displayed when showing which materials are included in a preset + */ + PRESET_MATERIALS, + + /** + * The format for displaying the exact duration of a blacksmith's cool-down or delay + */ + DURATION_FORMAT, + + /** + * The text to display for 0 seconds + */ + UNIT_NOW, + + /** + * The text to display for 1 second + */ + UNIT_SECOND, + + /** + * The text to display for a number of seconds + */ + UNIT_SECONDS, + + /** + * The text to display for 1 minute + */ + UNIT_MINUTE, + + /** + * The text to display for a number of minutes + */ + UNIT_MINUTES, + + /** + * The text to display when describing less than 10 seconds remaining + */ + INTERVAL_LESS_THAN_10_SECONDS, + + /** + * The text to display when describing less than 30 seconds remaining + */ + INTERVAL_LESS_THAN_30_SECONDS, + + /** + * The text to display when describing less than 1 minute remaining + */ + INTERVAL_LESS_THAN_1_MINUTE, + + /** + * The text to display when describing less than 5 minutes remaining + */ + INTERVAL_LESS_THAN_5_MINUTES, + + /** + * The text to display when describing more than 5 minutes remaining + */ + INTERVAL_MORE_THAN_5_MINUTES; /** * Gets the message to display when displaying the raw value of messages diff --git a/src/main/java/net/knarcraft/blacksmith/trait/BlacksmithTrait.java b/src/main/java/net/knarcraft/blacksmith/trait/BlacksmithTrait.java index bd71815..d4aacba 100644 --- a/src/main/java/net/knarcraft/blacksmith/trait/BlacksmithTrait.java +++ b/src/main/java/net/knarcraft/blacksmith/trait/BlacksmithTrait.java @@ -5,7 +5,7 @@ import net.citizensnpcs.api.trait.Trait; import net.citizensnpcs.api.util.DataKey; import net.knarcraft.blacksmith.BlacksmithPlugin; import net.knarcraft.blacksmith.config.NPCSettings; -import net.knarcraft.blacksmith.formatting.StringFormatter; +import net.knarcraft.blacksmith.formatting.TimeFormatter; import net.knarcraft.blacksmith.manager.EconomyManager; import net.knarcraft.blacksmith.util.ItemHelper; import org.bukkit.Bukkit; @@ -23,6 +23,7 @@ import java.util.Map; import java.util.Objects; import java.util.UUID; +import static net.knarcraft.blacksmith.formatting.StringFormatter.replacePlaceholder; import static net.knarcraft.blacksmith.formatting.StringFormatter.sendNPCMessage; /** @@ -107,9 +108,10 @@ public class BlacksmithTrait extends Trait { * @return

True if preparations were successful. False if a session shouldn't be started

*/ public boolean prepareForSession(Player player) { + UUID playerId = player.getUniqueId(); //If cool-down has been disabled after it was set for this player, remove the cool-down - if (config.getDisableCoolDown() && coolDowns.get(player.getUniqueId()) != null) { - coolDowns.remove(player.getUniqueId()); + if (config.getDisableCoolDown() && coolDowns.get(playerId) != null) { + coolDowns.remove(playerId); } //Deny if permission is missing if (!player.hasPermission("blacksmith.reforge")) { @@ -117,17 +119,21 @@ public class BlacksmithTrait extends Trait { } //Deny if on cool-down, or remove cool-down if expired - if (coolDowns.get(player.getUniqueId()) != null) { - if (!Calendar.getInstance().after(coolDowns.get(player.getUniqueId()))) { - sendNPCMessage(this.npc, player, config.getCoolDownUnexpiredMessage()); + if (coolDowns.get(playerId) != null) { + Calendar calendar = Calendar.getInstance(); + if (!calendar.after(coolDowns.get(playerId))) { + int secondDifference = (int) ((calendar.getTimeInMillis() - coolDowns.get(playerId).getTimeInMillis()) * 1000); + boolean exactTime = BlacksmithPlugin.getInstance().getSettings().getShowExactTime(); + sendNPCMessage(this.npc, player, replacePlaceholder(config.getCoolDownUnexpiredMessage(), + "{time}", TimeFormatter.formatTime(exactTime, secondDifference))); return false; } - coolDowns.remove(player.getUniqueId()); + coolDowns.remove(playerId); } - //If already in a session, but the player has failed to interact, or left the blacksmith, allow a new session + //If already in a session, but the player has failed to interact, and left the blacksmith, allow a new session if (session != null) { - if (System.currentTimeMillis() > _sessionStart + 10 * 1000 || + if (System.currentTimeMillis() > _sessionStart + 10 * 1000 && this.npc.getEntity().getLocation().distance(session.getPlayer().getLocation()) > 20) { session = null; } @@ -149,7 +155,10 @@ public class BlacksmithTrait extends Trait { //The blacksmith is already reforging for the player if (session.isRunning()) { - sendNPCMessage(this.npc, player, config.getBusyReforgingMessage()); + int timeRemaining = (int) ((session.getFinishTime() - System.currentTimeMillis()) / 1000); + boolean showExactTime = BlacksmithPlugin.getInstance().getSettings().getShowExactTime(); + sendNPCMessage(this.npc, player, replacePlaceholder(config.getBusyReforgingMessage(), "{time}", + TimeFormatter.formatTime(showExactTime, timeRemaining))); return; } if (session.endSession()) { @@ -171,7 +180,7 @@ public class BlacksmithTrait extends Trait { //Refuse if not repairable, or if reforge-able items is set, but doesn't include the held item List reforgeAbleItems = config.getReforgeAbleItems(); if (!isRepairable(hand) || (!reforgeAbleItems.isEmpty() && !reforgeAbleItems.contains(hand.getType()))) { - String invalidMessage = StringFormatter.replacePlaceholder(config.getInvalidItemMessage(), + String invalidMessage = replacePlaceholder(config.getInvalidItemMessage(), "{title}", config.getBlacksmithTitle()); sendNPCMessage(this.npc, player, invalidMessage); return; @@ -189,7 +198,8 @@ public class BlacksmithTrait extends Trait { //Tell the player the cost of repairing the item String cost = EconomyManager.formatCost(player); String itemName = hand.getType().name().toLowerCase().replace('_', ' '); - sendNPCMessage(this.npc, player, config.getCostMessage().replace("{cost}", cost).replace("{item}", itemName)); + sendNPCMessage(this.npc, player, config.getCostMessage().replace("{cost}", cost).replace("{item}", + itemName)); } /** diff --git a/src/main/java/net/knarcraft/blacksmith/trait/ReforgeSession.java b/src/main/java/net/knarcraft/blacksmith/trait/ReforgeSession.java index 80c8569..3937ecc 100644 --- a/src/main/java/net/knarcraft/blacksmith/trait/ReforgeSession.java +++ b/src/main/java/net/knarcraft/blacksmith/trait/ReforgeSession.java @@ -12,6 +12,7 @@ import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.Damageable; import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.scheduler.BukkitScheduler; import java.util.ArrayList; import java.util.Calendar; @@ -32,6 +33,7 @@ public class ReforgeSession implements Runnable { private final NPC npc; private final ItemStack itemToReforge; private int taskId; + private long finishTime = 0; private final NPCSettings config; private static final String[] enchantments = new String[Enchantment.values().length]; private static final Random random = new Random(); @@ -48,15 +50,27 @@ public class ReforgeSession implements Runnable { this.blacksmithTrait = blacksmithTrait; this.player = player; this.npc = npc; - itemToReforge = player.getInventory().getItemInMainHand(); + this.itemToReforge = player.getInventory().getItemInMainHand(); this.config = config; - int i = 0; - for (Enchantment enchantment : Enchantment.values()) { - enchantments[i++] = enchantment.getKey().toString(); + //Populate enchantments the first time this is run + if (enchantments[0] == null) { + int i = 0; + for (Enchantment enchantment : Enchantment.values()) { + enchantments[i++] = enchantment.getKey().toString(); + } } } + /** + * Gets the time in milliseconds when this reforging session will finish + * + * @return

The time the reforging will finish

+ */ + public long getFinishTime() { + return this.finishTime; + } + /** * Runs the actual reforge which fixes the item and gives it back to the player */ @@ -237,16 +251,17 @@ public class ReforgeSession implements Runnable { * Begins the actual item reforging */ public void beginReforge() { + BukkitScheduler scheduler = BlacksmithPlugin.getInstance().getServer().getScheduler(); + int reforgeDelay; if (!config.getDisableCoolDown()) { //Finish the reforging after a random delay between the max and min - taskId = BlacksmithPlugin.getInstance().getServer().getScheduler().scheduleSyncDelayedTask( - BlacksmithPlugin.getInstance(), this, (new Random().nextInt(config.getMaxReforgeDelay()) + - config.getMinReforgeDelay()) * 20L); + reforgeDelay = new Random().nextInt(config.getMaxReforgeDelay()) + config.getMinReforgeDelay(); } else { //Finish the reforging as soon as possible - taskId = BlacksmithPlugin.getInstance().getServer().getScheduler().scheduleSyncDelayedTask( - BlacksmithPlugin.getInstance(), this, 0); + reforgeDelay = 0; } + this.finishTime = System.currentTimeMillis() + (reforgeDelay * 1000L); + taskId = scheduler.scheduleSyncDelayedTask(BlacksmithPlugin.getInstance(), this, reforgeDelay * 20L); } /** diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 24e9f4e..9a6ff38 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -21,6 +21,10 @@ global: # blacksmith behavior instead useNaturalCost: true + # Exact time displays the exact number of seconds and minutes remaining as part of the reforging cool-down and + # reforging delay messages, instead of just vaguely hinting at the remaining time. + showExactTime: false + # The settings which are set to any new NPC. To change any of these settings for an existing NPC, you must change the # Citizens NPC file, or use the /blacksmith command defaults: @@ -61,10 +65,10 @@ defaults: busyPlayerMessage: "&cI'm busy at the moment. Come back later!" # The message to display when the blacksmith is working on the reforging - busyReforgeMessage: "&cI'm working on it. Be patient!" + busyReforgeMessage: "&cI'm working on it. Be patient! I'll finish {time}!" # The message to display when the blacksmith is still on a cool-down from the previous re-forging - coolDownUnexpiredMessage: "&cYou've already had your chance! Give me a break!" + coolDownUnexpiredMessage: "&cYou've already had your chance! Give me a break! I'll be ready {time}!" # The message to display when informing a player about the reforging cost costMessage: "&eIt will cost &a{cost}&e to reforge that &a{item}&e! Click again to reforge!" diff --git a/src/main/resources/strings.yml b/src/main/resources/strings.yml index a2dce8c..cd314d7 100644 --- a/src/main/resources/strings.yml +++ b/src/main/resources/strings.yml @@ -17,4 +17,15 @@ en: PLUGIN_RELOADED: "Blacksmith config reloaded!" INVALID_FILTER_FOR_PRESET: "The specified filter is not valid for that preset" INVALID_PRESET_OR_FILTER: "You specified an invalid preset or an invalid filter" - PRESET_MATERIALS: "Materials in preset: {materials}" \ No newline at end of file + PRESET_MATERIALS: "Materials in preset: {materials}" + DURATION_FORMAT: "{time} {unit}" + UNIT_NOW: "imminently" + UNIT_SECOND: "second" + UNIT_SECONDS: "seconds" + UNIT_MINUTE: "minute" + UNIT_MINUTES: "minutes" + INTERVAL_LESS_THAN_10_SECONDS: "momentarily" + INTERVAL_LESS_THAN_30_SECONDS: "in a little while" + INTERVAL_LESS_THAN_1_MINUTE: "in a while" + INTERVAL_LESS_THAN_5_MINUTES: "after some time" + INTERVAL_MORE_THAN_5_MINUTES: "in quite a while" \ No newline at end of file