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
8 changes: 8 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,11 @@ jobs:
name: unit-test-reports-${{ github.run_id }}-${{ github.run_attempt }}
path: target/surefire-reports/
if-no-files-found: error

- name: Upload coverage reports
if: ${{ !cancelled() && hashFiles('target/site/jacoco/jacoco.xml') != '' }}
uses: actions/upload-artifact@v7
with:
name: coverage-reports-${{ github.run_id }}-${{ github.run_attempt }}
path: target/site/jacoco/
if-no-files-found: error
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ The same gemstone links discovery and craft: its infusion determines the startin

Technical documentation is maintained in [TF-Minecraft/Docs](https://github.com/TF-Minecraft/Docs).

## Tests

With Java 21 and the pinned plugin dependencies installed, run `mvn clean verify`.
Tests use JUnit, Mockito, and MockBukkit; JaCoCo reports are written to
`target/site/jacoco/index.html` and uploaded by CI. Tests run locally without a live
Minecraft server. Verification requires 100% line, branch, and instruction
coverage of production code, with no coverage exclusions.

## License

Copyright (c) 2026 TF-Minecraft contributors.
Expand Down
67 changes: 67 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,24 @@
</properties>

<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.20.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockbukkit.mockbukkit</groupId>
<artifactId>mockbukkit-v1.21</artifactId>
<version>4.95.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>local</groupId>
<artifactId>gson</artifactId>
Expand Down Expand Up @@ -86,6 +104,55 @@
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.4</version>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.15</version>
<executions>
<execution>
<goals><goal>prepare-agent</goal></goals>
</execution>
<execution>
<id>coverage-report</id>
<phase>verify</phase>
<goals><goal>report</goal></goals>
</execution>
<execution>
<id>coverage-check</id>
<phase>verify</phase>
<goals><goal>check</goal></goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>INSTRUCTION</counter>
<value>COVEREDRATIO</value>
<minimum>1.0</minimum>
</limit>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>1.0</minimum>
</limit>
<limit>
<counter>BRANCH</counter>
<value>COVEREDRATIO</value>
<minimum>1.0</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,6 @@ public double readAttribute(Player player) {
}

private static double lerp(double a, double b, double t) {
if (t < 0) {
t = 0;
}
if (t > 1) {
t = 1;
}
return a + (b - a) * t;
}
}
65 changes: 42 additions & 23 deletions src/main/java/net/tfminecraft/geminfusion/InfusionEvents.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.EquipmentSlot;
import net.tfminecraft.geminfusion.goldsmith.GoldsmithLog;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.util.Vector;

Expand All @@ -33,6 +35,7 @@ public class InfusionEvents implements Listener{
@SuppressWarnings("deprecation")
@EventHandler
public void addGemEvent(PlayerInteractEvent e) {
if (e.getHand() == EquipmentSlot.OFF_HAND) return;
if(!e.getAction().equals(Action.RIGHT_CLICK_BLOCK)) return;
Material block = e.getClickedBlock().getType();
if(!ConfigLoader.stations.contains(block)) return;
Expand Down Expand Up @@ -76,7 +79,7 @@ public void addGemEvent(PlayerInteractEvent e) {
p.sendMessage(ChatColor.RED + "Infusion has already started, you cannot add more gems at this point!");
return;
}
if(b.getCurrentItems().size() >= 10) {
if(loc.equals(b.getLocation()) && b.getCurrentItems().size() >= 10) {
p.sendMessage(ChatColor.RED + "You can only infuse 10 gems at a time!");
return;
}
Expand Down Expand Up @@ -108,6 +111,7 @@ public void addGemEvent(PlayerInteractEvent e) {
@SuppressWarnings("deprecation")
@EventHandler
public void infuseHitEvent(PlayerInteractEvent e) {
if (e.getHand() == EquipmentSlot.OFF_HAND) return;
if(!e.getAction().equals(Action.LEFT_CLICK_BLOCK)) return;
Material block = e.getClickedBlock().getType();
if(!ConfigLoader.stations.contains(block)) return;
Expand All @@ -124,6 +128,18 @@ public void infuseHitEvent(PlayerInteractEvent e) {
String itemType = ConfigLoader.infusionStaff.split("\\.")[0];
String itemID = ConfigLoader.infusionStaff.split("\\.")[1];
if(nbt.getType().equalsIgnoreCase(itemType) && nbt.getString("MMOITEMS_ITEM_ID").equalsIgnoreCase(itemID)) {
// Resolve the whole batch before consuming the staff or clearing the bench.
// A later config reload must not strand an already consumed batch.
List<GemRarity> batchRarities = new ArrayList<>();
if (b.getInfusionHits() >= 4) {
try {
for (Gemstone ignored : b.getCurrentItems()) batchRarities.add(getRarity());
} catch (IllegalStateException invalidRarities) {
GoldsmithLog.warn(invalidRarities.getMessage());
p.sendMessage(ChatColor.RED + "Infusion rarities are not configured correctly. Contact staff; your gems remain on the bench.");
return;
}
}
b.setInfustionHits(b.getInfusionHits()+1);
p.sendTitle(ChatColor.LIGHT_PURPLE + "Infusing...", ChatColor.GREEN + "Progress: " + ChatColor.YELLOW + b.getInfusionHits() + "/5", 1, 40, 20);
b.getLocation().getWorld().spawnParticle(Particle.FLAME, b.getParticleLocation(), 60);
Expand All @@ -143,7 +159,7 @@ public void run()
{
if(i < b.getCurrentItems().size()) {
Gemstone gem = b.getCurrentItems().get(i);
GemRarity r = getRarity();
GemRarity r = batchRarities.get(i);
if(r.shouldAnnounce()) {
for(Player player : Bukkit.getOnlinePlayers()) {
player.sendMessage("§e"+p.getName()+" just infused a "+r.getName()+" "+gem.getColour()+gem.getName()+"§e Gemstone");
Expand Down Expand Up @@ -175,31 +191,34 @@ public void run()
}
}
public GemRarity getRarity() {
Double maxWeight = 0.0;
for(GemRarity r : ConfigLoader.loadedRarities) {
maxWeight = maxWeight+r.getChance();
}
Integer dropped = 0;
GemRarity rarity = null;
while(dropped < 1) {
Double random = Math.random();
Double previous = 0.0;
for(GemRarity r : ConfigLoader.loadedRarities) {
if(maxWeight <= 0) {
maxWeight = 1.0;
}
Double chance = r.getChance() / maxWeight;
Double max = previous+chance;
if(random <= max && random > previous) {
dropped++;
rarity = r;
}
previous = max;
return getRarity(Math.random());
}

GemRarity getRarity(double draw) {
List<GemRarity> valid = new ArrayList<>();
double largest = 0;
for (GemRarity rarity : ConfigLoader.loadedRarities) {
double weight = rarity.getChance();
if (Double.isFinite(weight) && weight > 0) {
valid.add(rarity);
largest = Math.max(largest, weight);
}
}
return rarity;
if (valid.isEmpty()) {
throw new IllegalStateException("Gem infusion requires at least one finite positive rarity weight.");
}
// Scale before summing so individually valid large weights cannot overflow.
double total = 0;
for (GemRarity rarity : valid) total += rarity.getChance() / largest;
double roll = draw * total;
for (GemRarity rarity : valid) {
roll -= rarity.getChance() / largest;
if (roll < 0) return rarity;
}
return valid.getLast(); // Floating-point rounding at the upper boundary.
}


public ItemStack getInfusedGem(Player player, Gemstone gem, Integer amount, GemRarity r) {
return InfusedGemBuilder.buildInfusedGem(gem, r, amount, player);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,6 @@ public static void restore(Player player, String rarityId, Gemstone gem, Map<Int

PlayerInventory inventory = player.getInventory();
ItemStack item = inventory.getItem(slot);
if (item == null || item.getType().isAir()) {
return;
}
inventory.setItem(slot, InfusedGemBuilder.applyCosmeticsToItem(item, gem, rarity));
}

Expand Down Expand Up @@ -68,7 +65,7 @@ private static Integer findTargetSlot(Player player, Gemstone gem, Map<Integer,
for (int slot : needsRestore) {
ItemStack item = inventory.getItem(slot);
ItemStack before = beforeInventory.get(slot);
if (before != null && !before.getType().isAir() && item.getAmount() > before.getAmount()) {
if (item.getAmount() > before.getAmount()) {
return slot;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ public int openMenu(Player player) {
@SuppressWarnings("deprecation")
private ItemStack decorate(ItemStack icon, JewelryProject project) {
ItemMeta meta = icon.getItemMeta();
if (meta == null) return icon;
meta.setDisplayName(project.getName());
List<String> lore = new ArrayList<>();
lore.add("§7Tier: §e" + formatTierName(project.getTierId()));
Expand All @@ -74,7 +73,7 @@ private ItemStack iconFromPath(String path, String label) {
return null;
}
ItemStack stack = TLibs.getItemAPI().getCreator().getItemFromPath(path);
if (stack == null) {
if (stack == null || stack.getItemMeta() == null) {
GoldsmithLog.warn("Could not build item for " + label + " (" + path + "), skipping menu slot.");
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,7 @@ public static double hitPercentPenalized(Map<GoldsmithHit, Integer> required, Ma
int lineCurrent = counter == null ? 0 : Math.max(0, counter.getCurrent());
double d = lineHitPercentage(lineCurrent, lineNeeded);
if (d >= 200.0) continue;
if (d <= 100.0) amount += d;
if (d > 100.0 && d <= 200.0) amount += (200.0 - d);
amount += d <= 100.0 ? d : 200.0 - d;
}
if (lineCount <= 0) return 0;
return Math.round(amount / lineCount);
Expand All @@ -132,8 +131,7 @@ public static double hitPercentFromCounts(Map<GoldsmithHit, Integer> required, M
int lineCurrent = currentCounts == null ? 0 : Math.max(0, currentCounts.getOrDefault(entry.getKey(), 0));
double d = lineHitPercentage(lineCurrent, lineNeeded);
if (d >= 200.0) continue;
if (d <= 100.0) amount += d;
if (d > 100.0 && d <= 200.0) amount += (200.0 - d);
amount += d <= 100.0 ? d : 200.0 - d;
}
if (lineCount <= 0) return 0;
return Math.round(amount / lineCount);
Expand All @@ -148,7 +146,6 @@ public static boolean meetsMinHitPercent(double hitPercent) {
}

private static double lineHitPercentage(int current, int needed) {
if (needed <= 0) return 0;
return Math.round((double) current / needed * 100.0);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,16 +238,16 @@ public boolean checkItems() {
if (!c.isEqual()) return false;
}
if (project.requiresGem() && gem == null) return false;
if (project.requiresGem() && gem != null && !InfusedGemValidator.isInfused(gem)) return false;
if (project.requiresGem() && !InfusedGemValidator.isInfused(gem)) return false;
return true;
}

public GoldsmithFeedback canFinish() {
if (project == null) return GoldsmithFeedback.NO_PROJECT;
if (!checkItems()) return GoldsmithFeedback.LACKING_ITEMS;
if (project.requiresGem() && gem != null && !InfusedGemValidator.isInfused(gem)) {
return GoldsmithFeedback.NOT_INFUSED;
}
if (!checkItems()) return GoldsmithFeedback.LACKING_ITEMS;
if (!GoldsmithMath.meetsMinHitPercent(getHitPercent())) return GoldsmithFeedback.LACKING_HITS;
return GoldsmithFeedback.SUCCESS;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ public void onMenuClick(InventoryClickEvent e) {
p.playSound(p.getLocation(), Sound.BLOCK_ANVIL_PLACE, 0.8f, 2f);
}

@EventHandler
@EventHandler(ignoreCancelled = true)
public void onBreak(BlockBreakEvent e) {
if (!isGoldsmithStation(e.getBlock())) return;
Location loc = e.getBlock().getLocation();
Expand Down Expand Up @@ -303,18 +303,12 @@ private void handleLeftClick(PlayerInteractEvent e) {
Player p = e.getPlayer();
GoldsmithStation station = get(e.getClickedBlock().getLocation());
if (station == null || !station.hasProject()) {
if (!onCooldown(p)) {
p.sendMessage("§7Right-click the bench to choose a project.");
markCooldown(p);
}
p.sendMessage("§7Right-click the bench to choose a project.");
markCooldown(p);
return;
}

ItemStack hand = p.getInventory().getItemInMainHand();
if (hand == null || hand.getType().isAir()) {
return;
}

if (isBranding(hand)) {
markCooldown(p);
if (p.isSneaking()) {
Expand Down Expand Up @@ -346,7 +340,6 @@ private void handleLeftClick(PlayerInteractEvent e) {
}

GoldsmithHit hit = GoldsmithHitLoader.getByItem(hand);
if (hit == null) return;

markCooldown(p);
GoldsmithFeedback feedback = station.hit(hit);
Expand Down Expand Up @@ -398,11 +391,9 @@ private void completeCraft(Player p, GoldsmithStation station) {
return;
}
Location drop = station.getLoc().clone().add(0.5, 1, 0.5);
if (drop.getWorld() != null) {
drop.getWorld().dropItemNaturally(drop, result.getItem());
drop.getWorld().playSound(drop, Sound.ENTITY_PLAYER_LEVELUP, 1f, 1f);
drop.getWorld().playSound(drop, Sound.BLOCK_ANVIL_PLACE, 1f, 1f);
}
drop.getWorld().dropItemNaturally(drop, result.getItem());
drop.getWorld().playSound(drop, Sound.ENTITY_PLAYER_LEVELUP, 1f, 1f);
drop.getWorld().playSound(drop, Sound.BLOCK_ANVIL_PLACE, 1f, 1f);
p.sendTitle("§aYou made a " + station.getProject().getName(), "", 5, 40, 10);
p.sendMessage("§7Recipe: §e" + Math.round(result.getRecipePercent()) + "%");
p.sendMessage("§7Hits: §e" + Math.round(result.getHitPercent()) + "%");
Expand Down Expand Up @@ -436,7 +427,6 @@ private GoldsmithMaterial matchMaterial(ItemStack item) {

private void consumeOne(Player p) {
ItemStack hand = p.getInventory().getItemInMainHand();
if (hand == null || hand.getType().isAir()) return;
hand.setAmount(hand.getAmount() - 1);
}

Expand All @@ -450,15 +440,13 @@ private void giveOrDrop(Player p, List<ItemStack> items) {
}

private void dropAt(Location loc, List<ItemStack> items) {
if (loc.getWorld() == null) return;
Location drop = loc.clone().add(0.5, 1, 0.5);
for (ItemStack item : items) {
loc.getWorld().dropItemNaturally(drop, item);
}
}

private void playWorkFx(Location loc, Material dust) {
if (loc.getWorld() == null) return;
loc.getWorld().spawnParticle(
Particle.BLOCK,
loc.clone().add(0.5, 1, 0.5),
Expand Down
Loading