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
6 changes: 6 additions & 0 deletions src/main/java/net/tfminecraft/magic/Cache.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ public final class Cache {
public static double secondsPerHour = 3600.0;
public static boolean artifactGlint = true;
public static double artifactAuraCap = 150.0;
public static final double DEFAULT_ARTIFACT_WEAR_MULTIPLIER = 1.0;
/**
* Lifetime resonance an artifact can grant, as a multiple of its highest aura cap.
* At that total it crumbles into a spent husk. 0 turns wear off.
*/
public static double artifactWearMultiplier = DEFAULT_ARTIFACT_WEAR_MULTIPLIER;
public static boolean loggingEnabled = true;
public static boolean wipeLog = true;
/** Real mana spent divided by this equals drift. 100 means 4 mana -> 0.04. */
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/net/tfminecraft/magic/artifact/Artifact.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ public static Artifact fromItem(ItemStack stack) {
artifact.primary = readPrimary(stack);
return artifact;
}
// A husk has no aura data, and its lore must never be read back into an artifact.
if (ArtifactWear.isHusk(stack)) {
return null;
}
Artifact fromLore = ArtifactLore.readAura(stack);
if (fromLore != null && !ChargeIds.isCharge(stack)) {
fromLore.primary = readPrimary(stack);
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/net/tfminecraft/magic/artifact/ArtifactKeys.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ public static NamespacedKey artifactId() {
return new NamespacedKey(Magic.plugin, "artifact_id");
}

/** Resonance this artifact has granted in meditation, summed over every user. */
public static NamespacedKey artifactWear() {
return new NamespacedKey(Magic.plugin, "artifact_wear");
}

/** Set on a spent husk. Holds the id the artifact had before it crumbled. */
public static NamespacedKey artifactSpent() {
return new NamespacedKey(Magic.plugin, "artifact_spent");
}

public static NamespacedKey attuneStart() {
return new NamespacedKey(Magic.plugin, "attune_start");
}
Expand Down
37 changes: 37 additions & 0 deletions src/main/java/net/tfminecraft/magic/artifact/ArtifactLore.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.bukkit.persistence.PersistentDataType;

import net.tfminecraft.magic.Cache;
import net.tfminecraft.magic.Messages;
import net.tfminecraft.magic.artifact.config.ArtifactRarityDef;
import net.tfminecraft.magic.artifact.config.ArtifactRarityRegistry;
import net.tfminecraft.magic.artifact.sacrifice.SacrificeElementDef;
Expand Down Expand Up @@ -144,6 +145,38 @@ static boolean careVisibleWouldChange(ItemStack stack, double newMuffle, double
return false;
}

/** Whole percent, rounded up so a living artifact never reads 0%. */
static String integrityLine(double integrity) {
long percent = (long) Math.ceil(Math.max(0.0, Math.min(1.0, integrity)) * 100.0 - 0.0001);
return MagicText.format("{color:label_muted}Integrity: " + percent + "%");
}

/**
* Replaces the aura block with the spent-husk block and renames the item. The husk
* keeps its rarity line, so the Recycler's price is still visible.
*/
// Keep the existing legacy text representation, formatting, and exact-string comparisons.
@SuppressWarnings("deprecation")
public static void applyHusk(ItemStack stack) {
ItemMeta meta = stack.getItemMeta();
List<String> lore = meta.hasLore() ? new ArrayList<>(meta.getLore()) : new ArrayList<>();
lore = stripMagicBlocks(lore);
List<String> block = new ArrayList<>();
String rarityLine = rarityLine(stack);
if (rarityLine != null) {
block.add(rarityLine);
}
block.add(Messages.get("artifact.husk.lore"));
block.set(0, BEGIN + block.get(0));
lore.addAll(block);
meta.setLore(lore);
if (meta.hasDisplayName()) {
meta.setDisplayName(Messages.get("artifact.husk.name", "name", plain(meta.getDisplayName())));
}
meta.setEnchantmentGlintOverride(Boolean.FALSE);
stack.setItemMeta(meta);
}

private static String muffleLine(double muffle) {
if (muffle <= 0.0001) {
return "";
Expand Down Expand Up @@ -227,6 +260,10 @@ private static List<String> buildAttuneLines(ItemStack stack, Artifact artifact)

private static List<String> buildAttuneLines(ItemStack stack, Artifact artifact, double muffle) {
List<String> lines = new ArrayList<>();
// Shown once the artifact has been worn, so untouched artifacts keep their lore.
if (ArtifactWear.enabled() && ArtifactWear.readWear(stack) > 0.0) {
lines.add(integrityLine(ArtifactWear.integrity(stack, artifact)));
}
if (muffle > 0.0001) {
lines.add(muffleLine(muffle));
}
Expand Down
150 changes: 150 additions & 0 deletions src/main/java/net/tfminecraft/magic/artifact/ArtifactWear.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package net.tfminecraft.magic.artifact;

import org.bukkit.NamespacedKey;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;

import net.tfminecraft.magic.Cache;

/**
* Lifetime wear. Every point of resonance an artifact grants in meditation wears it,
* whoever receives it. Its budget is {@code artifacts.wear.budget_multiplier} times its
* highest aura cap. At the budget it crumbles into a spent husk: no aura and no artifact
* id, so every Magic system ignores it, and only the Recycler still takes it.
*/
public final class ArtifactWear {

private static final double EPSILON = 0.0001;

public enum Result {
NONE,
WORN,
CRUMBLED
}

private ArtifactWear() {}

public static boolean enabled() {
return Cache.artifactWearMultiplier > 0.0;
}

public static double readWear(ItemStack stack) {
if (stack == null || stack.getType().isAir()) {
return 0.0;
}
Double stored = stack.getItemMeta().getPersistentDataContainer().get(
ArtifactKeys.artifactWear(), PersistentDataType.DOUBLE);
if (stored == null || !Double.isFinite(stored) || stored < 0.0) {
return 0.0;
}
return stored;
}

/** Total resonance the artifact can grant before it crumbles. 0 when wear is off. */
public static double budget(Artifact artifact) {
if (artifact == null || !enabled()) {
return 0.0;
}
double highest = 0.0;
for (String elementId : artifact.getCappedElementIds()) {
highest = Math.max(highest, artifact.getCap(elementId));
}
return highest * Cache.artifactWearMultiplier;
}

/**
* Resonance the artifact can still grant before it crumbles. Infinite when wear is
* off or the stack is not an artifact, so callers can always clamp with it.
*/
public static double remaining(ItemStack stack) {
Artifact artifact = Artifact.fromItem(stack);
double budget = budget(artifact);
if (budget <= 0.0) {
return Double.POSITIVE_INFINITY;
}
return Math.max(0.0, budget - readWear(stack));
}

/** Share of the budget left, 0-1. Always 1 when wear is off. */
public static double integrity(ItemStack stack, Artifact artifact) {
double budget = budget(artifact);
if (budget <= 0.0) {
return 1.0;
}
return Math.max(0.0, Math.min(1.0, 1.0 - readWear(stack) / budget));
}

/**
* Adds {@code amount} of wear to the artifact. Crumbles it in place when that reaches
* its budget. The caller writes the stack back to wherever it lives.
*/
public static Result wear(ItemStack stack, double amount) {
if (amount <= 0.0 || !enabled()) {
return Result.NONE;
}
Artifact artifact = Artifact.fromItem(stack);
if (artifact == null) {
return Result.NONE;
}
double budget = budget(artifact);
double next = readWear(stack) + amount;
if (next + EPSILON >= budget) {
crumble(stack);
return Result.CRUMBLED;
}
ItemMeta meta = stack.getItemMeta();
meta.getPersistentDataContainer().set(ArtifactKeys.artifactWear(), PersistentDataType.DOUBLE, next);
stack.setItemMeta(meta);
ArtifactLore.refreshAttune(stack);
return Result.WORN;
}

/** The id the husk had as an artifact, or null when the stack is not a husk. */
public static String spentId(ItemStack stack) {
if (!isHusk(stack)) {
return null;
}
return stack.getItemMeta().getPersistentDataContainer().get(
ArtifactKeys.artifactSpent(), PersistentDataType.STRING);
}

public static boolean isHusk(ItemStack stack) {
if (stack == null || stack.getType().isAir()) {
return false;
}
return stack.getItemMeta().getPersistentDataContainer().has(
ArtifactKeys.artifactSpent(), PersistentDataType.STRING);
}

/**
* Turns the artifact into a spent husk. Its aura, care state, users and id are removed;
* rarity, primary element and sacrifice history stay, so the husk still shows what it
* was and the Recycler can price it.
*/
public static void crumble(ItemStack stack) {
if (stack == null || stack.getType().isAir()) {
return;
}
ItemMeta meta = stack.getItemMeta();
PersistentDataContainer root = meta.getPersistentDataContainer();
String id = root.get(ArtifactKeys.artifactId(), PersistentDataType.STRING);
for (NamespacedKey key : new NamespacedKey[] {
ArtifactKeys.auraCap(),
ArtifactKeys.auraFill(),
ArtifactKeys.auraData(),
ArtifactKeys.careUsers(),
ArtifactKeys.careMuffle(),
ArtifactKeys.careLastTick(),
ArtifactKeys.attuneStart(),
ArtifactKeys.attuneCount(),
ArtifactKeys.artifactId(),
ArtifactKeys.artifactWear()}) {
root.remove(key);
}
root.set(ArtifactKeys.artifactSpent(), PersistentDataType.STRING, id == null || id.isBlank() ? "-" : id);
stack.setItemMeta(meta);
ArtifactLore.applyHusk(stack);
}
}
4 changes: 4 additions & 0 deletions src/main/java/net/tfminecraft/magic/loader/ConfigLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,11 @@ public boolean loadSafe(File configFile) {
if (auraCap > 0) {
Cache.artifactAuraCap = auraCap;
}
double wear = artifacts.getDouble(
"wear.budget_multiplier", Cache.DEFAULT_ARTIFACT_WEAR_MULTIPLIER);
Cache.artifactWearMultiplier = Math.max(0.0, wear);
} else {
Cache.artifactWearMultiplier = Cache.DEFAULT_ARTIFACT_WEAR_MULTIPLIER;
Cache.artifactGlint = config.getBoolean("artifacts.glint", Cache.artifactGlint);
double auraCap = config.getDouble("artifacts.aura_cap", Cache.artifactAuraCap);
if (auraCap > 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

import org.bukkit.Bukkit;
Expand All @@ -25,6 +27,8 @@
import net.tfminecraft.magic.artifact.ArtifactCareStore;
import net.tfminecraft.magic.artifact.ArtifactIds;
import net.tfminecraft.magic.artifact.ArtifactLore;
import net.tfminecraft.magic.artifact.ArtifactWear;
import net.tfminecraft.magic.attunement.AttunementCaptureService;
import net.tfminecraft.magic.model.ElementDef;
import net.tfminecraft.magic.registry.ElementRegistry;
import net.tfminecraft.magic.session.ResonanceSession;
Expand Down Expand Up @@ -173,6 +177,66 @@ public ItemStack itemOn(Furniture furniture) {
return items.isEmpty() ? null : items.get(0);
}

/**
* Wears the artifact with this id on the pedestal by up to {@code gain}: never more
* than it has left, so other meditators sharing it cannot push it past its budget.
* A crumbled artifact becomes a husk in the same slot. The outcome carries how much
* the artifact actually granted and its name as it was before.
*/
public WearOutcome wearArtifact(Furniture furniture, String artifactId, double gain) {
if (furniture != null && artifactId != null) {
for (PlacedSlot slot : pedestalSlots(furniture)) {
ItemStack item = itemFromSlot(slot);
UUID id = ArtifactIds.read(item);
if (id == null || !artifactId.equals(id.toString())) {
continue;
}
String name = AttunementCaptureService.displayNameOf(item);
double granted = Math.min(gain, ArtifactWear.remaining(item));
ArtifactWear.Result result;
if (granted <= 0.0) {
ArtifactWear.crumble(item);
result = ArtifactWear.Result.CRUMBLED;
} else {
result = ArtifactWear.wear(item, granted);
}
if (result != ArtifactWear.Result.NONE) {
slot.setCurrentItem(item);
}
return new WearOutcome(result, name, granted);
}
}
return new WearOutcome(ArtifactWear.Result.NONE, "", gain);
}

/** Ids of artifacts that crumbled on this ring and now sit on it as husks. */
public Set<String> spentIds() {
Set<String> spent = new HashSet<>();
for (Furniture furniture : pedestals) {
for (ItemStack item : pedestalItems(furniture)) {
String id = ArtifactWear.spentId(item);
if (id != null) {
spent.add(id);
}
}
}
return spent;
}

public record WearOutcome(ArtifactWear.Result result, String name, double granted) {}

/** Saves the pedestal's items through InteractibleFurniture. */
public static void persist(Furniture furniture) {
if (furniture == null) {
return;
}
try {
InteractibleFurniture.getInstance().getFurnitureManager().persistFurniture(furniture);
} catch (Exception ignored) {
// IF may already be disabled
}
}

public String artifactIdOn(Furniture furniture) {
UUID id = ArtifactIds.read(itemOn(furniture));
return id != null ? id.toString() : null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ private void tick() {
continue;
}
session.tick(player, session.getResonance());
if (session.isStalledFinished()) {
complete(player, session);
}
}
for (Player player : Bukkit.getOnlinePlayers()) {
UUID id = player.getUniqueId();
Expand Down Expand Up @@ -255,13 +258,17 @@ private void tryHit(Player player) {
ResonanceSession resonance = session.getResonance();
boolean complete = session.onOrbHit(player, orb, resonance, profileService);
if (complete) {
session.playComplete(player);
player.sendMessage(Messages.get("meditation.complete"));
sitNotices.add(player.getUniqueId());
session.beginWindDown();
complete(player, session);
}
}

private void complete(Player player, MeditationSession session) {
session.playComplete(player);
player.sendMessage(Messages.get("meditation.complete"));
sitNotices.add(player.getUniqueId());
session.beginWindDown();
}

private MeditationOrb hitscan(Player player, MeditationSession session) {
Location eye = player.getEyeLocation();
Vector dir = eye.getDirection().normalize();
Expand Down
Loading
Loading