From e504ad7c099c8f7bd744fd51cf08cfb2a1f40f8e Mon Sep 17 00:00:00 2001 From: XxFran10xX <318299142+XxFran10xX@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:39:55 +0200 Subject: [PATCH] Make passive drift work and split the meditation ceiling between users Resonance decay and equilibrium drift were lost every tick: the per-second step (0.02/h is about 0.0000056) fell below the 0.0001 change threshold and the two-decimal rounding. ResonanceSession now carries the remainder and applies it in whole hundredths, only while the character is online. Profiles are saved when a value actually moves instead of every second. Meditation capped resonance at the circle's total stored aura, and the 1/n user split only applied within one sit, so standing up and sitting again got every sharer to the full total. The ceiling is now the sitter's share: each artifact's usable aura divided by its users in the last 7 days, summed per element. Characters already above their share keep it until decay lowers it. Co-Authored-By: Claude Opus 5.5 --- .../magic/meditation/MeditationCeiling.java | 11 +-- .../magic/meditation/MeditationCircle.java | 69 +++++++++++------- .../magic/meditation/MeditationSession.java | 6 +- .../magic/meditation/MeditationSitYield.java | 33 +++++++++ .../magic/service/EquilibriumService.java | 8 +- .../magic/service/ResonanceService.java | 7 +- .../magic/session/ResonanceSession.java | 73 +++++++++++++++++++ src/main/resources/config.yml | 2 + .../net/tfminecraft/magic/DomainTest.java | 12 +++ .../magic/MeditationCircleTest.java | 52 ++++++++++--- .../magic/MeditationSessionTest.java | 26 +++++-- .../tfminecraft/magic/SessionServiceTest.java | 56 ++++++++++++++ 12 files changed, 300 insertions(+), 55 deletions(-) diff --git a/src/main/java/net/tfminecraft/magic/meditation/MeditationCeiling.java b/src/main/java/net/tfminecraft/magic/meditation/MeditationCeiling.java index 5de90dc..85f3268 100644 --- a/src/main/java/net/tfminecraft/magic/meditation/MeditationCeiling.java +++ b/src/main/java/net/tfminecraft/magic/meditation/MeditationCeiling.java @@ -1,8 +1,9 @@ package net.tfminecraft.magic.meditation; /** - * Meditation cannot raise an element above the aura of that element stored in the circle. - * A character already at or above that total gains nothing; otherwise the hit is cut to the gap. + * Meditation cannot raise an element above the sitter's share of that element's aura in the + * circle (see {@link MeditationSitYield#elementShare}). A character already at or above the + * share gains nothing; otherwise the hit is cut to the gap. */ public final class MeditationCeiling { @@ -10,11 +11,11 @@ public final class MeditationCeiling { private MeditationCeiling() {} - public static double allowed(double current, double circlePower, double elementMax, double offered) { - if (offered <= 0.0 || circlePower <= 0.0) { + public static double allowed(double current, double share, double elementMax, double offered) { + if (offered <= 0.0 || share <= 0.0) { return 0.0; } - double ceiling = Math.min(circlePower, Math.max(0.0, elementMax)); + double ceiling = Math.min(share, Math.max(0.0, elementMax)); double headroom = ceiling - current; if (headroom <= EPSILON) { return 0.0; diff --git a/src/main/java/net/tfminecraft/magic/meditation/MeditationCircle.java b/src/main/java/net/tfminecraft/magic/meditation/MeditationCircle.java index e574802..c1b1047 100644 --- a/src/main/java/net/tfminecraft/magic/meditation/MeditationCircle.java +++ b/src/main/java/net/tfminecraft/magic/meditation/MeditationCircle.java @@ -105,7 +105,11 @@ public MeditationSitYield snapshotYield(long nowMs) { public MeditationSitYield snapshotYield(long nowMs, String extraCharacterId) { Map sessionCaps = new HashMap<>(); Map users = new HashMap<>(); + Map shares = new HashMap<>(); for (Furniture furniture : artifactPedestals) { + for (ItemStack item : pedestalItems(furniture)) { + addShares(item, nowMs, extraCharacterId, shares); + } ItemStack item = itemOn(furniture); UUID uuid = ArtifactIds.read(item); if (uuid == null) { @@ -113,11 +117,7 @@ public MeditationSitYield snapshotYield(long nowMs, String extraCharacterId) { } String artifactId = uuid.toString(); MeditationCache.ArtifactDef def = artifactByFurniture.get(furniture.getEntityId()); - int n = ArtifactCareStore.activeUserCount(item, nowMs); - if (countsAsExtraUser(item, extraCharacterId, nowMs)) { - n++; - } - n = Math.max(1, n); + int n = userCount(item, extraCharacterId, nowMs); Artifact aura = Artifact.fromItem(item); double fill = aura != null ? aura.getFill(def.elementId) : 0.0; double cap = aura != null ? aura.getCap(def.elementId) : 0.0; @@ -126,7 +126,43 @@ public MeditationSitYield snapshotYield(long nowMs, String extraCharacterId) { sessionCaps.put(artifactId, usable / n); users.put(artifactId, n); } - return new MeditationSitYield(sessionCaps, users); + return new MeditationSitYield(sessionCaps, users, shares); + } + + /** + * Adds one pedestal item's usable aura, divided by its users, to each element's share. + * Counts the same items as circle power: stored-aura artifacts, never charges. + */ + private static void addShares( + ItemStack item, long nowMs, String extraCharacterId, Map shares) { + if (ChargeIds.isCharge(item)) { + return; + } + Artifact aura = Artifact.fromItem(item); + if (aura == null || !aura.hasStoredAura()) { + return; + } + int n = userCount(item, extraCharacterId, nowMs); + double muffle = ArtifactCareStore.readMuffle(item); + for (String elementId : aura.getCappedElementIds()) { + if (ElementRegistry.getById(elementId) == null) { + continue; + } + double usable = ArtifactCareStore.usableFill( + aura.getFill(elementId), aura.getCap(elementId), muffle); + if (usable <= 0.0) { + continue; + } + shares.merge(elementId, usable / n, Double::sum); + } + } + + private static int userCount(ItemStack item, String extraCharacterId, long nowMs) { + int n = ArtifactCareStore.activeUserCount(item, nowMs); + if (countsAsExtraUser(item, extraCharacterId, nowMs)) { + n++; + } + return Math.max(1, n); } public ItemStack itemOn(Furniture furniture) { @@ -153,26 +189,9 @@ public Map getPowerByElement() { return powerByElement; } - /** Stored fill of one element across every artifact in the circle. */ - public double elementPower(String elementId) { - if (elementId == null || elementId.isBlank()) { - return 0.0; - } - Double exact = powerByElement.get(elementId); - if (exact != null) { - return exact; - } - for (Map.Entry entry : powerByElement.entrySet()) { - if (entry.getKey() != null && entry.getKey().equalsIgnoreCase(elementId)) { - return entry.getValue() != null ? entry.getValue() : 0.0; - } - } - return 0.0; - } - /** * True when this sit can still raise at least one artifact's element. - * The character's resonance in that element must sit below the circle's stored total. + * The character's resonance in that element must sit below their share of it. */ public boolean canGainResonance(Player player, ResonanceSession session, MeditationSitYield yield) { if (session == null || yield == null) { @@ -195,7 +214,7 @@ public boolean canGainResonance(Player player, ResonanceSession session, Meditat double elementMax = element != null ? element.getMaxResonance() : Double.MAX_VALUE; double allowed = MeditationCeiling.allowed( session.getResonance(def.elementId), - elementPower(def.elementId), + yield.elementShare(def.elementId), elementMax, cap); if (allowed > MeditationCeiling.EPSILON) { diff --git a/src/main/java/net/tfminecraft/magic/meditation/MeditationSession.java b/src/main/java/net/tfminecraft/magic/meditation/MeditationSession.java index b5f54fc..e91f6e9 100644 --- a/src/main/java/net/tfminecraft/magic/meditation/MeditationSession.java +++ b/src/main/java/net/tfminecraft/magic/meditation/MeditationSession.java @@ -504,7 +504,7 @@ private void applyRewards( double offered = Math.min(MeditationCache.resonancePerHit / n, remaining); double gain = MeditationCeiling.allowed( session.getResonance(artifact.elementId), - circle.elementPower(artifact.elementId), + yield.elementShare(artifact.elementId), elementMax(artifact.elementId), offered); if (gain <= 0) { @@ -516,7 +516,7 @@ private void applyRewards( AttunementCaptureService.credit(player, session, artifactId, artifact.elementId, gain); if (MeditationCeiling.allowed( session.getResonance(artifact.elementId), - circle.elementPower(artifact.elementId), + yield.elementShare(artifact.elementId), elementMax(artifact.elementId), remaining) <= 0) { closeElement(artifact.elementId); @@ -551,7 +551,7 @@ private void closeCappedElements(Player player, ResonanceSession session) { } double allowed = MeditationCeiling.allowed( session.getResonance(def.elementId), - circle.elementPower(def.elementId), + yield.elementShare(def.elementId), elementMax(def.elementId), cap); if (allowed <= MeditationCeiling.EPSILON) { diff --git a/src/main/java/net/tfminecraft/magic/meditation/MeditationSitYield.java b/src/main/java/net/tfminecraft/magic/meditation/MeditationSitYield.java index 35a7971..f2ed02e 100644 --- a/src/main/java/net/tfminecraft/magic/meditation/MeditationSitYield.java +++ b/src/main/java/net/tfminecraft/magic/meditation/MeditationSitYield.java @@ -2,6 +2,7 @@ import java.util.Collections; import java.util.HashMap; +import java.util.Locale; import java.util.Map; public final class MeditationSitYield { @@ -10,14 +11,31 @@ public final class MeditationSitYield { private final Map sessionCapByArtifact; private final Map usersByArtifact; + private final Map shareByElement; public MeditationSitYield(Map sessionCapByArtifact, Map usersByArtifact) { + this(sessionCapByArtifact, usersByArtifact, null); + } + + public MeditationSitYield( + Map sessionCapByArtifact, + Map usersByArtifact, + Map shareByElement) { this.sessionCapByArtifact = sessionCapByArtifact != null ? Map.copyOf(sessionCapByArtifact) : Map.of(); this.usersByArtifact = usersByArtifact != null ? Map.copyOf(usersByArtifact) : Map.of(); + Map shares = new HashMap<>(); + if (shareByElement != null) { + for (Map.Entry entry : shareByElement.entrySet()) { + if (entry.getKey() != null && entry.getValue() != null) { + shares.merge(key(entry.getKey()), entry.getValue(), Double::sum); + } + } + } + this.shareByElement = Map.copyOf(shares); } public static MeditationSitYield empty() { @@ -38,6 +56,17 @@ public int users(String artifactId) { return Math.max(1, usersByArtifact.getOrDefault(artifactId, 1)); } + /** + * This sitter's share of the circle's aura in one element: each artifact's usable + * aura divided by its users, summed. Meditation cannot raise the element above it. + */ + public double elementShare(String elementId) { + if (elementId == null || elementId.isBlank()) { + return 0.0; + } + return Math.max(0.0, shareByElement.getOrDefault(key(elementId), 0.0)); + } + public boolean hasAnyCap() { for (double cap : sessionCapByArtifact.values()) { if (cap > EPSILON) { @@ -66,4 +95,8 @@ public boolean exhausted(Map attunedByArtifact) { public Map sessionCaps() { return Collections.unmodifiableMap(sessionCapByArtifact); } + + private static String key(String elementId) { + return elementId.trim().toLowerCase(Locale.ROOT); + } } diff --git a/src/main/java/net/tfminecraft/magic/service/EquilibriumService.java b/src/main/java/net/tfminecraft/magic/service/EquilibriumService.java index 64f5a6f..cae7a7a 100644 --- a/src/main/java/net/tfminecraft/magic/service/EquilibriumService.java +++ b/src/main/java/net/tfminecraft/magic/service/EquilibriumService.java @@ -23,6 +23,7 @@ public static void tickOnlineSessions(ResonanceSessionManager sessionManager) { private static void tickSession(Player player, ResonanceSession session) { double eq = session.getEquilibrium(); if (eq == 0.0) { + session.clearEquilibriumDrift(); return; } @@ -30,14 +31,11 @@ private static void tickSession(Player player, ResonanceSession session) { if (eq < 0) { double ratePerHour = EquilibriumRates.corruptionDriftPerHour(eq); - eq -= ratePerHour * dtHours; - eq = Math.max(GuiCache.equilibriumMin, eq); + session.driftEquilibrium(-ratePerHour * dtHours, GuiCache.equilibriumMin); } else { - eq -= EquilibriumRates.tranquilityDecayPerHour(eq) * dtHours; - eq = Math.max(0.0, eq); + session.driftEquilibrium(-EquilibriumRates.tranquilityDecayPerHour(eq) * dtHours, 0.0); } - session.setEquilibrium(eq); if (Cache.debug) { Magic.plugin.getLogger().fine("[Magic] Equilibrium tick for " + player.getName() + ": " + MagicNumbers.format(session.getEquilibrium())); diff --git a/src/main/java/net/tfminecraft/magic/service/ResonanceService.java b/src/main/java/net/tfminecraft/magic/service/ResonanceService.java index bd0df0b..02a3c50 100644 --- a/src/main/java/net/tfminecraft/magic/service/ResonanceService.java +++ b/src/main/java/net/tfminecraft/magic/service/ResonanceService.java @@ -32,13 +32,14 @@ private static void tickSession(Player player, ResonanceSession session) { continue; } double current = session.getResonance(element.getId()); - if (ratePerHour < 0.0 && current <= EPSILON) { + if ((ratePerHour < 0.0 && current <= EPSILON) + || (ratePerHour > 0.0 && !element.isUnlocked(player))) { + session.clearResonanceDrift(element.getId()); continue; } - if (ratePerHour > 0.0 && !element.isUnlocked(player)) { + if (!session.driftResonance(element.getId(), ratePerHour * dtHours)) { continue; } - session.addResonance(element.getId(), ratePerHour * dtHours); dirty = true; if (Cache.debug) { Magic.plugin.getLogger().fine("[Magic] Resonance tick for " diff --git a/src/main/java/net/tfminecraft/magic/session/ResonanceSession.java b/src/main/java/net/tfminecraft/magic/session/ResonanceSession.java index 497f20f..0ce16c0 100644 --- a/src/main/java/net/tfminecraft/magic/session/ResonanceSession.java +++ b/src/main/java/net/tfminecraft/magic/session/ResonanceSession.java @@ -13,10 +13,14 @@ public final class ResonanceSession { private static final double EPSILON = 0.0001; + /** Stored values keep two decimals, so passive drift is applied in hundredths. */ + private static final double DRIFT_STEP = 0.01; private String castModeId; private double equilibrium; private final Map resonance = new HashMap<>(); + private final Map resonanceDrift = new HashMap<>(); + private double equilibriumDrift; private long modifierRevision; public ResonanceSession() { @@ -86,9 +90,78 @@ public void addResonance(String elementId, double delta) { setResonance(elementId, getResonance(elementId) + delta); } + /** + * Passive drift per tick is far below the two stored decimals, so it is carried here + * until it reaches whole hundredths and applied then. Hitting 0 or the element's max + * drops the remainder. Only kept in memory: offline time does not drift. + * + * @return true when the stored resonance changed + */ + public boolean driftResonance(String elementId, double delta) { + ElementDef element = ElementRegistry.getById(elementId); + if (element == null) { + return false; + } + String id = element.getId(); + double pending = resonanceDrift.getOrDefault(id, 0.0) + delta; + double step = wholeSteps(pending); + if (step == 0.0) { + resonanceDrift.put(id, pending); + return false; + } + double before = getResonance(id); + setResonance(id, before + step); + double after = getResonance(id); + if (Math.abs(after - before - step) > EPSILON) { + resonanceDrift.remove(id); + } else { + resonanceDrift.put(id, pending - step); + } + return after != before; + } + + public void clearResonanceDrift(String elementId) { + ElementDef element = ElementRegistry.getById(elementId); + if (element != null) { + resonanceDrift.remove(element.getId()); + } + } + + /** + * Moves equilibrium down by {@code delta} (negative) without passing {@code floor}, carried + * like {@link #driftResonance} until it reaches whole hundredths. + */ + public void driftEquilibrium(double delta, double floor) { + double pending = equilibriumDrift + delta; + double step = wholeSteps(pending); + if (step == 0.0) { + equilibriumDrift = pending; + return; + } + double target = equilibrium + step; + if (target < floor) { + setEquilibrium(floor); + equilibriumDrift = 0.0; + return; + } + setEquilibrium(target); + equilibriumDrift = pending - step; + } + + public void clearEquilibriumDrift() { + equilibriumDrift = 0.0; + } + + private static double wholeSteps(double pending) { + double steps = pending / DRIFT_STEP; + double whole = pending > 0.0 ? Math.floor(steps + 1e-9) : Math.ceil(steps - 1e-9); + return whole * DRIFT_STEP; + } + public void setResonanceMap(Map next) { boolean hadValues = !resonance.isEmpty(); resonance.clear(); + resonanceDrift.clear(); if (next != null) { for (Map.Entry entry : next.entrySet()) { if (entry.getKey() == null || entry.getValue() == null) { diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 703c055..b4f547e 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -95,6 +95,8 @@ attunement: users: ttl_days: 7 # yield: divide (sessionCap = usableFill / n, orbGain = min(resonance_per_hit / n, remaining)) + # ceiling: meditation cannot raise an element above the sitter's share, + # sum over the circle's artifacts of (usable fill of that element / n) # Muffle 0-1. Scales with tick.seconds_per_hour (3600 = real time). # Off: 1/24 per hour (full muffle in 24h). Recover: 1/168 per hour (clear in 7d). # Accepted: player inv, pedestal, artifact_display, item frames. diff --git a/src/test/java/net/tfminecraft/magic/DomainTest.java b/src/test/java/net/tfminecraft/magic/DomainTest.java index 7d144f9..23527d3 100644 --- a/src/test/java/net/tfminecraft/magic/DomainTest.java +++ b/src/test/java/net/tfminecraft/magic/DomainTest.java @@ -88,6 +88,18 @@ void sitYieldCopiesInputsAndTracksIndividualArtifactExhaustion() { assertFalse(MeditationSitYield.empty().hasAnyCap()); assertTrue(new MeditationSitYield(null, null).exhausted(null)); assertFalse(new MeditationSitYield(Map.of("a", -.1), null).hasAnyCap()); + assertEquals(0, yield.elementShare("fire")); + var shares = new HashMap(); + shares.put("Fire", 2.); + shares.put(" fire ", 1.); + shares.put(null, 5.); + shares.put("water", null); + shares.put("ice", -1.); + var split = new MeditationSitYield(null, null, shares); + shares.put("Fire", 20.); + assertEquals(3, split.elementShare("FIRE")); + assertEquals(0, split.elementShare("water")); + assertEquals(0, split.elementShare("ice")); } @Test diff --git a/src/test/java/net/tfminecraft/magic/MeditationCircleTest.java b/src/test/java/net/tfminecraft/magic/MeditationCircleTest.java index 10bf1ef..b25213e 100644 --- a/src/test/java/net/tfminecraft/magic/MeditationCircleTest.java +++ b/src/test/java/net/tfminecraft/magic/MeditationCircleTest.java @@ -132,11 +132,12 @@ void completeRingDetectsArtifactPowerAndSnapshotsExistingUsers() { assertEquals(1, circle.getArtifactPedestals().size()); assertEquals(5, circle.getTotalPower()); assertEquals(Map.of("fire", 5.), circle.getPowerByElement()); - assertEquals(5, circle.elementPower("fire")); - assertEquals(5, circle.elementPower("FIRE")); - assertEquals(0, circle.elementPower(null)); - assertEquals(0, circle.elementPower(" ")); - assertEquals(0, circle.elementPower("missing")); + var alone = circle.snapshotYield(1000); + assertEquals(5, alone.elementShare("fire")); + assertEquals(5, alone.elementShare("FIRE")); + assertEquals(0, alone.elementShare(null)); + assertEquals(0, alone.elementShare(" ")); + assertEquals(0, alone.elementShare("missing")); assertEquals("fire", circle.artifactFor(furniture.getFirst().getEntityId()).elementId); assertSame(item, circle.itemOn(furniture.getFirst())); assertNull(circle.itemOn(null)); @@ -150,6 +151,7 @@ void completeRingDetectsArtifactPowerAndSnapshotsExistingUsers() { assertEquals(1, circle.snapshotYield(1000, "user").users(id)); assertEquals(2, circle.snapshotYield(1000, "second").users(id)); assertEquals(2.5, circle.snapshotYield(1000, "second").sessionCap(id)); + assertEquals(2.5, circle.snapshotYield(1000, "second").elementShare("fire")); assertFalse(circle.isFullyAttuned(Map.of(), yield)); assertTrue(circle.isFullyAttuned(Map.of(id, 5.), yield)); assertTrue(circle.isFullyAttuned(Map.of(), null)); @@ -157,6 +159,9 @@ void completeRingDetectsArtifactPowerAndSnapshotsExistingUsers() { assertFalse(circle.canGainResonance(null, null, yield)); assertFalse(circle.canGainResonance(null, session, null)); assertTrue(circle.canGainResonance(null, session, yield)); + session.setResonance("fire", 3); + assertTrue(circle.canGainResonance(null, session, yield)); + assertFalse(circle.canGainResonance(null, session, circle.snapshotYield(1000, "second"))); session.setResonance("fire", 5); assertFalse(circle.canGainResonance(null, session, yield)); assertFalse(circle.canGainResonance(null, session, MeditationSitYield.empty())); @@ -165,6 +170,34 @@ void completeRingDetectsArtifactPowerAndSnapshotsExistingUsers() { assertTrue(ArtifactCareStore.readUsers(item).isEmpty()); } + @Test + void sharesSplitEachArtifactByItsOwnUsersAndCountOnlyUsableAura() { + ring(); + var shared = artifact(); + ArtifactCareStore.stampUser(shared, "a", 1000); + var own = artifact(); + when(slots.get(0).getCurrentItem()).thenReturn(shared); + when(slots.get(1).getCurrentItem()).thenReturn(own); + var circle = MeditationCircle.detect(center); + assertEquals(10, circle.getTotalPower()); + assertEquals(10, circle.snapshotYield(1000, "a").elementShare("fire")); + assertEquals(7.5, circle.snapshotYield(1000, "b").elementShare("fire")); + var session = new ResonanceSession(); + session.setResonance("fire", 8); + assertTrue(circle.canGainResonance(null, session, circle.snapshotYield(1000, "a"))); + assertFalse(circle.canGainResonance(null, session, circle.snapshotYield(1000, "b"))); + ArtifactCareStore.setMuffle(own, .8, 1000); + assertEquals(4.5, circle.snapshotYield(1000, "b").elementShare("fire")); + var empty = artifact(); + var aura = Artifact.fromItem(empty); + aura.setFill("fire", 0); + aura.persistPdc(empty); + when(slots.get(1).getCurrentItem()).thenReturn(empty); + assertEquals(2.5, circle.snapshotYield(1000, "b").elementShare("fire")); + when(slots.get(1).getCurrentItem()).thenReturn(new ItemStack(Material.STONE)); + assertEquals(2.5, circle.snapshotYield(1000, "b").elementShare("fire")); + } + @Test void carriedWrongTypeOrMisplacedPostBreaksDetection() { assertFalse(MeditationCircle.containing(null)); @@ -248,6 +281,11 @@ void displayFallbackAndMultipleElementsContributeOnlyStoredArtifactFill() { when(slots.getFirst().getDisplayStandId()).thenReturn(display.getUniqueId()); var circle = MeditationCircle.detect(center); assertEquals(8, circle.getTotalPower()); + var shares = circle.snapshotYield(1000); + assertEquals(5, shares.elementShare("fire")); + assertEquals(3, shares.elementShare("water")); + assertEquals(0, shares.elementShare("unknown")); + assertEquals(0, shares.elementShare("empty")); verify(slots.getFirst(), atLeastOnce()).setModel(item); MeditationCache.pedestalSlot = "top"; circle.stampArtifacts("user", 1000); @@ -297,10 +335,6 @@ void snapshotsTolerateItemChangesAndPublicPowerMapLegacyValues() { charge.setItemMeta(meta); when(slots.getFirst().getCurrentItem()).thenReturn(charge); assertFalse(circle.snapshotYield(1, " ").hasAnyCap()); - circle.getPowerByElement().put(null, 3d); - circle.getPowerByElement().put("Fire", null); - circle.getPowerByElement().remove("fire"); - assertEquals(0, circle.elementPower("FIRE")); var alien = mock(Furniture.class); var at = furniture.getFirst().getLoc(); when(alien.getLoc()).thenReturn(at); diff --git a/src/test/java/net/tfminecraft/magic/MeditationSessionTest.java b/src/test/java/net/tfminecraft/magic/MeditationSessionTest.java index 2bf41ee..bbd76c5 100644 --- a/src/test/java/net/tfminecraft/magic/MeditationSessionTest.java +++ b/src/test/java/net/tfminecraft/magic/MeditationSessionTest.java @@ -38,7 +38,6 @@ void setup() { when(circle.getArtifactPedestals()).thenReturn(List.of(post)); when(circle.artifactFor(source)).thenReturn(new MeditationCache.ArtifactDef("fire", 5)); when(circle.artifactIdOn(post)).thenReturn("artifact"); - when(circle.elementPower("fire")).thenReturn(5.); ElementRegistry.clear(); ElementRegistry.register(DomainTest.element("fire")); Cache.defaultEquilibrium = 0; @@ -72,7 +71,7 @@ MeditationSession session(double cap) { return new MeditationSession( player, circle, - new MeditationSitYield(Map.of("artifact", cap), Map.of("artifact", 1)), + new MeditationSitYield(Map.of("artifact", cap), Map.of("artifact", 1), Map.of("fire", 5.)), resonance, null); } @@ -219,14 +218,14 @@ void multiplePedestalsSpawnOneOrbEachAndWindDownSkipsConsumedOrbs() { } when(circle.getPedestals()).thenReturn(posts); when(circle.getArtifactPedestals()).thenReturn(posts); - when(circle.elementPower("fire")).thenReturn(30d); + var shares = Map.of("fire", 30d); long oldSpawn = MeditationCache.spawnIntervalTicks; int oldLife = MeditationCache.orbLifetimeTicks; try { MeditationCache.spawnIntervalTicks = 5; MeditationCache.orbLifetimeTicks = 100; var session = - new MeditationSession(player, circle, new MeditationSitYield(caps, null), resonance, null); + new MeditationSession(player, circle, new MeditationSitYield(caps, null, shares), resonance, null); activate(session); for (int i = 0; i < 15; i++) session.tick(player, resonance); assertEquals(3, session.livingOrbs().size()); @@ -240,7 +239,7 @@ void multiplePedestalsSpawnOneOrbEachAndWindDownSkipsConsumedOrbs() { for (double equilibrium : new double[] {-100, 100}) { resonance.setEquilibrium(equilibrium); var mixed = - new MeditationSession(player, circle, new MeditationSitYield(caps, null), resonance, null); + new MeditationSession(player, circle, new MeditationSitYield(caps, null, shares), resonance, null); activate(mixed); for (int i = 0; i < 15; i++) mixed.tick(player, resonance); assertEquals( @@ -254,6 +253,23 @@ void multiplePedestalsSpawnOneOrbEachAndWindDownSkipsConsumedOrbs() { } } + @Test + void sittingAgainCannotRaiseResonancePastTheSplitShare() { + MeditationCache.resonancePerHit = 10; + var shared = + new MeditationSitYield(Map.of("artifact", 2.5), Map.of("artifact", 2), Map.of("fire", 2.5)); + var first = new MeditationSession(player, circle, shared, resonance, null); + activate(first); + first.onOrbHit(player, first.livingOrbs().getFirst(), resonance, null); + assertEquals(2.5, resonance.getResonance("fire")); + first.end(); + var again = new MeditationSession(player, circle, shared, resonance, null); + activate(again); + assertTrue(again.livingOrbs().isEmpty()); + assertEquals(2.5, resonance.getResonance("fire")); + again.end(); + } + @Test void itemRemovedBetweenOrbSpawnAndHitDoesNotCreditResonance() { var profiles = mock(MagicProfileService.class); diff --git a/src/test/java/net/tfminecraft/magic/SessionServiceTest.java b/src/test/java/net/tfminecraft/magic/SessionServiceTest.java index 2b275a0..d2be71b 100644 --- a/src/test/java/net/tfminecraft/magic/SessionServiceTest.java +++ b/src/test/java/net/tfminecraft/magic/SessionServiceTest.java @@ -345,6 +345,62 @@ void resonanceTicksDecayGrowUnlockedElementsAndPersistChanges() { assertEquals(0, s.getResonance("decay")); } + @Test + void realTimeDriftBuildsUpToWholeHundredthsAndOnlySavesWhenTheValueMoves() { + Cache.secondsPerHour = 3600; + ElementRegistry.clear(); + var cfg = new YamlConfiguration(); + cfg.set("decay_per_hour", -.02); + ElementRegistry.register(new ElementDef("fire", cfg)); + var player = server.addPlayer(); + service.activate(player, new CharacterTestSupport().start(player, "slow")); + var s = sessions.get(player); + s.setResonance("fire", 100); + s.setEquilibrium(20); + clearInvocations(store); + for (int i = 0; i < 1199; i++) EquilibriumService.tickOnlineSessions(sessions); + assertEquals(20, s.getEquilibrium()); + EquilibriumService.tickOnlineSessions(sessions); + assertEquals(19.99, s.getEquilibrium()); + for (int i = 0; i < 1799; i++) ResonanceService.tickOnlineSessions(sessions); + assertEquals(100, s.getResonance("fire")); + verify(store, never()).save(any()); + ResonanceService.tickOnlineSessions(sessions); + assertEquals(99.99, s.getResonance("fire")); + verify(store).save(any()); + for (int i = 0; i < 1800; i++) ResonanceService.tickOnlineSessions(sessions); + assertEquals(99.98, s.getResonance("fire")); + } + + @Test + void driftStopsAtTheElementLimitsAndIgnoresUnknownElements() { + ElementRegistry.clear(); + var cfg = new YamlConfiguration(); + cfg.set("decay_per_hour", .5); + ElementRegistry.register(new ElementDef("grow", cfg)); + var player = server.addPlayer(); + service.activate(player, new CharacterTestSupport().start(player, "full")); + var s = sessions.get(player); + s.setResonance("grow", 100); + clearInvocations(store); + ResonanceService.tickOnlineSessions(sessions); + assertEquals(100, s.getResonance("grow")); + verify(store, never()).save(any()); + s.setResonance("grow", 99.8); + assertTrue(s.driftResonance("grow", .5)); + assertEquals(100, s.getResonance("grow")); + assertFalse(s.driftResonance("grow", -.004)); + assertEquals(100, s.getResonance("grow")); + assertTrue(s.driftResonance("grow", -.006)); + assertEquals(99.99, s.getResonance("grow")); + assertFalse(s.driftResonance("grow", -.004)); + assertFalse(s.driftResonance("missing", -1)); + s.clearResonanceDrift("missing"); + s.clearResonanceDrift("grow"); + assertFalse(s.driftResonance("grow", -.006)); + assertEquals(99.99, s.getResonance("grow")); + } + @Test void activatingCorruptProfilePreservesOriginalBytesBeforeCreatingFreshDefaults() throws Exception { String id = CharacterTestSupport.id("corrupt");