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. Each finished piece records the metal materials actually used, so recycling can return those rather than the listed recipe.
- **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. Projects set to `gem: 0`, such as the Golden Key, need only gold and give the item without a stat or quality.
- **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
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ private ItemStack decorate(ItemStack icon, JewelryProject project) {
ItemMeta meta = icon.getItemMeta();
meta.setDisplayName(project.getName());
List<String> lore = new ArrayList<>();
lore.add("§7Tier: §e" + formatTierName(project.getTierId()));
if (project.requiresGem()) {
lore.add("§7Tier: §e" + formatTierName(project.getTierId()));
}
for (Map.Entry<String, Integer> e : project.getMaterialsByType().entrySet()) {
lore.add("§7Requires §a" + e.getValue() + " " + GoldsmithMaterialTypeLoader.display(e.getKey()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ private void handleLeftClick(PlayerInteractEvent e) {
}
GoldsmithFeedback finish = station.canFinish();
if (finish == GoldsmithFeedback.LACKING_ITEMS) {
p.sendMessage("§cYou have to add all the gold and the gem before finishing");
p.sendMessage(lackingItemsMessage(station, "finishing"));
p.playSound(p.getLocation(), Sound.ENTITY_VILLAGER_NO, 1f, 1f);
return;
}
Expand Down Expand Up @@ -354,7 +354,7 @@ private void handleLeftClick(PlayerInteractEvent e) {
markDirty();
break;
case LACKING_ITEMS:
p.sendMessage("§cYou have to add all the gold and the gem before working");
p.sendMessage(lackingItemsMessage(station, "working"));
p.playSound(p.getLocation(), Sound.ENTITY_VILLAGER_NO, 1f, 1f);
break;
case WRONG_TYPE:
Expand Down Expand Up @@ -401,11 +401,18 @@ private void completeCraft(Player p, GoldsmithStation station) {
if (result.getQuality() != null) {
p.sendMessage("§7Quality: " + result.getQuality().getName());
}
p.sendMessage("§7Stat carry: §e" + Math.round(result.getStatCarryPercent()) + "%");
if (station.getProject().requiresGem()) {
p.sendMessage("§7Stat carry: §e" + Math.round(result.getStatCarryPercent()) + "%");
}
station.cancel();
remove(station.getLoc());
}

private static String lackingItemsMessage(GoldsmithStation station, String action) {
String gem = station.getProject().requiresGem() ? " and the gem" : "";
return "§cYou have to add all the gold" + gem + " before " + action;
}

private boolean isBranding(ItemStack item) {
return TLibs.getItemAPI().getChecker().checkItemWithPath(item, GoldsmithCache.brandingTool);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ private JewelryOutput() {

public static JewelryCraftResult build(GoldsmithStation station, Player player) {
JewelryProject project = station.getProject();
if (project != null && !project.requiresGem()) {
return buildPlain(station, project);
}
ItemStack gemStack = station.getGem();
if (project == null || gemStack == null) {
GoldsmithLog.warn("Jewelry output missing project or gem.");
Expand Down Expand Up @@ -61,12 +64,8 @@ public static JewelryCraftResult build(GoldsmithStation station, Player player)
double attributeMult = 1.0 + AttributeInfluence.jewelry.forPlayer(player);
double amount = Math.floor(roll.value * projectMult * qualityMult * attributeMult * 10000) / 10000;

String path = project.getItem();
ItemStack base = TLibs.getItemAPI().getCreator().getItemFromPath(path);
if (base == null || (path != null && path.toLowerCase().startsWith("ia.") && base.getType() == Material.DIRT)) {
GoldsmithLog.warn("Could not build output for project " + project.getId() + " (" + path + "). Station left intact.");
return null;
}
ItemStack base = baseItem(project);
if (base == null) return null;

LiveMMOItem mmo = new LiveMMOItem(NBTItem.get(base));
if (!applyStat(mmo, roll.statId, amount)) {
Expand All @@ -85,6 +84,28 @@ public static JewelryCraftResult build(GoldsmithStation station, Player player)
return new JewelryCraftResult(out, recipePct, hitPct, finishedTotal, statCarry, quality);
}

/** Gem-free projects (e.g. keys) give the configured item as is: no stat, no quality. */
private static JewelryCraftResult buildPlain(GoldsmithStation station, JewelryProject project) {
ItemStack out = baseItem(project);
if (out == null) return null;
out.setAmount(1);
GoldsmithProvenance.stamp(out, station.getDepositedByMaterial());
double recipePct = station.getRecipePercent();
double hitPct = station.getHitPercent();
return new JewelryCraftResult(out, recipePct, hitPct,
GoldsmithMath.finishedTotal(recipePct, hitPct), 0, null);
}

private static ItemStack baseItem(JewelryProject project) {
String path = project.getItem();
ItemStack base = TLibs.getItemAPI().getCreator().getItemFromPath(path);
if (base == null || (path != null && path.toLowerCase().startsWith("ia.") && base.getType() == Material.DIRT)) {
GoldsmithLog.warn("Could not build output for project " + project.getId() + " (" + path + "). Station left intact.");
return null;
}
return base;
}

private static void applyQualityLore(MMOItem mmo, Quality quality) {
List<String> loreList = new ArrayList<>();
loreList.add("§fQuality: " + quality.getName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ public JewelryProject(String key, ConfigurationSection config) {

String tier = config.getString("tier");
if (tier == null || tier.isBlank()) {
GoldsmithLog.warn("Project '" + id + "' has no tier set, defaulting to greater.");
// Tiers only scale the gem stat, so gem-free projects need none.
if (requiresGem) GoldsmithLog.warn("Project '" + id + "' has no tier set, defaulting to greater.");
this.tierId = "greater";
} else {
this.tierId = tier.toLowerCase();
Expand Down
9 changes: 9 additions & 0 deletions src/main/resources/goldsmithing/projects.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# gem: 1 means one infused gem must sit on the bench before hammering.
# gem: 0 projects give the item as is (no stat, no quality); tier is not needed.
gold_ring:
name: "§fGolden Ring"
item: m.ring.fine_ring
Expand Down Expand Up @@ -114,3 +115,11 @@ bracelet:
- rough_gold.1
- moldable_gold.4
- shiny_gold.5
gold_key:
name: "§6Golden Key"
item: m.keys.gold_key
gem: 0
recipe:
- rough_gold.5
- moldable_gold.1
- shiny_gold.4
45 changes: 45 additions & 0 deletions src/test/java/net/tfminecraft/geminfusion/GemOutputTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ void infusedValidatorRecognizesOnlyConfiguredInfusedGemsAndCandidates() {
void jewelryRejectsMissingInputsInvalidGemsAndUnreadableStats() {
GoldsmithStation station = mock(GoldsmithStation.class);
JewelryProject project = mock(JewelryProject.class);
when(project.requiresGem()).thenReturn(true);
ItemStack gemItem = tagged(Material.DIAMOND);
try (var log = mockStatic(GoldsmithLog.class);
var valid = mockStatic(InfusedGemValidator.class);
Expand All @@ -227,8 +228,12 @@ void jewelryRejectsMissingInputsInvalidGemsAndUnreadableStats() {

@Test
void jewelryCarriesScaledStatIntoFreshHistoryAndQualityLore() throws Exception {
// The rarity boost is a random roll; switch it off so the carry is exact.
var boostChances = GoldsmithCache.jewelryGemStatBoostChances;
GoldsmithCache.jewelryGemStatBoostChances = Map.of();
GoldsmithStation station = mock(GoldsmithStation.class);
JewelryProject project = mock(JewelryProject.class);
when(project.requiresGem()).thenReturn(true);
ItemStack gemItem = tagged(Material.DIAMOND),
base = tagged(Material.GOLD_INGOT),
out = tagged(Material.GOLD_INGOT);
Expand Down Expand Up @@ -282,13 +287,16 @@ void jewelryCarriesScaledStatIntoFreshHistoryAndQualityLore() throws Exception {
argThat(v -> v instanceof DoubleData d && d.getValue() == 3.0));
verify(history)
.registerExternalData(argThat(v -> v instanceof DoubleData d && d.getValue() == 3.0));
} finally {
GoldsmithCache.jewelryGemStatBoostChances = boostChances;
}
}

@Test
void jewelryFallsBackAfterMatchingNonnumericStatAndHandlesFailedBuild() throws Exception {
GoldsmithStation station = mock(GoldsmithStation.class);
JewelryProject project = mock(JewelryProject.class);
when(project.requiresGem()).thenReturn(true);
ItemStack gemItem = tagged(Material.DIAMOND), base = tagged(Material.DIAMOND);
when(station.getProject()).thenReturn(project);
when(station.getGem()).thenReturn(gemItem);
Expand Down Expand Up @@ -326,6 +334,7 @@ void jewelryFallsBackAfterMatchingNonnumericStatAndHandlesFailedBuild() throws E
void jewelryHandlesFallbackStatsInvalidOutputPathsAndAbsentHistory() throws Exception {
GoldsmithStation station = mock(GoldsmithStation.class);
JewelryProject project = mock(JewelryProject.class);
when(project.requiresGem()).thenReturn(true);
ItemStack gemItem = tagged(Material.DIAMOND),
base = tagged(Material.DIAMOND),
out = tagged(Material.DIAMOND);
Expand Down Expand Up @@ -377,4 +386,40 @@ void jewelryHandlesFallbackStatsInvalidOutputPathsAndAbsentHistory() throws Exce
assertNull(JewelryOutput.build(station, player));
}
}

@Test
void gemFreeProjectGivesPlainItemWithProvenanceAndNoQuality() {
GoldsmithStation station = mock(GoldsmithStation.class);
JewelryProject project = mock(JewelryProject.class);
ItemStack key = tagged(Material.GOLD_NUGGET);
key.setAmount(3);
when(station.getProject()).thenReturn(project);
when(station.getRecipePercent()).thenReturn(90.0);
when(station.getHitPercent()).thenReturn(80.0);
when(project.getItem()).thenReturn("m.keys.gold_key");
GoldsmithMaterial material = mock(GoldsmithMaterial.class);
when(material.getPath()).thenReturn("m.materials.rough_gold");
when(station.getDepositedByMaterial()).thenReturn(Map.of(material, 5));
ItemAPI api = mock(ItemAPI.class, RETURNS_DEEP_STUBS);
when(api.getCreator().getItemFromPath("m.keys.gold_key")).thenReturn(key);
try (var libs = mockStatic(TLibs.class);
var log = mockStatic(GoldsmithLog.class);
var constructed = live(m -> {})) {
libs.when(TLibs::getItemAPI).thenReturn(api);
JewelryCraftResult result = JewelryOutput.build(station, player);
assertNotNull(result);
assertSame(key, result.getItem());
assertEquals(1, key.getAmount());
assertEquals("Original", key.getItemMeta().getDisplayName());
assertEquals(Map.of("m.materials.rough_gold", 5), GoldsmithProvenance.read(key));
assertEquals(GoldsmithMath.finishedTotal(90, 80), result.getFinishedTotal());
assertEquals(0, result.getStatCarryPercent());
assertNull(result.getQuality());
assertTrue(constructed.constructed().isEmpty());
verify(station, never()).getGem();
when(api.getCreator().getItemFromPath("m.keys.gold_key")).thenReturn(null);
assertNull(JewelryOutput.build(station, player));
log.verify(() -> GoldsmithLog.warn(contains("Could not build output")));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ void shippedConfigurationDefinitionsLoadAndReferenceEachOther() throws Exception
assertTrue(p.getTierMultiplier() > 0);
assertFalse(p.getRecipe().isEmpty());
assertFalse(p.getMaterialsByType().isEmpty());
assertTrue(p.requiresGem());
assertEquals(!p.getId().equals("gold_key"), p.requiresGem());
assertThrows(UnsupportedOperationException.class, () -> p.getRecipe().clear());
}
for (GoldsmithMaterial m : GoldsmithMaterialLoader.get().values()) {
Expand Down Expand Up @@ -185,8 +185,14 @@ void invalidDefinitionsAreSkippedAndValidHitCountsAreMerged() throws Exception {
+ " gold.-1, unknown.3]\n"
+ "blankTier:\n"
+ " item: m.ring\n"
+ " tier: ' '\n"));
assertEquals(Set.of("ring", "blankTier"), JewelryProjectLoader.get().keySet());
+ " tier: ' '\n"
+ "gemNoTier:\n"
+ " item: m.ring\n"
+ " gem: 1\n"));
assertEquals(
Set.of("ring", "blankTier", "gemNoTier"), JewelryProjectLoader.get().keySet());
assertEquals("greater", JewelryProjectLoader.getByString("gemNoTier").getTierId());
assertTrue(JewelryProjectLoader.getByString("gemNoTier").requiresGem());
JewelryProject ring = JewelryProjectLoader.getByString("ring");
assertEquals("greater", ring.getTierId());
assertEquals("greater", JewelryProjectLoader.getByString("blankTier").getTierId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,18 @@ Inventory opened() {
@Test
void validIconsAreClonedDecoratedAndRemainingSlotsFilled() {
project("ring", "minecraft.DIAMOND", "mAJOR", true);
project("band", "ia.band", null, false);
project("blank", "minecraft.GOLD_INGOT", " ", false);
project("band", "ia.band", null, true);
project("blank", "minecraft.GOLD_INGOT", " ", true);
project("key", "minecraft.GOLD_NUGGET", null, false);
ItemStack original = new ItemStack(Material.DIAMOND, 6);
when(items.getCreator().getItemFromPath("minecraft.DIAMOND")).thenReturn(original);
when(items.getCreator().getItemFromPath("ia.band"))
.thenReturn(new ItemStack(Material.GOLD_INGOT));
when(items.getCreator().getItemFromPath("minecraft.GOLD_INGOT"))
.thenReturn(new ItemStack(Material.GOLD_INGOT));
assertEquals(3, new GoldsmithInventoryManager().openMenu(player));
when(items.getCreator().getItemFromPath("minecraft.GOLD_NUGGET"))
.thenReturn(new ItemStack(Material.GOLD_NUGGET));
assertEquals(4, new GoldsmithInventoryManager().openMenu(player));
Inventory inv = opened();
ItemStack icon = inv.getItem(0);
assertEquals(1, icon.getAmount());
Expand All @@ -92,7 +95,9 @@ void validIconsAreClonedDecoratedAndRemainingSlotsFilled() {
.get(GoldsmithInventoryManager.projectKey(), PersistentDataType.STRING));
assertTrue(inv.getItem(1).getItemMeta().getLore().contains("§7Tier: §eGreater"));
assertTrue(inv.getItem(2).getItemMeta().getLore().contains("§7Tier: §eGreater"));
for (int slot = 3; slot < 27; slot++)
assertTrue(
inv.getItem(3).getItemMeta().getLore().stream().noneMatch(l -> l.startsWith("§7Tier")));
for (int slot = 4; slot < 27; slot++)
assertEquals(Material.GRAY_STAINED_GLASS_PANE, inv.getItem(slot).getType());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ void toolHitsReportProgressWarningsAndFeedback() {
when(station.markOverworkWarnedIfNeeded()).thenReturn(feedback == GoldsmithFeedback.SUCCESS);
manager.onInteract(click(Action.LEFT_CLICK_BLOCK));
}
verify(player).sendMessage("§cYou have to add all the gold before working");
reset();
when(station.hit(hit)).thenReturn(GoldsmithFeedback.SUCCESS);
when(station.markOverworkWarnedIfNeeded()).thenReturn(false);
Expand Down Expand Up @@ -333,6 +334,12 @@ void finishingReportsMissingRequirementsAndFailedOutputWithoutClearingStation()
manager.onInteract(click(Action.LEFT_CLICK_BLOCK));
assertSame(station, manager.get(loc));
}
verify(player).sendMessage("§cYou have to add all the gold before finishing");
reset();
when(project.requiresGem()).thenReturn(true);
when(station.canFinish()).thenReturn(GoldsmithFeedback.LACKING_ITEMS);
manager.onInteract(click(Action.LEFT_CLICK_BLOCK));
verify(player).sendMessage("§cYou have to add all the gold and the gem before finishing");
try (var output = mockStatic(JewelryOutput.class)) {
reset();
when(station.canFinish()).thenReturn(GoldsmithFeedback.SUCCESS);
Expand All @@ -352,6 +359,8 @@ void successfulCraftDropsOutputAndClearsProjectWithOptionalQuality() {
try (var output = mockStatic(JewelryOutput.class)) {
for (Quality chosen : Arrays.asList(null, quality)) {
reset();
// Gem-free projects (keys) have no quality and no stat carry line.
when(project.requiresGem()).thenReturn(chosen != null);
JewelryCraftResult result =
new JewelryCraftResult(new ItemStack(Material.DIAMOND), 90, 80, 80, 60, chosen);
output.when(() -> JewelryOutput.build(station, player)).thenReturn(result);
Expand All @@ -361,6 +370,7 @@ void successfulCraftDropsOutputAndClearsProjectWithOptionalQuality() {
assertEquals(2, world.getEntitiesByClass(org.bukkit.entity.Item.class).size());
verify(station, times(2)).cancel();
verify(player).sendMessage("§7Quality: Fine");
verify(player).sendMessage("§7Stat carry: §e60%");
}
}

Expand Down