diff --git a/docs/api/CustomBoostEffect.md b/docs/api/CustomBoostEffect.md index 9088a21..fe8d1e5 100644 --- a/docs/api/CustomBoostEffect.md +++ b/docs/api/CustomBoostEffect.md @@ -10,66 +10,74 @@ ```java public interface CustomBoostEffect { /** - * Called when the boost is activated for a player. - * @param player The player receiving the boost. - * @param boost The boost definition. + * Apply the effect to the player. + * @param player the player to apply the effect to + * @param amplifier effect strength as configured in the boost */ - void onEnable(Player player, BoostDefinition boost); + void apply(org.bukkit.entity.Player player, int amplifier); /** - * Called when the boost is deactivated for a player. - * @param player The player losing the boost. - * @param boost The boost definition. + * Remove the effect from the player. + * @param player the player to remove the effect from */ - void onDisable(Player player, BoostDefinition boost); + void remove(org.bukkit.entity.Player player); /** - * @return The unique effect type key (e.g., "mycustom"). + * @return unique effect name used in configuration */ - String getType(); + String getName(); + + /** + * @return cooldown duration in seconds (default 0 = no cooldown) + */ + default int getCooldownSeconds() { + return 0; + } } ``` ## Methods -### `void onEnable(Player player, BoostDefinition boost)` -Called when the boost is activated for a player. -- **player**: The player receiving the boost. -- **boost**: The boost definition being applied. +### `void apply(Player player, int amplifier)` +Called when the boost's custom effect should be applied to a player. +- **player**: The player receiving the effect. +- **amplifier**: The configured amplifier for this effect in the boost. **Usage Example:** ```java @Override -public void onEnable(Player player, BoostDefinition boost) { +public void apply(Player player, int amplifier) { player.sendMessage("Your custom boost is now active!"); } ``` --- -### `void onDisable(Player player, BoostDefinition boost)` -Called when the boost is deactivated for a player. -- **player**: The player losing the boost. -- **boost**: The boost definition being removed. +### `void remove(Player player)` +Called when the boost's custom effect should be removed from a player. +- **player**: The player losing the effect. **Usage Example:** ```java @Override -public void onDisable(Player player, BoostDefinition boost) { +public void remove(Player player) { player.sendMessage("Your custom boost has ended."); } ``` --- -### `String getType()` -Returns the unique effect type key. This is used in boost configuration to identify the effect. -- **Returns**: The effect type key (e.g., "mycustom"). +### `String getName()` +Returns the unique effect name used in boost configuration to identify the custom effect. +- **Returns**: The effect name (e.g., "mycustom"). + +### `int getCooldownSeconds()` +Returns the cooldown duration (in seconds) associated with this custom effect. When `settings.cooldown-per-effect` is enabled, this value is used to set per-effect cooldown timestamps after activation. Returning `0` means no cooldown. **Usage Example:** ```java @Override -public String getType() { +public String getName() { return "mycustom"; } ``` @@ -86,19 +94,19 @@ import org.bukkit.entity.Player; public class MyCustomEffect implements CustomBoostEffect { @Override - public void onEnable(Player player, BoostDefinition boost) { + public void apply(Player player, int amplifier) { player.sendMessage("You received a special custom effect!"); // Add your custom logic here } @Override - public void onDisable(Player player, BoostDefinition boost) { + public void remove(Player player) { player.sendMessage("Your special custom effect has ended."); // Cleanup logic here } @Override - public String getType() { + public String getName() { return "mycustom"; } } @@ -106,7 +114,7 @@ public class MyCustomEffect implements CustomBoostEffect { // Register the effect in your plugin's onEnable method: @Override public void onEnable() { - EzBoostAPI.registerCustomEffect(new MyCustomEffect()); + EzBoostAPI.registerCustomBoostEffect(new MyCustomEffect()); } ``` diff --git a/docs/api/EzBoostAPI.md b/docs/api/EzBoostAPI.md index 756031a..e6a924f 100644 --- a/docs/api/EzBoostAPI.md +++ b/docs/api/EzBoostAPI.md @@ -15,68 +15,45 @@ public final class EzBoostAPI { ## Methods -### `static void registerCustomEffect(CustomBoostEffect effect)` -Registers a custom boost effect implementation. This allows other plugins to add new effect types that will be executed when a boost is activated or deactivated. -- **effect**: The custom effect implementation to register. Must implement `CustomBoostEffect`. +### `static boolean registerCustomBoostEffect(CustomBoostEffect effect)` +Register a custom boost effect implementation so other plugins can provide new effect types. +- **effect**: The `CustomBoostEffect` implementation to register. +- **Returns**: `true` if registration succeeded; `false` if the API isn't initialized or the effect name is already registered. **Usage Example:** ```java -EzBoostAPI.registerCustomEffect(new MyCustomEffect()); +boolean ok = EzBoostAPI.registerCustomBoostEffect(new MyCustomEffect()); ``` --- -### `static boolean isBoostActive(Player player)` -Checks if a player currently has an active boost. -- **player**: The player to check. -- **Returns**: `true` if the player has an active boost, `false` otherwise. +### `static Map getCustomBoostEffects()` +Returns an unmodifiable map of all registered custom boost effects keyed by their normalized names. **Usage Example:** ```java -if (EzBoostAPI.isBoostActive(player)) { - // Player has a boost -} -``` - ---- - -### `static BoostDefinition getActiveBoost(Player player)` -Gets the active `BoostDefinition` for a player, or `null` if none. -- **player**: The player to query. -- **Returns**: The active `BoostDefinition`, or `null` if no boost is active. - -**Usage Example:** -```java -BoostDefinition boost = EzBoostAPI.getActiveBoost(player); -if (boost != null) { - // Do something with the boost -} +Map effects = EzBoostAPI.getCustomBoostEffects(); ``` --- -### `static void clearBoost(Player player)` -Removes any active boost from a player. -- **player**: The player whose boost should be cleared. +### `static BoostManager getBoostManager()` +Returns the internal `BoostManager` instance. This exposes advanced integration points but should be used carefully. **Usage Example:** ```java -EzBoostAPI.clearBoost(player); +BoostManager manager = EzBoostAPI.getBoostManager(); ``` --- -### `static void setBoost(Player player, BoostDefinition definition, int durationSeconds)` -Forces a boost to start for a player with a specific definition and duration. -- **player**: The player to receive the boost. -- **definition**: The `BoostDefinition` to apply. -- **durationSeconds**: The duration of the boost in seconds. +### `static long getCooldownRemainingForEffect(Player player, com.skyblockexp.ezboost.boost.BoostEffect effect)` +Convenience helper that returns remaining cooldown (in seconds) for a specific `BoostEffect` on a player. Returns `0` if no cooldown is present or the API is not initialized. **Usage Example:** ```java -EzBoostAPI.setBoost(player, myBoostDefinition, 120); +long remaining = EzBoostAPI.getCooldownRemainingForEffect(player, myBoostEffect); ``` - ## Notes - All methods are static for ease of use. - Designed for open-source extensibility and professional integrations. diff --git a/docs/boosts.md b/docs/boosts.md index 64f165c..f964985 100644 --- a/docs/boosts.md +++ b/docs/boosts.md @@ -52,4 +52,10 @@ speed: - **Permission**: Controls who can use each boost. - **Enabled**: Set to `false` to hide a boost from the GUI. +### Per-effect cooldowns and custom effects + +- EzBoost supports optional per-effect cooldown tracking. When `settings.cooldown-per-effect` is enabled (see `settings.yml`), each effect in a boost may have its own cooldown tracked independently. This is useful when a boost contains multiple effects and you want separate cooldowns for each. +- For built-in potion effects the boost uses the boost-level `cooldown` unless a custom effect provides its own cooldown value. +- For custom effects, implement `CustomBoostEffect.getCooldownSeconds()` to return the desired cooldown (default 0 = no cooldown). See the API docs for details. + For more details, see the [EzBoost documentation](https://github.com/ez-plugins/ezboost). diff --git a/docs/config.md b/docs/config.md index b902b8c..9f6c7e4 100644 --- a/docs/config.md +++ b/docs/config.md @@ -29,6 +29,17 @@ settings: cooldown-per-boost-type: true # Separate cooldowns for each boost type ``` +The plugin now supports optional per-effect cooldowns. When enabled, each effect (including custom effects) can define its own cooldown duration. To enable per-effect cooldown tracking, set: + +```yaml +settings: + cooldown-per-effect: false # When true, cooldowns are tracked per-effect instead of per-boost +``` + +Behavior: +- `cooldown-per-boost-type: true` (legacy) keeps the existing per-boost-type cooldown behavior. +- `cooldown-per-effect: true` enables per-effect cooldown checks. Lookups will fall back to the boost-level/global cooldown if no effect-specific cooldown is present. + --- ## limits.yml diff --git a/src/main/java/com/skyblockexp/ezboost/api/EzBoostAPI.java b/src/main/java/com/skyblockexp/ezboost/api/EzBoostAPI.java index aa8263d..84fd609 100644 --- a/src/main/java/com/skyblockexp/ezboost/api/EzBoostAPI.java +++ b/src/main/java/com/skyblockexp/ezboost/api/EzBoostAPI.java @@ -41,4 +41,13 @@ public static Map getCustomBoostEffects() { public static BoostManager getBoostManager() { return boostManager; } + + /** + * Convenience API: get cooldown remaining (seconds) for a specific effect on a player. + * Returns 0 if API not initialized or inputs invalid. + */ + public static long getCooldownRemainingForEffect(org.bukkit.entity.Player player, com.skyblockexp.ezboost.boost.BoostEffect effect) { + if (boostManager == null || player == null || effect == null) return 0L; + return boostManager.getCooldownRemainingForEffect(player, effect); + } } diff --git a/src/main/java/com/skyblockexp/ezboost/boost/BoostManager.java b/src/main/java/com/skyblockexp/ezboost/boost/BoostManager.java index 770fc0d..8337dd1 100644 --- a/src/main/java/com/skyblockexp/ezboost/boost/BoostManager.java +++ b/src/main/java/com/skyblockexp/ezboost/boost/BoostManager.java @@ -159,10 +159,40 @@ public long getCooldownRemaining(Player player, String boostKey) { if (state == null) { return 0L; } - long remaining = state.cooldownEnd(cooldownKey(boostKey)) - System.currentTimeMillis(); + long now = System.currentTimeMillis(); + // If per-effect cooldowns are enabled, compute the maximum remaining across effects for the effective boost + if (config.settings().cooldownPerEffect()) { + Optional def = getBoost(boostKey, player); + if (def.isPresent()) { + long maxRem = 0L; + for (BoostEffect effect : def.get().effects()) { + long end = state.cooldownEnd(effectCooldownKey(effect)); + long rem = Math.max(0L, (end - now) / 1000L); + if (rem > maxRem) maxRem = rem; + } + // fallback to boost-level key as well + long boostRem = Math.max(0L, (state.cooldownEnd(cooldownKey(boostKey)) - now) / 1000L); + return Math.max(maxRem, boostRem); + } + } + long remaining = state.cooldownEnd(cooldownKey(boostKey)) - now; return Math.max(0L, remaining / 1000L); } + /** + * Returns remaining cooldown in seconds for a specific effect for the given player. + * Returns 0 if no cooldown is present. + */ + public long getCooldownRemainingForEffect(Player player, BoostEffect effect) { + if (player == null || effect == null) return 0L; + BoostState state = states.get(player.getUniqueId()); + if (state == null) return 0L; + long now = System.currentTimeMillis(); + long end = state.cooldownEnd(effectCooldownKey(effect)); + long rem = Math.max(0L, (end - now) / 1000L); + return rem; + } + public boolean activate(Player player, String boostKey, ActivationSource source) { Optional definition = getBoost(boostKey, player); @@ -210,11 +240,45 @@ public boolean activate(Player player, BoostDefinition boost, ActivationSource s player.sendMessage(messages.message("boost-replaced")); } if (!player.hasPermission("ezboost.cooldown.bypass")) { - long cooldownEnd = state.cooldownEnd(cooldownKey(effective.key())); - if (cooldownEnd > now) { - long remaining = Math.max(0L, (cooldownEnd - now) / 1000L); - player.sendMessage(messages.message("boost-cooldown", Placeholder.parsed("time", String.valueOf(remaining)))); - return false; + if (config.settings().cooldownPerEffect()) { + for (BoostEffect effect : effective.effects()) { + String key = effectCooldownKey(effect); + long end = state.cooldownEnd(key); + if (end > now) { + long remaining = Math.max(0L, (end - now) / 1000L); + // Determine a human-friendly effect name + String effectName; + if (effect.type() != null) { + effectName = effect.type().getName(); + } else { + CustomBoostEffect custom = customEffects.get(effect.customName().toLowerCase(Locale.ROOT)); + effectName = custom != null ? custom.getName() : effect.customName(); + } + try { + player.sendMessage(messages.message("boost-effect-cooldown", + Placeholder.parsed("time", String.valueOf(remaining)), + Placeholder.parsed("effect", effectName))); + } catch (Exception ex) { + // Fallback to generic message if key missing + player.sendMessage(messages.message("boost-cooldown", Placeholder.parsed("time", String.valueOf(remaining)))); + } + return false; + } + } + // also check boost-level fallback + long cooldownEnd = state.cooldownEnd(cooldownKey(effective.key())); + if (cooldownEnd > now) { + long remaining = Math.max(0L, (cooldownEnd - now) / 1000L); + player.sendMessage(messages.message("boost-cooldown", Placeholder.parsed("time", String.valueOf(remaining)))); + return false; + } + } else { + long cooldownEnd = state.cooldownEnd(cooldownKey(effective.key())); + if (cooldownEnd > now) { + long remaining = Math.max(0L, (cooldownEnd - now) / 1000L); + player.sendMessage(messages.message("boost-cooldown", Placeholder.parsed("time", String.valueOf(remaining)))); + return false; + } } } double cost = effective.cost(); @@ -250,8 +314,27 @@ public boolean activate(Player player, BoostDefinition boost, ActivationSource s } long endTimestamp = now + (effective.durationSeconds() * 1000L); state.setActiveBoost(effective.key(), endTimestamp); - if (effective.cooldownSeconds() > 0) { - state.setCooldownEnd(cooldownKey(effective.key()), now + (effective.cooldownSeconds() * 1000L)); + if (config.settings().cooldownPerEffect()) { + for (BoostEffect effect : effective.effects()) { + int seconds = 0; + if (effect.type() == null) { + CustomBoostEffect custom = customEffects.get(effect.customName().toLowerCase(Locale.ROOT)); + if (custom != null) { + seconds = custom.getCooldownSeconds(); + } else { + seconds = effective.cooldownSeconds(); + } + } else { + seconds = effective.cooldownSeconds(); + } + if (seconds > 0) { + state.setCooldownEnd(effectCooldownKey(effect), now + (seconds * 1000L)); + } + } + } else { + if (effective.cooldownSeconds() > 0) { + state.setCooldownEnd(cooldownKey(effective.key()), now + (effective.cooldownSeconds() * 1000L)); + } } scheduleExpiry(player, effective, endTimestamp); scheduleActionbar(player, effective); @@ -472,6 +555,17 @@ private String cooldownKey(String boostKey) { return GLOBAL_COOLDOWN_KEY; } + private String effectCooldownKey(BoostEffect effect) { + if (effect == null) return GLOBAL_COOLDOWN_KEY; + if (effect.type() != null) { + String name = effect.type().getName(); + return ("effect:potion:" + name).toLowerCase(Locale.ROOT); + } else { + String cname = effect.customName() != null ? effect.customName() : ""; + return ("effect:custom:" + cname).toLowerCase(Locale.ROOT); + } + } + private void sendActionBar(Player player, String message) { if (message == null || message.isBlank()) { return; diff --git a/src/main/java/com/skyblockexp/ezboost/boost/CustomBoostEffect.java b/src/main/java/com/skyblockexp/ezboost/boost/CustomBoostEffect.java index 1849324..a7792dc 100644 --- a/src/main/java/com/skyblockexp/ezboost/boost/CustomBoostEffect.java +++ b/src/main/java/com/skyblockexp/ezboost/boost/CustomBoostEffect.java @@ -20,4 +20,12 @@ public interface CustomBoostEffect { * @return unique effect name */ String getName(); + + /** + * @return cooldown duration in seconds (default 0 = no cooldown) + */ + default int getCooldownSeconds() { + return 0; + } + } diff --git a/src/main/java/com/skyblockexp/ezboost/config/EzBoostConfig.java b/src/main/java/com/skyblockexp/ezboost/config/EzBoostConfig.java index f79be66..a94a1f4 100644 --- a/src/main/java/com/skyblockexp/ezboost/config/EzBoostConfig.java +++ b/src/main/java/com/skyblockexp/ezboost/config/EzBoostConfig.java @@ -62,7 +62,8 @@ public void reload() { config.getBoolean("settings.keep-boost-on-death", true), config.getBoolean("settings.reapply-on-join", true), config.getBoolean("settings.send-expired-message", true), - config.getBoolean("settings.cooldown-per-boost-type", true) + config.getBoolean("settings.cooldown-per-boost-type", true), + config.getBoolean("settings.cooldown-per-effect", false) ); limits = new Limits( config.getInt("limits.duration-min", 5), @@ -539,7 +540,8 @@ public record Settings(boolean replaceActiveBoost, boolean keepBoostOnDeath, boolean reapplyOnJoin, boolean sendExpiredMessage, - boolean cooldownPerBoostType) { + boolean cooldownPerBoostType, + boolean cooldownPerEffect) { } public record Limits(int durationMin, int durationMax, int amplifierMin, int amplifierMax) { diff --git a/src/main/java/com/skyblockexp/ezboost/gui/BoostGui.java b/src/main/java/com/skyblockexp/ezboost/gui/BoostGui.java index 9d6c2f5..600f4f5 100644 --- a/src/main/java/com/skyblockexp/ezboost/gui/BoostGui.java +++ b/src/main/java/com/skyblockexp/ezboost/gui/BoostGui.java @@ -125,16 +125,22 @@ private ItemStack createBoostItem(Player player, BoostDefinition boost) { } ItemMetaCompat.setDisplayName(meta, miniMessage.deserialize(boost.displayName())); List lore = new ArrayList<>(); - // Add effect info to lore + // Add effect info to lore (include per-effect cooldown remaining when applicable) for (BoostEffect effect : boost.effects()) { + String effectLine; if (effect.type() != null) { - lore.add(Component.text("Effect: " + effect.type().getName() + " (" + effect.amplifier() + ")")); + effectLine = "Effect: " + effect.type().getName() + " (" + effect.amplifier() + ")"; } else { - // Try to show custom effect name - for (var custom : boostManager.getCustomEffects().values()) { - lore.add(Component.text("Effect: " + custom.getName() + " (" + effect.amplifier() + ")")); - } + CustomBoostEffect custom = boostManager.getCustomEffect(effect.customName()); + String name = custom != null ? custom.getName() : effect.customName(); + effectLine = "Effect: " + name + " (" + effect.amplifier() + ")"; + } + // If per-effect cooldowns are enabled, append remaining seconds if present + long rem = boostManager.getCooldownRemainingForEffect(player, effect); + if (rem > 0) { + effectLine += " - Cooldown: " + rem + "s"; } + lore.add(Component.text(effectLine)); } String status = statusFor(player, boost); for (String line : settings.loreLines()) { diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml index e3a85b7..63be754 100644 --- a/src/main/resources/messages.yml +++ b/src/main/resources/messages.yml @@ -27,6 +27,7 @@ boost-replaced: "Your active boost was replaced." boost-activated: "Boost activated: ." boost-expired: "Your boost has expired." boost-cooldown: "That boost is on cooldown for seconds." +boost-effect-cooldown: "The effect is on cooldown for seconds." insufficient-funds: "You need to activate this boost." economy-unavailable: "Economy is unavailable. Please contact an admin." cost-charged: "Charged for ." diff --git a/src/main/resources/settings.yml b/src/main/resources/settings.yml index baef8f5..b41b8c7 100644 --- a/src/main/resources/settings.yml +++ b/src/main/resources/settings.yml @@ -5,3 +5,7 @@ settings: reapply-on-join: true send-expired-message: true cooldown-per-boost-type: true + # When true, cooldowns are tracked per-effect (including custom effects). + # Effect-specific cooldowns use the effect's canonical key; lookups fall back + # to the boost-level/global cooldown when no effect-specific cooldown exists. + cooldown-per-effect: false