From c68eef235fb3db6ede078eeda73c5c77ce8c7bbd Mon Sep 17 00:00:00 2001 From: Drefvelin <54400154+Drefvelin@users.noreply.github.com> Date: Mon, 28 Sep 2026 20:54:45 +0000 Subject: [PATCH 1/2] fix: cap meditation resonance at the circle's stored aura A character can only gain the gap between their element resonance and that element's total in the circle, and staff can set any element's resonance on an online character. Co-authored-by: Cursor --- .../magic/command/MagicCommand.java | 27 +----- .../magic/meditation/MeditationCeiling.java | 24 ++++++ .../magic/meditation/MeditationCircle.java | 51 +++++++++++ .../magic/meditation/MeditationService.java | 15 +++- .../magic/meditation/MeditationSession.java | 84 ++++++++++++++++++- 5 files changed, 171 insertions(+), 30 deletions(-) create mode 100644 src/main/java/net/tfminecraft/magic/meditation/MeditationCeiling.java diff --git a/src/main/java/net/tfminecraft/magic/command/MagicCommand.java b/src/main/java/net/tfminecraft/magic/command/MagicCommand.java index a1e9586..f6c418f 100644 --- a/src/main/java/net/tfminecraft/magic/command/MagicCommand.java +++ b/src/main/java/net/tfminecraft/magic/command/MagicCommand.java @@ -342,25 +342,12 @@ private static boolean applyResonance( return false; } if ("all".equalsIgnoreCase(elementArg)) { - List skipped = new ArrayList<>(); - int applied = 0; for (ElementDef element : ElementRegistry.getAll()) { double current = session.getResonance(element.getId()); double next = add ? current + amount : amount; - if (isGain(current, next) && !element.isUnlocked(target)) { - skipped.add(element.getId()); - continue; - } session.setResonance(element.getId(), next); - applied++; - } - if (!skipped.isEmpty()) { - sender.sendMessage(Messages.get( - "resonance.admin.locked_skipped", - "player", target.getName(), - "elements", String.join(", ", skipped))); } - return applied > 0; + return true; } ElementDef element = ElementRegistry.getById(elementArg.toLowerCase(Locale.ROOT)); if (element == null) { @@ -369,22 +356,10 @@ private static boolean applyResonance( } double current = session.getResonance(element.getId()); double next = add ? current + amount : amount; - if (isGain(current, next) && !element.isUnlocked(target)) { - sender.sendMessage(Messages.get( - "resonance.admin.locked", - "player", target.getName(), - "permission", element.getPermission(), - "element", element.getId())); - return false; - } session.setResonance(element.getId(), next); return true; } - private static boolean isGain(double current, double next) { - return next > current + 0.0001; - } - private static void persistResonance(Player target, ResonanceSession session) { MagicProfileService profiles = Magic.plugin.getProfileService(); if (profiles != null) { diff --git a/src/main/java/net/tfminecraft/magic/meditation/MeditationCeiling.java b/src/main/java/net/tfminecraft/magic/meditation/MeditationCeiling.java new file mode 100644 index 0000000..5de90dc --- /dev/null +++ b/src/main/java/net/tfminecraft/magic/meditation/MeditationCeiling.java @@ -0,0 +1,24 @@ +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. + */ +public final class MeditationCeiling { + + static final double EPSILON = 0.005; + + private MeditationCeiling() {} + + public static double allowed(double current, double circlePower, double elementMax, double offered) { + if (offered <= 0.0 || circlePower <= 0.0) { + return 0.0; + } + double ceiling = Math.min(circlePower, Math.max(0.0, elementMax)); + double headroom = ceiling - current; + if (headroom <= EPSILON) { + return 0.0; + } + return Math.min(offered, headroom); + } +} diff --git a/src/main/java/net/tfminecraft/magic/meditation/MeditationCircle.java b/src/main/java/net/tfminecraft/magic/meditation/MeditationCircle.java index 62e32dc..d80fef2 100644 --- a/src/main/java/net/tfminecraft/magic/meditation/MeditationCircle.java +++ b/src/main/java/net/tfminecraft/magic/meditation/MeditationCircle.java @@ -24,7 +24,9 @@ import net.tfminecraft.magic.artifact.ArtifactCareStore; import net.tfminecraft.magic.artifact.ArtifactIds; import net.tfminecraft.magic.artifact.ArtifactLore; +import net.tfminecraft.magic.model.ElementDef; import net.tfminecraft.magic.registry.ElementRegistry; +import net.tfminecraft.magic.session.ResonanceSession; public final class MeditationCircle { @@ -142,6 +144,55 @@ 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. + */ + public boolean canGainResonance(ResonanceSession session, MeditationSitYield yield) { + if (session == null || yield == null) { + return false; + } + for (Furniture furniture : artifactPedestals) { + String artifactId = artifactIdOn(furniture); + MeditationCache.ArtifactDef def = artifactByFurniture.get(furniture.getEntityId()); + if (artifactId == null || def == null || def.elementId == null) { + continue; + } + double cap = yield.sessionCap(artifactId); + if (cap <= MeditationCeiling.EPSILON) { + continue; + } + ElementDef element = ElementRegistry.getById(def.elementId); + double elementMax = element != null ? element.getMaxResonance() : Double.MAX_VALUE; + double allowed = MeditationCeiling.allowed( + session.getResonance(def.elementId), + elementPower(def.elementId), + elementMax, + cap); + if (allowed > MeditationCeiling.EPSILON) { + return true; + } + } + return false; + } + public double getTotalPower() { return totalPower; } diff --git a/src/main/java/net/tfminecraft/magic/meditation/MeditationService.java b/src/main/java/net/tfminecraft/magic/meditation/MeditationService.java index 57896b1..a898a43 100644 --- a/src/main/java/net/tfminecraft/magic/meditation/MeditationService.java +++ b/src/main/java/net/tfminecraft/magic/meditation/MeditationService.java @@ -165,12 +165,23 @@ private void tryStart(Player player) { } long nowMs = System.currentTimeMillis(); String characterId = profileService != null ? profileService.characterId(player) : null; + ResonanceSession resonance = sessionManager.getOrCreate(player); + MeditationSitYield preview = circle.snapshotYield(nowMs); + if (!preview.hasAnyCap()) { + circle.stampArtifacts(characterId, nowMs); + notifySit(player, Messages.get("meditation.nothing")); + return; + } + if (!circle.canGainResonance(resonance, preview)) { + notifySit(player, Messages.get("meditation.nothing")); + return; + } MeditationSitYield yield = circle.stampAndSnapshot(characterId, nowMs); - if (!yield.hasAnyCap()) { + if (!yield.hasAnyCap() || !circle.canGainResonance(resonance, yield)) { notifySit(player, Messages.get("meditation.nothing")); return; } - sessions.put(player.getUniqueId(), new MeditationSession(player, circle, yield)); + sessions.put(player.getUniqueId(), new MeditationSession(player, circle, yield, resonance)); } private void notifySit(Player player, String message) { diff --git a/src/main/java/net/tfminecraft/magic/meditation/MeditationSession.java b/src/main/java/net/tfminecraft/magic/meditation/MeditationSession.java index 834c3fa..636a36b 100644 --- a/src/main/java/net/tfminecraft/magic/meditation/MeditationSession.java +++ b/src/main/java/net/tfminecraft/magic/meditation/MeditationSession.java @@ -18,7 +18,9 @@ import net.tfminecraft.interactiblefurniture.furniture.Furniture; import net.tfminecraft.magic.GuiCache; import net.tfminecraft.magic.attunement.AttunementCaptureService; +import net.tfminecraft.magic.model.ElementDef; import net.tfminecraft.magic.profile.MagicProfileService; +import net.tfminecraft.magic.registry.ElementRegistry; import net.tfminecraft.magic.session.ResonanceSession; import net.tfminecraft.magic.util.MagicText; import net.tfminecraft.magic.util.PedestalFx; @@ -47,9 +49,14 @@ public enum Phase { private final Map attunedByArtifact = new HashMap<>(); private boolean windingDown; - public MeditationSession(Player player, MeditationCircle circle, MeditationSitYield yield) { + public MeditationSession( + Player player, + MeditationCircle circle, + MeditationSitYield yield, + ResonanceSession resonance) { this.circle = circle; this.yield = yield != null ? yield : MeditationSitYield.empty(); + closeCappedElements(resonance); spawnStarter(player); } @@ -463,6 +470,9 @@ private void applyRewards( ResonanceSession session, MeditationOrb orb, MagicProfileService profiles) { + if (session == null) { + return; + } ThreadLocalRandom random = ThreadLocalRandom.current(); double eqDelta; if (orb.isFlow()) { @@ -475,28 +485,98 @@ private void applyRewards( UUID sourceId = orb.getSourceId(); MeditationCache.ArtifactDef artifact = sourceId == null ? null : circle.artifactFor(sourceId); if (artifact == null) { + save(player, profiles); return; } Furniture furniture = furniture(sourceId); String artifactId = circle.artifactIdOn(furniture); if (artifactId == null) { + save(player, profiles); return; } double cap = yield.sessionCap(artifactId); double credit = attunedByArtifact.getOrDefault(artifactId, 0.0); int n = yield.users(artifactId); double remaining = Math.max(0.0, cap - credit); - double gain = Math.min(MeditationCache.resonancePerHit / n, remaining); + double offered = Math.min(MeditationCache.resonancePerHit / n, remaining); + double gain = MeditationCeiling.allowed( + session.getResonance(artifact.elementId), + circle.elementPower(artifact.elementId), + elementMax(artifact.elementId), + offered); if (gain <= 0) { + closeElement(artifact.elementId); + save(player, profiles); return; } attunedByArtifact.put(artifactId, credit + gain); AttunementCaptureService.credit(player, session, artifactId, artifact.elementId, gain); + if (MeditationCeiling.allowed( + session.getResonance(artifact.elementId), + circle.elementPower(artifact.elementId), + elementMax(artifact.elementId), + remaining) <= 0) { + closeElement(artifact.elementId); + } + save(player, profiles); + } + + private static void save(Player player, MagicProfileService profiles) { if (profiles != null) { profiles.savePlayer(player); } } + private void closeCappedElements(ResonanceSession session) { + if (session == null) { + return; + } + for (Furniture furniture : circle.getArtifactPedestals()) { + MeditationCache.ArtifactDef def = circle.artifactFor(furniture.getEntityId()); + if (def == null || def.elementId == null) { + continue; + } + String artifactId = circle.artifactIdOn(furniture); + if (artifactId == null) { + continue; + } + double cap = yield.sessionCap(artifactId); + if (cap <= MeditationCeiling.EPSILON) { + continue; + } + double allowed = MeditationCeiling.allowed( + session.getResonance(def.elementId), + circle.elementPower(def.elementId), + elementMax(def.elementId), + cap); + if (allowed <= MeditationCeiling.EPSILON) { + attunedByArtifact.put(artifactId, cap); + } + } + } + + private void closeElement(String elementId) { + if (elementId == null) { + return; + } + for (Furniture furniture : circle.getArtifactPedestals()) { + MeditationCache.ArtifactDef def = circle.artifactFor(furniture.getEntityId()); + if (def == null || def.elementId == null || !elementId.equalsIgnoreCase(def.elementId)) { + continue; + } + String artifactId = circle.artifactIdOn(furniture); + if (artifactId == null) { + continue; + } + attunedByArtifact.put(artifactId, yield.sessionCap(artifactId)); + } + } + + private static double elementMax(String elementId) { + ElementDef element = ElementRegistry.getById(elementId); + return element != null ? element.getMaxResonance() : Double.MAX_VALUE; + } + private Furniture furniture(UUID furnitureId) { if (furnitureId == null) { return null; From 43a7ddce1ece801480aa9c43d5ff8c4fed7c5c0d Mon Sep 17 00:00:00 2001 From: Drefvelin <54400154+Drefvelin@users.noreply.github.com> Date: Mon, 28 Sep 2026 21:01:19 +0000 Subject: [PATCH 2/2] fix: skip meditation when the sit cannot raise resonance Locked elements and a share that would leave no usable aura no longer start a sit or stamp the character as a user. Co-authored-by: Cursor --- .../magic/meditation/MeditationCircle.java | 24 ++++++++++++++++++- .../magic/meditation/MeditationService.java | 13 ++-------- .../magic/meditation/MeditationSession.java | 9 +++++-- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/main/java/net/tfminecraft/magic/meditation/MeditationCircle.java b/src/main/java/net/tfminecraft/magic/meditation/MeditationCircle.java index d80fef2..55d4460 100644 --- a/src/main/java/net/tfminecraft/magic/meditation/MeditationCircle.java +++ b/src/main/java/net/tfminecraft/magic/meditation/MeditationCircle.java @@ -13,6 +13,7 @@ import org.bukkit.block.Block; import org.bukkit.entity.Entity; import org.bukkit.entity.ItemDisplay; +import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.Plugin; @@ -95,6 +96,13 @@ public void stampArtifacts(String characterId, long nowMs) { } public MeditationSitYield snapshotYield(long nowMs) { + return snapshotYield(nowMs, null); + } + + /** + * @param extraCharacterId counted as a user when not already active, without writing the item + */ + public MeditationSitYield snapshotYield(long nowMs, String extraCharacterId) { Map sessionCaps = new HashMap<>(); Map users = new HashMap<>(); for (Furniture furniture : artifactPedestals) { @@ -109,6 +117,9 @@ public MeditationSitYield snapshotYield(long nowMs) { continue; } int n = Math.max(1, ArtifactCareStore.activeUserCount(item, nowMs)); + if (countsAsExtraUser(item, extraCharacterId, nowMs)) { + n++; + } 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; @@ -165,7 +176,7 @@ public double elementPower(String elementId) { * 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. */ - public boolean canGainResonance(ResonanceSession session, MeditationSitYield yield) { + public boolean canGainResonance(Player player, ResonanceSession session, MeditationSitYield yield) { if (session == null || yield == null) { return false; } @@ -180,6 +191,9 @@ public boolean canGainResonance(ResonanceSession session, MeditationSitYield yie continue; } ElementDef element = ElementRegistry.getById(def.elementId); + if (element != null && !element.isUnlocked(player)) { + continue; + } double elementMax = element != null ? element.getMaxResonance() : Double.MAX_VALUE; double allowed = MeditationCeiling.allowed( session.getResonance(def.elementId), @@ -193,6 +207,14 @@ public boolean canGainResonance(ResonanceSession session, MeditationSitYield yie return false; } + private static boolean countsAsExtraUser(ItemStack item, String characterId, long nowMs) { + if (item == null || characterId == null || characterId.isBlank()) { + return false; + } + Long until = ArtifactCareStore.readUsers(item).get(characterId.trim()); + return until == null || until <= nowMs; + } + public double getTotalPower() { return totalPower; } diff --git a/src/main/java/net/tfminecraft/magic/meditation/MeditationService.java b/src/main/java/net/tfminecraft/magic/meditation/MeditationService.java index a898a43..5aca060 100644 --- a/src/main/java/net/tfminecraft/magic/meditation/MeditationService.java +++ b/src/main/java/net/tfminecraft/magic/meditation/MeditationService.java @@ -166,21 +166,12 @@ private void tryStart(Player player) { long nowMs = System.currentTimeMillis(); String characterId = profileService != null ? profileService.characterId(player) : null; ResonanceSession resonance = sessionManager.getOrCreate(player); - MeditationSitYield preview = circle.snapshotYield(nowMs); - if (!preview.hasAnyCap()) { - circle.stampArtifacts(characterId, nowMs); - notifySit(player, Messages.get("meditation.nothing")); - return; - } - if (!circle.canGainResonance(resonance, preview)) { + MeditationSitYield preview = circle.snapshotYield(nowMs, characterId); + if (!preview.hasAnyCap() || !circle.canGainResonance(player, resonance, preview)) { notifySit(player, Messages.get("meditation.nothing")); return; } MeditationSitYield yield = circle.stampAndSnapshot(characterId, nowMs); - if (!yield.hasAnyCap() || !circle.canGainResonance(resonance, yield)) { - notifySit(player, Messages.get("meditation.nothing")); - return; - } sessions.put(player.getUniqueId(), new MeditationSession(player, circle, yield, resonance)); } diff --git a/src/main/java/net/tfminecraft/magic/meditation/MeditationSession.java b/src/main/java/net/tfminecraft/magic/meditation/MeditationSession.java index 636a36b..b7695d1 100644 --- a/src/main/java/net/tfminecraft/magic/meditation/MeditationSession.java +++ b/src/main/java/net/tfminecraft/magic/meditation/MeditationSession.java @@ -56,7 +56,7 @@ public MeditationSession( ResonanceSession resonance) { this.circle = circle; this.yield = yield != null ? yield : MeditationSitYield.empty(); - closeCappedElements(resonance); + closeCappedElements(player, resonance); spawnStarter(player); } @@ -527,7 +527,7 @@ private static void save(Player player, MagicProfileService profiles) { } } - private void closeCappedElements(ResonanceSession session) { + private void closeCappedElements(Player player, ResonanceSession session) { if (session == null) { return; } @@ -544,6 +544,11 @@ private void closeCappedElements(ResonanceSession session) { if (cap <= MeditationCeiling.EPSILON) { continue; } + ElementDef element = ElementRegistry.getById(def.elementId); + if (element != null && !element.isUnlocked(player)) { + attunedByArtifact.put(artifactId, cap); + continue; + } double allowed = MeditationCeiling.allowed( session.getResonance(def.elementId), circle.elementPower(def.elementId),