getLines(Player player) {
line = line
.replace("%tps%", String.valueOf(TPSUtil.get1MinTPSRounded()))
.replace("%arenas%", String.valueOf(ArenaManager.getInstance().getArenaList().size()))
- .replace("%enabledArenas%", String.valueOf(ArenaManager.getInstance().getEnabledArenas().size()));
-
+ .replace("%enabledArenas%", String.valueOf(
+ ArenaManager.getInstance().getEnabledArenas().size() +
+ ArenaManager.getInstance().getEnabledFFAArenas().size()
+ ));
sidebar.add(PAPIUtil.runThroughFormat(player, line));
}
}
diff --git a/core/src/main/java/dev/nandi0813/practice/module/interfaces/AbstractBuildListener.java b/core/src/main/java/dev/nandi0813/practice/module/interfaces/AbstractBuildListener.java
index 219f18ac..bf308a6f 100644
--- a/core/src/main/java/dev/nandi0813/practice/module/interfaces/AbstractBuildListener.java
+++ b/core/src/main/java/dev/nandi0813/practice/module/interfaces/AbstractBuildListener.java
@@ -18,6 +18,7 @@
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
+import org.bukkit.event.block.BlockBurnEvent;
import org.bukkit.event.block.BlockFormEvent;
import org.bukkit.event.block.BlockFromToEvent;
import org.bukkit.event.block.BlockPistonExtendEvent;
@@ -425,22 +426,126 @@ public void onBlockFromTo(BlockFromToEvent e) {
// BLOCK SPREAD (fire, mushrooms, etc.)
// =========================================================================
- @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
+ @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onBlockSpread(BlockSpreadEvent e) {
Block source = e.getSource();
- if (!source.hasMetadata(PLACED_IN_FIGHT)) return;
- MetadataValue mv = source.getMetadata(PLACED_IN_FIGHT).get(0);
- if (!(mv.value() instanceof Spectatable spectatable)) return;
+ Spectatable spectatable = null;
+
+ if (source.hasMetadata(PLACED_IN_FIGHT)) {
+ MetadataValue mv = source.getMetadata(PLACED_IN_FIGHT).get(0);
+ if (mv.value() instanceof Spectatable s) {
+ spectatable = s;
+ }
+ }
+
+ if (spectatable == null) {
+ spectatable = getByBlock(source);
+ if (spectatable == null) return;
+ }
+
if (!spectatable.isBuild()) return;
+ // Cancel fire spread during rollback to prevent fire from re-igniting
+ // freshly-restored flammable blocks while the multi-tick rollback is in progress.
+ if (spectatable.getFightChange() != null && spectatable.getFightChange().isRollingBack()) {
+ e.setCancelled(true);
+ return;
+ }
+
+ final Spectatable finalSpectatable = spectatable;
final Block newBlock = e.getNewState().getBlock();
org.bukkit.Bukkit.getScheduler().runTask(ZonePractice.getInstance(), () -> {
if (newBlock.hasMetadata(PLACED_IN_FIGHT)) return;
- tagAndTrack(newBlock, spectatable);
+ tagAndTrack(newBlock, finalSpectatable);
});
}
+ // =========================================================================
+ // BLOCK BURN (fire destroying blocks)
+ // =========================================================================
+
+ /**
+ * Tracks blocks destroyed by fire so they are restored during rollback.
+ * Runs at LOWEST so the block still holds its original material when captured.
+ *
+ * Also tracks adjacent fire blocks (the igniting fire and fire above) so that
+ * rollback removes the fire along with restoring the burned block.
+ */
+ @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
+ public void onBlockBurn(BlockBurnEvent e) {
+ Block block = e.getBlock();
+
+ Spectatable spectatable = null;
+
+ // Fast path: block already tagged
+ if (block.hasMetadata(PLACED_IN_FIGHT)) {
+ MetadataValue mv = block.getMetadata(PLACED_IN_FIGHT).get(0);
+ if (mv.value() instanceof Spectatable s) {
+ spectatable = s;
+ }
+ }
+
+ // Slow path: natural arena block — look up by cuboid
+ if (spectatable == null) {
+ spectatable = getByBlock(block);
+ if (spectatable == null) return;
+ }
+
+ if (!spectatable.isBuild()) return;
+
+ // Cancel burns during rollback to prevent fire from destroying
+ // freshly-restored blocks while the multi-tick rollback is in progress.
+ if (spectatable.getFightChange() != null && spectatable.getFightChange().isRollingBack()) {
+ e.setCancelled(true);
+ return;
+ }
+
+ // Track the block's original state for rollback
+ if (block.hasMetadata(PLACED_IN_FIGHT)) {
+ spectatable.addBlockChange(ClassImport.createChangeBlock(block));
+ } else {
+ spectatable.getFightChange().addArenaBlockChange(ClassImport.createChangeBlock(block));
+ }
+
+ // Track adjacent fire blocks so rollback removes the fire that caused/surrounds the burn.
+ // Without this, restoring the burned block leaves fire sitting on top of it.
+ trackAdjacentFire(block, spectatable);
+
+ // After the burn event, the burned block may be replaced with fire.
+ // Schedule a 1-tick delayed check to track it if so.
+ final Spectatable finalSpectatable = spectatable;
+ org.bukkit.Bukkit.getScheduler().runTask(ZonePractice.getInstance(), () -> {
+ String typeName = block.getType().name();
+ if ((typeName.equals("FIRE") || typeName.equals("SOUL_FIRE")) && !block.hasMetadata(PLACED_IN_FIGHT)) {
+ tagAndTrack(block, finalSpectatable);
+ }
+ });
+ }
+
+ /**
+ * Checks all six faces around a block for fire (FIRE / SOUL_FIRE) and tracks
+ * any untracked fire blocks for rollback so they are removed when the arena resets.
+ */
+ private void trackAdjacentFire(Block center, Spectatable spectatable) {
+ final Block[] adjacent = {
+ center.getRelative(0, 1, 0), // above
+ center.getRelative(0, -1, 0), // below
+ center.getRelative(1, 0, 0), // east
+ center.getRelative(-1, 0, 0), // west
+ center.getRelative(0, 0, 1), // south
+ center.getRelative(0, 0, -1) // north
+ };
+
+ for (Block adj : adjacent) {
+ String typeName = adj.getType().name();
+ if (!typeName.equals("FIRE") && !typeName.equals("SOUL_FIRE")) continue;
+ if (adj.hasMetadata(PLACED_IN_FIGHT)) continue;
+
+ tagAndTrack(adj, spectatable);
+ }
+ }
+
// =========================================================================
// FALLING BLOCKS (sand, gravel, concrete powder, anvils, etc.)
// =========================================================================
diff --git a/core/src/main/java/dev/nandi0813/practice/module/interfaces/ArenaCopyUtil.java b/core/src/main/java/dev/nandi0813/practice/module/interfaces/ArenaCopyUtil.java
index c2c0f049..95f32c89 100644
--- a/core/src/main/java/dev/nandi0813/practice/module/interfaces/ArenaCopyUtil.java
+++ b/core/src/main/java/dev/nandi0813/practice/module/interfaces/ArenaCopyUtil.java
@@ -234,8 +234,8 @@ public void run() {
if (finalActionBar != null) {
finalActionBar.setMessage(LanguageManager.getString("ARENA.ACTION-BAR-MSG")
.replace("%arena%", Common.serializeNormalToMMString(arenaCopy.getMainArena().getDisplayName()))
- .replace("%progress_bar%", StatisticUtil.getProgressBar(progress))
- .replace("%progress_percent%", String.valueOf(progress)));
+ .replace("%progress_bar%", Common.serializeNormalToMMString(StatisticUtil.getProgressBar(progress)))
+ .replace("%progress_percent%", Common.serializeNormalToMMString(String.valueOf(progress))));
}
Location newLoc = new Location(copyWorld, originLoc.getX(), originLoc.getY(), originLoc.getZ()).clone().subtract(reference).add(newLocation);
diff --git a/core/src/main/java/dev/nandi0813/practice/util/ChatFormatUtil.java b/core/src/main/java/dev/nandi0813/practice/util/ChatFormatUtil.java
new file mode 100644
index 00000000..f92d11ff
--- /dev/null
+++ b/core/src/main/java/dev/nandi0813/practice/util/ChatFormatUtil.java
@@ -0,0 +1,74 @@
+package dev.nandi0813.practice.util;
+
+import dev.nandi0813.practice.manager.backend.ConfigManager;
+import dev.nandi0813.practice.manager.backend.LanguageManager;
+import dev.nandi0813.practice.manager.profile.Profile;
+import dev.nandi0813.practice.manager.profile.group.Group;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public enum ChatFormatUtil {
+ ;
+
+ /**
+ * Builds the fully-replaced party chat format string.
+ */
+ public static String buildPartyChatMessage(Player player, String rawMessage) {
+ return LanguageManager.getString("GENERAL-CHAT.PARTY-CHAT")
+ .replace("%%player%%", player.getName())
+ .replace("%%message%%", rawMessage.replaceFirst("@", ""));
+ }
+
+ /**
+ * Builds the fully-replaced staff chat format string.
+ */
+ public static String buildStaffChatMessage(Player player, String rawMessage) {
+ return LanguageManager.getString("GENERAL-CHAT.STAFF-CHAT")
+ .replace("%%player%%", player.getName())
+ .replace("%%message%%", rawMessage);
+ }
+
+ /**
+ * Collects all online players with the staff chat permission.
+ */
+ public static List getStaffRecipients() {
+ List staff = new ArrayList<>();
+ for (Player online : Bukkit.getOnlinePlayers()) {
+ if (online.hasPermission("zpp.staffmode.chat")) {
+ staff.add(online);
+ }
+ }
+ return staff;
+ }
+
+ /**
+ * Resolves the server chat format string (group format or default),
+ * then replaces all static placeholders (division, player, message).
+ */
+ public static String buildServerChatMessage(Profile profile, Player player, String message) {
+ final String format;
+ if (ConfigManager.getBoolean("PLAYER.GROUP-CHAT.ENABLED")) {
+ Group group = profile.getGroup();
+ if (group != null && group.getChatFormat() != null) {
+ format = group.getChatFormat();
+ } else {
+ format = LanguageManager.getString("GENERAL-CHAT.SERVER-CHAT");
+ }
+ } else {
+ format = LanguageManager.getString("GENERAL-CHAT.SERVER-CHAT");
+ }
+
+ String division = profile.getStats().getDivision() != null ? profile.getStats().getDivision().getFullName() : "";
+ String divisionShort = profile.getStats().getDivision() != null ? profile.getStats().getDivision().getShortName() : "";
+
+ return format
+ .replace("%%division%%", division)
+ .replace("%%division_short%%", divisionShort)
+ .replace("%%player%%", player.getName())
+ .replace("%%message%%", message);
+ }
+}
+
diff --git a/core/src/main/java/dev/nandi0813/practice/util/Cuboid.java b/core/src/main/java/dev/nandi0813/practice/util/Cuboid.java
index 2e78ce22..08a67eb9 100644
--- a/core/src/main/java/dev/nandi0813/practice/util/Cuboid.java
+++ b/core/src/main/java/dev/nandi0813/practice/util/Cuboid.java
@@ -544,18 +544,25 @@ public Block getRelativeBlock(World w, int x, int y, int z) {
* @return A list of Chunk objects
*/
public List getChunks() {
- List res = new ArrayList();
+ List res = new ArrayList<>();
World w = this.getWorld();
- int x1 = this.getLowerX() & ~0xf;
- int x2 = this.getUpperX() & ~0xf;
- int z1 = this.getLowerZ() & ~0xf;
- int z2 = this.getUpperZ() & ~0xf;
- for (int x = x1; x <= x2; x += 16) {
- for (int z = z1; z <= z2; z += 16) {
- res.add(w.getChunkAt(x >> 4, z >> 4));
+ int x1 = this.getLowerX() >> 4;
+ int x2 = this.getUpperX() >> 4;
+ int z1 = this.getLowerZ() >> 4;
+ int z2 = this.getUpperZ() >> 4;
+
+ for (int x = x1; x <= x2; x++) {
+ for (int z = z1; z <= z2; z++) {
+ // Only collect already-loaded chunks. Chunk loading is handled
+ // separately by ArenaUtil.loadArenaChunks() to avoid blocking
+ // the main server thread.
+ if (w.isChunkLoaded(x, z)) {
+ res.add(w.getChunkAt(x, z));
+ }
}
}
+
return res;
}
diff --git a/core/src/main/java/dev/nandi0813/practice/util/fightmapchange/FightChangeOptimized.java b/core/src/main/java/dev/nandi0813/practice/util/fightmapchange/FightChangeOptimized.java
index b7cf008e..d1947b26 100644
--- a/core/src/main/java/dev/nandi0813/practice/util/fightmapchange/FightChangeOptimized.java
+++ b/core/src/main/java/dev/nandi0813/practice/util/fightmapchange/FightChangeOptimized.java
@@ -59,6 +59,14 @@ public class FightChangeOptimized {
// Reusable rollback task
private RollbackTask rollbackTask;
+ /**
+ * True while a rollback is in progress. Used by block spread/burn listeners to
+ * cancel new fire spread during the multi-tick rollback window so fire doesn't
+ * re-appear on blocks that have already been restored.
+ */
+ @Getter
+ private volatile boolean rollingBack = false;
+
/**
* Constructor for all fight types (Match, Event, FFA).
*
@@ -235,6 +243,8 @@ public void rollback(int maxCheck, int maxChange) {
* @param onComplete Called on the main thread when rollback finishes, or {@code null}
*/
public void rollback(int maxCheck, int maxChange, @org.jetbrains.annotations.Nullable Runnable onComplete) {
+ rollingBack = true;
+
// Remove all entities (both tracked and cuboid entities in one pass)
removeAllEntities();
@@ -245,7 +255,9 @@ public void rollback(int maxCheck, int maxChange, @org.jetbrains.annotations.Nul
}
if (blocks.isEmpty()) {
- // Nothing to restore — fire callback immediately
+ // Nothing to restore — still extinguish any lingering fire
+ extinguishFire();
+ rollingBack = false;
if (onComplete != null) {
if (org.bukkit.Bukkit.isPrimaryThread()) {
onComplete.run();
@@ -259,6 +271,8 @@ public void rollback(int maxCheck, int maxChange, @org.jetbrains.annotations.Nul
// Quick rollback if server is shutting down
if (!ZonePractice.getInstance().isEnabled()) {
quickRollback();
+ extinguishFire();
+ rollingBack = false;
if (onComplete != null) onComplete.run();
return;
}
@@ -328,6 +342,24 @@ public void quickRollback() {
}
}
+ /**
+ * Scans the arena cuboid and extinguishes any remaining fire (FIRE / SOUL_FIRE) blocks.
+ *
+ * During multi-tick rollback, fire can spread to freshly-restored flammable blocks
+ * before the rollback finishes. This sweep ensures no fire persists after rollback.
+ */
+ private void extinguishFire() {
+ if (cuboid == null) return;
+
+ for (Block block : cuboid) {
+ String typeName = block.getType().name();
+ if (typeName.equals("FIRE") || typeName.equals("SOUL_FIRE")) {
+ block.setType(org.bukkit.Material.AIR, false);
+ block.removeMetadata(PLACED_IN_FIGHT, ZonePractice.getInstance());
+ }
+ }
+ }
+
/**
* Reusable rollback task that processes blocks over multiple ticks.
*
@@ -412,6 +444,10 @@ public void run() {
isRunning = false;
blocks.clear(); // Clear the map
+ // Extinguish any fire that spread during the multi-tick rollback
+ extinguishFire();
+ rollingBack = false;
+
if (onComplete != null) {
onComplete.run(); // already on main thread (runTaskTimer)
}
@@ -428,6 +464,7 @@ public void run() {
} catch (Exception e) {
this.cancel();
isRunning = false;
+ rollingBack = false;
Common.sendConsoleMMMessage("Rollback error at block " + processedBlocks + "/" + totalBlocks + ": " + e.getMessage());
e.printStackTrace();
}
diff --git a/core/src/main/resources/1.8.8/config.yml b/core/src/main/resources/1.8.8/config.yml
index fc39340b..7d49d82d 100644
--- a/core/src/main/resources/1.8.8/config.yml
+++ b/core/src/main/resources/1.8.8/config.yml
@@ -1,4 +1,4 @@
-VERSION: 15
+VERSION: 17
# Mysql database setup.
MYSQL-DATABASE:
@@ -38,31 +38,31 @@ QUEUE:
FIRST-CATEGORY:
SIZE: 4
GO-TO-SECOND-CATEGORY-SLOT: 35
- LADDERS:
- BATTLERUSH: 10
- BEDWARS: 11
- BOXING: 12
- BRIDGES: 14
- FIREBALL: 15
- GAPPLE: 16
- NODEBUFF: 20
- COMBO: 21
- SUMO: 22
- BUILDUHC: 23
- PEARLFIGHT: 24
+ LADDER-SLOTS:
+ - "BATTLERUSH::10"
+ - "BEDWARS::11"
+ - "BOXING::12"
+ - "BRIDGES::14"
+ - "FIREBALL::15"
+ - "GAPPLE::16"
+ - "NODEBUFF::20"
+ - "COMBO::21"
+ - "SUMO::22"
+ - "BUILDUHC::23"
+ - "PEARLFIGHT::24"
SECOND-CATEGORY:
ENABLED: true
BACK-TO-FIRST-CATEGORY-SLOT: 35
SIZE: 4
- LADDERS:
- ARCHER: 10
- AXE: 11
- DEBUFF: 12
- SG: 14
- SKYWARS: 15
- SOUP: 16
- SPLEEF: 20
- VANILLA: 24
+ LADDER-SLOTS:
+ - "ARCHER::10"
+ - "AXE::11"
+ - "DEBUFF::12"
+ - "SG::14"
+ - "SKYWARS::15"
+ - "SOUP::16"
+ - "SPLEEF::20"
+ - "VANILLA::24"
RANKED:
MAX-QUEUE-TIME: 300 # In sec.
GUI-UPDATE-MINUTE: 1
@@ -76,28 +76,28 @@ QUEUE:
FIRST-CATEGORY:
SIZE: 4
GO-TO-SECOND-CATEGORY-SLOT: 35
- LADDERS:
- BATTLERUSH: 10
- BEDWARS: 11
- BOXING: 12
- BRIDGES: 14
- FIREBALL: 15
- GAPPLE: 16
- NODEBUFF: 20
- COMBO: 21
- SUMO: 22
- BUILDUHC: 23
- PEARLFIGHT: 24
+ LADDER-SLOTS:
+ - "BATTLERUSH::10"
+ - "BEDWARS::11"
+ - "BOXING::12"
+ - "BRIDGES::14"
+ - "FIREBALL::15"
+ - "GAPPLE::16"
+ - "NODEBUFF::20"
+ - "COMBO::21"
+ - "SUMO::22"
+ - "BUILDUHC::23"
+ - "PEARLFIGHT::24"
SECOND-CATEGORY:
ENABLED: true
BACK-TO-FIRST-CATEGORY-SLOT: 18
SIZE: 3
- LADDERS:
- DEBUFF: 11
- SG: 12
- SKYWARS: 13
- SOUP: 14
- SPLEEF: 15
+ LADDER-SLOTS:
+ - "DEBUFF::11"
+ - "SG::12"
+ - "SKYWARS::13"
+ - "SOUP::14"
+ - "SPLEEF::15"
#
# Admin settings
ADMIN-SETTINGS:
@@ -192,6 +192,7 @@ MATCH-SETTINGS:
- "REGENERATION::20::2"
REMOVE-EMPTY-BOTTLE: true # Removes empty potion bottles after the player use it.
DUEL:
+ RIGHT-CLICK-TO-DUEL: true # When enabled, players can right-click another player in the lobby to send them a duel request.
INVITATION-EXPIRY: 60 # After the seconds, the duel request will expire.
ROUND-SELECTOR:
MAX: 10
diff --git a/core/src/main/resources/1.8.8/guis.yml b/core/src/main/resources/1.8.8/guis.yml
index df6040c0..f27a1582 100644
--- a/core/src/main/resources/1.8.8/guis.yml
+++ b/core/src/main/resources/1.8.8/guis.yml
@@ -1,4 +1,4 @@
-VERSION: 11
+VERSION: 12
GENERAL-FILLER-ITEM:
NAME: " "
@@ -2130,6 +2130,26 @@ GUIS:
- "&7placed during the match."
- ""
- "&e&lClick here &7to &aenable&7."
+ FIREBALL-BLOCK-DESTROY:
+ ENABLED:
+ NAME: "&7Fireball Block Destroy: &aEnabled"
+ MATERIAL: FIREBALL
+ LORE:
+ - ""
+ - "&7Fireball explosions will destroy"
+ - "&7blocks placed by players."
+ - "&8(Arena blocks are not affected)"
+ - ""
+ - "&e&lClick here &7to &cdisable&7."
+ DISABLED:
+ NAME: "&7Fireball Block Destroy: &cDisabled"
+ MATERIAL: FIREBALL
+ LORE:
+ - ""
+ - "&7Fireball explosions will not"
+ - "&7destroy any blocks."
+ - ""
+ - "&e&lClick here &7to &aenable&7."
SPLEEF-SNOWBALL-MODE:
ENABLED:
NAME: "&7Snowball Mode: &aEnabled"
diff --git a/core/src/main/resources/language.yml b/core/src/main/resources/language.yml
index 0abbcdcb..723b0471 100644
--- a/core/src/main/resources/language.yml
+++ b/core/src/main/resources/language.yml
@@ -1,4 +1,4 @@
-VERSION: 17
+VERSION: 19
CONSOLE-NAME: "Console"
CANT-USE-CONSOLE: "You can't use this command from the console."
@@ -546,6 +546,7 @@ COMMAND:
- "Setup Commands:"
- " » Enable the event.'>/%label% sumo enable"
- " » Disable the event.'>/%label% sumo disable"
+ - " » Set the kit for the event.'>/%label% sumo setkit"
- ""
- "Note: Use /setup event GUI to get the"
- "Event Wand for corner & spawn position setup."
@@ -565,6 +566,8 @@ COMMAND:
NO-WINNER: "No one won the event in time."
STARTED-SPECTATING: "%spectator% started spectating the event."
PLAYER-OUT: "%player% is out of the game."
+ CANT-EDIT: "You cannot edit an enabled Event."
+ KIT-SET: "You successfully set the kit for the sumo event."
TNTTAG:
COMMAND-HELP:
- "---------------------------------------"
@@ -1415,6 +1418,7 @@ MATCH:
- "%rankedExtension%"
PARTY-FFA:
PLAYER-LEFT: "%player% left the match."
+ PLAYER-DIE: "%player% has been eliminated. (%alivePlayers% players remaining)"
START:
MATCH-STARTING: "Match is starting in %seconds% %secondName%."
MATCH-STARTED: "Match has been started."
diff --git a/core/src/main/resources/modern/config.yml b/core/src/main/resources/modern/config.yml
index ce66858d..42840ddf 100644
--- a/core/src/main/resources/modern/config.yml
+++ b/core/src/main/resources/modern/config.yml
@@ -1,4 +1,4 @@
-VERSION: 15
+VERSION: 17
# Mysql database setup.
MYSQL-DATABASE:
@@ -38,33 +38,33 @@ QUEUE:
FIRST-CATEGORY:
SIZE: 5
GO-TO-SECOND-CATEGORY-SLOT: 44
- LADDERS:
- BATTLERUSH: 10
- BEDWARS: 11
- BOXING: 12
- BRIDGES: 14
- FIREBALL: 15
- GAPPLE: 16
- NODEBUFF: 20
- MACE: 21
- SUMO: 22
- BUILDUHC: 23
- PEARLFIGHT: 24
- CRYSTAL: 30
- SPEAR: 32
+ LADDER-SLOTS:
+ - "BATTLERUSH::10"
+ - "BEDWARS::11"
+ - "BOXING::12"
+ - "BRIDGES::14"
+ - "FIREBALL::15"
+ - "GAPPLE::16"
+ - "NODEBUFF::20"
+ - "MACE::21"
+ - "SUMO::22"
+ - "BUILDUHC::23"
+ - "PEARLFIGHT::24"
+ - "CRYSTAL::30"
+ - "SPEAR::32"
SECOND-CATEGORY:
ENABLED: true
BACK-TO-FIRST-CATEGORY-SLOT: 35
SIZE: 4
- LADDERS:
- ARCHER: 10
- AXE: 11
- DEBUFF: 12
- SG: 14
- SKYWARS: 15
- SOUP: 16
- SPLEEF: 20
- VANILLA: 24
+ LADDER-SLOTS:
+ - "ARCHER::10"
+ - "AXE::11"
+ - "DEBUFF::12"
+ - "SG::14"
+ - "SKYWARS::15"
+ - "SOUP::16"
+ - "SPLEEF::20"
+ - "VANILLA::24"
RANKED:
MAX-QUEUE-TIME: 300 # In sec.
GUI-UPDATE-MINUTE: 1
@@ -78,30 +78,30 @@ QUEUE:
FIRST-CATEGORY:
SIZE: 5
GO-TO-SECOND-CATEGORY-SLOT: 44
- LADDERS:
- BATTLERUSH: 10
- BEDWARS: 11
- BOXING: 12
- BRIDGES: 14
- FIREBALL: 15
- GAPPLE: 16
- NODEBUFF: 20
- MACE: 21
- SUMO: 22
- BUILDUHC: 23
- PEARLFIGHT: 24
- CRYSTAL: 30
- SPEAR: 32
+ LADDER-SLOTS:
+ - "BATTLERUSH::10"
+ - "BEDWARS::11"
+ - "BOXING::12"
+ - "BRIDGES::14"
+ - "FIREBALL::15"
+ - "GAPPLE::16"
+ - "NODEBUFF::20"
+ - "MACE::21"
+ - "SUMO::22"
+ - "BUILDUHC::23"
+ - "PEARLFIGHT::24"
+ - "CRYSTAL::30"
+ - "SPEAR::32"
SECOND-CATEGORY:
ENABLED: true
BACK-TO-FIRST-CATEGORY-SLOT: 18
SIZE: 3
- LADDERS:
- DEBUFF: 11
- SG: 12
- SKYWARS: 13
- SOUP: 14
- SPLEEF: 15
+ LADDER-SLOTS:
+ - "DEBUFF::11"
+ - "SG::12"
+ - "SKYWARS::13"
+ - "SOUP::14"
+ - "SPLEEF::15"
#
# Admin settings
ADMIN-SETTINGS:
@@ -195,6 +195,7 @@ MATCH-SETTINGS:
- "REGENERATION::20::2"
REMOVE-EMPTY-BOTTLE: true # Removes empty potion bottles after the player use it.
DUEL:
+ RIGHT-CLICK-TO-DUEL: true # When enabled, players can right-click another player in the lobby to send them a duel request.
INVITATION-EXPIRY: 60 # After the seconds, the duel request will expire.
ROUND-SELECTOR:
MAX: 10
@@ -205,15 +206,15 @@ MATCH-SETTINGS:
FIREWORK-ROCKET:
COOLDOWN: 1 # Cooldown in seconds for using firework rockets with elytra. Default is 1 second.
FIREBALL-FIGHT: # These are multiplier values, so please test what is optimal for you and don't change them too much at once or the difference will multiply.
- FIREBALL-SPEED: 1.3
- FIREBALL-YIELD: 2
+ FIREBALL-SPEED: 1.0
+ FIREBALL-YIELD: 1.8
EXPLOSION:
TNT:
- HORIZONTAL: 1.9
- VERTICAL: 2.1
+ HORIZONTAL: 1.4
+ VERTICAL: 1.3
FIREBALL:
- HORIZONTAL: 1.8
- VERTICAL: 1.7
+ HORIZONTAL: 1.6
+ VERTICAL: 1.4
BOXING:
CUSTOM-ATTACK-COOLDOWN:
ENABLED: true
diff --git a/core/src/main/resources/modern/guis.yml b/core/src/main/resources/modern/guis.yml
index 6ce69d65..47c7852d 100644
--- a/core/src/main/resources/modern/guis.yml
+++ b/core/src/main/resources/modern/guis.yml
@@ -1,4 +1,4 @@
-VERSION: 11
+VERSION: 12
GENERAL-FILLER-ITEM:
NAME: " "
@@ -2075,6 +2075,26 @@ GUIS:
- "&7placed during the match."
- ""
- "&e&lClick here &7to &aenable&7."
+ FIREBALL-BLOCK-DESTROY:
+ ENABLED:
+ NAME: "&7Fireball Block Destroy: &aEnabled"
+ MATERIAL: FIRE_CHARGE
+ LORE:
+ - ""
+ - "&7Fireball explosions will destroy"
+ - "&7blocks placed by players."
+ - "&8(Arena blocks are not affected)"
+ - ""
+ - "&e&lClick here &7to &cdisable&7."
+ DISABLED:
+ NAME: "&7Fireball Block Destroy: &cDisabled"
+ MATERIAL: FIRE_CHARGE
+ LORE:
+ - ""
+ - "&7Fireball explosions will not"
+ - "&7destroy any blocks."
+ - ""
+ - "&e&lClick here &7to &aenable&7."
SPLEEF-SNOWBALL-MODE:
ENABLED:
NAME: "&7Snowball Mode: &aEnabled"
diff --git a/distribution/pom.xml b/distribution/pom.xml
index 5dd03c02..557c0d91 100644
--- a/distribution/pom.xml
+++ b/distribution/pom.xml
@@ -10,7 +10,7 @@
dev.nandi0813
practice-parent
- 6.4.5-SNAPSHOT
+ 6.4.6-SNAPSHOT
@@ -46,7 +46,7 @@
clean install
- ZonePractice Pro v6.4.5-SNAPSHOT
+ ZonePractice Pro v6.4.6-SNAPSHOT
diff --git a/pom.xml b/pom.xml
index b9fb2b88..e08fd06d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
dev.nandi0813
practice-parent
- 6.4.5-SNAPSHOT
+ 6.4.6-SNAPSHOT
pom
ZonePractice Pro
diff --git a/spigot_1_8_8/pom.xml b/spigot_1_8_8/pom.xml
index 02d8db70..97a978d7 100644
--- a/spigot_1_8_8/pom.xml
+++ b/spigot_1_8_8/pom.xml
@@ -7,7 +7,7 @@
dev.nandi0813
practice-parent
- 6.4.5-SNAPSHOT
+ 6.4.6-SNAPSHOT
practice-spigot_1_8_8
diff --git a/spigot_1_8_8/src/main/java/dev/nandi0813/practice_1_8_8/interfaces/ArenaUtil.java b/spigot_1_8_8/src/main/java/dev/nandi0813/practice_1_8_8/interfaces/ArenaUtil.java
index dae82d27..395f77da 100644
--- a/spigot_1_8_8/src/main/java/dev/nandi0813/practice_1_8_8/interfaces/ArenaUtil.java
+++ b/spigot_1_8_8/src/main/java/dev/nandi0813/practice_1_8_8/interfaces/ArenaUtil.java
@@ -4,7 +4,6 @@
import dev.nandi0813.practice.manager.ladder.abstraction.Ladder;
import dev.nandi0813.practice.manager.ladder.abstraction.normal.NormalLadder;
import dev.nandi0813.practice.util.BasicItem;
-import org.bukkit.Chunk;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.ArmorStand;
@@ -84,14 +83,25 @@ public void loadArenaChunks(BasicArena arena) {
// 1.8.8 has no async chunk-load API — stagger each chunk one tick apart so
// the server never freezes trying to load all chunks in a single tick.
org.bukkit.plugin.Plugin plugin = dev.nandi0813.practice.ZonePractice.getInstance();
+ org.bukkit.World world = arena.getCuboid().getWorld();
+ if (world == null) return;
+
+ int minCX = arena.getCuboid().getLowerX() >> 4;
+ int maxCX = arena.getCuboid().getUpperX() >> 4;
+ int minCZ = arena.getCuboid().getLowerZ() >> 4;
+ int maxCZ = arena.getCuboid().getUpperZ() >> 4;
+
long delay = 0;
- for (Chunk chunk : arena.getCuboid().getChunks()) {
- final Chunk c = chunk;
- org.bukkit.Bukkit.getScheduler().runTaskLater(plugin, () -> {
- if (!c.isLoaded()) {
- c.load(true);
- }
- }, delay++);
+ for (int cx = minCX; cx <= maxCX; cx++) {
+ for (int cz = minCZ; cz <= maxCZ; cz++) {
+ final int x = cx;
+ final int z = cz;
+ org.bukkit.Bukkit.getScheduler().runTaskLater(plugin, () -> {
+ if (!world.isChunkLoaded(x, z)) {
+ world.loadChunk(x, z);
+ }
+ }, delay++);
+ }
}
}
diff --git a/spigot_1_8_8/src/main/java/dev/nandi0813/practice_1_8_8/interfaces/PlayerUtil.java b/spigot_1_8_8/src/main/java/dev/nandi0813/practice_1_8_8/interfaces/PlayerUtil.java
index fcb4742e..bdb337b5 100644
--- a/spigot_1_8_8/src/main/java/dev/nandi0813/practice_1_8_8/interfaces/PlayerUtil.java
+++ b/spigot_1_8_8/src/main/java/dev/nandi0813/practice_1_8_8/interfaces/PlayerUtil.java
@@ -121,30 +121,54 @@ public void applyFireballKnockback(final Player player, final Fireball fireball)
final float yield = fireball.getYield() > 0 ? fireball.getYield() : 1.0f;
Bukkit.getScheduler().runTaskLater(ZonePractice.getInstance(), () -> {
- double distance = playerLoc.distance(fireballLoc);
+ double dx = playerLoc.getX() - fireballLoc.getX();
+ double dz = playerLoc.getZ() - fireballLoc.getZ();
+ double horizontalDistance = Math.sqrt(dx * dx + dz * dz);
+
+ // Use only horizontal distance for factor calculation so that jumping
+ // (which only increases vertical separation) does not reduce the knockback strength.
+ double effectiveDistance = horizontalDistance;
double safeDistance = 0.6;
double factor = 1.0;
- if (distance > safeDistance) {
+ if (effectiveDistance > safeDistance) {
double decayRange = yield * 2.0;
- factor = 1.0 - ((distance - safeDistance) / decayRange);
+ factor = 1.0 - ((effectiveDistance - safeDistance) / decayRange);
}
if (factor < 0) factor = 0;
if (factor > 1) factor = 1;
- Vector direction = playerLoc.toVector().subtract(fireballLoc.toVector());
- if (direction.lengthSquared() == 0) {
- direction = new Vector(0, 0.1, 0);
+ // Compute horizontal direction separately so the vertical component
+ // of the direction vector doesn't steal from horizontal velocity.
+ double horizontalLen = Math.sqrt(dx * dx + dz * dz);
+ double horizDirX;
+ double horizDirZ;
+
+ if (horizontalLen < 0.001) {
+ // Fireball is almost directly below – pick the player's facing direction
+ Vector facing = playerLoc.getDirection();
+ double facingLen = Math.sqrt(facing.getX() * facing.getX() + facing.getZ() * facing.getZ());
+ if (facingLen < 0.001) {
+ horizDirX = 0;
+ horizDirZ = 0;
+ } else {
+ horizDirX = facing.getX() / facingLen;
+ horizDirZ = facing.getZ() / facingLen;
+ }
} else {
- direction.normalize();
+ horizDirX = dx / horizontalLen;
+ horizDirZ = dz / horizontalLen;
}
+ // Apply a slight reduction when airborne so it's weaker than grounded, but not drastically
+ double airMultiplier = player.isOnGround() ? 1.0 : 0.8;
+
Vector velocity = new Vector(
- direction.getX() * FB_VELOCITY_HORIZONTAL_MULTIPLICATIVE * factor,
+ horizDirX * FB_VELOCITY_HORIZONTAL_MULTIPLICATIVE * factor * airMultiplier,
FB_VELOCITY_VERTICAL_MULTIPLICATIVE * factor,
- direction.getZ() * FB_VELOCITY_HORIZONTAL_MULTIPLICATIVE * factor
+ horizDirZ * FB_VELOCITY_HORIZONTAL_MULTIPLICATIVE * factor * airMultiplier
);
player.setVelocity(velocity);
diff --git a/spigot_1_8_8/src/main/java/dev/nandi0813/practice_1_8_8/listener/PlayerChatListener.java b/spigot_1_8_8/src/main/java/dev/nandi0813/practice_1_8_8/listener/PlayerChatListener.java
index 5afaf046..dee256a8 100644
--- a/spigot_1_8_8/src/main/java/dev/nandi0813/practice_1_8_8/listener/PlayerChatListener.java
+++ b/spigot_1_8_8/src/main/java/dev/nandi0813/practice_1_8_8/listener/PlayerChatListener.java
@@ -6,9 +6,8 @@
import dev.nandi0813.practice.manager.party.PartyManager;
import dev.nandi0813.practice.manager.profile.Profile;
import dev.nandi0813.practice.manager.profile.ProfileManager;
-import dev.nandi0813.practice.manager.profile.group.Group;
+import dev.nandi0813.practice.util.ChatFormatUtil;
import dev.nandi0813.practice.util.Common;
-import dev.nandi0813.practice.util.playerutil.PlayerUtil;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
@@ -28,15 +27,12 @@ public void onPlayerChat(AsyncPlayerChatEvent e) {
// --- Party chat ---
if (ConfigManager.getBoolean("CHAT.PARTY-CHAT-ENABLED") && profile.isParty() && party != null && message.startsWith("@")) {
- e.setCancelled(true);
-
if (party.isPartyChat() || party.getLeader() == player) {
- final String partyMsg = LanguageManager.getString("GENERAL-CHAT.PARTY-CHAT")
- .replace("%%player%%", player.getName())
- .replace("%%message%%", message.replaceFirst("@", ""));
- Bukkit.getScheduler().runTask(dev.nandi0813.practice.ZonePractice.getInstance(),
- () -> party.sendMessage(partyMsg));
+ e.getRecipients().clear();
+ e.getRecipients().addAll(party.getMembers());
+ applyLegacyFormat(e, ChatFormatUtil.buildPartyChatMessage(player, message));
} else {
+ e.setCancelled(true);
final String cantUse = LanguageManager.getString("PARTY.CANT-USE-PARTY-CHAT");
Bukkit.getScheduler().runTask(dev.nandi0813.practice.ZonePractice.getInstance(),
() -> Common.sendMMMessage(player, cantUse));
@@ -46,55 +42,39 @@ public void onPlayerChat(AsyncPlayerChatEvent e) {
// --- Staff chat (toggle) ---
if (profile.isStaffChat()) {
- e.setCancelled(true);
- Bukkit.getScheduler().runTask(dev.nandi0813.practice.ZonePractice.getInstance(),
- () -> PlayerUtil.sendStaffMessage(player, message));
+ applyStaffChat(e, player, message);
return;
}
// --- Staff chat (shortcut: #message) ---
if (player.hasPermission("zpp.staff") && ConfigManager.getBoolean("CHAT.STAFF-CHAT.SHORTCUT") && message.startsWith("#")) {
- e.setCancelled(true);
- final String staffMsg = message.replaceFirst("#", "");
- Bukkit.getScheduler().runTask(dev.nandi0813.practice.ZonePractice.getInstance(),
- () -> PlayerUtil.sendStaffMessage(player, staffMsg));
+ applyStaffChat(e, player, message.replaceFirst("#", ""));
return;
}
// --- Custom server chat ---
if (ConfigManager.getBoolean("CHAT.SERVER-CHAT-ENABLED")) {
- // Build the format string
- final String format;
- if (ConfigManager.getBoolean("PLAYER.GROUP-CHAT.ENABLED")) {
- Group group = profile.getGroup();
- if (group != null && group.getChatFormat() != null) {
- format = group.getChatFormat();
- } else {
- format = LanguageManager.getString("GENERAL-CHAT.SERVER-CHAT");
- }
- } else {
- format = LanguageManager.getString("GENERAL-CHAT.SERVER-CHAT");
- }
-
- String division = profile.getStats().getDivision() != null ? profile.getStats().getDivision().getFullName() : "";
- String divisionShort = profile.getStats().getDivision() != null ? profile.getStats().getDivision().getShortName() : "";
-
- String preFormatted = format
- .replace("%%division%%", division)
- .replace("%%division_short%%", divisionShort)
- .replace("%%player%%", player.getName())
- .replace("%%message%%", message);
+ applyLegacyFormat(e, ChatFormatUtil.buildServerChatMessage(profile, player, message));
+ }
+ }
- // Serialize the MiniMessage string to a legacy §-coloured string and inject
- // it as the entire chat line via setFormat(). Bukkit calls:
- // String.format(format, playerName, message)
- // We put the fully-built line in slot %2$s and suppress slot %1$s,
- // so the event is NOT cancelled — Bukkit delivers it natively to all recipients.
- String legacy = LegacyComponentSerializer.legacySection()
- .serialize(Common.deserializeMiniMessage(preFormatted));
+ /**
+ * Restricts recipients to staff and applies the staff chat format.
+ */
+ private void applyStaffChat(AsyncPlayerChatEvent e, Player player, String rawMessage) {
+ e.getRecipients().clear();
+ e.getRecipients().addAll(ChatFormatUtil.getStaffRecipients());
+ applyLegacyFormat(e, ChatFormatUtil.buildStaffChatMessage(player, rawMessage));
+ }
- e.setFormat("%2$s");
- e.setMessage(legacy);
- }
+ /**
+ * Serialises a MiniMessage string to a legacy §-coloured string and injects
+ * it into the event via setFormat / setMessage so Bukkit delivers it natively.
+ */
+ private void applyLegacyFormat(AsyncPlayerChatEvent e, String miniMessageString) {
+ String legacy = LegacyComponentSerializer.legacySection()
+ .serialize(Common.deserializeMiniMessage(miniMessageString));
+ e.setFormat("%2$s");
+ e.setMessage(legacy);
}
}
diff --git a/spigot_modern/pom.xml b/spigot_modern/pom.xml
index 2df95872..6b4f54e7 100644
--- a/spigot_modern/pom.xml
+++ b/spigot_modern/pom.xml
@@ -7,7 +7,7 @@
dev.nandi0813
practice-parent
- 6.4.5-SNAPSHOT
+ 6.4.6-SNAPSHOT
practice-spigot_modern
diff --git a/spigot_modern/src/main/java/dev/nandi0813/practice_modern/interfaces/ArenaUtil.java b/spigot_modern/src/main/java/dev/nandi0813/practice_modern/interfaces/ArenaUtil.java
index 54a6b917..f1d629c8 100644
--- a/spigot_modern/src/main/java/dev/nandi0813/practice_modern/interfaces/ArenaUtil.java
+++ b/spigot_modern/src/main/java/dev/nandi0813/practice_modern/interfaces/ArenaUtil.java
@@ -4,7 +4,6 @@
import dev.nandi0813.practice.manager.ladder.abstraction.Ladder;
import dev.nandi0813.practice.manager.ladder.abstraction.normal.NormalLadder;
import dev.nandi0813.practice.util.BasicItem;
-import org.bukkit.Chunk;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.ArmorStand;
@@ -71,12 +70,23 @@ public boolean requiresSupport(Block block) {
@Override
public void loadArenaChunks(BasicArena arena) {
if (arena.getCuboid() == null) return;
- // getChunkAtAsync schedules real async chunk loading on the I/O thread —
- // no main-thread stall and no need to call chunk.load() manually.
org.bukkit.World world = arena.getCuboid().getWorld();
if (world == null) return;
- for (Chunk chunk : arena.getCuboid().getChunks()) {
- world.getChunkAtAsync(chunk.getX(), chunk.getZ());
+
+ // Calculate chunk coordinate range directly from the cuboid bounds
+ // instead of calling getChunks() which synchronously loads all chunks.
+ int minCX = arena.getCuboid().getLowerX() >> 4;
+ int maxCX = arena.getCuboid().getUpperX() >> 4;
+ int minCZ = arena.getCuboid().getLowerZ() >> 4;
+ int maxCZ = arena.getCuboid().getUpperZ() >> 4;
+
+ org.bukkit.plugin.Plugin plugin = dev.nandi0813.practice.ZonePractice.getInstance();
+ for (int cx = minCX; cx <= maxCX; cx++) {
+ for (int cz = minCZ; cz <= maxCZ; cz++) {
+ // addPluginChunkTicket loads the chunk asynchronously if needed
+ // and prevents it from being unloaded — no main-thread stall.
+ world.addPluginChunkTicket(cx, cz, plugin);
+ }
}
}
diff --git a/spigot_modern/src/main/java/dev/nandi0813/practice_modern/interfaces/ModernItemCooldownHandler.java b/spigot_modern/src/main/java/dev/nandi0813/practice_modern/interfaces/ModernItemCooldownHandler.java
index b5955c5d..4d34ecda 100644
--- a/spigot_modern/src/main/java/dev/nandi0813/practice_modern/interfaces/ModernItemCooldownHandler.java
+++ b/spigot_modern/src/main/java/dev/nandi0813/practice_modern/interfaces/ModernItemCooldownHandler.java
@@ -32,7 +32,9 @@ public class ModernItemCooldownHandler implements ItemCooldownHandler {
public void handleEnderPearlFFA(Player player, FightPlayer fightPlayer, int duration, boolean expBar,
Cancellable event, String langKey) {
if (player.hasCooldown(Material.ENDER_PEARL)) {
- event.setCancelled(true);
+ if (event != null) {
+ event.setCancelled(true);
+ }
}
// If no cooldown: let the throw proceed; PlayerItemCooldownEvent will set the correct duration.
}
@@ -41,7 +43,9 @@ public void handleEnderPearlFFA(Player player, FightPlayer fightPlayer, int dura
public void handleEnderPearlMatch(Player player, FightPlayer fightPlayer, int duration, boolean expBar,
Cancellable event, String langKey) {
if (player.hasCooldown(Material.ENDER_PEARL)) {
- event.setCancelled(true);
+ if (event != null) {
+ event.setCancelled(true);
+ }
}
// If no cooldown: let the throw proceed; PlayerItemCooldownEvent will set the correct duration.
}
@@ -53,7 +57,9 @@ public void handleEnderPearlMatch(Player player, FightPlayer fightPlayer, int du
@Override
public void handleGoldenAppleFFA(Player player, int duration, Cancellable event, String langKey) {
if (player.hasCooldown(Material.GOLDEN_APPLE)) {
- event.setCancelled(true);
+ if (event != null) {
+ event.setCancelled(true);
+ }
} else {
player.setCooldown(Material.GOLDEN_APPLE, duration * 20);
}
@@ -62,7 +68,9 @@ public void handleGoldenAppleFFA(Player player, int duration, Cancellable event,
@Override
public void handleGoldenAppleMatch(Player player, int duration, Cancellable event, String langKey) {
if (player.hasCooldown(Material.GOLDEN_APPLE)) {
- event.setCancelled(true);
+ if (event != null) {
+ event.setCancelled(true);
+ }
} else {
player.setCooldown(Material.GOLDEN_APPLE, duration * 20);
}
@@ -77,7 +85,9 @@ public void handleFireworkRocketFFA(Player player, FightPlayer fightPlayer, int
Cancellable event, String langKey) {
Bukkit.getScheduler().runTaskLater(ZonePractice.getInstance(), () -> {
if (player.hasCooldown(Material.FIREWORK_ROCKET)) {
- event.setCancelled(true);
+ if (event != null) {
+ event.setCancelled(true);
+ }
} else {
player.setCooldown(Material.FIREWORK_ROCKET, duration * 20);
}
@@ -89,7 +99,9 @@ public void handleFireworkRocketMatch(Player player, FightPlayer fightPlayer, in
Cancellable event, String langKey) {
Bukkit.getScheduler().runTaskLater(ZonePractice.getInstance(), () -> {
if (player.hasCooldown(Material.FIREWORK_ROCKET)) {
- event.setCancelled(true);
+ if (event != null) {
+ event.setCancelled(true);
+ }
} else {
player.setCooldown(Material.FIREWORK_ROCKET, duration * 20);
}
diff --git a/spigot_modern/src/main/java/dev/nandi0813/practice_modern/interfaces/PlayerUtil.java b/spigot_modern/src/main/java/dev/nandi0813/practice_modern/interfaces/PlayerUtil.java
index b0ea5cdc..6b28e557 100644
--- a/spigot_modern/src/main/java/dev/nandi0813/practice_modern/interfaces/PlayerUtil.java
+++ b/spigot_modern/src/main/java/dev/nandi0813/practice_modern/interfaces/PlayerUtil.java
@@ -114,35 +114,58 @@ public void applyFireballKnockback(final Player player, final Fireball fireball)
final float yield = fireball.getYield() > 0 ? fireball.getYield() : 1.0f;
Bukkit.getScheduler().runTaskLater(ZonePractice.getInstance(), () -> {
- double distance = playerLoc.distance(fireballLoc);
+ double dx = playerLoc.getX() - fireballLoc.getX();
+ double dz = playerLoc.getZ() - fireballLoc.getZ();
+ double horizontalDistance = Math.sqrt(dx * dx + dz * dz);
- double safeDistance = 0.6;
+ // Use only horizontal distance for factor calculation so that jumping
+ // (which only increases vertical separation) does not reduce the knockback strength.
+ double effectiveDistance = horizontalDistance;
+
+ double safeDistance = 0.8;
double factor = 1.0;
- if (distance > safeDistance) {
+ if (effectiveDistance > safeDistance) {
double impactRadius = yield * 2.5;
double decayRange = impactRadius - safeDistance;
if (decayRange <= 0.1) decayRange = 1.0;
- factor = 1.0 - ((distance - safeDistance) / decayRange);
+ factor = 1.0 - ((effectiveDistance - safeDistance) / decayRange);
}
if (factor <= 0.1) return;
if (factor > 1) factor = 1;
- Vector direction = playerLoc.toVector().subtract(fireballLoc.toVector());
-
- if (direction.lengthSquared() == 0) {
- direction = new Vector(0, 0.1, 0);
+ // Compute horizontal direction separately so the vertical component
+ // of the direction vector doesn't steal from horizontal velocity.
+ double horizontalLen = Math.sqrt(dx * dx + dz * dz);
+ double horizDirX;
+ double horizDirZ;
+
+ if (horizontalLen < 0.001) {
+ // Fireball is almost directly below – pick the player's facing direction
+ Vector facing = playerLoc.getDirection();
+ double facingLen = Math.sqrt(facing.getX() * facing.getX() + facing.getZ() * facing.getZ());
+ if (facingLen < 0.001) {
+ horizDirX = 0;
+ horizDirZ = 0;
+ } else {
+ horizDirX = facing.getX() / facingLen;
+ horizDirZ = facing.getZ() / facingLen;
+ }
} else {
- direction.normalize();
+ horizDirX = dx / horizontalLen;
+ horizDirZ = dz / horizontalLen;
}
+ // Apply a slight reduction when airborne so it's weaker than grounded, but not drastically
+ double airMultiplier = player.isOnGround() ? 1.0 : 0.8;
+
Vector velocity = new Vector(
- direction.getX() * FB_VELOCITY_HORIZONTAL_MULTIPLICATIVE * factor,
+ horizDirX * FB_VELOCITY_HORIZONTAL_MULTIPLICATIVE * factor * airMultiplier,
FB_VELOCITY_VERTICAL_MULTIPLICATIVE * factor,
- direction.getZ() * FB_VELOCITY_HORIZONTAL_MULTIPLICATIVE * factor
+ horizDirZ * FB_VELOCITY_HORIZONTAL_MULTIPLICATIVE * factor * airMultiplier
);
player.setVelocity(velocity);
diff --git a/spigot_modern/src/main/java/dev/nandi0813/practice_modern/listener/ArenaListener.java b/spigot_modern/src/main/java/dev/nandi0813/practice_modern/listener/ArenaListener.java
index 916f0b3f..797a489a 100644
--- a/spigot_modern/src/main/java/dev/nandi0813/practice_modern/listener/ArenaListener.java
+++ b/spigot_modern/src/main/java/dev/nandi0813/practice_modern/listener/ArenaListener.java
@@ -1,5 +1,6 @@
package dev.nandi0813.practice_modern.listener;
+import dev.nandi0813.practice.ZonePractice;
import dev.nandi0813.practice.manager.arena.util.ArenaWorldUtil;
import dev.nandi0813.practice.manager.server.ServerManager;
import org.bukkit.event.EventHandler;
@@ -16,7 +17,10 @@ public class ArenaListener implements Listener {
public void onChunkUnload(ChunkUnloadEvent e) {
if (LOAD_CHUNKS) {
if (LOADED_CHUNKS.contains(e.getChunk())) {
- e.getChunk().getWorld().getChunkAtAsync(e.getChunk().getX(), e.getChunk().getZ());
+ // Use addPluginChunkTicket to force-keep the chunk loaded.
+ // This is safe from recursion (unlike getChunkAtAsync which can
+ // trigger chunk scheduling → more unloads → StackOverflowError).
+ e.getChunk().addPluginChunkTicket(ZonePractice.getInstance());
}
}
}
diff --git a/spigot_modern/src/main/java/dev/nandi0813/practice_modern/listener/FireworkRocketCooldownListener.java b/spigot_modern/src/main/java/dev/nandi0813/practice_modern/listener/FireworkRocketCooldownListener.java
index 7d5201bb..395faa60 100644
--- a/spigot_modern/src/main/java/dev/nandi0813/practice_modern/listener/FireworkRocketCooldownListener.java
+++ b/spigot_modern/src/main/java/dev/nandi0813/practice_modern/listener/FireworkRocketCooldownListener.java
@@ -1,23 +1,16 @@
package dev.nandi0813.practice_modern.listener;
-import dev.nandi0813.practice.ZonePractice;
import dev.nandi0813.practice.manager.fight.ffa.FFAManager;
import dev.nandi0813.practice.manager.fight.ffa.game.FFA;
import dev.nandi0813.practice.manager.fight.match.Match;
import dev.nandi0813.practice.manager.fight.match.MatchManager;
import dev.nandi0813.practice.manager.fight.match.enums.RoundStatus;
import dev.nandi0813.practice.module.util.ClassImport;
-import org.bukkit.Material;
-import org.bukkit.Bukkit;
+import org.bukkit.entity.Firework;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
-import org.bukkit.event.player.PlayerInteractEvent;
-import org.bukkit.inventory.ItemStack;
-
-import java.util.HashSet;
-import java.util.Set;
-import java.util.UUID;
+import org.bukkit.event.entity.EntitySpawnEvent;
/**
* Handles firework rocket cooldown for elytra boost in modern Minecraft versions.
@@ -27,62 +20,37 @@
*/
public class FireworkRocketCooldownListener implements Listener {
- // Tracks players whose interact event was already handled this tick to prevent
- // duplicate processing from the MAIN_HAND + OFF_HAND double-fire.
- private final Set handledThisTick = new HashSet<>();
-
@EventHandler
- public void onFireworkRocketUse(PlayerInteractEvent e) {
- Player player = e.getPlayer();
- ItemStack item = e.getItem();
+ public void onFireworkSpawn(EntitySpawnEvent e) {
- // Check if player is using a firework rocket
- if (item == null || item.getType() != Material.FIREWORK_ROCKET) {
- return;
- }
+ if (!(e.getEntity() instanceof Firework firework)) return;
+ if (!(firework.getShooter() instanceof Player player)) return;
- // Check if player is wearing elytra
- ItemStack chestplate = player.getInventory().getChestplate();
- if (chestplate == null || chestplate.getType() != Material.ELYTRA) {
- return;
- }
-
- // Deduplicate: PlayerInteractEvent fires once per hand (MAIN + OFF), skip the second call this tick
- UUID uuid = player.getUniqueId();
- if (!handledThisTick.add(uuid)) {
- return;
- }
- // Clean up after this tick so the next click is processed normally
- Bukkit.getScheduler().runTask(ZonePractice.getInstance(), () -> handledThisTick.remove(uuid));
-
- // Check if player is in FFA
+ // FFA
FFA ffa = FFAManager.getInstance().getFFAByPlayer(player);
if (ffa != null) {
+
int duration = ffa.getPlayers().get(player).getFireworkRocketCooldown();
- if (duration <= 0) {
- return;
- }
+ if (duration <= 0) return;
ClassImport.getClasses().getItemCooldownHandler().handleFireworkRocketFFA(
player,
ffa.getFightPlayers().get(player),
duration,
- e,
+ null,
"MATCH.COOLDOWN.FIREWORK-ROCKET-COOLDOWN"
);
return;
}
- // Check if player is in a match
+ // Match
Match match = MatchManager.getInstance().getLiveMatchByPlayer(player);
if (match != null) {
+
int duration = match.getLadder().getFireworkRocketCooldown();
- if (duration <= 0) {
- return;
- }
+ if (duration <= 0) return;
if (!match.getCurrentRound().getRoundStatus().equals(RoundStatus.LIVE)) {
- e.setCancelled(true);
return;
}
@@ -90,10 +58,9 @@ public void onFireworkRocketUse(PlayerInteractEvent e) {
player,
match.getMatchPlayers().get(player),
duration,
- e,
+ null,
"MATCH.COOLDOWN.FIREWORK-ROCKET-COOLDOWN"
);
}
}
-
-}
+}
\ No newline at end of file
diff --git a/spigot_modern/src/main/java/dev/nandi0813/practice_modern/listener/PlayerChatListener.java b/spigot_modern/src/main/java/dev/nandi0813/practice_modern/listener/PlayerChatListener.java
index 8ff801b4..7763c78e 100644
--- a/spigot_modern/src/main/java/dev/nandi0813/practice_modern/listener/PlayerChatListener.java
+++ b/spigot_modern/src/main/java/dev/nandi0813/practice_modern/listener/PlayerChatListener.java
@@ -7,12 +7,12 @@
import dev.nandi0813.practice.manager.party.PartyManager;
import dev.nandi0813.practice.manager.profile.Profile;
import dev.nandi0813.practice.manager.profile.ProfileManager;
-import dev.nandi0813.practice.manager.profile.group.Group;
+import dev.nandi0813.practice.util.ChatFormatUtil;
import dev.nandi0813.practice.util.Common;
import dev.nandi0813.practice.util.SoftDependUtil;
import dev.nandi0813.practice.util.PAPIUtil;
-import dev.nandi0813.practice.util.playerutil.PlayerUtil;
import io.papermc.paper.event.player.AsyncChatEvent;
+import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
@@ -20,6 +20,9 @@
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
+import java.util.Collection;
+import java.util.Set;
+
public class PlayerChatListener implements Listener {
@EventHandler(priority = EventPriority.NORMAL)
@@ -31,15 +34,11 @@ public void onPlayerChat(AsyncChatEvent e) {
// --- Party chat ---
if (ConfigManager.getBoolean("CHAT.PARTY-CHAT-ENABLED") && profile.isParty() && party != null && message.startsWith("@")) {
- e.setCancelled(true);
-
if (party.isPartyChat() || party.getLeader() == player) {
- final String partyMsg = LanguageManager.getString("GENERAL-CHAT.PARTY-CHAT")
- .replace("%%player%%", player.getName())
- .replace("%%message%%", message.replaceFirst("@", ""));
- Bukkit.getScheduler().runTask(ZonePractice.getInstance(),
- () -> party.sendMessage(partyMsg));
+ setViewers(e, party.getMembers());
+ applyRenderer(e, ChatFormatUtil.buildPartyChatMessage(player, message));
} else {
+ e.setCancelled(true);
final String cantUse = LanguageManager.getString("PARTY.CANT-USE-PARTY-CHAT");
Bukkit.getScheduler().runTask(ZonePractice.getInstance(),
() -> Common.sendMMMessage(player, cantUse));
@@ -49,61 +48,50 @@ public void onPlayerChat(AsyncChatEvent e) {
// --- Staff chat (toggle) ---
if (profile.isStaffChat()) {
- e.setCancelled(true);
- Bukkit.getScheduler().runTask(ZonePractice.getInstance(),
- () -> PlayerUtil.sendStaffMessage(player, message));
+ applyStaffChat(e, player, message);
return;
}
// --- Staff chat (shortcut: #message) ---
if (player.hasPermission("zpp.staff") && ConfigManager.getBoolean("CHAT.STAFF-CHAT.SHORTCUT") && message.startsWith("#")) {
- e.setCancelled(true);
- final String staffMsg = message.replaceFirst("#", "");
- Bukkit.getScheduler().runTask(ZonePractice.getInstance(),
- () -> PlayerUtil.sendStaffMessage(player, staffMsg));
+ applyStaffChat(e, player, message.replaceFirst("#", ""));
return;
}
// --- Custom server chat ---
if (ConfigManager.getBoolean("CHAT.SERVER-CHAT-ENABLED")) {
- // Build the format string
- final String format;
- if (ConfigManager.getBoolean("PLAYER.GROUP-CHAT.ENABLED")) {
- Group group = profile.getGroup();
- if (group != null && group.getChatFormat() != null) {
- format = group.getChatFormat();
- } else {
- format = LanguageManager.getString("GENERAL-CHAT.SERVER-CHAT");
- }
- } else {
- format = LanguageManager.getString("GENERAL-CHAT.SERVER-CHAT");
- }
-
- String division = profile.getStats().getDivision() != null ? profile.getStats().getDivision().getFullName() : "";
- String divisionShort = profile.getStats().getDivision() != null ? profile.getStats().getDivision().getShortName() : "";
+ applyRenderer(e, ChatFormatUtil.buildServerChatMessage(profile, player, message));
+ }
+ }
- // Replace all static placeholders now; leave PAPI to the renderer (per-viewer)
- String preFormatted = format
- .replace("%%division%%", division)
- .replace("%%division_short%%", divisionShort)
- .replace("%%player%%", player.getName())
- .replace("%%message%%", message);
+ /**
+ * Restricts viewers to staff and applies the staff chat renderer.
+ */
+ private void applyStaffChat(AsyncChatEvent e, Player player, String rawMessage) {
+ setViewers(e, ChatFormatUtil.getStaffRecipients());
+ applyRenderer(e, ChatFormatUtil.buildStaffChatMessage(player, rawMessage));
+ }
- // Use the Adventure ChatRenderer so we don't cancel — the event itself
- // delivers the component to each viewer through the normal pipeline.
- // NOTE: AsyncChatEvent fires on an async thread; the renderer is also invoked
- // async by Paper. PlaceholderAPI is NOT thread-safe, so PAPI placeholders are
- // resolved here on the async thread only when isPAPI_ENABLED is true.
- // Most PAPI expansions are effectively read-only and safe in practice, but if
- // stricter safety is needed, pre-resolve per-viewer on the main thread before
- // the event fires (e.g. via a sync task cache).
- e.renderer((source, sourceDisplayName, msg, viewer) -> {
- if (SoftDependUtil.isPAPI_ENABLED && viewer instanceof Player viewerPlayer) {
- return PAPIUtil.runThroughFormat(viewerPlayer, preFormatted);
- }
+ /**
+ * Replaces the viewer set with the given collection of players.
+ */
+ private void setViewers(AsyncChatEvent e, Collection extends Audience> targets) {
+ Set viewers = e.viewers();
+ viewers.clear();
+ viewers.add(ZonePractice.getAdventure().console());
+ viewers.addAll(targets);
+ }
- return ZonePractice.getMiniMessage().deserialize(preFormatted);
- });
- }
+ /**
+ * Sets a ChatRenderer that deserialises the given MiniMessage string,
+ * resolving PAPI placeholders per-viewer when available.
+ */
+ private void applyRenderer(AsyncChatEvent e, String miniMessageString) {
+ e.renderer((source, sourceDisplayName, msg, viewer) -> {
+ if (SoftDependUtil.isPAPI_ENABLED && viewer instanceof Player viewerPlayer) {
+ return PAPIUtil.runThroughFormat(viewerPlayer, miniMessageString);
+ }
+ return ZonePractice.getMiniMessage().deserialize(miniMessageString);
+ });
}
}