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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/net/tfminecraft/geminfusion/PDCKeys.java
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
Original file line number Diff line number Diff line change
@@ -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<LinkedHashMap<String, Integer>>() {
}.getType();

private GoldsmithProvenance() {
}

public static void stamp(ItemStack item, Map<GoldsmithMaterial, Integer> deposited) {
LinkedHashMap<String, Integer> inputs = new LinkedHashMap<>();
for (Map.Entry<GoldsmithMaterial, Integer> 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<String, Integer> 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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
4 changes: 4 additions & 0 deletions src/test/java/net/tfminecraft/geminfusion/GemOutputTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
@@ -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<GoldsmithMaterial, Integer> 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));
}
}