diff --git a/src/main/java/dev/marcospaulo/canalhandia/Canalhandia.java b/src/main/java/dev/marcospaulo/canalhandia/Canalhandia.java index 9e6d8bc..e5029c9 100644 --- a/src/main/java/dev/marcospaulo/canalhandia/Canalhandia.java +++ b/src/main/java/dev/marcospaulo/canalhandia/Canalhandia.java @@ -161,6 +161,7 @@ public final class Canalhandia extends JavaPlugin implements Listener { getServer().getPluginManager().registerEvents(this, this); getServer().getPluginManager().registerEvents(new TitleChatListener(this), this); getServer().getPluginManager().registerEvents(new ChunkLoaderListener(this), this); + getServer().getPluginManager().registerEvents(new XpAndItemMergeListener(this), this); rescheduleTimer(); rescheduleMilestones(); diff --git a/src/main/java/dev/marcospaulo/canalhandia/XpAndItemMergeListener.java b/src/main/java/dev/marcospaulo/canalhandia/XpAndItemMergeListener.java new file mode 100644 index 0000000..1440181 --- /dev/null +++ b/src/main/java/dev/marcospaulo/canalhandia/XpAndItemMergeListener.java @@ -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); + } +} diff --git a/src/test/java/dev/marcospaulo/canalhandia/XpAndItemMergeTest.java b/src/test/java/dev/marcospaulo/canalhandia/XpAndItemMergeTest.java new file mode 100644 index 0000000..46bf14f --- /dev/null +++ b/src/test/java/dev/marcospaulo/canalhandia/XpAndItemMergeTest.java @@ -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)); + } +}