feat(xp-items): implement instant xp vacuum and spawned item consolidation listener
This commit is contained in:
@@ -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();
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user