diff --git a/README.md b/README.md index 23469c2..a3fb46a 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ GemInfusion gives gemstones rolled bonuses and turns them into a resource for eq - **Rarity and stat rolls** — gems range from common to legendary and provide bonuses such as health, armour, damage, or damage reduction according to their type. - **Character influence** — Intelligence affects an infused gem's rolled stat, while Dexterity separately influences the bonus carried into jewellery. - **Socketed equipment** — connects gems with MMOItems sockets and preserves rarity information through socketing and removal. -- **Goldsmithing projects** — craft rings, necklaces, medals, and other pieces from metal materials and an infused gem at a smithing station. +- **Goldsmithing projects** — craft rings, necklaces, medals, and other pieces from metal materials and an infused gem at a smithing station. Each finished piece records the metal materials actually used, so recycling can return those rather than the listed recipe. - **Craftsmanship matters** — recipe accuracy, tool work, project tier, and finishing quality shape how much of the gem's bonus reaches the finished piece. The same gemstone links discovery and craft: its infusion determines the starting bonus, and the goldsmith's work shapes the jewellery made from it. Legendary infusions can also be announced to other players. diff --git a/src/main/java/net/tfminecraft/geminfusion/PDCKeys.java b/src/main/java/net/tfminecraft/geminfusion/PDCKeys.java index 2d19aee..6c55265 100644 --- a/src/main/java/net/tfminecraft/geminfusion/PDCKeys.java +++ b/src/main/java/net/tfminecraft/geminfusion/PDCKeys.java @@ -13,4 +13,8 @@ public static NamespacedKey gemRarity() { public static NamespacedKey socketRarities() { return new NamespacedKey(InfusionMain.plugin, "socket_rarities"); } + + public static NamespacedKey goldsmithInputs() { + return new NamespacedKey(InfusionMain.plugin, "goldsmith_inputs"); + } } diff --git a/src/main/java/net/tfminecraft/geminfusion/goldsmith/GoldsmithProvenance.java b/src/main/java/net/tfminecraft/geminfusion/goldsmith/GoldsmithProvenance.java new file mode 100644 index 0000000..988480b --- /dev/null +++ b/src/main/java/net/tfminecraft/geminfusion/goldsmith/GoldsmithProvenance.java @@ -0,0 +1,59 @@ +package net.tfminecraft.geminfusion.goldsmith; + +import java.lang.reflect.Type; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataType; + +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import com.google.gson.reflect.TypeToken; + +import net.tfminecraft.geminfusion.PDCKeys; + +/** + * Records the materials a finished jewelry piece was actually made from, as item path to amount. + * A slot accepts any material of its type, so this can differ from the project recipe. + */ +public final class GoldsmithProvenance { + + private static final Gson GSON = new Gson(); + private static final Type INPUTS_TYPE = new TypeToken>() { + }.getType(); + + private GoldsmithProvenance() { + } + + public static void stamp(ItemStack item, Map deposited) { + LinkedHashMap inputs = new LinkedHashMap<>(); + for (Map.Entry entry : deposited.entrySet()) { + inputs.merge(entry.getKey().getPath(), entry.getValue(), Integer::sum); + } + ItemMeta meta = item.getItemMeta(); + meta.getPersistentDataContainer().set(PDCKeys.goldsmithInputs(), PersistentDataType.STRING, + GSON.toJson(inputs)); + item.setItemMeta(meta); + } + + /** + * @return deposited material paths and amounts, or null for jewelry made before inputs were recorded + */ + public static Map read(ItemStack item) { + if (item == null || !item.hasItemMeta()) { + return null; + } + String json = item.getItemMeta().getPersistentDataContainer().get(PDCKeys.goldsmithInputs(), + PersistentDataType.STRING); + if (json == null) { + return null; + } + try { + return GSON.fromJson(json, INPUTS_TYPE); + } catch (JsonParseException ex) { + return null; + } + } +} diff --git a/src/main/java/net/tfminecraft/geminfusion/goldsmith/JewelryOutput.java b/src/main/java/net/tfminecraft/geminfusion/goldsmith/JewelryOutput.java index 2ed149e..49c7c8c 100644 --- a/src/main/java/net/tfminecraft/geminfusion/goldsmith/JewelryOutput.java +++ b/src/main/java/net/tfminecraft/geminfusion/goldsmith/JewelryOutput.java @@ -77,6 +77,7 @@ public static JewelryCraftResult build(GoldsmithStation station, Player player) return null; } out.setAmount(1); + GoldsmithProvenance.stamp(out, station.getDepositedByMaterial()); return new JewelryCraftResult(out, recipePct, hitPct, finishedTotal, statCarry, quality); } diff --git a/src/test/java/net/tfminecraft/geminfusion/GemOutputTest.java b/src/test/java/net/tfminecraft/geminfusion/GemOutputTest.java index 44c3760..d8cd4a1 100644 --- a/src/test/java/net/tfminecraft/geminfusion/GemOutputTest.java +++ b/src/test/java/net/tfminecraft/geminfusion/GemOutputTest.java @@ -263,9 +263,13 @@ void jewelryCarriesScaledStatIntoFreshHistoryAndQualityLore() throws Exception { valid.when(() -> InfusedGemValidator.isInfused(gemItem)).thenReturn(true); qualityApi.when(() -> QualityLoader.getByAmount(80)).thenReturn(quality); qualityApi.when(() -> QualityLoader.resolveStatFactor(80)).thenReturn(50.0); + GoldsmithMaterial material = mock(GoldsmithMaterial.class); + when(material.getPath()).thenReturn("m.materials.shiny_gold"); + when(station.getDepositedByMaterial()).thenReturn(Map.of(material, 3)); JewelryCraftResult result = JewelryOutput.build(station, player); assertNotNull(result); assertSame(out, result.getItem()); + assertEquals(Map.of("m.materials.shiny_gold", 3), GoldsmithProvenance.read(out)); assertEquals(80, result.getFinishedTotal()); assertEquals(90, result.getRecipePercent()); assertEquals(80, result.getHitPercent()); diff --git a/src/test/java/net/tfminecraft/geminfusion/goldsmith/GoldsmithProvenanceTest.java b/src/test/java/net/tfminecraft/geminfusion/goldsmith/GoldsmithProvenanceTest.java new file mode 100644 index 0000000..5208f80 --- /dev/null +++ b/src/test/java/net/tfminecraft/geminfusion/goldsmith/GoldsmithProvenanceTest.java @@ -0,0 +1,74 @@ +package net.tfminecraft.geminfusion.goldsmith; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.*; +import net.tfminecraft.geminfusion.InfusionMain; +import net.tfminecraft.geminfusion.PDCKeys; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataType; +import org.junit.jupiter.api.*; +import org.mockbukkit.mockbukkit.MockBukkit; + +class GoldsmithProvenanceTest { + InfusionMain previous; + + @BeforeEach + void setup() { + MockBukkit.mock(); + previous = InfusionMain.plugin; + InfusionMain.plugin = mock(InfusionMain.class); + when(InfusionMain.plugin.namespace()).thenReturn("geminfusion"); + } + + @AfterEach + void cleanup() { + InfusionMain.plugin = previous; + MockBukkit.unmock(); + } + + private static GoldsmithMaterial material(String path) { + GoldsmithMaterial material = mock(GoldsmithMaterial.class); + when(material.getPath()).thenReturn(path); + return material; + } + + @Test + void stampsTheDepositedMaterialsNotTheRecipe() { + ItemStack ring = new ItemStack(Material.GOLD_INGOT); + LinkedHashMap deposited = new LinkedHashMap<>(); + deposited.put(material("m.materials.shiny_gold"), 3); + deposited.put(material("m.materials.rough_gold"), 1); + deposited.put(material("m.materials.shiny_gold"), 2); + + GoldsmithProvenance.stamp(ring, deposited); + + assertEquals( + Map.of("m.materials.shiny_gold", 5, "m.materials.rough_gold", 1), + GoldsmithProvenance.read(ring)); + } + + @Test + void stampsAnEmptyListWhenNothingWasDeposited() { + ItemStack ring = new ItemStack(Material.GOLD_INGOT); + GoldsmithProvenance.stamp(ring, Map.of()); + assertEquals(Map.of(), GoldsmithProvenance.read(ring)); + } + + @Test + void readsNothingFromUnstampedOrUnreadableItems() { + assertNull(GoldsmithProvenance.read(null)); + assertNull(GoldsmithProvenance.read(new ItemStack(Material.AIR))); + assertNull(GoldsmithProvenance.read(new ItemStack(Material.GOLD_INGOT))); + + ItemStack broken = new ItemStack(Material.GOLD_INGOT); + ItemMeta meta = broken.getItemMeta(); + meta.getPersistentDataContainer() + .set(PDCKeys.goldsmithInputs(), PersistentDataType.STRING, "{not json"); + broken.setItemMeta(meta); + assertNull(GoldsmithProvenance.read(broken)); + } +}