feat(xp-items): implement instant xp vacuum and spawned item consolidation listener

This commit is contained in:
Marcos Paulo
2026-08-20 16:55:01 -03:00
parent aa56f385f8
commit 45f129df56
3 changed files with 138 additions and 0 deletions
@@ -161,6 +161,7 @@ public final class Canalhandia extends JavaPlugin implements Listener {
getServer().getPluginManager().registerEvents(this, this); getServer().getPluginManager().registerEvents(this, this);
getServer().getPluginManager().registerEvents(new TitleChatListener(this), this); getServer().getPluginManager().registerEvents(new TitleChatListener(this), this);
getServer().getPluginManager().registerEvents(new ChunkLoaderListener(this), this); getServer().getPluginManager().registerEvents(new ChunkLoaderListener(this), this);
getServer().getPluginManager().registerEvents(new XpAndItemMergeListener(this), this);
rescheduleTimer(); rescheduleTimer();
rescheduleMilestones(); rescheduleMilestones();
@@ -0,0 +1,109 @@
package dev.marcospaulo.canalhandia;
import com.destroystokyo.paper.event.player.PlayerPickupExperienceEvent;
import org.bukkit.Location;
import org.bukkit.Sound;
import org.bukkit.entity.Entity;
import org.bukkit.entity.ExperienceOrb;
import org.bukkit.entity.Item;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.ItemSpawnEvent;
import org.bukkit.inventory.ItemStack;
/**
* 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
* full stacks.
*/
public final class XpAndItemMergeListener implements Listener {
private final Canalhandia plugin;
public XpAndItemMergeListener(Canalhandia plugin) {
this.plugin = plugin;
}
/**
* Vacuums all experience orbs within radius instantly on player contact.
*/
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onPlayerPickupXp(PlayerPickupExperienceEvent event) {
Player player = event.getPlayer();
Location loc = player.getLocation();
if (loc.getWorld() == null) {
return;
}
int totalExp = 0;
try {
for (Entity entity : loc.getWorld().getNearbyEntities(loc, 6.0, 6.0, 6.0)) {
if (entity instanceof ExperienceOrb orb && orb.isValid()) {
totalExp += orb.getExperience();
orb.remove();
}
}
} catch (Exception ignored) {
}
if (totalExp > 0) {
player.giveExp(totalExp);
try {
player.playSound(loc, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 0.5f, 1.2f);
} catch (Exception ignored) {
}
event.setCancelled(true);
}
}
/**
* Consolidates spawned items of the same type in a 4-block radius into unified stacks.
*/
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onItemSpawn(ItemSpawnEvent event) {
Item item = event.getEntity();
ItemStack stack = item.getItemStack();
if (stack.getAmount() >= stack.getMaxStackSize() || item.getWorld() == null) {
return;
}
try {
for (Entity e : item.getWorld().getNearbyEntities(item.getLocation(), 4.0, 4.0, 4.0)) {
if (e instanceof Item other && other != item && other.isValid()) {
ItemStack otherStack = other.getItemStack();
if (otherStack.isSimilar(stack)) {
int toTransfer = calculateTransfer(stack.getAmount(), stack.getMaxStackSize(), otherStack.getAmount());
if (toTransfer > 0) {
stack.setAmount(stack.getAmount() + toTransfer);
otherStack.setAmount(otherStack.getAmount() - toTransfer);
if (otherStack.getAmount() <= 0) {
other.remove();
} else {
other.setItemStack(otherStack);
}
item.setItemStack(stack);
if (stack.getAmount() >= stack.getMaxStackSize()) {
break;
}
}
}
}
}
} catch (Exception ignored) {
}
}
/** Pure transfer amount calculator for stack consolidation. */
public static int calculateTransfer(int targetAmount, int targetMax, int sourceAmount) {
if (targetAmount < 0 || targetMax <= 0 || sourceAmount <= 0) {
return 0;
}
int canAdd = targetMax - targetAmount;
if (canAdd <= 0) {
return 0;
}
return Math.min(canAdd, sourceAmount);
}
}
@@ -0,0 +1,28 @@
package dev.marcospaulo.canalhandia;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class XpAndItemMergeTest {
@Test
void handlesZeroOrNegativeAmountsGracefully() {
assertEquals(0, XpAndItemMergeListener.calculateTransfer(0, 0, 0));
assertEquals(0, XpAndItemMergeListener.calculateTransfer(-1, 16, 5));
assertEquals(0, XpAndItemMergeListener.calculateTransfer(16, 16, 5));
assertEquals(0, XpAndItemMergeListener.calculateTransfer(10, 16, 0));
}
@Test
void calculatesTransfersUpToMaxCapacity() {
// Target has 10, max is 16, source has 10 -> can transfer 6
assertEquals(6, XpAndItemMergeListener.calculateTransfer(10, 16, 10));
// Target has 5, max is 64, source has 20 -> can transfer all 20
assertEquals(20, XpAndItemMergeListener.calculateTransfer(5, 64, 20));
// Target has 63, max is 64, source has 10 -> can transfer 1
assertEquals(1, XpAndItemMergeListener.calculateTransfer(63, 64, 10));
}
}