diff --git a/pom.xml b/pom.xml index e528fb6..1d53045 100644 --- a/pom.xml +++ b/pom.xml @@ -16,7 +16,8 @@ 2.2.5 2.2.5 2.0.6 - 0.4.7 + + 0.9.0 2.6.0 diff --git a/src/main/java/net/tfminecraft/recycler/Cache.java b/src/main/java/net/tfminecraft/recycler/Cache.java index d250790..46ad1ec 100644 --- a/src/main/java/net/tfminecraft/recycler/Cache.java +++ b/src/main/java/net/tfminecraft/recycler/Cache.java @@ -29,6 +29,8 @@ private Cache() {} "common", 4, "uncommon", 8, "rare", 12, "epic", 16, "legendary", 28); public static int artifactDefaultReturn = 4; public static double artifactMinMuffle = 0.0; + /** Share of the rarity amount a spent artifact husk returns, rounded down. */ + public static double artifactHuskFraction = 0.5; public static boolean blockConfirmWhenZeroYield = true; public static boolean depositWhitelistMode = false; diff --git a/src/main/java/net/tfminecraft/recycler/loader/ConfigLoader.java b/src/main/java/net/tfminecraft/recycler/loader/ConfigLoader.java index cdb7ec5..8460eaf 100644 --- a/src/main/java/net/tfminecraft/recycler/loader/ConfigLoader.java +++ b/src/main/java/net/tfminecraft/recycler/loader/ConfigLoader.java @@ -19,6 +19,7 @@ public final class ConfigLoader implements LoaderInterface { private static final double DEFAULT_SCRAP_RATE = 0.5; private static final double DEFAULT_RECIPE_RATE = 1.0; private static final String DEFAULT_ARTIFACT_ITEM = "m.currency.enchanted_dust"; + private static final double DEFAULT_HUSK_FRACTION = 0.5; private static final java.util.Map 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