Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
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 {

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) {
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,19 +105,19 @@ public MeditationSitYield snapshotYield(long nowMs) {
public MeditationSitYield snapshotYield(long nowMs, String extraCharacterId) {
Map<String, Double> sessionCaps = new HashMap<>();
Map<String, Integer> users = new HashMap<>();
Map<String, Double> 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) {
continue;
}
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;
Expand All @@ -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<String, Double> 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) {
Expand All @@ -153,26 +189,9 @@ public Map<String, Double> 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<String, Double> 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) {
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

public final class MeditationSitYield {
Expand All @@ -10,14 +11,31 @@ public final class MeditationSitYield {

private final Map<String, Double> sessionCapByArtifact;
private final Map<String, Integer> usersByArtifact;
private final Map<String, Double> shareByElement;

public MeditationSitYield(Map<String, Double> sessionCapByArtifact, Map<String, Integer> usersByArtifact) {
this(sessionCapByArtifact, usersByArtifact, null);
}

public MeditationSitYield(
Map<String, Double> sessionCapByArtifact,
Map<String, Integer> usersByArtifact,
Map<String, Double> shareByElement) {
this.sessionCapByArtifact = sessionCapByArtifact != null
? Map.copyOf(sessionCapByArtifact)
: Map.of();
this.usersByArtifact = usersByArtifact != null
? Map.copyOf(usersByArtifact)
: Map.of();
Map<String, Double> shares = new HashMap<>();
if (shareByElement != null) {
for (Map.Entry<String, Double> 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() {
Expand All @@ -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) {
Expand Down Expand Up @@ -66,4 +95,8 @@ public boolean exhausted(Map<String, Double> attunedByArtifact) {
public Map<String, Double> sessionCaps() {
return Collections.unmodifiableMap(sessionCapByArtifact);
}

private static String key(String elementId) {
return elementId.trim().toLowerCase(Locale.ROOT);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,19 @@ 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;
}

double dtHours = Cache.tickHours();

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()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 "
Expand Down
73 changes: 73 additions & 0 deletions src/main/java/net/tfminecraft/magic/session/ResonanceSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Double> resonance = new HashMap<>();
private final Map<String, Double> resonanceDrift = new HashMap<>();
private double equilibriumDrift;
private long modifierRevision;

public ResonanceSession() {
Expand Down Expand Up @@ -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<String, Double> next) {
boolean hadValues = !resonance.isEmpty();
resonance.clear();
resonanceDrift.clear();
if (next != null) {
for (Map.Entry<String, Double> entry : next.entrySet()) {
if (entry.getKey() == null || entry.getValue() == null) {
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 12 additions & 0 deletions src/test/java/net/tfminecraft/magic/DomainTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Double>();
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
Expand Down
Loading
Loading