DEFAULT_ARTIFACT_RETURNS = java.util.Map.of(
"common", 4, "uncommon", 8, "rare", 12, "epic", 16, "legendary", 28);
@@ -151,8 +152,11 @@ private static void applyArtifactReturns(ConfigurationSection section) {
Cache.artifactReturnItem = DEFAULT_ARTIFACT_ITEM;
Cache.artifactDefaultReturn = 4;
Cache.artifactMinMuffle = 0.0;
+ Cache.artifactHuskFraction = DEFAULT_HUSK_FRACTION;
if (section != null) {
Cache.artifactMinMuffle = readRate(section, "min_muffle", 0.0, 0.0);
+ Cache.artifactHuskFraction = readRate(
+ section, "husk_fraction", DEFAULT_HUSK_FRACTION, DEFAULT_HUSK_FRACTION);
Cache.artifactReturnItem = section.getString("item", DEFAULT_ARTIFACT_ITEM).trim();
Cache.artifactDefaultReturn = readAmount(section, "default", 4);
ConfigurationSection rarities = section.getConfigurationSection("rarities");
diff --git a/src/main/java/net/tfminecraft/recycler/provider/ArtifactProvider.java b/src/main/java/net/tfminecraft/recycler/provider/ArtifactProvider.java
index c8fd852..ca2b780 100644
--- a/src/main/java/net/tfminecraft/recycler/provider/ArtifactProvider.java
+++ b/src/main/java/net/tfminecraft/recycler/provider/ArtifactProvider.java
@@ -10,6 +10,7 @@
import net.tfminecraft.magic.artifact.Artifact;
import net.tfminecraft.magic.artifact.ArtifactCareStore;
import net.tfminecraft.magic.artifact.ArtifactKeys;
+import net.tfminecraft.magic.artifact.ArtifactWear;
import net.tfminecraft.recycler.Cache;
import net.tfminecraft.recycler.model.RecycleOutput;
@@ -17,9 +18,32 @@
* Magic artifacts muffled at least {@code artifact_returns.min_muffle} (0 = any artifact).
* They are found rather than crafted, so there is no record of what went into them: the
* return is a fixed amount by rarity from {@code artifact_returns}. Stored aura is not returned.
+ *
+ * Spent husks (artifacts worn out in meditation) hold no aura, so min_muffle does not
+ * apply to them. They return {@code artifact_returns.husk_fraction} of their rarity's amount,
+ * rounded down.
*/
public final class ArtifactProvider implements RecycleProvider {
+ /**
+ * Husks exist from Magic 0.9.0. Against an older Magic the class is missing, and
+ * touching it would break every recycle, since the chain asks this provider first.
+ */
+ public static boolean huskSupport = classPresent("net.tfminecraft.magic.artifact.ArtifactWear");
+
+ public static boolean classPresent(String name) {
+ try {
+ Class.forName(name, false, ArtifactProvider.class.getClassLoader());
+ return true;
+ } catch (ClassNotFoundException | LinkageError ex) {
+ return false;
+ }
+ }
+
+ private static boolean isHusk(ItemStack item) {
+ return huskSupport && ArtifactWear.isHusk(item);
+ }
+
@Override
public int priority() {
return 16;
@@ -32,6 +56,9 @@ public double returnRate() {
@Override
public boolean canHandle(ItemStack item) {
+ if (isHusk(item)) {
+ return true;
+ }
return Artifact.fromItem(item) != null && !belowMinMuffle(item);
}
@@ -41,6 +68,9 @@ public boolean canHandle(ItemStack item) {
* artifact cannot get around either setting.
*/
public boolean refuses(ItemStack item) {
+ if (isHusk(item)) {
+ return outputsFor(item).isEmpty();
+ }
return Artifact.fromItem(item) != null && (belowMinMuffle(item) || outputsFor(item).isEmpty());
}
@@ -50,7 +80,7 @@ private static boolean belowMinMuffle(ItemStack item) {
@Override
public List resolveBaseOutputs(ItemStack item) {
- if (Artifact.fromItem(item) == null) {
+ if (!isHusk(item) && Artifact.fromItem(item) == null) {
return List.of();
}
return outputsFor(item);
@@ -58,6 +88,9 @@ public List resolveBaseOutputs(ItemStack item) {
private static List outputsFor(ItemStack item) {
int amount = Cache.artifactRarityReturns.getOrDefault(rarity(item), Cache.artifactDefaultReturn);
+ if (isHusk(item)) {
+ amount = (int) Math.floor(amount * Cache.artifactHuskFraction);
+ }
if (amount <= 0 || Cache.artifactReturnItem.isBlank()) {
return List.of();
}
diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml
index 5541b13..90100a8 100644
--- a/src/main/resources/config.yml
+++ b/src/main/resources/config.yml
@@ -60,11 +60,15 @@ artifact_returns:
# Lowest muffle (0.0-1.0) an artifact needs before it can be recycled. 0.0 accepts any
# artifact; 1.0 accepts only fully muffled ones (about a day in a chest).
min_muffle: 0.0
+ # Spent husks (artifacts worn out in meditation) return this share of their rarity's
+ # amount, rounded down. min_muffle does not apply to them. 0.0-1.0.
+ husk_fraction: 0.5
item: m.currency.enchanted_dust
rarities:
common: 4
uncommon: 8
rare: 12
+ # Magic no longer rolls Epic; this keeps older Epic artifacts recycling at their value.
epic: 16
legendary: 28
default: 4
diff --git a/src/test/java/net/tfminecraft/recycler/ArtifactProviderTest.java b/src/test/java/net/tfminecraft/recycler/ArtifactProviderTest.java
index 752628c..720b7ba 100644
--- a/src/test/java/net/tfminecraft/recycler/ArtifactProviderTest.java
+++ b/src/test/java/net/tfminecraft/recycler/ArtifactProviderTest.java
@@ -36,6 +36,7 @@ void artifactsReturnDustByRarityAndMinMuffleGatesThem() {
var unrecorded = artifact(null);
try (var art = mockStatic(Artifact.class);
var keys = mockStatic(ArtifactKeys.class);
+ var wear = mockStatic(ArtifactWear.class);
var care = mockStatic(ArtifactCareStore.class)) {
keys.when(ArtifactKeys::artifactRarity).thenReturn(RARITY);
var found = mock(Artifact.class);
@@ -69,6 +70,47 @@ void artifactsReturnDustByRarityAndMinMuffleGatesThem() {
}
}
+ @Test
+ void spentHusksReturnTheirShareOfTheRarityAmountWhateverTheirMuffle() {
+ var p = new ArtifactProvider();
+ var legendary = artifact("legendary");
+ var common = artifact("common");
+ try (var art = mockStatic(Artifact.class);
+ var keys = mockStatic(ArtifactKeys.class);
+ var wear = mockStatic(ArtifactWear.class);
+ var care = mockStatic(ArtifactCareStore.class)) {
+ keys.when(ArtifactKeys::artifactRarity).thenReturn(RARITY);
+ wear.when(() -> ArtifactWear.isHusk(legendary)).thenReturn(true);
+ wear.when(() -> ArtifactWear.isHusk(common)).thenReturn(true);
+ Cache.artifactMinMuffle = 1.0;
+ assertTrue(p.canHandle(legendary), "A husk has no aura, so min_muffle does not apply");
+ assertFalse(p.refuses(legendary));
+ assertEquals(
+ List.of(new RecycleOutput("m.currency.enchanted_dust", 14)),
+ p.resolveBaseOutputs(legendary));
+ Cache.artifactHuskFraction = .3;
+ assertEquals(
+ List.of(new RecycleOutput("m.currency.enchanted_dust", 1)),
+ p.resolveBaseOutputs(common),
+ "Rounded down");
+ Cache.artifactHuskFraction = 0;
+ assertTrue(p.refuses(common), "A husk worth nothing is refused, not sent to recipes");
+ assertTrue(p.resolveBaseOutputs(common).isEmpty());
+ art.verifyNoInteractions();
+ care.verifyNoInteractions();
+ ArtifactProvider.huskSupport = false;
+ try {
+ assertFalse(p.canHandle(common), "Without husk support it is just a non-artifact");
+ assertFalse(p.refuses(common));
+ assertTrue(p.resolveBaseOutputs(common).isEmpty());
+ } finally {
+ ArtifactProvider.huskSupport = true;
+ }
+ }
+ assertTrue(ArtifactProvider.classPresent("net.tfminecraft.magic.artifact.ArtifactWear"));
+ assertFalse(ArtifactProvider.classPresent("net.tfminecraft.magic.artifact.Missing"));
+ }
+
@Test
void chainRefusesArtifactsBelowMinMuffleEvenWhenARecipeMatches() throws Exception {
yaml("recipes.yml", "recipes:\n rod:\n input: v.blaze_rod\n outputs: ['v.stick 1']");
@@ -81,6 +123,7 @@ void chainRefusesArtifactsBelowMinMuffleEvenWhenARecipeMatches() throws Exceptio
try (var art = mockStatic(Artifact.class);
var keys = mockStatic(ArtifactKeys.class);
var care = mockStatic(ArtifactCareStore.class);
+ var wear = mockStatic(ArtifactWear.class);
var gear = mockStatic(net.tfminecraft.magic.gear.GearProvenance.class)) {
keys.when(ArtifactKeys::artifactRarity).thenReturn(RARITY);
art.when(() -> Artifact.fromItem(stack)).thenReturn(mock(Artifact.class));
diff --git a/src/test/java/net/tfminecraft/recycler/LoadersTest.java b/src/test/java/net/tfminecraft/recycler/LoadersTest.java
index fd6d351..4ad39d4 100644
--- a/src/test/java/net/tfminecraft/recycler/LoadersTest.java
+++ b/src/test/java/net/tfminecraft/recycler/LoadersTest.java
@@ -89,17 +89,19 @@ void artifactReturnsUseDefaultsOverridesAndClampNegatives() throws Exception {
Cache.artifactRarityReturns);
assertEquals(4, Cache.artifactDefaultReturn);
assertEquals(0, Cache.artifactMinMuffle, "Any artifact is accepted by default");
+ assertEquals(.5, Cache.artifactHuskFraction, "Husks return half by default");
loader.loadSafe(
yaml(
"artifacts.yml",
"return_rates:\n artifacts: 0.5\nartifact_returns:\n item: ' m.currency.other '\n"
- + " min_muffle: 2\n default: -2\n rarities:\n Common: 0\n mythic: 9\n rare: -1\n"
+ + " min_muffle: 2\n husk_fraction: 3\n default: -2\n rarities:\n Common: 0\n mythic: 9\n rare: -1\n"
+ " legendary: seven\n odd: x\n")
.toFile());
assertEquals(.5, Cache.artifactReturnRate);
assertEquals("m.currency.other", Cache.artifactReturnItem);
assertEquals(0, Cache.artifactDefaultReturn, "Negative amounts become 0");
assertEquals(1, Cache.artifactMinMuffle, "min_muffle is clamped to 0.0-1.0");
+ assertEquals(1, Cache.artifactHuskFraction, "husk_fraction is clamped to 0.0-1.0");
assertEquals(
Map.of(
"common", 0, "uncommon", 8, "rare", 0, "epic", 16, "legendary", 28, "mythic", 9, "odd",
@@ -107,15 +109,18 @@ void artifactReturnsUseDefaultsOverridesAndClampNegatives() throws Exception {
Cache.artifactRarityReturns,
"A non-number keeps the built-in amount, or default for a new rarity");
loader.loadSafe(
- yaml("no-rarities.yml", "artifact_returns:\n default: 3\n min_muffle: 0.5\n").toFile());
+ yaml("no-rarities.yml", "artifact_returns:\n default: 3\n min_muffle: 0.5\n husk_fraction: 0.25\n")
+ .toFile());
assertEquals(3, Cache.artifactDefaultReturn);
assertEquals(.5, Cache.artifactMinMuffle);
+ assertEquals(.25, Cache.artifactHuskFraction);
assertEquals(28, Cache.artifactRarityReturns.get("legendary"));
loader.loadSafe(yaml("none.yml", "{}").toFile());
assertEquals("m.currency.enchanted_dust", Cache.artifactReturnItem);
assertEquals(4, Cache.artifactDefaultReturn);
assertEquals(4, Cache.artifactRarityReturns.get("common"));
assertEquals(0, Cache.artifactMinMuffle);
+ assertEquals(.5, Cache.artifactHuskFraction);
}
@Test