fix(xp): prioritize repairing equipped mending items before giving leftover xp to player

This commit is contained in:
Marcos Paulo
2026-08-22 13:44:54 -03:00
parent 2a63a04e4f
commit 3a21d3cfd5
2 changed files with 109 additions and 4 deletions
@@ -13,10 +13,18 @@ import org.bukkit.event.Listener;
import org.bukkit.event.entity.ItemSpawnEvent; import org.bukkit.event.entity.ItemSpawnEvent;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.Material;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.inventory.PlayerInventory;
import org.bukkit.inventory.meta.Damageable;
import java.util.ArrayList;
import java.util.List;
/** /**
* Optimises XP and Item collection by instantly vacuuming nearby XP orbs into the player's * Optimises XP and Item collection by instantly vacuuming nearby XP orbs into the player's
* XP bar (bypassing Vanilla's 2-tick per orb delay) and consolidating spawned item drops into * XP bar (bypassing Vanilla's 2-tick per orb delay), repairing Mending equipment first,
* full stacks. * and consolidating spawned item drops into full stacks.
*/ */
public final class XpAndItemMergeListener implements Listener { public final class XpAndItemMergeListener implements Listener {
@@ -27,7 +35,7 @@ public final class XpAndItemMergeListener implements Listener {
} }
/** /**
* Vacuums all experience orbs within radius instantly on player contact. * Vacuums all experience orbs within radius instantly on player contact and repairs Mending items.
*/ */
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onPlayerPickupXp(PlayerPickupExperienceEvent event) { public void onPlayerPickupXp(PlayerPickupExperienceEvent event) {
@@ -49,7 +57,10 @@ public final class XpAndItemMergeListener implements Listener {
} }
if (totalExp > 0) { if (totalExp > 0) {
player.giveExp(totalExp); int leftoverExp = applyMending(player, totalExp);
if (leftoverExp > 0) {
player.giveExp(leftoverExp);
}
try { try {
player.playSound(loc, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 0.5f, 1.2f); player.playSound(loc, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 0.5f, 1.2f);
} catch (Exception ignored) { } catch (Exception ignored) {
@@ -58,6 +69,73 @@ public final class XpAndItemMergeListener implements Listener {
} }
} }
/**
* Repairs equipped items with Mending enchantment using the collected XP.
* Returns remaining XP to be added to the player's experience bar.
*/
public static int applyMending(Player player, int totalExp) {
if (player == null || totalExp <= 0) {
return totalExp;
}
int remainingXp = totalExp;
PlayerInventory inv = player.getInventory();
List<ItemStack> candidates = new ArrayList<>();
ItemStack main = inv.getItemInMainHand();
if (isDamagedMending(main)) candidates.add(main);
ItemStack off = inv.getItemInOffHand();
if (isDamagedMending(off)) candidates.add(off);
for (ItemStack armor : inv.getArmorContents()) {
if (isDamagedMending(armor)) candidates.add(armor);
}
for (ItemStack item : candidates) {
if (remainingXp <= 0) {
break;
}
if (item.getItemMeta() instanceof Damageable dmg && dmg.hasDamage()) {
int damage = dmg.getDamage();
int[] repair = calculateMendingRepair(damage, remainingXp);
int repaired = repair[0];
int xpUsed = repair[1];
if (repaired > 0) {
dmg.setDamage(damage - repaired);
item.setItemMeta(dmg);
remainingXp -= xpUsed;
}
}
}
return remainingXp;
}
private static boolean isDamagedMending(ItemStack item) {
if (item == null || item.getType() == Material.AIR || !item.hasItemMeta()) {
return false;
}
if (!item.containsEnchantment(Enchantment.MENDING)) {
return false;
}
if (item.getItemMeta() instanceof Damageable dmg) {
return dmg.hasDamage() && dmg.getDamage() > 0;
}
return false;
}
/**
* Calculates repair amount and used XP for a given damage and available XP.
* Returns an int array: [durabilityRepaired, xpUsed].
*/
public static int[] calculateMendingRepair(int damage, int availableXp) {
if (damage <= 0 || availableXp <= 0) {
return new int[]{0, 0};
}
int durabilityToRepair = Math.min(damage, availableXp * 2);
int xpUsed = (durabilityToRepair + 1) / 2;
return new int[]{durabilityToRepair, xpUsed};
}
/** /**
* Consolidates spawned items of the same type in a 4-block radius into unified stacks. * Consolidates spawned items of the same type in a 4-block radius into unified stacks.
*/ */
@@ -25,4 +25,31 @@ class XpAndItemMergeTest {
// Target has 63, max is 64, source has 10 -> can transfer 1 // Target has 63, max is 64, source has 10 -> can transfer 1
assertEquals(1, XpAndItemMergeListener.calculateTransfer(63, 64, 10)); assertEquals(1, XpAndItemMergeListener.calculateTransfer(63, 64, 10));
} }
@Test
void calculatesMendingRepairCorrectly() {
// Zero damage or zero XP -> 0 repair
int[] r1 = XpAndItemMergeListener.calculateMendingRepair(0, 50);
assertEquals(0, r1[0]);
assertEquals(0, r1[1]);
int[] r2 = XpAndItemMergeListener.calculateMendingRepair(50, 0);
assertEquals(0, r2[0]);
assertEquals(0, r2[1]);
// 10 damage, 100 XP -> repairs 10 durability, uses 5 XP
int[] r3 = XpAndItemMergeListener.calculateMendingRepair(10, 100);
assertEquals(10, r3[0]);
assertEquals(5, r3[1]);
// 5 damage, 10 XP -> repairs 5 durability, uses 3 XP
int[] r4 = XpAndItemMergeListener.calculateMendingRepair(5, 10);
assertEquals(5, r4[0]);
assertEquals(3, r4[1]);
// 10 damage, 2 XP -> repairs 4 durability (2 * 2), uses 2 XP
int[] r5 = XpAndItemMergeListener.calculateMendingRepair(10, 2);
assertEquals(4, r5[0]);
assertEquals(2, r5[1]);
}
} }