From c84d2406395ffde3172747bf48a4cfdb264eb832 Mon Sep 17 00:00:00 2001 From: Ally Piechowski Date: Sat, 27 Jun 2026 14:01:20 +0800 Subject: [PATCH] Show real decay rate in pearl time remaining The pearl tooltip computed "Time remaining" and "Cost per X" from the base decay amount in config, but EssenceGlue speeds decay up by the prisoner's vote streak (1 + streak * 0.5) through a PearlDecayEvent listener. So a streaked prisoner's pearl drained far faster than the tag claimed. One freed in about 27 hours while reading "2 day", which is what the bug reports kept describing. Lore now asks for the effective decay amount via a preview PearlDecayEvent, the same event the real decay loop fires, so the displayed rate matches what actually happens. ExilePearl stays unaware of EssenceGlue; the event is the seam. The preview flag lets side-effecting listeners (logging, metrics) skip these dry runs while modifiers still adjust the amount. The streak drifts down as the prisoner stays offline, so the timer is a current estimate, not a fixed countdown. That's the honest best we can show without predicting future logins. --- .../devotedmc/ExilePearl/ExilePearlApi.java | 12 ++++++++++ .../ExilePearl/core/CoreLoreGenerator.java | 3 ++- .../ExilePearl/core/ExilePearlCore.java | 8 +++++++ .../ExilePearl/event/PearlDecayEvent.java | 22 +++++++++++++++++++ .../core/CoreLoreGeneratorTest.java | 17 ++++++++++++-- 5 files changed, 59 insertions(+), 3 deletions(-) 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