diff --git a/plugins/exilepearl-paper/src/main/java/com/devotedmc/ExilePearl/ExilePearlApi.java b/plugins/exilepearl-paper/src/main/java/com/devotedmc/ExilePearl/ExilePearlApi.java index f9249e169c..b9b6eeaf8d 100644 --- a/plugins/exilepearl-paper/src/main/java/com/devotedmc/ExilePearl/ExilePearlApi.java +++ b/plugins/exilepearl-paper/src/main/java/com/devotedmc/ExilePearl/ExilePearlApi.java @@ -25,6 +25,18 @@ public interface ExilePearlApi extends Plugin, PearlAccess, PearlLogger, PlayerP */ PearlConfig getPearlConfig(); + /** + * Gets the effective per-tick decay amount for a pearl, accounting for external modifiers + * (e.g. vote-streak multipliers from other plugins) applied via {@link com.devotedmc.ExilePearl.event.PearlDecayEvent}. + *

+ * This fires a preview decay event and reads back the resulting amount, so lore and the real + * decay loop agree on how fast a pearl actually decays. + * + * @param pearl The pearl to evaluate + * @return The decay amount that would be applied per decay tick, or 0 if decay is cancelled + */ + int getEffectivePearlDecayAmount(ExilePearl pearl); + /** * Gets the pearl lore provider * diff --git a/plugins/exilepearl-paper/src/main/java/com/devotedmc/ExilePearl/core/CoreLoreGenerator.java b/plugins/exilepearl-paper/src/main/java/com/devotedmc/ExilePearl/core/CoreLoreGenerator.java index ab9b2e4c99..a1b39a817c 100644 --- a/plugins/exilepearl-paper/src/main/java/com/devotedmc/ExilePearl/core/CoreLoreGenerator.java +++ b/plugins/exilepearl-paper/src/main/java/com/devotedmc/ExilePearl/core/CoreLoreGenerator.java @@ -85,10 +85,11 @@ private List generateLoreInternal(ExilePearl pearl, int health, boolean lore.add(parse("Health: %s/%s", health, config.getPearlHealthMaxValue())); String unit = config.getPearlHealthDecayHumanInterval(); + int effectiveDecayAmount = ExilePearlPlugin.getApi().getEffectivePearlDecayAmount(pearl); int decayPerHumanInterval = PearlDecayMath.decayPerHumanInterval( config.getPearlHealthDecayHumanIntervalMin(), config.getPearlHealthDecayIntervalMin(), - config.getPearlHealthDecayAmount()); + effectiveDecayAmount); int intervalsRemaining = PearlDecayMath.intervalsRemaining(health, decayPerHumanInterval); if (intervalsRemaining > 0 && pearl.isActive()) { lore.add(parse("Time remaining: %d %s", intervalsRemaining, unit)); diff --git a/plugins/exilepearl-paper/src/main/java/com/devotedmc/ExilePearl/core/ExilePearlCore.java b/plugins/exilepearl-paper/src/main/java/com/devotedmc/ExilePearl/core/ExilePearlCore.java index 4d097af8ab..c1c95eee8d 100644 --- a/plugins/exilepearl-paper/src/main/java/com/devotedmc/ExilePearl/core/ExilePearlCore.java +++ b/plugins/exilepearl-paper/src/main/java/com/devotedmc/ExilePearl/core/ExilePearlCore.java @@ -3,6 +3,7 @@ import com.devotedmc.ExilePearl.*; import com.devotedmc.ExilePearl.command.*; import com.devotedmc.ExilePearl.config.PearlConfig; +import com.devotedmc.ExilePearl.event.PearlDecayEvent; import com.devotedmc.ExilePearl.holder.PearlHolder; import com.devotedmc.ExilePearl.listener.BanStickListener; import com.devotedmc.ExilePearl.listener.BastionListener; @@ -259,6 +260,13 @@ public PearlConfig getPearlConfig() { return pearlConfig; } + @Override + public int getEffectivePearlDecayAmount(ExilePearl pearl) { + PearlDecayEvent preview = new PearlDecayEvent(pearl, pearlConfig.getPearlHealthDecayAmount(), true); + getServer().getPluginManager().callEvent(preview); + return preview.isCancelled() ? 0 : preview.getDamageAmount(); + } + /** * Gets the plugin storage provider * diff --git a/plugins/exilepearl-paper/src/main/java/com/devotedmc/ExilePearl/event/PearlDecayEvent.java b/plugins/exilepearl-paper/src/main/java/com/devotedmc/ExilePearl/event/PearlDecayEvent.java index 3694fd9e3b..f7370f4826 100644 --- a/plugins/exilepearl-paper/src/main/java/com/devotedmc/ExilePearl/event/PearlDecayEvent.java +++ b/plugins/exilepearl-paper/src/main/java/com/devotedmc/ExilePearl/event/PearlDecayEvent.java @@ -22,6 +22,7 @@ public class PearlDecayEvent extends Event implements Cancellable { private boolean cancelled; private ExilePearl pearl; private int amount; + private final boolean preview; /** * Creates a new PearlDecayEvent instance. @@ -30,9 +31,30 @@ public class PearlDecayEvent extends Event implements Cancellable { * @param amount Health amount the pearl health is reduced by */ public PearlDecayEvent(ExilePearl pearl, int amount) { + this(pearl, amount, false); + } + + /** + * Creates a new PearlDecayEvent instance. + * + * @param pearl Pearl to decay + * @param amount Health amount the pearl health is reduced by + * @param preview true when fired only to compute the effective decay amount (e.g. for lore), + * without any health actually being removed. Listeners that apply side effects + * (logging, metrics) should skip preview events; modifiers may still adjust the amount. + */ + public PearlDecayEvent(ExilePearl pearl, int amount, boolean preview) { Preconditions.checkNotNull(pearl, "pearl"); this.pearl = pearl; this.amount = amount; + this.preview = preview; + } + + /** + * @return true when this event is a dry-run to compute the effective decay amount, not a real decay + */ + public boolean isPreview() { + return preview; } /** diff --git a/plugins/exilepearl-paper/src/test/java/com/devotedmc/ExilePearl/core/CoreLoreGeneratorTest.java b/plugins/exilepearl-paper/src/test/java/com/devotedmc/ExilePearl/core/CoreLoreGeneratorTest.java index bf7615c7f6..98dac4c331 100644 --- a/plugins/exilepearl-paper/src/test/java/com/devotedmc/ExilePearl/core/CoreLoreGeneratorTest.java +++ b/plugins/exilepearl-paper/src/test/java/com/devotedmc/ExilePearl/core/CoreLoreGeneratorTest.java @@ -21,6 +21,7 @@ class CoreLoreGeneratorTest { private PearlConfig config; private ExilePearl pearl; private CoreLoreGenerator generator; + private ExilePearlApi api; private MockedStatic pluginStatic; @BeforeEach @@ -47,8 +48,9 @@ void setUp() { Mockito.when(pearl.isActive()).thenReturn(true); Mockito.when(pearl.getLongTimeMultiplier()).thenReturn(1.0); - ExilePearlApi api = Mockito.mock(ExilePearlApi.class); + api = Mockito.mock(ExilePearlApi.class); Mockito.when(api.isBanStickEnabled()).thenReturn(false); + Mockito.when(api.getEffectivePearlDecayAmount(Mockito.any())).thenReturn(1); // base, no streak multiplier pluginStatic = Mockito.mockStatic(ExilePearlPlugin.class); pluginStatic.when(ExilePearlPlugin::getApi).thenReturn(api); @@ -86,11 +88,22 @@ void generateLore_omitsTimeRemainingWhenHealthIsZero() { @Test void generateLore_omitsTimeRemainingWhenDecayDisabled() { - Mockito.when(config.getPearlHealthDecayAmount()).thenReturn(0); + Mockito.when(api.getEffectivePearlDecayAmount(Mockito.any())).thenReturn(0); List lore = generator.generateLore(pearl); Assertions.assertNull(findLine(lore, "Time remaining:"), "Decay disabled should hide time remaining; got: " + lore); } + @Test + void generateLore_timeRemainingReflectsStreakMultiplier() { + // EssenceGlue doubles decay for a streaked player: 24/day -> 48/day, so 240 health = 5 days, not 10 + Mockito.when(api.getEffectivePearlDecayAmount(Mockito.any())).thenReturn(2); + List lore = generator.generateLore(pearl); + String timeRemaining = findLine(lore, "Time remaining:"); + Assertions.assertNotNull(timeRemaining); + Assertions.assertTrue(timeRemaining.contains("5"), + "Doubled decay should halve time remaining (240/48 = 5 days), got: " + timeRemaining); + } + @Test void generateLore_timeRemainingRoundsUpForPartialInterval() { Mockito.when(pearl.getHealth()).thenReturn(241); // 24*10 + 1