From ee6f64b434189fde2c919613153bd2df30c87d47 Mon Sep 17 00:00:00 2001 From: Nandor Dukat Date: Tue, 17 Mar 2026 11:42:23 +0100 Subject: [PATCH 01/26] fixed event host bug --- .../manager/fight/event/EventManager.java | 54 +++++++++++++++++-- .../manager/fight/event/interfaces/Event.java | 8 +-- .../runnables/queue/QueueStartRunnable.java | 8 --- 3 files changed, 54 insertions(+), 16 deletions(-) diff --git a/core/src/main/java/dev/nandi0813/practice/manager/fight/event/EventManager.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/event/EventManager.java index 6de40871..3089b0f0 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/fight/event/EventManager.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/fight/event/EventManager.java @@ -31,6 +31,8 @@ import dev.nandi0813.practice.manager.gui.GUIManager; import dev.nandi0813.practice.manager.gui.guis.EventHostGui; import dev.nandi0813.practice.manager.gui.setup.event.EventSetupManager; +import dev.nandi0813.practice.manager.profile.Profile; +import dev.nandi0813.practice.manager.profile.ProfileManager; import dev.nandi0813.practice.util.Common; import dev.nandi0813.practice.util.StartUpCallback; import lombok.Getter; @@ -133,14 +135,44 @@ public void saveEventData() { } } - public void startEvent(Player starter, EventType eventType) { + public boolean startEvent(Player starter, EventType eventType) { if (eventType == null) { - return; + return false; } if (!getEventData().get(eventType).isEnabled()) { ZonePractice.getInstance().getLogger().warning("Event " + eventType.getName() + " is not enabled."); - return; + return false; + } + + if (starter != null) { + if (!starter.hasPermission("zpp.event.host") || + (!starter.hasPermission("zpp.event.host." + eventType.name().toLowerCase()) && !starter.hasPermission("zpp.event.host.all"))) { + Common.sendMMMessage(starter, LanguageManager.getString("EVENT.CANT-HOST-EVENT").replace("%event%", eventType.getName())); + return false; + } + + Profile starterProfile = ProfileManager.getInstance().getProfile(starter); + if (starterProfile == null || starterProfile.getEventStartLeft() <= 0) { + Common.sendMMMessage(starter, LanguageManager.getString("EVENT.CANT-HOST-EVENT-TODAY")); + return false; + } + } + + if (!this.events.isEmpty() && ConfigManager.getBoolean("EVENT.MULTIPLE")) { + for (Event liveEvent : this.events) { + if (liveEvent.getStatus().equals(dev.nandi0813.practice.manager.fight.event.enums.EventStatus.COLLECTING)) { + if (starter != null) { + Common.sendMMMessage(starter, LanguageManager.getString("COMMAND.EVENT.ARGUMENTS.HOST.CANT-HOST-NOW")); + } + return false; + } + } + } else if (!this.events.isEmpty() && !ConfigManager.getBoolean("EVENT.MULTIPLE")) { + if (starter != null) { + Common.sendMMMessage(starter, LanguageManager.getString("EVENT.ONLY-ONE-EVENT")); + } + return false; } if (this.isEventLive(eventType)) { @@ -149,7 +181,7 @@ public void startEvent(Player starter, EventType eventType) { else Common.sendConsoleMMMessage(LanguageManager.getString("EVENT.CANT-START-EVENT").replace("%event%", eventType.getName())); - return; + return false; } Event event = switch (eventType) { @@ -163,7 +195,19 @@ public void startEvent(Player starter, EventType eventType) { }; events.add(event); - event.startQueue(); + if (!event.startQueue()) { + events.remove(event); + return false; + } + + if (starter != null) { + Profile starterProfile = ProfileManager.getInstance().getProfile(starter); + if (starterProfile != null) { + starterProfile.setEventStartLeft(Math.max(0, starterProfile.getEventStartLeft() - 1)); + } + } + + return true; } public boolean isEventLive(EventType eventType) { diff --git a/core/src/main/java/dev/nandi0813/practice/manager/fight/event/interfaces/Event.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/event/interfaces/Event.java index c9e08148..b1d4f39b 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/fight/event/interfaces/Event.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/fight/event/interfaces/Event.java @@ -126,15 +126,15 @@ public void removePlayer(Player player, boolean message) { public abstract void killPlayer(Player player, boolean teleport); - public void startQueue() { + public boolean startQueue() { EventStartEvent event = new EventStartEvent(this); Bukkit.getPluginManager().callEvent(event); if (event.isCancelled()) { - return; + return false; } if (!this.status.equals(EventStatus.COLLECTING)) { - return; + return false; } this.queueRunnable.begin(); @@ -150,6 +150,8 @@ public void startQueue() { if (starter instanceof Player) { this.addPlayer((Player) starter); } + + return true; } public void stopQueue() { diff --git a/core/src/main/java/dev/nandi0813/practice/manager/fight/event/runnables/queue/QueueStartRunnable.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/event/runnables/queue/QueueStartRunnable.java index 4d651190..d6695d66 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/fight/event/runnables/queue/QueueStartRunnable.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/fight/event/runnables/queue/QueueStartRunnable.java @@ -2,13 +2,10 @@ import dev.nandi0813.practice.manager.backend.LanguageManager; import dev.nandi0813.practice.manager.fight.event.interfaces.Event; -import dev.nandi0813.practice.manager.profile.Profile; -import dev.nandi0813.practice.manager.profile.ProfileManager; import dev.nandi0813.practice.manager.server.sound.SoundEffect; import dev.nandi0813.practice.manager.server.sound.SoundManager; import dev.nandi0813.practice.manager.server.sound.SoundType; import dev.nandi0813.practice.util.interfaces.Runnable; -import org.bukkit.entity.Player; public class QueueStartRunnable extends Runnable { @@ -42,11 +39,6 @@ public void run() { this.cancel(); event.start(); event.getQueueRunnable().cancel(); - - if (event.getStarter() instanceof Player starter) { - Profile starterProfile = ProfileManager.getInstance().getProfile(starter); - starterProfile.setEventStartLeft(starterProfile.getEventStartLeft() - 1); - } } this.seconds--; From 79bb2cc15df6af2f3cec1e3783ffbbbf08869dc5 Mon Sep 17 00:00:00 2001 From: Nandor Dukat Date: Wed, 18 Mar 2026 08:07:38 +0100 Subject: [PATCH 02/26] fixed PlayerInteractEvent NPE on tracker use --- .../manager/fight/event/EventListener.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/core/src/main/java/dev/nandi0813/practice/manager/fight/event/EventListener.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/event/EventListener.java index cc3da2da..74ed3319 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/fight/event/EventListener.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/fight/event/EventListener.java @@ -44,6 +44,10 @@ public void onTrackerUse(PlayerInteractEvent e) { Player player = e.getPlayer(); Profile profile = ProfileManager.getInstance().getProfile(player); + if (profile == null) { + return; + } + if (!profile.getStatus().equals(ProfileStatus.EVENT)) { return; } @@ -53,6 +57,10 @@ public void onTrackerUse(PlayerInteractEvent e) { } Event event = EventManager.getInstance().getEventByPlayer(player); + if (event == null) { + return; + } + if (!event.getStatus().equals(EventStatus.LIVE)) { return; } @@ -99,11 +107,19 @@ public void onHunger(FoodLevelChangeEvent e) { Player player = (Player) e.getEntity(); Profile profile = ProfileManager.getInstance().getProfile(player); + if (profile == null) { + return; + } + if (!profile.getStatus().equals(ProfileStatus.EVENT)) { return; } Event event = EventManager.getInstance().getEventByPlayer(player); + if (event == null) { + return; + } + if (!(event instanceof Brackets) && !(event instanceof LMS)) { e.setFoodLevel(20); } From 8619062bd9780ee743ff1eaa11b1fa324c2e722d Mon Sep 17 00:00:00 2001 From: Nandor Dukat Date: Wed, 18 Mar 2026 08:30:09 +0100 Subject: [PATCH 03/26] fixed soup regen on FFA --- .../practice/listener/PlayerInteract.java | 51 ++++++++++--------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/core/src/main/java/dev/nandi0813/practice/listener/PlayerInteract.java b/core/src/main/java/dev/nandi0813/practice/listener/PlayerInteract.java index 42f03406..89278d96 100644 --- a/core/src/main/java/dev/nandi0813/practice/listener/PlayerInteract.java +++ b/core/src/main/java/dev/nandi0813/practice/listener/PlayerInteract.java @@ -2,7 +2,6 @@ import dev.nandi0813.practice.manager.profile.Profile; import dev.nandi0813.practice.manager.profile.ProfileManager; -import dev.nandi0813.practice.manager.profile.enums.ProfileStatus; import io.papermc.paper.event.player.PlayerFlowerPotManipulateEvent; import org.bukkit.Material; import org.bukkit.attribute.Attribute; @@ -47,37 +46,41 @@ public void onSoup(PlayerInteractEvent e) { Player player = e.getPlayer(); Profile profile = ProfileManager.getInstance().getProfile(player); + if (profile == null) { + return; + } + Action action = e.getAction(); + if (!action.equals(Action.RIGHT_CLICK_BLOCK) && !action.equals(Action.RIGHT_CLICK_AIR)) { + return; + } + ItemStack item = e.getItem(); + if (item == null || !item.getType().equals(Material.MUSHROOM_STEW)) { + return; + } switch (profile.getStatus()) { case MATCH: case FFA: case EVENT: - if (profile.getStatus().equals(ProfileStatus.MATCH) || profile.getStatus().equals(ProfileStatus.EVENT)) { - // Soup listener - if (action.equals(Action.RIGHT_CLICK_BLOCK) || action.equals(Action.RIGHT_CLICK_AIR)) { - if (item != null && item.getType().equals(Material.MUSHROOM_STEW)) { - int food = player.getFoodLevel(); - double health = player.getHealth(); - double maxHealth = Objects.requireNonNull(player.getAttribute(Attribute.MAX_HEALTH)).getValue(); - double regen = 6.5; - - if (food < 20) e.setCancelled(true); - - if (health == maxHealth) return; - - if ((health + regen) < maxHealth) { - player.getInventory().setItemInMainHand(new ItemStack(Material.BOWL)); - player.setHealth(health + regen); - } else if ((health + regen) >= maxHealth) { - player.getInventory().setItemInMainHand(new ItemStack(Material.BOWL)); - player.setHealth(maxHealth); - } - player.updateInventory(); - } - } + int food = player.getFoodLevel(); + double health = player.getHealth(); + double maxHealth = Objects.requireNonNull(player.getAttribute(Attribute.MAX_HEALTH)).getValue(); + double regen = 6.5; + + if (food < 20) e.setCancelled(true); + + if (health == maxHealth) return; + + if ((health + regen) < maxHealth) { + player.getInventory().setItemInMainHand(new ItemStack(Material.BOWL)); + player.setHealth(health + regen); + } else if ((health + regen) >= maxHealth) { + player.getInventory().setItemInMainHand(new ItemStack(Material.BOWL)); + player.setHealth(maxHealth); } + player.updateInventory(); break; } } From ed74b0affc6289540b15ead1565832517d72a722 Mon Sep 17 00:00:00 2001 From: Nandor Dukat Date: Wed, 18 Mar 2026 09:05:14 +0100 Subject: [PATCH 04/26] fixed spectators stuck after rollback complete --- .../manager/fight/ffa/game/BuildRollback.java | 11 ++++++- .../practice/manager/fight/ffa/game/FFA.java | 28 +++++++++++++++- .../practice/manager/fight/match/Match.java | 32 +++++++++++++++++++ .../manager/fight/util/PlayerUtil.java | 11 +++++++ 4 files changed, 80 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/game/BuildRollback.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/game/BuildRollback.java index 53c5ffc6..18123289 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/game/BuildRollback.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/game/BuildRollback.java @@ -13,10 +13,16 @@ public class BuildRollback extends Runnable { private static final int ROLLBACK_SECONDS = ConfigManager.getInt("FFA.ROLLBACK.SECONDS"); private final FightChangeOptimized fightChange; + private final java.lang.Runnable onRollbackComplete; public BuildRollback(FightChangeOptimized fightChange) { + this(fightChange, null); + } + + public BuildRollback(FightChangeOptimized fightChange, java.lang.Runnable onRollbackComplete) { super(20L, 20L, false); this.fightChange = fightChange; + this.onRollbackComplete = onRollbackComplete; this.seconds = ROLLBACK_SECONDS; } @@ -43,9 +49,12 @@ public void rollback() { this.seconds = ROLLBACK_SECONDS; if (ZonePractice.getInstance().isEnabled()) { - fightChange.rollback(300, 100); + fightChange.rollback(300, 100, onRollbackComplete); } else { fightChange.quickRollback(); + if (onRollbackComplete != null) { + onRollbackComplete.run(); + } } } diff --git a/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/game/FFA.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/game/FFA.java index 50332bf0..763ef016 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/game/FFA.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/game/FFA.java @@ -78,7 +78,7 @@ public void open() { this.open = true; if (this.build) { - this.buildRollback = new BuildRollback(new FightChangeOptimized(this)); + this.buildRollback = new BuildRollback(new FightChangeOptimized(this), this::teleportStuckSpectatorsAfterRollback); this.buildRollback.begin(); } @@ -258,6 +258,32 @@ public void sendMessage(String message, boolean spectator) { } } + private void teleportStuckSpectatorsAfterRollback() { + if (!this.open || !this.build || this.spectators.isEmpty()) { + return; + } + + List activePlayers = new ArrayList<>(this.players.keySet()); + + for (Player spectator : new ArrayList<>(this.spectators)) { + if (spectator == null || !spectator.isOnline()) { + continue; + } + + if (!dev.nandi0813.practice.manager.fight.util.PlayerUtil.isPlayerStuck(spectator)) { + continue; + } + + if (!activePlayers.isEmpty()) { + spectator.teleport(activePlayers.get(random.nextInt(activePlayers.size()))); + } else if (!this.arena.getFfaPositions().isEmpty()) { + spectator.teleport(this.arena.getFfaPositions().get(random.nextInt(this.arena.getFfaPositions().size()))); + } else { + spectator.teleport(this.arena.getCuboid().getCenter().add(0, 1, 0)); + } + } + } + @Override public FightChangeOptimized getFightChange() { if (this.getBuildRollback() == null) diff --git a/core/src/main/java/dev/nandi0813/practice/manager/fight/match/Match.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/match/Match.java index df73321e..fd58c164 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/fight/match/Match.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/fight/match/Match.java @@ -504,6 +504,10 @@ public void resetMap(@org.jetbrains.annotations.Nullable Runnable afterRollback) } Runnable onRollbackComplete = () -> { + if (this.isBuild()) { + this.teleportStuckSpectatorsAfterRollback(); + } + rollingBack = false; if (afterRollback != null) { afterRollback.run(); @@ -518,6 +522,34 @@ public void resetMap(@org.jetbrains.annotations.Nullable Runnable afterRollback) } } + private void teleportStuckSpectatorsAfterRollback() { + if (this.spectators.isEmpty()) { + return; + } + + for (Player spectator : new ArrayList<>(this.spectators)) { + if (spectator == null || !spectator.isOnline()) { + continue; + } + + if (!dev.nandi0813.practice.manager.fight.util.PlayerUtil.isPlayerStuck(spectator)) { + continue; + } + + if (!this.players.isEmpty()) { + spectator.teleport(this.players.get(random.nextInt(this.players.size()))); + continue; + } + + List standingLocations = this.arena.getStandingLocations(); + if (!standingLocations.isEmpty()) { + spectator.teleport(standingLocations.get(random.nextInt(standingLocations.size()))); + } else { + spectator.teleport(this.arena.getCuboid().getCenter().add(0, 1, 0)); + } + } + } + public List getPeople() { List people = new ArrayList<>(); people.addAll(players); diff --git a/core/src/main/java/dev/nandi0813/practice/manager/fight/util/PlayerUtil.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/util/PlayerUtil.java index 0a933eba..943decfd 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/fight/util/PlayerUtil.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/fight/util/PlayerUtil.java @@ -7,6 +7,7 @@ import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Material; +import org.bukkit.block.Block; import org.bukkit.entity.Entity; import org.bukkit.entity.Fireball; import org.bukkit.entity.Player; @@ -235,4 +236,14 @@ public static void setAttackSpeed(Player player, int hitDelay) { } } + public static boolean isPlayerStuck(Player player) { + Block feetBlock = player.getLocation().getBlock(); + Block headBlock = player.getEyeLocation().getBlock(); + + boolean isFeetSolid = feetBlock.getType().isSolid(); + boolean isHeadSolid = headBlock.getType().isSolid(); + + return isFeetSolid || isHeadSolid; + } + } From bb3389e229d9390bdfaea8313efa5fec9fecaf4c Mon Sep 17 00:00:00 2001 From: Nandor Dukat Date: Wed, 18 Mar 2026 09:08:57 +0100 Subject: [PATCH 05/26] fixed NPE during boxing statistic fetching --- .../practice/manager/fight/match/Match.java | 2 +- .../match/listener/LadderTypeListener.java | 22 ++++++++++++--- .../practice/manager/ladder/type/Boxing.java | 28 ++++++++++++++----- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/core/src/main/java/dev/nandi0813/practice/manager/fight/match/Match.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/match/Match.java index fd58c164..c2b8225a 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/fight/match/Match.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/fight/match/Match.java @@ -228,7 +228,7 @@ public void killPlayer(Player player, Player killer, String deathMessage) { } } - deathMessage = TeamUtil.replaceTeamNames(deathMessage, player, this instanceof Team team ? team.getTeam(player) : TeamEnum.FFA); + deathMessage = TeamUtil.replaceTeamNames((deathMessage != null ? deathMessage : ""), player, this instanceof Team team ? team.getTeam(player) : TeamEnum.FFA); matchPlayers.get(player).die(deathMessage, this.getCurrentStat(player)); if (ladder instanceof NormalLadder) { diff --git a/core/src/main/java/dev/nandi0813/practice/manager/fight/match/listener/LadderTypeListener.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/match/listener/LadderTypeListener.java index 3570058b..8580c6ac 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/fight/match/listener/LadderTypeListener.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/fight/match/listener/LadderTypeListener.java @@ -569,7 +569,9 @@ public void onPlayerDeath(PlayerDeathEvent e) { if (killer != null) { Statistic statistic = match.getCurrentStat(killer); - statistic.setKills(statistic.getKills() + 1); + if (statistic != null) { + statistic.setKills(statistic.getKills() + 1); + } } } @@ -594,18 +596,30 @@ private static void onEntityDamageByEntity(EntityDamageByEntityEvent e) { Profile attackerProfile = ProfileManager.getInstance().getProfile(attacker); Profile targetProfile = ProfileManager.getInstance().getProfile(target); + if (attackerProfile == null || targetProfile == null) return; + if (!attackerProfile.getStatus().equals(ProfileStatus.MATCH)) return; if (!targetProfile.getStatus().equals(ProfileStatus.MATCH)) return; - Match match = MatchManager.getInstance().getLiveMatchByPlayer(attacker); - if (match != MatchManager.getInstance().getLiveMatchByPlayer(target)) { + Match attackerMatch = MatchManager.getInstance().getLiveMatchByPlayer(attacker); + Match targetMatch = MatchManager.getInstance().getLiveMatchByPlayer(target); + if (attackerMatch == null || attackerMatch != targetMatch) { e.setCancelled(true); return; } + Match match = attackerMatch; + if (!match.getCurrentRound().getRoundStatus().equals(RoundStatus.LIVE)) return; - boolean cancel = match.getCurrentStat(attacker).isSet() || match.getCurrentStat(target).isSet(); + Statistic attackerStat = match.getCurrentStat(attacker); + Statistic targetStat = match.getCurrentStat(target); + if (attackerStat == null || targetStat == null) { + e.setCancelled(true); + return; + } + + boolean cancel = attackerStat.isSet() || targetStat.isSet(); if (!cancel) { cancel = TeamUtil.isSaveTeamMate(match, attacker, target); diff --git a/core/src/main/java/dev/nandi0813/practice/manager/ladder/type/Boxing.java b/core/src/main/java/dev/nandi0813/practice/manager/ladder/type/Boxing.java index ead5e04e..67718e4a 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/ladder/type/Boxing.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/ladder/type/Boxing.java @@ -7,6 +7,7 @@ import dev.nandi0813.practice.manager.fight.match.enums.TeamEnum; import dev.nandi0813.practice.manager.fight.match.interfaces.PlayerWinner; import dev.nandi0813.practice.manager.fight.match.type.playersvsplayers.PlayersVsPlayers; +import dev.nandi0813.practice.manager.fight.util.Stats.Statistic; import dev.nandi0813.practice.manager.ladder.abstraction.interfaces.CustomConfig; import dev.nandi0813.practice.manager.ladder.abstraction.interfaces.LadderHandle; import dev.nandi0813.practice.manager.ladder.abstraction.interfaces.ScoringLadder; @@ -38,7 +39,8 @@ public Boxing(String name, LadderType type) { public boolean shouldEndRound(Match match, Round round, Player player) { // Check if the player has reached the required hits int requiredStrokes = boxingWinHit - 1; - return match.getCurrentStat(player).getHit() == requiredStrokes; + Statistic statistic = match.getCurrentStat(player); + return statistic != null && statistic.getHit() == requiredStrokes; } @Override @@ -80,7 +82,15 @@ public void getCustomConfig(YamlConfiguration config) { } private static void onPlayerDamagePlayer(final @NotNull EntityDamageByEntityEvent e, final @NotNull Match match, final @NotNull Boxing ladder) { - Player attacker = (Player) e.getDamager(); + if (!(e.getDamager() instanceof Player attacker)) { + return; + } + + Statistic attackerStat = match.getCurrentStat(attacker); + if (attackerStat == null) { + return; + } + int requiredStrokes = ladder.getBoxingWinHit(); requiredStrokes--; @@ -92,10 +102,10 @@ private static void onPlayerDamagePlayer(final @NotNull EntityDamageByEntityEven switch (matchType) { case DUEL: case PARTY_FFA: - if (match.getCurrentStat(attacker).getHit() == requiredStrokes && round instanceof PlayerWinner) { + if (attackerStat.getHit() == requiredStrokes && round instanceof PlayerWinner) { PlayerWinner playerWinner = (PlayerWinner) match.getCurrentRound(); - if (!match.getCurrentStat(attacker).isSet()) { + if (!attackerStat.isSet()) { playerWinner.setRoundWinner(attacker); round.endRound(); } @@ -107,7 +117,7 @@ private static void onPlayerDamagePlayer(final @NotNull EntityDamageByEntityEven attackerTeam = playersVsPlayers.getTeam(attacker); if (getTeamBoxingStrokes(match, playersVsPlayers.getTeamPlayers(attackerTeam)) == requiredStrokes) { - if (!match.getCurrentStat(attacker).isSet()) { + if (!attackerStat.isSet()) { playersVsPlayers.getCurrentRound().setRoundWinner(attackerTeam); round.endRound(); } @@ -128,8 +138,12 @@ private static void onPlayerDamage(final @NotNull EntityDamageEvent e, final @No public static int getTeamBoxingStrokes(Match match, List team) { int strokes = 0; - for (Player player : team) - strokes += match.getCurrentStat(player).getHit(); + for (Player player : team) { + Statistic statistic = match.getCurrentStat(player); + if (statistic != null) { + strokes += statistic.getHit(); + } + } return strokes; } From 5d269d47ce232c4d7d29e96e070a9c192bd8eb34 Mon Sep 17 00:00:00 2001 From: Nandor Dukat Date: Wed, 18 Mar 2026 09:42:19 +0100 Subject: [PATCH 06/26] fixed non solid blocks and added rollback priority sorting --- .../manager/fight/listener/BuildListener.java | 22 ++++++++- .../manager/fight/util/ChangedBlock.java | 31 +++++++++++-- .../fightmapchange/FightChangeOptimized.java | 46 +++++++++++++++---- 3 files changed, 85 insertions(+), 14 deletions(-) diff --git a/core/src/main/java/dev/nandi0813/practice/manager/fight/listener/BuildListener.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/listener/BuildListener.java index 8ae0ae03..0d6f5a96 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/fight/listener/BuildListener.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/fight/listener/BuildListener.java @@ -16,6 +16,7 @@ import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.block.Block; +import org.bukkit.block.BlockState; import org.bukkit.entity.Creeper; import org.bukkit.entity.FallingBlock; import org.bukkit.entity.Player; @@ -108,6 +109,19 @@ protected static void tagAndTrack(Block block, Spectatable spectatable) { spectatable.addBlockChange(new ChangedBlock(block)); } + /** + * Replaceable flora/support blocks are overwritten by placement and must be + * snapshotted from BlockPlaceEvent#getBlockReplacedState for accurate rollback. + */ + private static boolean shouldTrackReplacedState(BlockState replacedState) { + Material replacedType = replacedState.getType(); + if (replacedType.isAir()) { + return false; + } + + return !replacedType.isSolid(); + } + /** * Resolves the {@link Ladder} from a {@link Spectatable}. * Returns {@code null} when the Spectatable is not a {@link Match} (e.g. FFA). @@ -176,6 +190,7 @@ public void onBlockBreak(BlockBreakEvent e) { @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onBlockPlace(BlockPlaceEvent event) { Block block = event.getBlockPlaced(); + BlockState replacedState = event.getBlockReplacedState(); Spectatable spectatable = null; boolean needsMetadata = false; @@ -197,7 +212,12 @@ public void onBlockPlace(BlockPlaceEvent event) { BlockUtil.setMetadata(block, PLACED_IN_FIGHT, spectatable); } - spectatable.addBlockChange(new ChangedBlock(event)); + if (shouldTrackReplacedState(replacedState)) { + spectatable.getFightChange().addArenaBlockChange(new ChangedBlock(replacedState)); + } else { + spectatable.addBlockChange(new ChangedBlock(event)); + } + trackUnderBlockIfDirt(block, spectatable); } diff --git a/core/src/main/java/dev/nandi0813/practice/manager/fight/util/ChangedBlock.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/util/ChangedBlock.java index 5a19844b..e02575f2 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/fight/util/ChangedBlock.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/fight/util/ChangedBlock.java @@ -8,6 +8,7 @@ import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; +import org.bukkit.block.BlockState; import org.bukkit.block.Chest; import org.bukkit.block.data.BlockData; import org.bukkit.block.data.type.Bed; @@ -51,11 +52,33 @@ public ChangedBlock(final Block block, final Material originalMaterial) { this.blockData = org.bukkit.Bukkit.createBlockData(originalMaterial); } + public ChangedBlock(final BlockState replacedState) { + this.block = replacedState.getBlock(); + this.location = replacedState.getLocation(); + this.material = replacedState.getType(); + + BlockState snapshot = replacedState.getBlock().getState(); + if (snapshot.getType() != replacedState.getType()) { + snapshot = replacedState; + } + + if (snapshot instanceof Chest chest) { + chestInventory = chest.getInventory().getContents().clone(); + } + + if (snapshot.getBlockData() instanceof Bed bed) { + bedFace = bed.getFacing(); + + if (bed.getPart().equals(Bed.Part.HEAD)) { + this.location = this.block.getRelative(bedFace.getOppositeFace(), 1).getLocation(); + } + } + + this.blockData = replacedState.getBlockData().clone(); + } + public ChangedBlock(final BlockPlaceEvent e) { - this.block = e.getBlockPlaced(); - this.location = block.getLocation(); - this.material = e.getBlockReplacedState().getType(); - this.blockData = e.getBlockReplacedState().getBlockData(); + this(e.getBlockReplacedState()); } private void saveChest(Location loc) { 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 111b3d35..7b36ddc7 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 @@ -220,6 +220,36 @@ private void removeTempBlock(BlockChangeEntry entry) { entry.changedBlock.reset(); } + private static boolean isVineLike(org.bukkit.Material material) { + String name = material.name(); + return name.equals("VINE") || name.contains("_VINE") || name.contains("_VINES"); + } + + private static int rollbackPriority(BlockChangeEntry entry) { + return isVineLike(entry.getChangedBlock().getMaterial()) ? 1 : 0; + } + + private static java.util.Comparator> rollbackComparator() { + return (a, b) -> { + int pa = rollbackPriority(a.getValue()); + int pb = rollbackPriority(b.getValue()); + if (pa != pb) { + return Integer.compare(pa, pb); + } + + int ay = BlockPosition.getY(a.getKey()); + int by = BlockPosition.getY(b.getKey()); + + // Vine-like hanging blocks must be restored from top to bottom. + if (pa == 1) { + return Integer.compare(by, ay); + } + + // Other blocks keep bottom-to-top restore (support first for gravity blocks). + return Integer.compare(ay, by); + }; + } + /** * Rolls back all changes with rate limiting to prevent lag. *

@@ -330,17 +360,16 @@ private boolean isHologramTextDisplay(Entity entity) { * Used when server is shutting down. */ public void quickRollback() { - Iterator> iterator = blocks.entrySet().iterator(); + List> sorted = new ArrayList<>(blocks.entrySet()); + sorted.sort(rollbackComparator()); - while (iterator.hasNext()) { - Map.Entry entry = iterator.next(); + for (Map.Entry entry : sorted) { entry.getValue().changedBlock.reset(); Block block = BlockPosition.getBlock(world, entry.getKey()); // PLACED_IN_FIGHT uses PersistentTagUtil, so clear through BlockUtil. BlockUtil.clearMetadata(block, PLACED_IN_FIGHT); - - iterator.remove(); + blocks.remove(entry.getKey()); } } @@ -387,11 +416,10 @@ private class RollbackTask extends BukkitRunnable { private final Runnable onComplete; RollbackTask(int maxCheck, int maxChange, @org.jetbrains.annotations.Nullable Runnable onComplete) { - // Sort ascending by Y so bottom blocks are restored first. - // This prevents gravity blocks (sand/gravel) from falling during rollback - // because their support blocks below are always placed before them. + // Default ordering is bottom-up (gravity support). Vine-like blocks are + // restored top-down so hanging segments do not immediately break. List> sorted = new ArrayList<>(blocks.entrySet()); - sorted.sort(java.util.Comparator.comparingInt(entry -> BlockPosition.getY(entry.getKey()))); + sorted.sort(rollbackComparator()); this.iterator = sorted.iterator(); this.maxCheck = maxCheck; this.maxChange = maxChange; From e99b6760f87be25d468f0d4e206b8da8e3bb87c0 Mon Sep 17 00:00:00 2001 From: Nandor Dukat Date: Wed, 18 Mar 2026 09:54:48 +0100 Subject: [PATCH 07/26] added new settings to FFA that lets players heal back to full HP when killing another player --- .../manager/arena/arenas/FFAArena.java | 21 ++++++++++++- .../manager/fight/ffa/FFAListener.java | 29 +++++++++++++++--- .../manager/fight/ffa/game/BuildRollback.java | 4 --- .../practice/manager/fight/ffa/game/FFA.java | 16 ++++++++++ .../fight/ffa/game/FFAArenaSelectorGui.java | 2 +- .../match/listener/LadderTypeListener.java | 5 ++-- .../manager/fight/util/PlayerUtil.java | 2 +- .../arenasettings/ffa/FFASettingsGui.java | 30 +++++++++++++------ core/src/main/resources/config.yml | 3 +- core/src/main/resources/guis.yml | 28 +++++++++++++++-- 10 files changed, 114 insertions(+), 26 deletions(-) diff --git a/core/src/main/java/dev/nandi0813/practice/manager/arena/arenas/FFAArena.java b/core/src/main/java/dev/nandi0813/practice/manager/arena/arenas/FFAArena.java index e851bf9a..24211a85 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/arena/arenas/FFAArena.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/arena/arenas/FFAArena.java @@ -4,6 +4,7 @@ import dev.nandi0813.practice.manager.arena.ArenaType; import dev.nandi0813.practice.manager.arena.arenas.interfaces.DisplayArena; import dev.nandi0813.practice.manager.arena.util.ArenaUtil; +import dev.nandi0813.practice.manager.backend.ConfigManager; import dev.nandi0813.practice.manager.fight.ffa.game.FFA; import dev.nandi0813.practice.manager.gui.GUI; import dev.nandi0813.practice.manager.gui.GUIType; @@ -14,14 +15,19 @@ import lombok.Getter; import org.bukkit.configuration.file.YamlConfiguration; -import java.util.*; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; @Getter public class FFAArena extends DisplayArena { + private static final boolean DEFAULT_HEALTH_RESET_ON_KILL = ConfigManager.getBoolean("FFA.HEALTH-RESET-ON-KILL"); + private final FFA ffa; private boolean reKitAfterKill; private boolean lobbyAfterDeath; + private boolean healthResetOnKill; public FFAArena(String name) { super(name, ArenaType.FFA); @@ -31,6 +37,7 @@ public FFAArena(String name) { this.portalLoc1 = null; this.portalLoc2 = null; this.portalProtection = false; + this.healthResetOnKill = DEFAULT_HEALTH_RESET_ON_KILL; this.getData(); @@ -52,6 +59,7 @@ public void setData() { config.set("build", this.build); config.set("reKitAfterKill", this.reKitAfterKill); config.set("lobbyAfterDeath", this.lobbyAfterDeath); + config.set("healthResetOnKill", this.healthResetOnKill); config.set("ladders", ArenaUtil.getLadderNames(this)); @@ -76,6 +84,9 @@ public void getData() { if (config.isBoolean("lobbyAfterDeath")) this.setLobbyAfterDeath(config.getBoolean("lobbyAfterDeath")); + if (config.isBoolean("healthResetOnKill")) + this.setHealthResetOnKill(config.getBoolean("healthResetOnKill")); + if (config.isList("ladders")) { for (String ladderName : config.getStringList("ladders")) { NormalLadder ladder = LadderManager.getInstance().getLadder(ladderName); @@ -135,6 +146,14 @@ public void setLobbyAfterDeath(boolean lobbyAfterDeath) throws IllegalStateExcep this.lobbyAfterDeath = lobbyAfterDeath; } + public void setHealthResetOnKill(boolean healthResetOnKill) throws IllegalStateException { + if (this.enabled) { + throw new IllegalStateException("Cannot edit while arena is enabled."); + } + + this.healthResetOnKill = healthResetOnKill; + } + public void setBuild(boolean build) { if (this.enabled) { throw new IllegalStateException("Cannot edit while arena is enabled."); diff --git a/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/FFAListener.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/FFAListener.java index 729bb69e..55e9a65e 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/FFAListener.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/FFAListener.java @@ -373,10 +373,7 @@ public void onPlayerDeath(PlayerDeathEvent e) { return; } - Player killer = null; - if (damageSource.getCausingEntity() instanceof Entity damageEntity) { - killer = FightUtil.getKiller(damageEntity); - } + Player killer = resolveKiller(player, ffa, damageSource); DeathCause cause = FightUtil.convert(damageSource.getDamageType()); ffa.killPlayer(player, killer, cause.getMessage().replace("%killer%", killer != null ? killer.getName() : "Unknown")); @@ -387,6 +384,30 @@ public void onPlayerDeath(PlayerDeathEvent e) { } } + private Player resolveKiller(Player victim, FFA ffa, DamageSource damageSource) { + Player killer = null; + + if (damageSource.getCausingEntity() instanceof Entity damageEntity) { + killer = FightUtil.getKiller(damageEntity); + } + + // Bukkit keeps killer attribution for recent direct/projectile PvP. + if (killer == null) { + killer = victim.getKiller(); + } + + // Fallback for delayed environmental deaths (e.g. fatal fall after knockback). + if (killer == null) { + killer = ffa.getLastAttacker(victim); + } + + if (killer != null && !ffa.getPlayers().containsKey(killer)) { + return null; + } + + return killer; + } + @EventHandler public void onEntityDamageByEntity(EntityDamageByEntityEvent e) { if (!(e.getEntity() instanceof Player target)) { diff --git a/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/game/BuildRollback.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/game/BuildRollback.java index 18123289..b4a249dc 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/game/BuildRollback.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/game/BuildRollback.java @@ -15,10 +15,6 @@ public class BuildRollback extends Runnable { private final FightChangeOptimized fightChange; private final java.lang.Runnable onRollbackComplete; - public BuildRollback(FightChangeOptimized fightChange) { - this(fightChange, null); - } - public BuildRollback(FightChangeOptimized fightChange, java.lang.Runnable onRollbackComplete) { super(20L, 20L, false); this.fightChange = fightChange; diff --git a/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/game/FFA.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/game/FFA.java index 763ef016..63bd4f8d 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/game/FFA.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/game/FFA.java @@ -27,6 +27,8 @@ import dev.nandi0813.practice.util.playerutil.PlayerUtil; import lombok.Getter; import org.bukkit.Bukkit; +import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeInstance; import org.bukkit.entity.EnderPearl; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; @@ -204,6 +206,10 @@ public void killPlayer(Player player, Player killer, String deathMessage) { if (arena.isReKitAfterKill()) { KitUtil.loadDefaultLadderKit(killer, TeamEnum.FFA, players.get(killer)); } + + if (arena.isHealthResetOnKill()) { + applyHealthResetOnKill(killer); + } } if (arena.isLobbyAfterDeath()) { @@ -217,6 +223,16 @@ public void killPlayer(Player player, Player killer, String deathMessage) { } } + private void applyHealthResetOnKill(Player killer) { + AttributeInstance maxHealth = killer.getAttribute(Attribute.MAX_HEALTH); + double maxHealthValue = maxHealth != null ? maxHealth.getValue() : 20.0D; + killer.setHealth(Math.max(1.0D, maxHealthValue)); + killer.setFoodLevel(20); + killer.setSaturation(20.0F); + killer.setFireTicks(0); + killer.setFallDistance(0.0F); + } + /** * Records that {@code attacker} last hit {@code victim}. * Called from damage listeners so void deaths can be attributed correctly. diff --git a/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/game/FFAArenaSelectorGui.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/game/FFAArenaSelectorGui.java index 63abe2b9..4a6a6b98 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/game/FFAArenaSelectorGui.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/game/FFAArenaSelectorGui.java @@ -21,7 +21,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; public class FFAArenaSelectorGui extends GUI { @@ -62,6 +61,7 @@ public void update() { .replace("%players%", String.valueOf(ffa.getPlayers().size())) .replace("%build_status%", arena.isBuild() ? BUILD_ON : BUILD_OFF) .replace("%rekit_after_kill%", arena.isReKitAfterKill() ? BUILD_ON : BUILD_OFF) + .replace("%health_reset_on_kill%", arena.isHealthResetOnKill() ? BUILD_ON : BUILD_OFF) .replace("%lobby_after_death%", arena.isLobbyAfterDeath() ? BUILD_ON : BUILD_OFF) .replace("%ladders%", String.valueOf(arena.getAssignedLadders().size())); diff --git a/core/src/main/java/dev/nandi0813/practice/manager/fight/match/listener/LadderTypeListener.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/match/listener/LadderTypeListener.java index 8580c6ac..6a83268f 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/fight/match/listener/LadderTypeListener.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/fight/match/listener/LadderTypeListener.java @@ -452,8 +452,9 @@ public void onTarget(EntityTargetEvent e) { } @EventHandler - public void onItemPickup(PlayerPickupItemEvent e) { - Player player = e.getPlayer(); + public void onItemPickup(EntityPickupItemEvent e) { + if (!(e.getEntity() instanceof Player player)) return; + Match match = MatchManager.getInstance().getLiveMatchByPlayer(player); if (match == null) return; diff --git a/core/src/main/java/dev/nandi0813/practice/manager/fight/util/PlayerUtil.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/util/PlayerUtil.java index 943decfd..405f63ae 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/fight/util/PlayerUtil.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/fight/util/PlayerUtil.java @@ -71,7 +71,7 @@ public static List dropPlayerInventory(Player player) { } // Drop cursor item if any ItemStack cursor = player.getItemOnCursor(); - if (cursor != null && !cursor.getType().equals(Material.AIR)) + if (!cursor.getType().equals(Material.AIR)) entities.add(player.getWorld().dropItemNaturally(player.getLocation(), cursor)); clearInventory(player); diff --git a/core/src/main/java/dev/nandi0813/practice/manager/gui/setup/arena/arenasettings/ffa/FFASettingsGui.java b/core/src/main/java/dev/nandi0813/practice/manager/gui/setup/arena/arenasettings/ffa/FFASettingsGui.java index 87f5d706..d469db86 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/gui/setup/arena/arenasettings/ffa/FFASettingsGui.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/gui/setup/arena/arenasettings/ffa/FFASettingsGui.java @@ -19,6 +19,8 @@ public class FFASettingsGui extends GUI { private static final ItemStack BUILD_DISABLED_ITEM = GUIFile.getGuiItem("GUIS.SETUP.FFA-ARENA.SETTINGS.ICONS.BUILD.DISABLED").get(); private static final ItemStack REKIT_ENABLED_ITEM = GUIFile.getGuiItem("GUIS.SETUP.FFA-ARENA.SETTINGS.ICONS.RE-KIT-AFTER-KILL.ENABLED").get(); private static final ItemStack REKIT_DISABLED_ITEM = GUIFile.getGuiItem("GUIS.SETUP.FFA-ARENA.SETTINGS.ICONS.RE-KIT-AFTER-KILL.DISABLED").get(); + private static final ItemStack HEALTH_RESET_ENABLED_ITEM = GUIFile.getGuiItem("GUIS.SETUP.FFA-ARENA.SETTINGS.ICONS.HEALTH-RESET-ON-KILL.ENABLED").get(); + private static final ItemStack HEALTH_RESET_DISABLED_ITEM = GUIFile.getGuiItem("GUIS.SETUP.FFA-ARENA.SETTINGS.ICONS.HEALTH-RESET-ON-KILL.DISABLED").get(); private static final ItemStack LOBBYDEATH_ENABLED_ITEM = GUIFile.getGuiItem("GUIS.SETUP.FFA-ARENA.SETTINGS.ICONS.LOBBY-AFTER-DEATH.ENABLED").get(); private static final ItemStack LOBBYDEATH_DISABLED_ITEM = GUIFile.getGuiItem("GUIS.SETUP.FFA-ARENA.SETTINGS.ICONS.LOBBY-AFTER-DEATH.DISABLED").get(); @@ -52,21 +54,27 @@ public void update() { Inventory inventory = this.gui.get(1); if (ffaArena.isBuild()) { - inventory.setItem(11, BUILD_ENABLED_ITEM); + inventory.setItem(10, BUILD_ENABLED_ITEM); } else { - inventory.setItem(11, BUILD_DISABLED_ITEM); + inventory.setItem(10, BUILD_DISABLED_ITEM); } if (ffaArena.isReKitAfterKill()) { - inventory.setItem(13, REKIT_ENABLED_ITEM); + inventory.setItem(12, REKIT_ENABLED_ITEM); } else { - inventory.setItem(13, REKIT_DISABLED_ITEM); + inventory.setItem(12, REKIT_DISABLED_ITEM); } if (ffaArena.isLobbyAfterDeath()) { - inventory.setItem(15, LOBBYDEATH_ENABLED_ITEM); + inventory.setItem(14, LOBBYDEATH_ENABLED_ITEM); } else { - inventory.setItem(15, LOBBYDEATH_DISABLED_ITEM); + inventory.setItem(14, LOBBYDEATH_DISABLED_ITEM); + } + + if (ffaArena.isHealthResetOnKill()) { + inventory.setItem(16, HEALTH_RESET_ENABLED_ITEM); + } else { + inventory.setItem(16, HEALTH_RESET_DISABLED_ITEM); } this.updatePlayers(); @@ -80,18 +88,22 @@ public void handleClickEvent(InventoryClickEvent e) { try { switch (e.getRawSlot()) { - case 11: + case 10: ffaArena.setBuild(!ffaArena.isBuild()); this.update(); break; - case 13: + case 12: ffaArena.setReKitAfterKill(!ffaArena.isReKitAfterKill()); this.update(); break; - case 15: + case 14: ffaArena.setLobbyAfterDeath(!ffaArena.isLobbyAfterDeath()); this.update(); break; + case 16: + ffaArena.setHealthResetOnKill(!ffaArena.isHealthResetOnKill()); + this.update(); + break; case 27: arenaMainGui.open(player); break; diff --git a/core/src/main/resources/config.yml b/core/src/main/resources/config.yml index 69a1e9f8..c8aab1c3 100644 --- a/core/src/main/resources/config.yml +++ b/core/src/main/resources/config.yml @@ -1,4 +1,4 @@ -VERSION: 19 +VERSION: 20 # Mysql database setup. MYSQL-DATABASE: @@ -245,6 +245,7 @@ MATCH-SETTINGS: FFA: ROLLBACK: SECONDS: 300 # After the seconds, the arena will be reseted back to its original state (only on build arenas). + HEALTH-RESET-ON-KILL: false # Default for newly created FFA arenas. If enabled, killers are fully healed on kill. DISPLAY-ARROW-HIT-HEALTH: true # If true, the player will see the health of the player they hit. ENDER-PEARL-EXP-BAR: true # If true, the ender pearl cooldown will be displayed on the exp bar. ALLOW-DESTROYABLE-BLOCK: true # If true, player can destroy the fixed blocks in the arena that their ladder has. diff --git a/core/src/main/resources/guis.yml b/core/src/main/resources/guis.yml index ee7f6149..74041663 100644 --- a/core/src/main/resources/guis.yml +++ b/core/src/main/resources/guis.yml @@ -1,4 +1,4 @@ -VERSION: 13 +VERSION: 14 GENERAL-FILLER-ITEM: NAME: " " @@ -576,6 +576,7 @@ GUIS: - "" - "&eBuild: &f%build_status%" - "&eRe-Kit After Kill: &f%rekit_after_kill%" + - "&eHealth Reset On Kill: &f%health_reset_on_kill%" - "&eLobby After Death: &f%lobby_after_death%" - "&eLadders: &f%ladders%" - "&ePlayers: &f%players%" @@ -1325,8 +1326,8 @@ GUIS: - "&e&lClick here &7to open the settings gui." - "" - "&c&lNote: &7You can change the build setting," - - "&7re-kit after kill setting and the lobby after" - - "&7death setting." + - "&7re-kit after kill, health reset on kill" + - "&7and the lobby after death setting." STATUS: ENABLED: NAME: "&7Status: &aEnabled" @@ -1443,6 +1444,27 @@ GUIS: - "" - "&e&lClick here &7to &aenable &7the" - "&2re-kit after kill &7in the arena." + HEALTH-RESET-ON-KILL: + ENABLED: + NAME: "&7Health Reset On Kill: &aEnabled" + MATERIAL: GOLDEN_APPLE + LORE: + - "" + - "&eAfter a player kills somebody" + - "ðey are fully healed." + - "" + - "&e&lClick here &7to &cdisable &7the" + - "&2health reset on kill &7in the arena." + DISABLED: + NAME: "&7Health Reset On Kill: &cDisabled" + MATERIAL: RED_STAINED_GLASS_PANE + LORE: + - "" + - "&eAfter a player kills somebody" + - "ðey are fully healed." + - "" + - "&e&lClick here &7to &aenable &7the" + - "&2health reset on kill &7in the arena." LOBBY-AFTER-DEATH: ENABLED: NAME: "&7Lobby After Death: &aEnabled" From 63f941151777e1169d45af7f6eea5a44f458a201 Mon Sep 17 00:00:00 2001 From: Nandor Dukat Date: Wed, 18 Mar 2026 10:05:18 +0100 Subject: [PATCH 08/26] implemented party member limit per group --- .../gui/guis/party/PartySettingsGui.java | 3 ++- .../nandi0813/practice/manager/party/Party.java | 13 +++++++++++-- .../practice/manager/party/PartyManager.java | 17 ++++++++++++++++- .../practice/manager/profile/Profile.java | 10 ++++++++++ .../practice/manager/profile/group/Group.java | 4 +++- .../manager/profile/group/GroupManager.java | 6 ++++++ core/src/main/resources/config.yml | 1 - core/src/main/resources/groups.yml | 7 ++++++- 8 files changed, 54 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/party/PartySettingsGui.java b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/party/PartySettingsGui.java index 33bc22ec..43485048 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/party/PartySettingsGui.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/party/PartySettingsGui.java @@ -98,13 +98,14 @@ public void handleClickEvent(InventoryClickEvent e) { Common.sendMMMessage(player, LanguageManager.getString("PARTY.NO-PERMISSION")); case 11: if (player.hasPermission("zpp.party.changelimit")) { + int groupPartyLimit = PartyManager.getInstance().resolvePartyMemberLimit(party.getLeader()); if (clickType.isLeftClick() && party.getMaxPlayerLimit() > 2) { if (party.getMembers().size() < party.getMaxPlayerLimit()) { party.setMaxPlayerLimit(party.getMaxPlayerLimit() - 1); update(); } else Common.sendMMMessage(player, LanguageManager.getString("PARTY.CANT-DECREASE-LIMIT")); - } else if (clickType.isRightClick() && party.getMaxPlayerLimit() < PartyManager.MAX_PARTY_MEMBERS) { + } else if (clickType.isRightClick() && party.getMaxPlayerLimit() < groupPartyLimit) { party.setMaxPlayerLimit(party.getMaxPlayerLimit() + 1); update(); } diff --git a/core/src/main/java/dev/nandi0813/practice/manager/party/Party.java b/core/src/main/java/dev/nandi0813/practice/manager/party/Party.java index 18afd2fa..fa74f459 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/party/Party.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/party/Party.java @@ -24,7 +24,6 @@ @Getter public class Party implements dev.nandi0813.api.Interface.Party { - private static final int DEFAULT_MAX_PARTY_MEMBERS = ConfigManager.getInt("PARTY.SETTINGS.MAX-PARTY-MEMBERS.DEFAULT"); private static final boolean DEFAULT_PUBLIC_PARTY = ConfigManager.getBoolean("PARTY.SETTINGS.DEFAULT.PUBLIC-PARTY"); private static final boolean DEFAULT_ALL_INVITE = ConfigManager.getBoolean("PARTY.SETTINGS.DEFAULT.ALL-INVITE"); private static final boolean DEFAULT_PARTY_CHAT = ConfigManager.getBoolean("PARTY.SETTINGS.DEFAULT.PARTY-CHAT"); @@ -59,7 +58,7 @@ public Party(Player owner) { this.leader = owner; this.members.add(owner); - this.maxPlayerLimit = DEFAULT_MAX_PARTY_MEMBERS; + this.maxPlayerLimit = PartyManager.getInstance().resolvePartyMemberLimit(owner); this.publicParty = DEFAULT_PUBLIC_PARTY; this.broadcastParty = DEFAULT_PUBLIC_PARTY; this.allInvite = DEFAULT_ALL_INVITE; @@ -74,9 +73,19 @@ public void setNewOwner(Player newOwner) { broadcastTask.cancel(); leader = newOwner; + refreshMaxPlayerLimitForLeader(); sendMessage(LanguageManager.getString("PARTY.NEW-LEADER").replace("%player%", newOwner.getName())); } + public void refreshMaxPlayerLimitForLeader() { + int leaderLimit = PartyManager.getInstance().resolvePartyMemberLimit(leader); + this.maxPlayerLimit = Math.max(members.size(), leaderLimit); + + if (members.size() >= this.maxPlayerLimit && isBroadcastParty()) { + broadcastTask.cancel(); + } + } + public void addMember(Player member) { Profile memberProfile = ProfileManager.getInstance().getProfile(member); diff --git a/core/src/main/java/dev/nandi0813/practice/manager/party/PartyManager.java b/core/src/main/java/dev/nandi0813/practice/manager/party/PartyManager.java index 7f345abb..e431d08c 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/party/PartyManager.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/party/PartyManager.java @@ -14,6 +14,7 @@ import dev.nandi0813.practice.manager.profile.Profile; import dev.nandi0813.practice.manager.profile.ProfileManager; import dev.nandi0813.practice.manager.profile.enums.ProfileStatus; +import dev.nandi0813.practice.manager.profile.group.Group; import dev.nandi0813.practice.util.Common; import lombok.Getter; import org.bukkit.Bukkit; @@ -41,7 +42,7 @@ public static PartyManager getInstance() { private final List parties = new ArrayList<>(); public static final long INVITE_COOLDOWN = ConfigManager.getInt("PARTY.PARTY-INVITE-COOLDOWN") * 1000L; - public static final int MAX_PARTY_MEMBERS = ConfigManager.getInt("PARTY.SETTINGS.MAX-PARTY-MEMBERS.PERMISSION"); + private static final int DEFAULT_MAX_PARTY_MEMBERS = ConfigManager.getInt("PARTY.SETTINGS.MAX-PARTY-MEMBERS.DEFAULT"); private PartyManager() { Bukkit.getPluginManager().registerEvents(this, ZonePractice.getInstance()); @@ -64,6 +65,20 @@ public Party getParty(Match match) { return null; } + public int resolvePartyMemberLimit(Player player) { + Profile profile = ProfileManager.getInstance().getProfile(player); + if (profile == null) { + return Math.max(2, DEFAULT_MAX_PARTY_MEMBERS); + } + + Group group = profile.getGroup(); + if (group == null) { + return Math.max(2, DEFAULT_MAX_PARTY_MEMBERS); + } + + return Math.max(2, group.getPartyMemberLimit()); + } + public void createParty(Player player) { Profile profile = ProfileManager.getInstance().getProfile(player); diff --git a/core/src/main/java/dev/nandi0813/practice/manager/profile/Profile.java b/core/src/main/java/dev/nandi0813/practice/manager/profile/Profile.java index 1613367e..316a1180 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/profile/Profile.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/profile/Profile.java @@ -6,6 +6,8 @@ import dev.nandi0813.practice.manager.gui.guis.profile.ProfileSettingsGui; import dev.nandi0813.practice.manager.ladder.abstraction.normal.NormalLadder; import dev.nandi0813.practice.manager.ladder.abstraction.playercustom.CustomLadder; +import dev.nandi0813.practice.manager.party.Party; +import dev.nandi0813.practice.manager.party.PartyManager; import dev.nandi0813.practice.manager.profile.enums.ProfileStatus; import dev.nandi0813.practice.manager.profile.enums.ProfileWorldTime; import dev.nandi0813.practice.manager.profile.group.Group; @@ -195,6 +197,14 @@ public void setGroup(Group group) throws IllegalArgumentException { this.eventStartLeft = group.getEventStartLimit(); this.partyBroadcastLeft = group.getPartyBroadcastLimit(); + Player onlinePlayer = this.player.getPlayer(); + if (onlinePlayer != null) { + Party partyObj = PartyManager.getInstance().getParty(onlinePlayer); + if (partyObj != null && onlinePlayer.equals(partyObj.getLeader())) { + partyObj.refreshMaxPlayerLimitForLeader(); + } + } + while (this.customLadders.size() < this.group.getCustomKitLimit()) { this.customLadders.add(new CustomLadder(this, "player-custom-kit." + customLadders.size(), this.customLadders.size() + 1)); } diff --git a/core/src/main/java/dev/nandi0813/practice/manager/profile/group/Group.java b/core/src/main/java/dev/nandi0813/practice/manager/profile/group/Group.java index f8676655..f611e2f4 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/profile/group/Group.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/profile/group/Group.java @@ -22,6 +22,7 @@ public class Group { private final int rankedLimit; private final int eventStartLimit; private final int partyBroadcastLimit; + private final int partyMemberLimit; private final int customKitLimit; private final int modifiableKitLimit; @@ -37,7 +38,7 @@ public class Group { // Set up in the sidebar.yml file private final List sidebarExtension; - public Group(String name, String displayName, int weight, int unrankedLimit, int rankedLimit, int eventStartLimit, int partyBroadcastLimit, int customKitLimit, int modifiableKitLimit, Component prefix, NamedTextColor nameColor, Component suffix, int sortPriority, String chatFormat, List sidebarExtension) { + public Group(String name, String displayName, int weight, int unrankedLimit, int rankedLimit, int eventStartLimit, int partyBroadcastLimit, int partyMemberLimit, int customKitLimit, int modifiableKitLimit, Component prefix, NamedTextColor nameColor, Component suffix, int sortPriority, String chatFormat, List sidebarExtension) { this.name = name; this.displayName = displayName; @@ -49,6 +50,7 @@ public Group(String name, String displayName, int weight, int unrankedLimit, int this.rankedLimit = rankedLimit; this.eventStartLimit = eventStartLimit; this.partyBroadcastLimit = partyBroadcastLimit; + this.partyMemberLimit = partyMemberLimit; if (customKitLimit < 0 || customKitLimit > 5) { this.customKitLimit = 0; diff --git a/core/src/main/java/dev/nandi0813/practice/manager/profile/group/GroupManager.java b/core/src/main/java/dev/nandi0813/practice/manager/profile/group/GroupManager.java index 63942ad1..226795c3 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/profile/group/GroupManager.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/profile/group/GroupManager.java @@ -2,6 +2,7 @@ import dev.nandi0813.practice.ZonePractice; import dev.nandi0813.practice.manager.backend.ConfigFile; +import dev.nandi0813.practice.manager.backend.ConfigManager; import dev.nandi0813.practice.manager.profile.Profile; import dev.nandi0813.practice.manager.profile.ProfileManager; import dev.nandi0813.practice.manager.sidebar.SidebarManager; @@ -18,6 +19,8 @@ @Getter public class GroupManager extends ConfigFile { + private static final int DEFAULT_PARTY_MEMBER_LIMIT = ConfigManager.getInt("PARTY.SETTINGS.MAX-PARTY-MEMBERS.DEFAULT"); + private static GroupManager instance; public static GroupManager getInstance() { @@ -54,6 +57,9 @@ public void loadGroups() { this.getInt("GROUPS." + groupName + ".RANKED-PER-DAY"), this.getInt("GROUPS." + groupName + ".EVENT-START-PER-DAY"), this.getInt("GROUPS." + groupName + ".PARTY-BROADCAST-PER-DAY"), + this.config.isInt("GROUPS." + groupName + ".PARTY-MEMBER-LIMIT") + ? this.getInt("GROUPS." + groupName + ".PARTY-MEMBER-LIMIT") + : DEFAULT_PARTY_MEMBER_LIMIT, this.getInt("GROUPS." + groupName + ".CUSTOM-KIT"), this.getInt("GROUPS." + groupName + ".MODIFIABLE-KIT-PER-LADDER"), ZonePractice.getMiniMessage().deserialize(this.getString("GROUPS." + groupName + ".LOBBY-NAMETAG.PREFIX")), diff --git a/core/src/main/resources/config.yml b/core/src/main/resources/config.yml index c8aab1c3..557d3399 100644 --- a/core/src/main/resources/config.yml +++ b/core/src/main/resources/config.yml @@ -264,7 +264,6 @@ PARTY: SETTINGS: MAX-PARTY-MEMBERS: DEFAULT: 6 # Maximum party members, if the owner doesn't have permission to change it. - PERMISSION: 20 # The maximum limit the party leader can set for the party. DEFAULT: PUBLIC-PARTY: false ALL-INVITE: false diff --git a/core/src/main/resources/groups.yml b/core/src/main/resources/groups.yml index 85bf2139..1806283d 100644 --- a/core/src/main/resources/groups.yml +++ b/core/src/main/resources/groups.yml @@ -1,4 +1,4 @@ -VERSION: 2 +VERSION: 3 # # Player Group settings # @@ -12,6 +12,7 @@ GROUPS: RANKED-PER-DAY: 10 EVENT-START-PER-DAY: 0 PARTY-BROADCAST-PER-DAY: 0 + PARTY-MEMBER-LIMIT: 6 CUSTOM-KIT: 1 MODIFIABLE-KIT-PER-LADDER: 1 LOBBY-NAMETAG: @@ -27,6 +28,7 @@ GROUPS: RANKED-PER-DAY: 30 EVENT-START-PER-DAY: 1 PARTY-BROADCAST-PER-DAY: 1 + PARTY-MEMBER-LIMIT: 10 CUSTOM-KIT: 2 MODIFIABLE-KIT-PER-LADDER: 2 LOBBY-NAMETAG: @@ -42,6 +44,7 @@ GROUPS: RANKED-PER-DAY: 50 EVENT-START-PER-DAY: 2 PARTY-BROADCAST-PER-DAY: 3 + PARTY-MEMBER-LIMIT: 14 CUSTOM-KIT: 3 MODIFIABLE-KIT-PER-LADDER: 4 LOBBY-NAMETAG: @@ -57,6 +60,7 @@ GROUPS: RANKED-PER-DAY: 0 EVENT-START-PER-DAY: 3 PARTY-BROADCAST-PER-DAY: 5 + PARTY-MEMBER-LIMIT: 20 CUSTOM-KIT: 0 MODIFIABLE-KIT-PER-LADDER: 0 LOBBY-NAMETAG: @@ -72,6 +76,7 @@ GROUPS: RANKED-PER-DAY: 100 EVENT-START-PER-DAY: 100 PARTY-BROADCAST-PER-DAY: 100 + PARTY-MEMBER-LIMIT: 25 CUSTOM-KIT: 5 # Maximum 5 MODIFIABLE-KIT-PER-LADDER: 4 LOBBY-NAMETAG: From 1672b887ad717ca41ff2698c2077d6c3f3363888 Mon Sep 17 00:00:00 2001 From: Nandor Dukat Date: Wed, 18 Mar 2026 15:09:47 +0100 Subject: [PATCH 09/26] added armor trim cosmetics --- .../dev/nandi0813/practice/ZonePractice.java | 7 + .../singlecommands/CosmeticsCommand.java | 48 +++ .../manager/fight/match/util/KitUtil.java | 99 ++++++ .../practice/manager/gui/GUIType.java | 10 + .../gui/guis/cosmetics/ArmorPieceHubGui.java | 195 ++++++++++++ .../gui/guis/cosmetics/CosmeticsGui.java | 281 ++++++++++++++++++ .../guis/cosmetics/MaterialSelectionGui.java | 195 ++++++++++++ .../guis/cosmetics/PatternSelectionGui.java | 165 ++++++++++ .../practice/manager/profile/Profile.java | 8 +- .../practice/manager/profile/ProfileFile.java | 107 ++++++- .../manager/profile/cosmetics/ArmorSlot.java | 22 ++ .../cosmetics/ArmorTrimPermissionManager.java | 139 +++++++++ .../profile/cosmetics/ArmorTrimTier.java | 101 +++++++ .../profile/cosmetics/CosmeticsData.java | 86 ++++++ core/src/main/resources/guis.yml | 171 ++++++++++- core/src/main/resources/plugin.yml | 30 ++ 16 files changed, 1657 insertions(+), 7 deletions(-) create mode 100644 core/src/main/java/dev/nandi0813/practice/command/singlecommands/CosmeticsCommand.java create mode 100644 core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/ArmorPieceHubGui.java create mode 100644 core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/CosmeticsGui.java create mode 100644 core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/MaterialSelectionGui.java create mode 100644 core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/PatternSelectionGui.java create mode 100644 core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/ArmorSlot.java create mode 100644 core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/ArmorTrimPermissionManager.java create mode 100644 core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/ArmorTrimTier.java create mode 100644 core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/CosmeticsData.java diff --git a/core/src/main/java/dev/nandi0813/practice/ZonePractice.java b/core/src/main/java/dev/nandi0813/practice/ZonePractice.java index 461684fa..dfdcd605 100644 --- a/core/src/main/java/dev/nandi0813/practice/ZonePractice.java +++ b/core/src/main/java/dev/nandi0813/practice/ZonePractice.java @@ -47,6 +47,7 @@ import dev.nandi0813.practice.manager.nametag.NametagManager; import dev.nandi0813.practice.manager.playerkit.PlayerKitManager; import dev.nandi0813.practice.manager.profile.ProfileManager; +import dev.nandi0813.practice.manager.profile.cosmetics.ArmorTrimPermissionManager; import dev.nandi0813.practice.manager.server.ServerManager; import dev.nandi0813.practice.manager.sidebar.SidebarManager; import dev.nandi0813.practice.util.*; @@ -118,6 +119,7 @@ public void onEnable() { DivisionManager.getInstance().getData(); ArenaWorldUtil.createArenaWorld(); BackendManager.createFile(this); + ArmorTrimPermissionManager.registerAllPermissions(); ZonePracticeApiImpl.setup(); StartUpUtil.loadStartUpProgressMap(); @@ -342,6 +344,11 @@ private void registerCommands(Server server) { server.getPluginCommand("ignorequeue").setTabCompleter(ignoreQueueCommand); } + CosmeticsCommand cosmeticsCommand = new CosmeticsCommand(); + if (server.getPluginCommand("cosmetics") != null) { + server.getPluginCommand("cosmetics").setExecutor(cosmeticsCommand); + } + if (ConfigManager.getBoolean("CHAT.PRIVATE-CHAT-ENABLED")) { new MessageCommand(); new ReplyCommand(); diff --git a/core/src/main/java/dev/nandi0813/practice/command/singlecommands/CosmeticsCommand.java b/core/src/main/java/dev/nandi0813/practice/command/singlecommands/CosmeticsCommand.java new file mode 100644 index 00000000..e4671840 --- /dev/null +++ b/core/src/main/java/dev/nandi0813/practice/command/singlecommands/CosmeticsCommand.java @@ -0,0 +1,48 @@ +package dev.nandi0813.practice.command.singlecommands; + +import dev.nandi0813.practice.manager.gui.guis.cosmetics.CosmeticsGui; +import dev.nandi0813.practice.manager.profile.Profile; +import dev.nandi0813.practice.manager.profile.ProfileManager; +import dev.nandi0813.practice.manager.profile.enums.ProfileStatus; +import dev.nandi0813.practice.util.Common; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jspecify.annotations.NonNull; + +/** + * Command to open the cosmetics GUI for armor trim customization. + */ +public class CosmeticsCommand implements CommandExecutor { + + @Override + public boolean onCommand(@NonNull CommandSender sender, @NonNull Command cmd, @NonNull String label, String @NonNull [] args) { + if (!(sender instanceof Player player)) { + return false; + } + + if (!player.hasPermission("zpp.cosmetics.main")) { + Common.sendMMMessage(player, "You don't have permission to use cosmetics!"); + return false; + } + + Profile profile = ProfileManager.getInstance().getProfile(player); + if (profile == null) { + Common.sendMMMessage(player, "Failed to load your profile!"); + return false; + } + + if (!profile.getStatus().equals(ProfileStatus.LOBBY) && !profile.getStatus().equals(ProfileStatus.STAFF_MODE)) { + Common.sendMMMessage(player, "You can only open cosmetics while in the lobby!"); + return false; + } + + // Open the main cosmetics GUI with no parent GUI (player can close it normally) + CosmeticsGui cosmeticsGui = new CosmeticsGui(profile, null); + cosmeticsGui.open(player); + + return true; + } +} + diff --git a/core/src/main/java/dev/nandi0813/practice/manager/fight/match/util/KitUtil.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/match/util/KitUtil.java index 081b1997..d9582419 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/fight/match/util/KitUtil.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/fight/match/util/KitUtil.java @@ -4,9 +4,18 @@ import dev.nandi0813.practice.manager.fight.util.PlayerUtil; import dev.nandi0813.practice.manager.ladder.abstraction.Ladder; import dev.nandi0813.practice.manager.ladder.util.LadderUtil; +import dev.nandi0813.practice.manager.profile.Profile; +import dev.nandi0813.practice.manager.profile.ProfileManager; +import dev.nandi0813.practice.manager.profile.cosmetics.ArmorSlot; +import dev.nandi0813.practice.manager.profile.cosmetics.ArmorTrimPermissionManager; +import dev.nandi0813.practice.manager.profile.cosmetics.ArmorTrimTier; import dev.nandi0813.practice.util.KitData; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ArmorMeta; +import org.bukkit.inventory.meta.trim.ArmorTrim; +import org.bukkit.inventory.meta.trim.TrimMaterial; +import org.bukkit.inventory.meta.trim.TrimPattern; import java.util.ArrayList; import java.util.Arrays; @@ -64,7 +73,97 @@ public static void loadKit(Player player, TeamEnum team, ItemStack[] armor, Item extra != null ? extraList.toArray(new ItemStack[0]) : null); } + applyArmorTrimCosmetics(player); player.updateInventory(); } + /** + * Apply armor trim cosmetics to the player's equipped armor. + * Retrieves the player's saved cosmetics from their profile and applies them to armor pieces. + */ + private static void applyArmorTrimCosmetics(Player player) { + try { + Profile profile = ProfileManager.getInstance().getProfile(player); + if (profile == null || profile.getCosmeticsData() == null) { + return; + } + + ItemStack[] armorContents = player.getInventory().getArmorContents(); + if (armorContents.length < 4) { + return; + } + + ArmorTrimTier activeTier = profile.getCosmeticsData().getActiveTier(); + if (!player.hasPermission(activeTier.getPermissionNode())) { + return; + } + + // Helmet (index 3) + applyTrimToArmor(player, armorContents[3], + profile.getCosmeticsData().getPattern(activeTier, ArmorSlot.HELMET), + profile.getCosmeticsData().getMaterial(activeTier, ArmorSlot.HELMET), 3); + + // Chestplate (index 2) + applyTrimToArmor(player, armorContents[2], + profile.getCosmeticsData().getPattern(activeTier, ArmorSlot.CHESTPLATE), + profile.getCosmeticsData().getMaterial(activeTier, ArmorSlot.CHESTPLATE), 2); + + // Leggings (index 1) + applyTrimToArmor(player, armorContents[1], + profile.getCosmeticsData().getPattern(activeTier, ArmorSlot.LEGGINGS), + profile.getCosmeticsData().getMaterial(activeTier, ArmorSlot.LEGGINGS), 1); + + // Boots (index 0) + applyTrimToArmor(player, armorContents[0], + profile.getCosmeticsData().getPattern(activeTier, ArmorSlot.BOOTS), + profile.getCosmeticsData().getMaterial(activeTier, ArmorSlot.BOOTS), 0); + + } catch (Exception e) { + // Silently fail - if cosmetics cannot be applied, continue with kit distribution + } + } + + /** + * Apply a trim pattern and material to an armor piece if both are set. + */ + private static void applyTrimToArmor(Player player, ItemStack item, + TrimPattern pattern, TrimMaterial material, int armorIndex) { + if (item == null || !item.hasItemMeta()) { + return; + } + + // Both pattern and material must be set to apply trim + if (pattern == null || material == null) { + return; + } + + // Verify permission before applying + if (!player.hasPermission("zpp.cosmetics.pattern." + getTrimId(pattern)) || + !player.hasPermission("zpp.cosmetics.material." + getTrimId(material))) { + return; + } + + try { + var meta = (ArmorMeta) item.getItemMeta(); + if (meta != null) { + meta.setTrim(new ArmorTrim(material, pattern)); + item.setItemMeta(meta); + + ItemStack[] armorContents = player.getInventory().getArmorContents(); + armorContents[armorIndex] = item; + player.getInventory().setArmorContents(armorContents); + } + } catch (Exception e) { + // Silently fail - trim application may not be supported on this version or item type + } + } + + private static String getTrimId(TrimPattern pattern) { + return ArmorTrimPermissionManager.getTrimId(pattern); + } + + private static String getTrimId(TrimMaterial material) { + return ArmorTrimPermissionManager.getTrimId(material); + } + } diff --git a/core/src/main/java/dev/nandi0813/practice/manager/gui/GUIType.java b/core/src/main/java/dev/nandi0813/practice/manager/gui/GUIType.java index 45662fb8..e5ff60dc 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/gui/GUIType.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/gui/GUIType.java @@ -96,4 +96,14 @@ public enum GUIType { FFA_Arena_Selector, FFA_Ladder_Selector, + // Cosmetics GUIs + Cosmetics_Main, + Cosmetics_Helmet, + Cosmetics_Chestplate, + Cosmetics_Leggings, + Cosmetics_Boots, + Cosmetics_Shield, + Cosmetics_Pattern_Selection, + Cosmetics_Material_Selection, + } diff --git a/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/ArmorPieceHubGui.java b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/ArmorPieceHubGui.java new file mode 100644 index 00000000..d8ffa052 --- /dev/null +++ b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/ArmorPieceHubGui.java @@ -0,0 +1,195 @@ +package dev.nandi0813.practice.manager.gui.guis.cosmetics; + +import dev.nandi0813.practice.manager.backend.GUIFile; +import dev.nandi0813.practice.manager.gui.GUI; +import dev.nandi0813.practice.manager.gui.GUIItem; +import dev.nandi0813.practice.manager.gui.GUIType; +import dev.nandi0813.practice.manager.profile.Profile; +import dev.nandi0813.practice.manager.profile.cosmetics.ArmorSlot; +import dev.nandi0813.practice.manager.profile.cosmetics.ArmorTrimPermissionManager; +import dev.nandi0813.practice.manager.profile.cosmetics.ArmorTrimTier; +import dev.nandi0813.practice.util.InventoryUtil; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ArmorMeta; +import org.bukkit.inventory.meta.trim.ArmorTrim; +import org.bukkit.inventory.meta.trim.TrimMaterial; +import org.bukkit.inventory.meta.trim.TrimPattern; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public class ArmorPieceHubGui extends GUI { + + private static final ItemStack FILLER_ITEM = GUIFile.getGuiItem("GENERAL-FILLER-ITEM").get(); + private static final ItemStack BACK_TO_ITEM = GUIFile.getGuiItem("GUIS.COSMETICS.ICONS.BACK-TO").get(); + + private static final int INVENTORY_ROWS = GUIFile.getConfig().getInt("GUIS.COSMETICS.ARMOR-PIECE-HUB.INVENTORY-ROWS", 4); + private static final int BACK_SLOT = GUIFile.getConfig().getInt("GUIS.COSMETICS.ARMOR-PIECE-HUB.SLOTS.BACK", 27); + private static final int PREVIEW_SLOT = GUIFile.getConfig().getInt("GUIS.COSMETICS.ARMOR-PIECE-HUB.SLOTS.PREVIEW", 13); + private static final int PATTERN_MENU_SLOT = GUIFile.getConfig().getInt("GUIS.COSMETICS.ARMOR-PIECE-HUB.SLOTS.PATTERN-MENU", 20); + private static final int MATERIAL_MENU_SLOT = GUIFile.getConfig().getInt("GUIS.COSMETICS.ARMOR-PIECE-HUB.SLOTS.MATERIAL-MENU", 24); + + private static final Material DEFAULT_PATTERN_BUTTON_MATERIAL = Material.valueOf( + GUIFile.getConfig().getString("GUIS.COSMETICS.ARMOR-PIECE-HUB.PATTERN-SELECTION-BUTTON.DEFAULT-MATERIAL", "SMITHING_TABLE") + ); + private static final Material DEFAULT_MATERIAL_BUTTON_MATERIAL = Material.valueOf( + GUIFile.getConfig().getString("GUIS.COSMETICS.ARMOR-PIECE-HUB.MATERIAL-SELECTION-BUTTON.DEFAULT-MATERIAL", "ANVIL") + ); + + private final Profile profile; + private final ArmorSlot armorSlot; + private final GUI backToGui; + + public ArmorPieceHubGui(Profile profile, ArmorSlot armorSlot, GUI backToGui) { + super(resolveGuiType(armorSlot)); + this.profile = profile; + this.armorSlot = armorSlot; + this.backToGui = backToGui; + this.gui.put(1, InventoryUtil.createInventory("&8" + armorSlot.getDisplayName() + " Cosmetics", INVENTORY_ROWS)); + build(); + } + + @Override + public void build() { + update(); + } + + @Override + public void update() { + Inventory inventory = gui.get(1); + inventory.clear(); + + for (int i = 0; i < inventory.getSize(); i++) { + inventory.setItem(i, FILLER_ITEM); + } + + ArmorTrimTier tier = profile.getCosmeticsData().getActiveTier(); + TrimPattern pattern = profile.getCosmeticsData().getPattern(tier, armorSlot); + TrimMaterial material = profile.getCosmeticsData().getMaterial(tier, armorSlot); + + inventory.setItem(BACK_SLOT, BACK_TO_ITEM == null ? new ItemStack(Material.ARROW) : BACK_TO_ITEM); + inventory.setItem(PREVIEW_SLOT, buildPreviewItem(tier, pattern, material)); + inventory.setItem(PATTERN_MENU_SLOT, buildPatternSelectionItem(pattern)); + inventory.setItem(MATERIAL_MENU_SLOT, buildMaterialSelectionItem(material)); + + updatePlayers(); + } + + @Override + public void handleClickEvent(InventoryClickEvent e) { + e.setCancelled(true); + + Player player = (Player) e.getWhoClicked(); + int slot = e.getRawSlot(); + + if (slot == BACK_SLOT) { + backToGui.update(true); + backToGui.open(player); + return; + } + + if (slot == PATTERN_MENU_SLOT) { + new PatternSelectionGui(profile, armorSlot, this).open(player); + return; + } + + if (slot == MATERIAL_MENU_SLOT) { + new MaterialSelectionGui(profile, armorSlot, this).open(player); + } + } + + private ItemStack buildPreviewItem(ArmorTrimTier tier, TrimPattern pattern, TrimMaterial material) { + Material previewMaterial = tier.getMaterial(armorSlot); + GUIItem item = new GUIItem(previewMaterial); + item.setName(GUIFile.getConfig().getString("GUIS.COSMETICS.ARMOR-PIECE-HUB.PREVIEW-ITEM.NAME", "&eCurrent Preview")); + + ItemStack itemStack = item.get(); + if (armorSlot != ArmorSlot.SHIELD && pattern != null && material != null && itemStack.getItemMeta() instanceof ArmorMeta armorMeta) { + armorMeta.setTrim(new ArmorTrim(material, pattern)); + itemStack.setItemMeta(armorMeta); + } + + return itemStack; + } + + private ItemStack buildNavigationItem(Material material, String name, String loreLine) { + GUIItem item = new GUIItem(material); + item.setName(name); + + List lore = new ArrayList<>(); + lore.add(loreLine); + lore.add("&eClick to open."); + item.setLore(lore); + + return item.get(); + } + + private ItemStack buildPatternSelectionItem(TrimPattern activePattern) { + Material buttonMaterial = DEFAULT_PATTERN_BUTTON_MATERIAL; + if (activePattern != null) { + String patternId = ArmorTrimPermissionManager.getTrimId(activePattern); + Material activePatternMaterial = resolveMaterial(patternId.toUpperCase(Locale.ROOT) + "_ARMOR_TRIM_SMITHING_TEMPLATE"); + if (activePatternMaterial != null) { + buttonMaterial = activePatternMaterial; + } + } + + String name = GUIFile.getConfig().getString("GUIS.COSMETICS.ARMOR-PIECE-HUB.PATTERN-SELECTION-BUTTON.NAME", "&bPattern Selection"); + String loreLine = GUIFile.getConfig().getStringList("GUIS.COSMETICS.ARMOR-PIECE-HUB.PATTERN-SELECTION-BUTTON.LORE").getFirst(); + return buildNavigationItem(buttonMaterial, name, loreLine); + } + + private ItemStack buildMaterialSelectionItem(TrimMaterial activeMaterial) { + Material buttonMaterial = DEFAULT_MATERIAL_BUTTON_MATERIAL; + if (activeMaterial != null) { + Material activeMaterialIcon = resolveTrimMaterialIcon(ArmorTrimPermissionManager.getTrimId(activeMaterial)); + if (activeMaterialIcon != null) { + buttonMaterial = activeMaterialIcon; + } + } + + String name = GUIFile.getConfig().getString("GUIS.COSMETICS.ARMOR-PIECE-HUB.MATERIAL-SELECTION-BUTTON.NAME", "&6Material Selection"); + String loreLine = GUIFile.getConfig().getStringList("GUIS.COSMETICS.ARMOR-PIECE-HUB.MATERIAL-SELECTION-BUTTON.LORE").getFirst(); + return buildNavigationItem(buttonMaterial, name, loreLine); + } + + private Material resolveTrimMaterialIcon(String materialId) { + return switch (materialId) { + case "lapis" -> Material.LAPIS_LAZULI; + case "amethyst" -> Material.AMETHYST_SHARD; + case "resin" -> resolveMaterial("RESIN_BRICK") == null ? Material.BRICK : resolveMaterial("RESIN_BRICK"); + default -> { + Material ingot = resolveMaterial(materialId.toUpperCase(Locale.ROOT) + "_INGOT"); + if (ingot != null) { + yield ingot; + } + yield resolveMaterial(materialId.toUpperCase(Locale.ROOT)); + } + }; + } + + private static GUIType resolveGuiType(ArmorSlot armorSlot) { + return switch (armorSlot) { + case HELMET -> GUIType.Cosmetics_Helmet; + case CHESTPLATE -> GUIType.Cosmetics_Chestplate; + case LEGGINGS -> GUIType.Cosmetics_Leggings; + case BOOTS -> GUIType.Cosmetics_Boots; + case SHIELD -> GUIType.Cosmetics_Shield; + }; + } + + private Material resolveMaterial(String materialName) { + try { + return Material.valueOf(materialName); + } catch (IllegalArgumentException ignored) { + return null; + } + } +} + + diff --git a/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/CosmeticsGui.java b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/CosmeticsGui.java new file mode 100644 index 00000000..43d4d974 --- /dev/null +++ b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/CosmeticsGui.java @@ -0,0 +1,281 @@ +package dev.nandi0813.practice.manager.gui.guis.cosmetics; + +import dev.nandi0813.practice.manager.backend.GUIFile; +import dev.nandi0813.practice.manager.gui.GUI; +import dev.nandi0813.practice.manager.gui.GUIItem; +import dev.nandi0813.practice.manager.gui.GUIType; +import dev.nandi0813.practice.manager.profile.Profile; +import dev.nandi0813.practice.manager.profile.cosmetics.ArmorSlot; +import dev.nandi0813.practice.manager.profile.cosmetics.ArmorTrimPermissionManager; +import dev.nandi0813.practice.manager.profile.cosmetics.ArmorTrimTier; +import dev.nandi0813.practice.util.Common; +import dev.nandi0813.practice.util.InventoryUtil; +import dev.nandi0813.practice.util.StringUtil; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ArmorMeta; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.inventory.meta.trim.ArmorTrim; +import org.bukkit.inventory.meta.trim.TrimMaterial; +import org.bukkit.inventory.meta.trim.TrimPattern; + +import java.util.ArrayList; +import java.util.List; + +/** + * Main cosmetics GUI that displays armor pieces for the player to customize. + */ +public class CosmeticsGui extends GUI { + + private static final int MAIN_ROWS = 5; + private static final int BACK_SLOT = 0; + private static final int HELMET_SLOT = 10; + private static final int CHESTPLATE_SLOT = 12; + private static final int LEGGINGS_SLOT = 14; + private static final int BOOTS_SLOT = 16; + private static final int INFO_SLOT = 31; + + private static final ItemStack FILLER_ITEM = GUIFile.getGuiItem("GENERAL-FILLER-ITEM").get(); + private static final GUIItem HELMET_ITEM = GUIFile.getGuiItem("GUIS.COSMETICS.ICONS.HELMET-ICON"); + private static final GUIItem CHESTPLATE_ITEM = GUIFile.getGuiItem("GUIS.COSMETICS.ICONS.CHESTPLATE-ICON"); + private static final GUIItem LEGGINGS_ITEM = GUIFile.getGuiItem("GUIS.COSMETICS.ICONS.LEGGINGS-ICON"); + private static final GUIItem BOOTS_ITEM = GUIFile.getGuiItem("GUIS.COSMETICS.ICONS.BOOTS-ICON"); + private static final GUIItem INFO_ITEM = GUIFile.getGuiItem("GUIS.COSMETICS.ICONS.INFO-ICON"); + private static final ItemStack BACK_TO_ITEM = GUIFile.getGuiItem("GUIS.COSMETICS.ICONS.BACK-TO").get(); + + private final Profile profile; + private final GUI backToGui; + + public CosmeticsGui(Profile profile, GUI backToGui) { + super(GUIType.Cosmetics_Main); + this.profile = profile; + this.backToGui = backToGui; + + this.gui.put(1, InventoryUtil.createInventory(GUIFile.getString("GUIS.COSMETICS.MAIN-TITLE"), MAIN_ROWS)); + this.build(); + } + + @Override + public void build() { + this.update(); + } + + @Override + public void update() { + Inventory inventory = this.gui.get(1); + inventory.clear(); + + for (int i = 0; i < inventory.getSize(); i++) { + inventory.setItem(i, FILLER_ITEM); + } + + ArmorTrimTier activeTier = profile.getCosmeticsData().getActiveTier(); + Player player = profile.getPlayer().getPlayer(); + if (player != null && !player.hasPermission(activeTier.getPermissionNode())) { + for (ArmorTrimTier tier : ArmorTrimTier.values()) { + if (player.hasPermission(tier.getPermissionNode())) { + activeTier = tier; + profile.getCosmeticsData().setActiveTier(tier); + break; + } + } + } + + inventory.setItem(HELMET_SLOT, buildArmorPreviewItem(HELMET_ITEM, activeTier, ArmorSlot.HELMET)); + inventory.setItem(CHESTPLATE_SLOT, buildArmorPreviewItem(CHESTPLATE_ITEM, activeTier, ArmorSlot.CHESTPLATE)); + inventory.setItem(LEGGINGS_SLOT, buildArmorPreviewItem(LEGGINGS_ITEM, activeTier, ArmorSlot.LEGGINGS)); + inventory.setItem(BOOTS_SLOT, buildArmorPreviewItem(BOOTS_ITEM, activeTier, ArmorSlot.BOOTS)); + inventory.setItem(INFO_SLOT, buildTierToggleItem(activeTier)); + + if (backToGui != null) { + ItemStack backItem = BACK_TO_ITEM == null ? new ItemStack(Material.ARROW) : BACK_TO_ITEM; + inventory.setItem(BACK_SLOT, backItem); + } + + this.updatePlayers(); + } + + @Override + public void handleClickEvent(InventoryClickEvent e) { + Player player = (Player) e.getWhoClicked(); + int slot = e.getRawSlot(); + + e.setCancelled(true); + + if (slot == BACK_SLOT && backToGui != null) { + backToGui.open(player); + return; + } + + if (slot == INFO_SLOT) { + if (e.isLeftClick()) { + handleTierToggleClick(player, false); + } else if (e.isRightClick()) { + handleTierToggleClick(player, true); + } + return; + } + + ArmorSlot armorSlot = getArmorSlotFromSlot(slot); + if (armorSlot != null) { + ArmorTrimTier activeTier = profile.getCosmeticsData().getActiveTier(); + if (!player.hasPermission(activeTier.getPermissionNode())) { + String deniedMessage = GUIFile.getConfig().getString("GUIS.COSMETICS.PERMISSION-DENIED-MESSAGE", "You do not have permission for the selected armor tier."); + Common.sendMMMessage(player, deniedMessage); + return; + } + openArmorSubGui(player, armorSlot); + } + } + + private ArmorSlot getArmorSlotFromSlot(int slot) { + return switch (slot) { + case HELMET_SLOT -> ArmorSlot.HELMET; + case CHESTPLATE_SLOT -> ArmorSlot.CHESTPLATE; + case LEGGINGS_SLOT -> ArmorSlot.LEGGINGS; + case BOOTS_SLOT -> ArmorSlot.BOOTS; + default -> null; + }; + } + + private ItemStack buildArmorPreviewItem(GUIItem configuredItem, ArmorTrimTier tier, ArmorSlot slot) { + Material baseMaterial = tier.getMaterial(slot); + GUIItem guiItem = configuredItem.cloneItem(); + guiItem.setMaterial(baseMaterial); + + TrimPattern activePattern = profile.getCosmeticsData().getPattern(tier, slot); + TrimMaterial activeMaterial = profile.getCosmeticsData().getMaterial(tier, slot); + + List lore = guiItem.getLore() == null ? new ArrayList<>() : new ArrayList<>(guiItem.getLore()); + + // Get dynamic lore template from guis.yml + List lorTemplate = GUIFile.getConfig().getStringList("GUIS.COSMETICS.ARMOR-PREVIEW-LORE"); + for (String line : lorTemplate) { + String processedLine = line + .replace("%tier%", tier.getDisplayName()) + .replace("%pattern%", activePattern == null ? "&cNone" : "&b" + formatDisplayName(activePattern)) + .replace("%material%", activeMaterial == null ? "&cNone" : "&6" + formatDisplayName(activeMaterial)); + lore.add(processedLine); + } + guiItem.setLore(lore); + + ItemStack item = guiItem.get(); + if (slot != ArmorSlot.SHIELD) { + applyTrimPreview(item, activePattern, activeMaterial); + } + return item; + } + + private ItemStack buildTierToggleItem(ArmorTrimTier activeTier) { + GUIItem guiItem = INFO_ITEM.cloneItem(); + guiItem.setMaterial(guiItem.getMaterial() == null ? Material.NETHER_STAR : guiItem.getMaterial()); + + Player player = profile.getPlayer().getPlayer(); + int totalPatternPermissions = ArmorTrimPermissionManager.getRegisteredPatterns().size(); + int totalMaterialPermissions = ArmorTrimPermissionManager.getRegisteredMaterials().size(); + int playerPatternPermissions = 0; + int playerMaterialPermissions = 0; + + if (player != null) { + for (TrimPattern pattern : ArmorTrimPermissionManager.getRegisteredPatterns()) { + if (player.hasPermission("zpp.cosmetics.pattern." + getPermissionId(pattern))) { + playerPatternPermissions++; + } + } + + for (TrimMaterial material : ArmorTrimPermissionManager.getRegisteredMaterials()) { + if (player.hasPermission("zpp.cosmetics.material." + getPermissionId(material))) { + playerMaterialPermissions++; + } + } + } + + guiItem.replace("%tier%", activeTier.getDisplayName()); + guiItem.replace("%tier_permission%", activeTier.getPermissionNode()); + guiItem.replace("%pattern_unlocked%", String.valueOf(playerPatternPermissions)); + guiItem.replace("%pattern_total%", String.valueOf(totalPatternPermissions)); + guiItem.replace("%material_unlocked%", String.valueOf(playerMaterialPermissions)); + guiItem.replace("%material_total%", String.valueOf(totalMaterialPermissions)); + guiItem.setName("&bArmor Tier: &e" + activeTier.getDisplayName()); + + List lore = guiItem.getLore() == null ? new ArrayList<>() : new ArrayList<>(guiItem.getLore()); + + // Get dynamic lore template from guis.yml + List loreTemplate = GUIFile.getConfig().getStringList("GUIS.COSMETICS.TIER-TOGGLE-LORE"); + for (String line : loreTemplate) { + String processedLine = line.replace("%tier_permission%", activeTier.getPermissionNode()); + lore.add(processedLine); + } + guiItem.setLore(lore); + return guiItem.get(); + } + + private void handleTierToggleClick(Player player, boolean forward) { + ArmorTrimTier currentTier = profile.getCosmeticsData().getActiveTier(); + ArmorTrimTier nextTier = forward ? currentTier.next() : previousTier(currentTier); + + for (int i = 0; i < ArmorTrimTier.values().length; i++) { + if (player.hasPermission(nextTier.getPermissionNode())) { + profile.getCosmeticsData().setActiveTier(nextTier); + profile.saveData(); + update(true); + return; + } + + nextTier = forward ? nextTier.next() : previousTier(nextTier); + } + + String noPermMessage = GUIFile.getConfig().getString("GUIS.COSMETICS.NO-TIER-PERMISSION-MESSAGE", "You do not have permission for any armor tier."); + Common.sendMMMessage(player, noPermMessage); + } + + private ArmorTrimTier previousTier(ArmorTrimTier tier) { + ArmorTrimTier[] tiers = ArmorTrimTier.values(); + int index = tier.ordinal() - 1; + if (index < 0) { + index = tiers.length - 1; + } + return tiers[index]; + } + + private static void applyTrimPreview(ItemStack item, TrimPattern pattern, TrimMaterial material) { + if (item == null || pattern == null || material == null || !item.hasItemMeta()) { + return; + } + + ItemMeta itemMeta = item.getItemMeta(); + if (!(itemMeta instanceof ArmorMeta armorMeta)) { + return; + } + + armorMeta.setTrim(new ArmorTrim(material, pattern)); + item.setItemMeta(armorMeta); + } + + private static String getPermissionId(Object trimValue) { + if (trimValue instanceof TrimPattern trimPattern) { + return ArmorTrimPermissionManager.getTrimId(trimPattern); + } + + if (trimValue instanceof TrimMaterial trimMaterial) { + return ArmorTrimPermissionManager.getTrimId(trimMaterial); + } + + return "unknown"; + } + + private static String formatDisplayName(Object trimValue) { + return StringUtil.getNormalizedName(getPermissionId(trimValue)); + } + + private void openArmorSubGui(Player player, ArmorSlot armorSlot) { + new ArmorPieceHubGui(profile, armorSlot, this).open(player); + } +} + + + + diff --git a/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/MaterialSelectionGui.java b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/MaterialSelectionGui.java new file mode 100644 index 00000000..4d7fee35 --- /dev/null +++ b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/MaterialSelectionGui.java @@ -0,0 +1,195 @@ +package dev.nandi0813.practice.manager.gui.guis.cosmetics; + +import dev.nandi0813.practice.manager.backend.GUIFile; +import dev.nandi0813.practice.manager.gui.GUI; +import dev.nandi0813.practice.manager.gui.GUIItem; +import dev.nandi0813.practice.manager.gui.GUIType; +import dev.nandi0813.practice.manager.profile.Profile; +import dev.nandi0813.practice.manager.profile.cosmetics.ArmorSlot; +import dev.nandi0813.practice.manager.profile.cosmetics.ArmorTrimPermissionManager; +import dev.nandi0813.practice.manager.profile.cosmetics.ArmorTrimTier; +import dev.nandi0813.practice.util.Common; +import dev.nandi0813.practice.util.InventoryUtil; +import dev.nandi0813.practice.util.StringUtil; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.trim.TrimMaterial; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MaterialSelectionGui extends GUI { + + private static final ItemStack FILLER_ITEM = GUIFile.getGuiItem("GENERAL-FILLER-ITEM").get(); + private static final ItemStack BACK_TO_ITEM = GUIFile.getGuiItem("GUIS.COSMETICS.ICONS.BACK-TO").get(); + + private static final int INVENTORY_ROWS = GUIFile.getConfig().getInt("GUIS.COSMETICS.MATERIAL-SELECTION.INVENTORY-ROWS", 5); + private static final int BACK_SLOT = GUIFile.getConfig().getInt("GUIS.COSMETICS.MATERIAL-SELECTION.BACK-SLOT", 36); + private static final int START_SLOT = GUIFile.getConfig().getInt("GUIS.COSMETICS.MATERIAL-SELECTION.START-SLOT", 10); + + private final Profile profile; + private final ArmorSlot armorSlot; + private final GUI backToGui; + private final Map materialBySlot = new HashMap<>(); + + public MaterialSelectionGui(Profile profile, ArmorSlot armorSlot, GUI backToGui) { + super(GUIType.Cosmetics_Material_Selection); + this.profile = profile; + this.armorSlot = armorSlot; + this.backToGui = backToGui; + String title = GUIFile.getConfig().getString("GUIS.COSMETICS.MATERIAL-SELECTION.INVENTORY-TITLE", "&8Select Material - %armor%") + .replace("%armor%", armorSlot.getDisplayName()); + this.gui.put(1, InventoryUtil.createInventory(title, INVENTORY_ROWS)); + build(); + } + + @Override + public void build() { + update(); + } + + @Override + public void update() { + Inventory inventory = gui.get(1); + inventory.clear(); + materialBySlot.clear(); + + for (int i = 0; i < inventory.getSize(); i++) { + inventory.setItem(i, FILLER_ITEM); + } + + inventory.setItem(BACK_SLOT, BACK_TO_ITEM == null ? new ItemStack(Material.ARROW) : BACK_TO_ITEM); + + ArmorTrimTier tier = profile.getCosmeticsData().getActiveTier(); + int slot = START_SLOT; + for (TrimMaterial material : ArmorTrimPermissionManager.getRegisteredMaterials()) { + if (slot >= inventory.getSize()) { + break; + } + + while (slot < inventory.getSize() && (slot % 9 == 0 || slot % 9 == 8)) { + slot++; + } + + if (slot >= inventory.getSize()) { + break; + } + + materialBySlot.put(slot, material); + inventory.setItem(slot, buildMaterialItem(profile.getPlayer().getPlayer(), tier, material)); + slot++; + } + + updatePlayers(); + } + + @Override + public void handleClickEvent(InventoryClickEvent e) { + e.setCancelled(true); + + Player player = (Player) e.getWhoClicked(); + int slot = e.getRawSlot(); + + if (slot == BACK_SLOT) { + backToGui.update(true); + backToGui.open(player); + return; + } + + TrimMaterial material = materialBySlot.get(slot); + if (material == null) { + return; + } + + ArmorTrimTier tier = profile.getCosmeticsData().getActiveTier(); + if (!player.hasPermission(tier.getPermissionNode())) { + String tierPermDeniedMessage = GUIFile.getConfig().getString("GUIS.COSMETICS.TIER-PERMISSION-DENIED-MESSAGE", "You do not have permission for this armor tier."); + Common.sendMMMessage(player, tierPermDeniedMessage); + return; + } + + String permissionNode = "zpp.cosmetics.material." + ArmorTrimPermissionManager.getTrimId(material); + if (!player.hasPermission(permissionNode)) { + String permDeniedMessage = GUIFile.getConfig().getString("GUIS.COSMETICS.MATERIAL-PERMISSION-DENIED-MESSAGE", "You do not have permission to use this trim material."); + Common.sendMMMessage(player, permDeniedMessage); + return; + } + + profile.getCosmeticsData().setMaterial(tier, armorSlot, material); + profile.saveData(); + update(true); + } + + private ItemStack buildMaterialItem(Player player, ArmorTrimTier tier, TrimMaterial material) { + String materialId = ArmorTrimPermissionManager.getTrimId(material); + String permissionNode = "zpp.cosmetics.material." + materialId; + boolean hasPermission = player != null && player.hasPermission(permissionNode); + + TrimMaterial activeMaterial = profile.getCosmeticsData().getMaterial(tier, armorSlot); + boolean active = activeMaterial != null + && ArmorTrimPermissionManager.getTrimId(activeMaterial).equalsIgnoreCase(materialId); + + GUIItem item = new GUIItem(resolveIcon(materialId)); + + String itemName = GUIFile.getConfig().getString("GUIS.COSMETICS.MATERIAL-SELECTION.MATERIAL-ITEM.NAME", "&6%material_name% Material") + .replace("%material_name%", StringUtil.getNormalizedName(materialId)); + item.setName(itemName); + + List configLore = GUIFile.getConfig().getStringList("GUIS.COSMETICS.MATERIAL-SELECTION.MATERIAL-ITEM.LORE"); + List lore = new ArrayList<>(); + for (String loreLine : configLore) { + lore.add(loreLine + .replace("%state%", active ? "&aActive" : "&cInactive") + .replace("%access%", hasPermission ? "&aUnlocked" : "&cLocked") + .replace("%permission%", permissionNode)); + } + item.setLore(lore); + item.setGlowing(active); + + return item.get(); + } + + private Material resolveIcon(String materialId) { + // Check if material icon is configured + String configuredMaterial = GUIFile.getConfig().getString("GUIS.COSMETICS.MATERIAL-SELECTION.MATERIAL-ICONS." + materialId.toUpperCase(), null); + if (configuredMaterial != null) { + try { + return Material.valueOf(configuredMaterial); + } catch (IllegalArgumentException ignored) { + // Fall through to default resolution + } + } + + // Default resolution logic + return switch (materialId) { + case "lapis" -> Material.LAPIS_LAZULI; + case "amethyst" -> Material.AMETHYST_SHARD; + case "resin" -> resolveByName("RESIN_BRICK", Material.BRICK); + default -> { + Material resolved = resolveByName(materialId.toUpperCase() + "_INGOT", null); + if (resolved == null) { + resolved = resolveByName(materialId.toUpperCase(), null); + } + yield resolved == null ? Material.NETHER_STAR : resolved; + } + }; + } + + private Material resolveByName(String materialName, Material fallback) { + try { + return Material.valueOf(materialName); + } catch (IllegalArgumentException ignored) { + return fallback; + } + } +} + + + + + diff --git a/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/PatternSelectionGui.java b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/PatternSelectionGui.java new file mode 100644 index 00000000..30577172 --- /dev/null +++ b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/PatternSelectionGui.java @@ -0,0 +1,165 @@ +package dev.nandi0813.practice.manager.gui.guis.cosmetics; + +import dev.nandi0813.practice.manager.backend.GUIFile; +import dev.nandi0813.practice.manager.gui.GUI; +import dev.nandi0813.practice.manager.gui.GUIItem; +import dev.nandi0813.practice.manager.gui.GUIType; +import dev.nandi0813.practice.manager.profile.Profile; +import dev.nandi0813.practice.manager.profile.cosmetics.ArmorSlot; +import dev.nandi0813.practice.manager.profile.cosmetics.ArmorTrimPermissionManager; +import dev.nandi0813.practice.manager.profile.cosmetics.ArmorTrimTier; +import dev.nandi0813.practice.util.Common; +import dev.nandi0813.practice.util.InventoryUtil; +import dev.nandi0813.practice.util.StringUtil; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.trim.TrimPattern; + +import java.util.*; + +public class PatternSelectionGui extends GUI { + + private static final ItemStack FILLER_ITEM = GUIFile.getGuiItem("GENERAL-FILLER-ITEM").get(); + private static final ItemStack BACK_TO_ITEM = GUIFile.getGuiItem("GUIS.COSMETICS.ICONS.BACK-TO").get(); + + private static final int INVENTORY_ROWS = GUIFile.getConfig().getInt("GUIS.COSMETICS.PATTERN-SELECTION.INVENTORY-ROWS", 5); + private static final int BACK_SLOT = GUIFile.getConfig().getInt("GUIS.COSMETICS.PATTERN-SELECTION.BACK-SLOT", 36); + private static final int START_SLOT = GUIFile.getConfig().getInt("GUIS.COSMETICS.PATTERN-SELECTION.START-SLOT", 10); + + private final Profile profile; + private final ArmorSlot armorSlot; + private final GUI backToGui; + private final Map patternBySlot = new HashMap<>(); + + public PatternSelectionGui(Profile profile, ArmorSlot armorSlot, GUI backToGui) { + super(GUIType.Cosmetics_Pattern_Selection); + this.profile = profile; + this.armorSlot = armorSlot; + this.backToGui = backToGui; + String title = GUIFile.getConfig().getString("GUIS.COSMETICS.PATTERN-SELECTION.INVENTORY-TITLE", "&8Select Pattern - %armor%") + .replace("%armor%", armorSlot.getDisplayName()); + this.gui.put(1, InventoryUtil.createInventory(title, INVENTORY_ROWS)); + build(); + } + + @Override + public void build() { + update(); + } + + @Override + public void update() { + Inventory inventory = gui.get(1); + inventory.clear(); + patternBySlot.clear(); + + for (int i = 0; i < inventory.getSize(); i++) { + inventory.setItem(i, FILLER_ITEM); + } + + inventory.setItem(BACK_SLOT, BACK_TO_ITEM == null ? new ItemStack(Material.ARROW) : BACK_TO_ITEM); + + ArmorTrimTier tier = profile.getCosmeticsData().getActiveTier(); + int slot = START_SLOT; + for (TrimPattern pattern : ArmorTrimPermissionManager.getRegisteredPatterns()) { + if (slot >= inventory.getSize()) { + break; + } + + while (slot < inventory.getSize() && (slot % 9 == 0 || slot % 9 == 8)) { + slot++; + } + + if (slot >= inventory.getSize()) { + break; + } + + patternBySlot.put(slot, pattern); + inventory.setItem(slot, buildPatternItem(profile.getPlayer().getPlayer(), tier, pattern)); + slot++; + } + + updatePlayers(); + } + + @Override + public void handleClickEvent(InventoryClickEvent e) { + e.setCancelled(true); + + Player player = (Player) e.getWhoClicked(); + int slot = e.getRawSlot(); + + if (slot == BACK_SLOT) { + backToGui.update(true); + backToGui.open(player); + return; + } + + TrimPattern pattern = patternBySlot.get(slot); + if (pattern == null) { + return; + } + + ArmorTrimTier tier = profile.getCosmeticsData().getActiveTier(); + if (!player.hasPermission(tier.getPermissionNode())) { + String tierPermDeniedMessage = GUIFile.getConfig().getString("GUIS.COSMETICS.TIER-PERMISSION-DENIED-MESSAGE", "You do not have permission for this armor tier."); + Common.sendMMMessage(player, tierPermDeniedMessage); + return; + } + + String permissionNode = "zpp.cosmetics.pattern." + ArmorTrimPermissionManager.getTrimId(pattern); + if (!player.hasPermission(permissionNode)) { + String permDeniedMessage = GUIFile.getConfig().getString("GUIS.COSMETICS.PATTERN-PERMISSION-DENIED-MESSAGE", "You do not have permission to use this trim pattern."); + Common.sendMMMessage(player, permDeniedMessage); + return; + } + + profile.getCosmeticsData().setPattern(tier, armorSlot, pattern); + profile.saveData(); + update(true); + } + + private ItemStack buildPatternItem(Player player, ArmorTrimTier tier, TrimPattern pattern) { + String patternId = ArmorTrimPermissionManager.getTrimId(pattern); + String permissionNode = "zpp.cosmetics.pattern." + patternId; + boolean hasPermission = player != null && player.hasPermission(permissionNode); + + TrimPattern activePattern = profile.getCosmeticsData().getPattern(tier, armorSlot); + boolean active = activePattern != null + && ArmorTrimPermissionManager.getTrimId(activePattern).equalsIgnoreCase(patternId); + + Material templateMaterial = resolveMaterial(patternId.toUpperCase(Locale.ROOT) + "_ARMOR_TRIM_SMITHING_TEMPLATE"); + if (templateMaterial == null) { + templateMaterial = resolveMaterial(patternId.toUpperCase(Locale.ROOT) + "_SMITHING_TEMPLATE"); + } + GUIItem item = new GUIItem(templateMaterial == null ? Material.PAPER : templateMaterial); + + String itemName = GUIFile.getConfig().getString("GUIS.COSMETICS.PATTERN-SELECTION.PATTERN-ITEM.NAME", "&b%pattern_name% Pattern") + .replace("%pattern_name%", StringUtil.getNormalizedName(patternId)); + item.setName(itemName); + + List configLore = GUIFile.getConfig().getStringList("GUIS.COSMETICS.PATTERN-SELECTION.PATTERN-ITEM.LORE"); + List lore = new ArrayList<>(); + for (String loreLine : configLore) { + lore.add(loreLine + .replace("%state%", active ? "&aActive" : "&cInactive") + .replace("%access%", hasPermission ? "&aUnlocked" : "&cLocked") + .replace("%permission%", permissionNode)); + } + item.setLore(lore); + item.setGlowing(active); + + return item.get(); + } + + private Material resolveMaterial(String materialName) { + try { + return Material.valueOf(materialName); + } catch (IllegalArgumentException ignored) { + return null; + } + } +} diff --git a/core/src/main/java/dev/nandi0813/practice/manager/profile/Profile.java b/core/src/main/java/dev/nandi0813/practice/manager/profile/Profile.java index 316a1180..d783d41e 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/profile/Profile.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/profile/Profile.java @@ -8,6 +8,7 @@ import dev.nandi0813.practice.manager.ladder.abstraction.playercustom.CustomLadder; import dev.nandi0813.practice.manager.party.Party; import dev.nandi0813.practice.manager.party.PartyManager; +import dev.nandi0813.practice.manager.profile.cosmetics.CosmeticsData; import dev.nandi0813.practice.manager.profile.enums.ProfileStatus; import dev.nandi0813.practice.manager.profile.enums.ProfileWorldTime; import dev.nandi0813.practice.manager.profile.group.Group; @@ -76,6 +77,9 @@ public class Profile { private ProfileSettingsGui settingsGui; private ActionBar actionBar = new ActionBar(this); + // Cosmetics data for armor trims + private CosmeticsData cosmeticsData = new CosmeticsData(); + // Custom ladder private PlayerCustomKitSelector playerCustomKitSelector; private final List customLadders = new ArrayList<>(); @@ -119,7 +123,7 @@ public void getData() { if (this.file.getConfig().isConfigurationSection("player-custom-kit")) { this.customLadders.clear(); - for (String ladder : this.file.getConfig().getConfigurationSection("player-custom-kit").getKeys(false)) { + for (String ladder : Objects.requireNonNull(this.file.getConfig().getConfigurationSection("player-custom-kit")).getKeys(false)) { try { int i = Integer.parseInt(ladder); if (i < 0 || i > 5) { @@ -210,7 +214,7 @@ public void setGroup(Group group) throws IllegalArgumentException { } while (this.customLadders.size() > this.group.getCustomKitLimit()) { - this.customLadders.remove(this.customLadders.size() - 1); + this.customLadders.removeLast(); } this.playerCustomKitSelector = new PlayerCustomKitSelector(this); diff --git a/core/src/main/java/dev/nandi0813/practice/manager/profile/ProfileFile.java b/core/src/main/java/dev/nandi0813/practice/manager/profile/ProfileFile.java index 7264b721..1f0f0f2d 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/profile/ProfileFile.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/profile/ProfileFile.java @@ -6,16 +6,25 @@ import dev.nandi0813.practice.manager.ladder.LadderManager; import dev.nandi0813.practice.manager.ladder.abstraction.Ladder; import dev.nandi0813.practice.manager.ladder.abstraction.normal.NormalLadder; +import dev.nandi0813.practice.manager.profile.cosmetics.ArmorSlot; +import dev.nandi0813.practice.manager.profile.cosmetics.ArmorTrimPermissionManager; +import dev.nandi0813.practice.manager.profile.cosmetics.ArmorTrimTier; import dev.nandi0813.practice.manager.profile.enums.ProfileWorldTime; import dev.nandi0813.practice.manager.profile.group.Group; import dev.nandi0813.practice.manager.profile.group.GroupManager; import dev.nandi0813.practice.util.Common; import dev.nandi0813.practice.util.ItemSerializationUtil; +import io.papermc.paper.registry.RegistryAccess; +import io.papermc.paper.registry.RegistryKey; +import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.trim.TrimMaterial; +import org.bukkit.inventory.meta.trim.TrimPattern; import java.util.HashMap; import java.util.Map; +import java.util.Objects; public class ProfileFile extends ConfigFile { @@ -61,6 +70,31 @@ public void setData() { config.set("settings.messages", profile.isPrivateMessages()); config.set("settings.worldtime", profile.getWorldTime().toString()); + // Cosmetics data for armor trims + if (profile.getCosmeticsData() != null) { + config.set("cosmetics.active-tier", profile.getCosmeticsData().getActiveTier().getId()); + + for (ArmorTrimTier tier : ArmorTrimTier.values()) { + for (ArmorSlot slot : ArmorSlot.values()) { + String basePath = "cosmetics.tiers." + tier.getId() + "." + slot.getId(); + + TrimPattern pattern = profile.getCosmeticsData().getPattern(tier, slot); + if (pattern != null) { + config.set(basePath + ".pattern", "minecraft:" + ArmorTrimPermissionManager.getTrimId(pattern)); + } else { + config.set(basePath + ".pattern", null); + } + + TrimMaterial material = profile.getCosmeticsData().getMaterial(tier, slot); + if (material != null) { + config.set(basePath + ".material", "minecraft:" + ArmorTrimPermissionManager.getTrimId(material)); + } else { + config.set(basePath + ".material", null); + } + } + } + } + // Ladder win/lose stats for (NormalLadder ladder : LadderManager.getInstance().getLadders()) { String name = ladder.getName().toLowerCase(); @@ -137,10 +171,10 @@ public void getData() { } if (config.isString("prefix")) - profile.setPrefix(Component.text(config.getString("prefix"))); + profile.setPrefix(Component.text(Objects.requireNonNull(config.getString("prefix")))); if (config.isString("suffix")) - profile.setSuffix(Component.text(config.getString("suffix"))); + profile.setSuffix(Component.text(Objects.requireNonNull(config.getString("suffix")))); if (config.isInt("allowed-custom-kits")) profile.setAllowedCustomKits(config.getInt("allowed-custom-kits")); @@ -154,6 +188,55 @@ public void getData() { profile.setPrivateMessages(config.getBoolean("settings.messages")); profile.setWorldTime(ProfileWorldTime.valueOf(config.getString("settings.worldtime"))); + // Load cosmetics data for armor trims + try { + ArmorTrimTier activeTier = ArmorTrimTier.fromId(config.getString("cosmetics.active-tier", "leather")); + profile.getCosmeticsData().setActiveTier(activeTier); + + boolean loadedTierData = false; + for (ArmorTrimTier tier : ArmorTrimTier.values()) { + for (ArmorSlot slot : ArmorSlot.values()) { + String basePath = "cosmetics.tiers." + tier.getId() + "." + slot.getId(); + + if (config.isString(basePath + ".pattern")) { + TrimPattern pattern = getTrimPatternByName(config.getString(basePath + ".pattern")); + if (pattern != null) { + profile.getCosmeticsData().setPattern(tier, slot, pattern); + loadedTierData = true; + } + } + + if (config.isString(basePath + ".material")) { + TrimMaterial material = getTrimMaterialByName(config.getString(basePath + ".material")); + if (material != null) { + profile.getCosmeticsData().setMaterial(tier, slot, material); + loadedTierData = true; + } + } + } + } + + if (!loadedTierData) { + for (ArmorSlot slot : ArmorSlot.values()) { + String legacyPath = "cosmetics." + slot.getId(); + if (config.isString(legacyPath + ".pattern")) { + TrimPattern pattern = getTrimPatternByName(config.getString(legacyPath + ".pattern")); + if (pattern != null) { + profile.getCosmeticsData().setPattern(ArmorTrimTier.LEATHER, slot, pattern); + } + } + if (config.isString(legacyPath + ".material")) { + TrimMaterial material = getTrimMaterialByName(config.getString(legacyPath + ".material")); + if (material != null) { + profile.getCosmeticsData().setMaterial(ArmorTrimTier.LEATHER, slot, material); + } + } + } + } + } catch (Exception e) { + // Handle invalid cosmetics data - silently ignore for graceful handling of removed/renamed cosmetics + } + for (NormalLadder ladder : LadderManager.getInstance().getLadders()) { String name = ladder.getName().toLowerCase(); @@ -199,4 +282,24 @@ public void deleteCustomKit(Ladder ladder) { saveFile(); } + private TrimPattern getTrimPatternByName(String name) { + if (name == null || name.isBlank()) return null; + String normalized = normalizeKey(name); + return RegistryAccess.registryAccess().getRegistry(RegistryKey.TRIM_PATTERN).get(Key.key(normalized)); + } + + private TrimMaterial getTrimMaterialByName(String name) { + if (name == null || name.isBlank()) return null; + String normalized = normalizeKey(name); + return RegistryAccess.registryAccess().getRegistry(RegistryKey.TRIM_MATERIAL).get(Key.key(normalized)); + } + + private String normalizeKey(String key) { + String normalized = key.trim().toLowerCase(); + if (!normalized.contains(":")) { + return "minecraft:" + normalized; + } + return normalized; + } + } diff --git a/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/ArmorSlot.java b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/ArmorSlot.java new file mode 100644 index 00000000..085987a0 --- /dev/null +++ b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/ArmorSlot.java @@ -0,0 +1,22 @@ +package dev.nandi0813.practice.manager.profile.cosmetics; + +import lombok.Getter; + +/** + * Enum representing the different armor slots available for cosmetics. + */ +@Getter +public enum ArmorSlot { + HELMET("helmet", "Helmet"), + CHESTPLATE("chestplate", "Chestplate"), + LEGGINGS("leggings", "Leggings"), + BOOTS("boots", "Boots"), + SHIELD("shield", "Shield"); + private final String id; + private final String displayName; + ArmorSlot(String id, String displayName) { + this.id = id; + this.displayName = displayName; + } + +} diff --git a/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/ArmorTrimPermissionManager.java b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/ArmorTrimPermissionManager.java new file mode 100644 index 00000000..1c5dc94b --- /dev/null +++ b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/ArmorTrimPermissionManager.java @@ -0,0 +1,139 @@ +package dev.nandi0813.practice.manager.profile.cosmetics; + +import io.papermc.paper.registry.RegistryAccess; +import io.papermc.paper.registry.RegistryKey; +import org.bukkit.Bukkit; +import org.bukkit.inventory.meta.trim.TrimMaterial; +import org.bukkit.inventory.meta.trim.TrimPattern; +import org.bukkit.permissions.Permission; +import org.bukkit.permissions.PermissionDefault; +import org.bukkit.plugin.PluginManager; + +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public enum ArmorTrimPermissionManager { + ; + + private static final List REGISTERED_PATTERNS = new ArrayList<>(); + private static final List REGISTERED_MATERIALS = new ArrayList<>(); + private static final Map PATTERN_IDS = new HashMap<>(); + private static final Map MATERIAL_IDS = new HashMap<>(); + private static final Pattern NAMESPACE_PATTERN = Pattern.compile("([a-z0-9_.-]+):([a-z0-9_./-]+)"); + + public static void registerAllPermissions() { + PluginManager pluginManager = Bukkit.getPluginManager(); + + for (ArmorTrimTier tier : ArmorTrimTier.values()) { + registerPermission(pluginManager, tier.getPermissionNode(), "Use " + tier.getDisplayName() + " armor tier cosmetics."); + } + + REGISTERED_PATTERNS.clear(); + PATTERN_IDS.clear(); + var patternRegistry = RegistryAccess.registryAccess().getRegistry(RegistryKey.TRIM_PATTERN); + patternRegistry.keyStream().forEach(key -> { + TrimPattern pattern = patternRegistry.get(key); + if (pattern == null) { + return; + } + + String id = sanitizeId(key.getKey()); + REGISTERED_PATTERNS.add(pattern); + PATTERN_IDS.put(pattern, id); + registerPermission(pluginManager, + "zpp.cosmetics.pattern." + id, + "Use armor trim pattern " + id + "."); + }); + + REGISTERED_MATERIALS.clear(); + MATERIAL_IDS.clear(); + var materialRegistry = RegistryAccess.registryAccess().getRegistry(RegistryKey.TRIM_MATERIAL); + materialRegistry.keyStream().forEach(key -> { + TrimMaterial material = materialRegistry.get(key); + if (material == null) { + return; + } + + String id = sanitizeId(key.getKey()); + REGISTERED_MATERIALS.add(material); + MATERIAL_IDS.put(material, id); + registerPermission(pluginManager, + "zpp.cosmetics.material." + id, + "Use armor trim material " + id + "."); + }); + + REGISTERED_PATTERNS.sort(Comparator.comparing(ArmorTrimPermissionManager::getTrimId)); + REGISTERED_MATERIALS.sort(Comparator.comparing(ArmorTrimPermissionManager::getTrimId)); + } + + public static List getRegisteredPatterns() { + return Collections.unmodifiableList(REGISTERED_PATTERNS); + } + + public static List getRegisteredMaterials() { + return Collections.unmodifiableList(REGISTERED_MATERIALS); + } + + public static String getTrimId(TrimPattern pattern) { + if (pattern == null) { + return "unknown"; + } + + String id = PATTERN_IDS.get(pattern); + if (id != null) { + return id; + } + + for (Map.Entry entry : PATTERN_IDS.entrySet()) { + if (entry.getKey().equals(pattern)) { + return entry.getValue(); + } + } + + return resolveTrimIdFallback(pattern); + } + + public static String getTrimId(TrimMaterial material) { + if (material == null) { + return "unknown"; + } + + String id = MATERIAL_IDS.get(material); + if (id != null) { + return id; + } + + for (Map.Entry entry : MATERIAL_IDS.entrySet()) { + if (entry.getKey().equals(material)) { + return entry.getValue(); + } + } + + return resolveTrimIdFallback(material); + } + + private static String resolveTrimIdFallback(Object trimValue) { + String raw = String.valueOf(trimValue).toLowerCase(Locale.ROOT); + Matcher matcher = NAMESPACE_PATTERN.matcher(raw); + if (matcher.find()) { + return sanitizeId(matcher.group(2)); + } + + return sanitizeId(raw); + } + + private static String sanitizeId(String value) { + return value.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9_]+", ""); + } + + private static void registerPermission(PluginManager pluginManager, String node, String description) { + if (pluginManager.getPermission(node) != null) { + return; + } + + pluginManager.addPermission(new Permission(node, description, PermissionDefault.OP)); + } +} + + diff --git a/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/ArmorTrimTier.java b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/ArmorTrimTier.java new file mode 100644 index 00000000..244e2137 --- /dev/null +++ b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/ArmorTrimTier.java @@ -0,0 +1,101 @@ +package dev.nandi0813.practice.manager.profile.cosmetics; + +import dev.nandi0813.practice.manager.backend.GUIFile; +import lombok.Getter; +import org.bukkit.Material; + +import java.util.Locale; + +/** + * Represents the armor base tier used for cosmetics preview and selection. + */ +public enum ArmorTrimTier { + LEATHER("leather", "Leather", Material.LEATHER_HELMET, Material.LEATHER_CHESTPLATE, Material.LEATHER_LEGGINGS, Material.LEATHER_BOOTS), + GOLD("gold", "Gold", Material.GOLDEN_HELMET, Material.GOLDEN_CHESTPLATE, Material.GOLDEN_LEGGINGS, Material.GOLDEN_BOOTS), + IRON("iron", "Iron", Material.IRON_HELMET, Material.IRON_CHESTPLATE, Material.IRON_LEGGINGS, Material.IRON_BOOTS), + DIAMOND("diamond", "Diamond", Material.DIAMOND_HELMET, Material.DIAMOND_CHESTPLATE, Material.DIAMOND_LEGGINGS, Material.DIAMOND_BOOTS), + NETHERITE("netherite", "Netherite", Material.NETHERITE_HELMET, Material.NETHERITE_CHESTPLATE, Material.NETHERITE_LEGGINGS, Material.NETHERITE_BOOTS); + + @Getter + private final String id; + private final String defaultDisplayName; + private final Material defaultHelmetMaterial; + private final Material defaultChestplateMaterial; + private final Material defaultLeggingsMaterial; + private final Material defaultBootsMaterial; + + ArmorTrimTier(String id, String defaultDisplayName, Material defaultHelmetMaterial, Material defaultChestplateMaterial, Material defaultLeggingsMaterial, Material defaultBootsMaterial) { + this.id = id; + this.defaultDisplayName = defaultDisplayName; + this.defaultHelmetMaterial = defaultHelmetMaterial; + this.defaultChestplateMaterial = defaultChestplateMaterial; + this.defaultLeggingsMaterial = defaultLeggingsMaterial; + this.defaultBootsMaterial = defaultBootsMaterial; + } + + public String getDisplayName() { + String configKey = "GUIS.COSMETICS.ARMOR-TIERS." + this.name() + ".DISPLAY-NAME"; + String configValue = GUIFile.getString(configKey); + return !configValue.isBlank() ? configValue : defaultDisplayName; + } + + public String getPermissionNode() { + return "zpp.cosmetics.base." + id; + } + + public Material getMaterial(ArmorSlot slot) { + if (slot == null) { + return Material.AIR; + } + + String configKey = "GUIS.COSMETICS.ARMOR-TIERS." + this.name() + "." + getConfigSlotKey(slot); + String materialName = GUIFile.getString(configKey); + + if (!materialName.isBlank()) { + try { + return Material.valueOf(materialName.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException ignored) { + // Fall through to default + } + } + + return switch (slot) { + case HELMET -> defaultHelmetMaterial; + case CHESTPLATE -> defaultChestplateMaterial; + case LEGGINGS -> defaultLeggingsMaterial; + case BOOTS -> defaultBootsMaterial; + case SHIELD -> Material.SHIELD; + }; + } + + private static String getConfigSlotKey(ArmorSlot slot) { + return switch (slot) { + case HELMET -> "HELMET-MATERIAL"; + case CHESTPLATE -> "CHESTPLATE-MATERIAL"; + case LEGGINGS -> "LEGGINGS-MATERIAL"; + case BOOTS -> "BOOTS-MATERIAL"; + case SHIELD -> "SHIELD-MATERIAL"; + }; + } + + public ArmorTrimTier next() { + ArmorTrimTier[] values = values(); + return values[(this.ordinal() + 1) % values.length]; + } + + public static ArmorTrimTier fromId(String id) { + if (id == null || id.isBlank()) { + return LEATHER; + } + + String normalized = id.toLowerCase(Locale.ROOT); + for (ArmorTrimTier tier : values()) { + if (tier.id.equals(normalized)) { + return tier; + } + } + + return LEATHER; + } +} + diff --git a/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/CosmeticsData.java b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/CosmeticsData.java new file mode 100644 index 00000000..5d8fffcc --- /dev/null +++ b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/CosmeticsData.java @@ -0,0 +1,86 @@ +package dev.nandi0813.practice.manager.profile.cosmetics; + +import lombok.Getter; +import lombok.Setter; +import org.bukkit.inventory.meta.trim.TrimMaterial; +import org.bukkit.inventory.meta.trim.TrimPattern; + +import java.util.EnumMap; +import java.util.Map; + +@Getter +@Setter +public class CosmeticsData { + + private ArmorTrimTier activeTier = ArmorTrimTier.LEATHER; + + private final Map> tierData = new EnumMap<>(ArmorTrimTier.class); + + public CosmeticsData() { + for (ArmorTrimTier tier : ArmorTrimTier.values()) { + Map bySlot = new EnumMap<>(ArmorSlot.class); + for (ArmorSlot armorSlot : ArmorSlot.values()) { + bySlot.put(armorSlot, new SlotData()); + } + tierData.put(tier, bySlot); + } + } + + public TrimPattern getPattern(ArmorTrimTier tier, ArmorSlot slot) { + if (slot == null) { + return null; + } + + return getSlotData(tier, slot).pattern; + } + + public TrimMaterial getMaterial(ArmorSlot slot) { + return getMaterial(activeTier, slot); + } + + public TrimMaterial getMaterial(ArmorTrimTier tier, ArmorSlot slot) { + if (slot == null) { + return null; + } + + return getSlotData(tier, slot).material; + } + + public void setPattern(ArmorTrimTier tier, ArmorSlot slot, TrimPattern pattern) { + if (slot == null) { + return; + } + + getSlotData(tier, slot).pattern = pattern; + } + + public void setMaterial(ArmorSlot slot, TrimMaterial material) { + setMaterial(activeTier, slot, material); + } + + public void setMaterial(ArmorTrimTier tier, ArmorSlot slot, TrimMaterial material) { + if (slot == null) { + return; + } + + getSlotData(tier, slot).material = material; + } + + private SlotData getSlotData(ArmorTrimTier tier, ArmorSlot slot) { + ArmorTrimTier resolvedTier = tier == null ? ArmorTrimTier.LEATHER : tier; + Map bySlot = tierData.get(resolvedTier); + if (bySlot == null) { + bySlot = new EnumMap<>(ArmorSlot.class); + tierData.put(resolvedTier, bySlot); + } + + return bySlot.computeIfAbsent(slot, k -> new SlotData()); + } + + private static final class SlotData { + private TrimPattern pattern; + private TrimMaterial material; + } +} + + diff --git a/core/src/main/resources/guis.yml b/core/src/main/resources/guis.yml index 74041663..cb69488a 100644 --- a/core/src/main/resources/guis.yml +++ b/core/src/main/resources/guis.yml @@ -1,4 +1,4 @@ -VERSION: 14 +VERSION: 15 GENERAL-FILLER-ITEM: NAME: " " @@ -1591,6 +1591,7 @@ GUIS: - "" - "&7You can set the arenas icon by using the" - "&7&l/arena set icon %arenaName% &7command." + - "" - "&c&lNote: &7You have to hold the item in your hand" - "&7and name it first with the &7&l/prac rename &7command." STATUS: @@ -2418,7 +2419,8 @@ GUIS: - "&6Event Information:" - " &7» &eState: %state%" - "" - - "&b&lClick here &bto open event settings." + - "&b&lLEFT-CLICK &bto open event settings." + - "&a&lRIGHT-CLICK &ato teleport to the event." EVENT-MAIN: TITLE: "%eventName% &8- Event" ICONS: @@ -2841,4 +2843,167 @@ GUIS: - "&8&m------------------------" - "&7Click here to manually" - "&7save the %data% data." - - "&8&m------------------------" \ No newline at end of file + - "&8&m------------------------" + COSMETICS: + MAIN-TITLE: "&8Armor Cosmetics" + # Error messages for permission denied + PERMISSION-DENIED-MESSAGE: "You do not have permission for the selected armor tier." + TIER-PERMISSION-DENIED-MESSAGE: "You do not have permission for this armor tier." + PATTERN-PERMISSION-DENIED-MESSAGE: "You do not have permission to use this trim pattern." + MATERIAL-PERMISSION-DENIED-MESSAGE: "You do not have permission to use this trim material." + NO-TIER-PERMISSION-MESSAGE: "You do not have permission for any armor tier." + # Armor Tier customization + ARMOR-TIERS: + LEATHER: + DISPLAY-NAME: "Leather" + HELMET-MATERIAL: LEATHER_HELMET + CHESTPLATE-MATERIAL: LEATHER_CHESTPLATE + LEGGINGS-MATERIAL: LEATHER_LEGGINGS + BOOTS-MATERIAL: LEATHER_BOOTS + GOLD: + DISPLAY-NAME: "Gold" + HELMET-MATERIAL: GOLDEN_HELMET + CHESTPLATE-MATERIAL: GOLDEN_CHESTPLATE + LEGGINGS-MATERIAL: GOLDEN_LEGGINGS + BOOTS-MATERIAL: GOLDEN_BOOTS + IRON: + DISPLAY-NAME: "Iron" + HELMET-MATERIAL: IRON_HELMET + CHESTPLATE-MATERIAL: IRON_CHESTPLATE + LEGGINGS-MATERIAL: IRON_LEGGINGS + BOOTS-MATERIAL: IRON_BOOTS + DIAMOND: + DISPLAY-NAME: "Diamond" + HELMET-MATERIAL: DIAMOND_HELMET + CHESTPLATE-MATERIAL: DIAMOND_CHESTPLATE + LEGGINGS-MATERIAL: DIAMOND_LEGGINGS + BOOTS-MATERIAL: DIAMOND_BOOTS + NETHERITE: + DISPLAY-NAME: "Netherite" + HELMET-MATERIAL: NETHERITE_HELMET + CHESTPLATE-MATERIAL: NETHERITE_CHESTPLATE + LEGGINGS-MATERIAL: NETHERITE_LEGGINGS + BOOTS-MATERIAL: NETHERITE_BOOTS + ICONS: + HELMET-ICON: + NAME: "&eHelmet" + MATERIAL: LEATHER_HELMET + LORE: + - "" + - "&7Click here to customize" + - "&7armor trim patterns and materials" + - "&7for your helmet." + - "" + CHESTPLATE-ICON: + NAME: "&eChestplate" + MATERIAL: LEATHER_CHESTPLATE + LORE: + - "" + - "&7Click here to customize" + - "&7armor trim patterns and materials" + - "&7for your chestplate." + - "" + LEGGINGS-ICON: + NAME: "&eLeggings" + MATERIAL: LEATHER_LEGGINGS + LORE: + - "" + - "&7Click here to customize" + - "&7armor trim patterns and materials" + - "&7for your leggings." + - "" + BOOTS-ICON: + NAME: "&eBoots" + MATERIAL: LEATHER_BOOTS + LORE: + - "" + - "&7Click here to customize" + - "&7armor trim patterns and materials" + - "&7for your boots." + - "" + SHIELD-ICON: + NAME: "&eShield" + MATERIAL: SHIELD + LORE: + - "" + - "&7Click here to customize" + - "&7the trim pattern for your shield." + - "" + INFO-ICON: + NAME: "&bCosmetics Information" + MATERIAL: BOOK + LORE: + - "&8&m------------------------" + - "&7Unlocked Patterns: &b%pattern_unlocked%&7/&b%pattern_total%" + - "&7Unlocked Materials: &6%material_unlocked%&7/&6%material_total%" + - "" + - "&7Select a category item to edit" + - "&7your active trim cosmetics." + - "&8&m------------------------" + BACK-TO: + NAME: "&cBack" + MATERIAL: ARROW + # Dynamically built lore templates for armor cosmetics + ARMOR-PREVIEW-LORE: + - "&7Tier: &e%tier%" + - "&7Active Pattern: %pattern%" + - "&7Active Material: %material%" + - "" + - "&eClick to customize" + TIER-TOGGLE-LORE: + - "" + - "&7Left-click: &cPrevious tier" + - "&7Right-click: &aNext tier" + - "&7Required Permission: &e%tier_permission%" + SUB-TITLE: "&8Customize %armor% Cosmetics" + # Armor Piece Hub Configuration + ARMOR-PIECE-HUB: + INVENTORY-ROWS: 4 + SLOTS: + BACK: 27 + PREVIEW: 13 + PATTERN-MENU: 20 + MATERIAL-MENU: 24 + PREVIEW-ITEM: + NAME: "&eCurrent Preview" + PATTERN-SELECTION-BUTTON: + NAME: "&bPattern Selection" + LORE: + - "&7Open all available trim patterns." + - "&eClick to open." + DEFAULT-MATERIAL: SMITHING_TABLE + MATERIAL-SELECTION-BUTTON: + NAME: "&6Material Selection" + LORE: + - "&7Open all available trim materials." + - "&eClick to open." + DEFAULT-MATERIAL: ANVIL + # Pattern Selection GUI Configuration + PATTERN-SELECTION: + INVENTORY-ROWS: 5 + BACK-SLOT: 36 + START-SLOT: 10 + INVENTORY-TITLE: "&8Select Pattern - %armor%" + PATTERN-ITEM: + NAME: "&b%pattern_name% Pattern" + LORE: + - "&7Status: %state%" + - "&7Access: %access%" + - "&7Permission: &8%permission%" + # Material Selection GUI Configuration + MATERIAL-SELECTION: + INVENTORY-ROWS: 5 + BACK-SLOT: 36 + START-SLOT: 10 + INVENTORY-TITLE: "&8Select Material - %armor%" + MATERIAL-ITEM: + NAME: "&6%material_name% Material" + LORE: + - "&7Status: %state%" + - "&7Access: %access%" + - "&7Permission: &8%permission%" + MATERIAL-ICONS: + LAPIS: LAPIS_LAZULI + AMETHYST: AMETHYST_SHARD + RESIN: RESIN_BRICK + diff --git a/core/src/main/resources/plugin.yml b/core/src/main/resources/plugin.yml index 02d7bc90..f69e3827 100644 --- a/core/src/main/resources/plugin.yml +++ b/core/src/main/resources/plugin.yml @@ -50,6 +50,9 @@ commands: description: Copy a custom kit from another player. ignorequeue: description: Ignore a player in unranked queue. + cosmetics: + aliases: [cosmetc, csmetic] + description: Open the cosmetics GUI for armor trim customization. permissions: zpp.admin: @@ -320,6 +323,33 @@ permissions: zpp.playerkit.copy: description: Copy custom kit from others. + # Cosmetics + zpp.cosmetics.main: + description: Open the cosmetics GUI. + zpp.cosmetics.base.*: + description: Allow access to all armor base tiers. + children: + zpp.cosmetics.base.leather: true + zpp.cosmetics.base.gold: true + zpp.cosmetics.base.iron: true + zpp.cosmetics.base.diamond: true + zpp.cosmetics.base.netherite: true + zpp.cosmetics.base.leather: + description: Allow access to Leather armor tier. + zpp.cosmetics.base.gold: + description: Allow access to Gold armor tier. + zpp.cosmetics.base.iron: + description: Allow access to Iron armor tier. + zpp.cosmetics.base.diamond: + description: Allow access to Diamond armor tier. + zpp.cosmetics.base.netherite: + description: Allow access to Netherite armor tier. + + # Note: zpp.cosmetics.pattern.* and zpp.cosmetics.material.* permissions are + # dynamically registered at runtime by ArmorTrimPermissionManager based on + # available trim patterns and materials (e.g., zpp.cosmetics.pattern.sentry, zpp.cosmetics.material.amethyst). + # They do NOT need to be declared here. + # Update notifier zpp.update.notify: description: Receive in-game notifications when a new ZonePractice Pro version is released. From 65ab9071bef9021fe7b23809f336de191f5ecd45 Mon Sep 17 00:00:00 2001 From: Nandor Dukat Date: Wed, 18 Mar 2026 15:19:45 +0100 Subject: [PATCH 10/26] updated guis.yml version --- core/src/main/resources/guis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/resources/guis.yml b/core/src/main/resources/guis.yml index cb69488a..ed9aa5f6 100644 --- a/core/src/main/resources/guis.yml +++ b/core/src/main/resources/guis.yml @@ -1,4 +1,4 @@ -VERSION: 15 +VERSION: 16 GENERAL-FILLER-ITEM: NAME: " " From 64c8ed5388c31ccd3f128bdd479ac6055a76f25c Mon Sep 17 00:00:00 2001 From: Nandor Dukat Date: Wed, 18 Mar 2026 15:31:08 +0100 Subject: [PATCH 11/26] updated plugin.yml --- core/src/main/resources/plugin.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/resources/plugin.yml b/core/src/main/resources/plugin.yml index f69e3827..e8a8a795 100644 --- a/core/src/main/resources/plugin.yml +++ b/core/src/main/resources/plugin.yml @@ -347,7 +347,7 @@ permissions: # Note: zpp.cosmetics.pattern.* and zpp.cosmetics.material.* permissions are # dynamically registered at runtime by ArmorTrimPermissionManager based on - # available trim patterns and materials (e.g., zpp.cosmetics.pattern.sentry, zpp.cosmetics.material.amethyst). + # available trim patterns and materials in the server's Minecraft version. # They do NOT need to be declared here. # Update notifier From ab81d7d5c56f549bf3c20cf395320e34ed6f033f Mon Sep 17 00:00:00 2001 From: Nandor Dukat Date: Wed, 18 Mar 2026 18:56:06 +0100 Subject: [PATCH 12/26] added cosmetic permission reset in case player loses it, added reset option to main gui --- .../practice/listener/PlayerJoin.java | 15 ++++ .../gui/guis/cosmetics/CosmeticsGui.java | 12 +++ .../cosmetics/ArmorTrimPermissionManager.java | 47 ++++++++++ .../CosmeticsPermissionSanitizer.java | 88 +++++++++++++++++++ core/src/main/resources/guis.yml | 6 +- 5 files changed, 164 insertions(+), 4 deletions(-) create mode 100644 core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/CosmeticsPermissionSanitizer.java diff --git a/core/src/main/java/dev/nandi0813/practice/listener/PlayerJoin.java b/core/src/main/java/dev/nandi0813/practice/listener/PlayerJoin.java index 83b68832..3d8f22b5 100644 --- a/core/src/main/java/dev/nandi0813/practice/listener/PlayerJoin.java +++ b/core/src/main/java/dev/nandi0813/practice/listener/PlayerJoin.java @@ -6,6 +6,7 @@ import dev.nandi0813.practice.manager.nametag.NametagManager; import dev.nandi0813.practice.manager.profile.Profile; import dev.nandi0813.practice.manager.profile.ProfileManager; +import dev.nandi0813.practice.manager.profile.cosmetics.CosmeticsPermissionSanitizer; import dev.nandi0813.practice.manager.profile.enums.ProfileStatus; import dev.nandi0813.practice.manager.sidebar.SidebarManager; import dev.nandi0813.practice.util.PermanentConfig; @@ -64,6 +65,20 @@ public void onPlayerJoin(PlayerJoinEvent e) { // Notify operators about available updates (delayed so the player is fully in the world) Bukkit.getScheduler().runTaskLater(ZonePractice.getInstance(), () -> UpdateChecker.notifyPlayer(player), 40L); + + // Revalidate saved cosmetics after permission plugins have finished loading player nodes. + Bukkit.getScheduler().runTaskLater(ZonePractice.getInstance(), () -> { + if (!player.isOnline()) { + return; + } + + Profile liveProfile = ProfileManager.getInstance().getProfile(player); + if (liveProfile == null) { + return; + } + + CosmeticsPermissionSanitizer.sanitize(player, liveProfile); + }, 40L); } } diff --git a/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/CosmeticsGui.java b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/CosmeticsGui.java index 43d4d974..3e2c40c6 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/CosmeticsGui.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/CosmeticsGui.java @@ -127,10 +127,22 @@ public void handleClickEvent(InventoryClickEvent e) { Common.sendMMMessage(player, deniedMessage); return; } + + if (e.isRightClick()) { + resetArmorCosmetic(activeTier, armorSlot); + return; + } + openArmorSubGui(player, armorSlot); } } + private void resetArmorCosmetic(ArmorTrimTier activeTier, ArmorSlot armorSlot) { + profile.getCosmeticsData().setPattern(activeTier, armorSlot, null); + profile.getCosmeticsData().setMaterial(activeTier, armorSlot, null); + update(true); + } + private ArmorSlot getArmorSlotFromSlot(int slot) { return switch (slot) { case HELMET_SLOT -> ArmorSlot.HELMET; diff --git a/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/ArmorTrimPermissionManager.java b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/ArmorTrimPermissionManager.java index 1c5dc94b..f23751c7 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/ArmorTrimPermissionManager.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/ArmorTrimPermissionManager.java @@ -3,6 +3,7 @@ import io.papermc.paper.registry.RegistryAccess; import io.papermc.paper.registry.RegistryKey; import org.bukkit.Bukkit; +import org.bukkit.entity.Player; import org.bukkit.inventory.meta.trim.TrimMaterial; import org.bukkit.inventory.meta.trim.TrimPattern; import org.bukkit.permissions.Permission; @@ -75,6 +76,52 @@ public static List getRegisteredMaterials() { return Collections.unmodifiableList(REGISTERED_MATERIALS); } + public static boolean hasBasePermission(Player player, ArmorTrimTier tier) { + if (player == null || tier == null) { + return false; + } + + return player.isOp() + || player.hasPermission("zpp.cosmetics.base.*") + || player.hasPermission(tier.getPermissionNode()); + } + + public static boolean hasPatternPermission(Player player, TrimPattern pattern) { + if (player == null || pattern == null) { + return false; + } + + return hasPatternPermission(player, "zpp.cosmetics.pattern." + getTrimId(pattern)); + } + + public static boolean hasPatternPermission(Player player, String node) { + if (player == null || node == null || node.isBlank()) { + return false; + } + + return player.isOp() + || player.hasPermission("zpp.cosmetics.pattern.*") + || player.hasPermission(node); + } + + public static boolean hasMaterialPermission(Player player, TrimMaterial material) { + if (player == null || material == null) { + return false; + } + + return hasMaterialPermission(player, "zpp.cosmetics.material." + getTrimId(material)); + } + + public static boolean hasMaterialPermission(Player player, String node) { + if (player == null || node == null || node.isBlank()) { + return false; + } + + return player.isOp() + || player.hasPermission("zpp.cosmetics.material.*") + || player.hasPermission(node); + } + public static String getTrimId(TrimPattern pattern) { if (pattern == null) { return "unknown"; diff --git a/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/CosmeticsPermissionSanitizer.java b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/CosmeticsPermissionSanitizer.java new file mode 100644 index 00000000..dd6c3a5c --- /dev/null +++ b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/CosmeticsPermissionSanitizer.java @@ -0,0 +1,88 @@ +package dev.nandi0813.practice.manager.profile.cosmetics; + +import dev.nandi0813.practice.manager.profile.Profile; +import org.bukkit.entity.Player; +import org.bukkit.inventory.meta.trim.TrimMaterial; +import org.bukkit.inventory.meta.trim.TrimPattern; + +import java.util.EnumSet; + +public enum CosmeticsPermissionSanitizer { + ; + + public static boolean sanitize(Player player, Profile profile) { + if (player == null || profile == null || profile.getCosmeticsData() == null) { + return false; + } + + boolean changed = false; + + // Shield customization is intentionally skipped until its dedicated rework. + EnumSet supportedSlots = EnumSet.of( + ArmorSlot.HELMET, + ArmorSlot.CHESTPLATE, + ArmorSlot.LEGGINGS, + ArmorSlot.BOOTS + ); + + for (ArmorTrimTier tier : ArmorTrimTier.values()) { + boolean hasTierPermission = ArmorTrimPermissionManager.hasBasePermission(player, tier); + + for (ArmorSlot slot : supportedSlots) { + TrimPattern pattern = profile.getCosmeticsData().getPattern(tier, slot); + TrimMaterial material = profile.getCosmeticsData().getMaterial(tier, slot); + + if (!hasTierPermission) { + if (pattern != null) { + profile.getCosmeticsData().setPattern(tier, slot, null); + changed = true; + } + + if (material != null) { + profile.getCosmeticsData().setMaterial(tier, slot, null); + changed = true; + } + + continue; + } + + if (pattern != null && !ArmorTrimPermissionManager.hasPatternPermission(player, pattern)) { + profile.getCosmeticsData().setPattern(tier, slot, null); + changed = true; + } + + if (material != null && !ArmorTrimPermissionManager.hasMaterialPermission(player, material)) { + profile.getCosmeticsData().setMaterial(tier, slot, null); + changed = true; + } + } + } + + ArmorTrimTier activeTier = profile.getCosmeticsData().getActiveTier(); + if (!ArmorTrimPermissionManager.hasBasePermission(player, activeTier)) { + ArmorTrimTier replacement = null; + for (ArmorTrimTier tier : ArmorTrimTier.values()) { + if (ArmorTrimPermissionManager.hasBasePermission(player, tier)) { + replacement = tier; + break; + } + } + + if (replacement == null) { + replacement = ArmorTrimTier.LEATHER; + } + + if (replacement != activeTier) { + profile.getCosmeticsData().setActiveTier(replacement); + changed = true; + } + } + + if (changed) { + profile.saveData(); + } + + return changed; + } +} + diff --git a/core/src/main/resources/guis.yml b/core/src/main/resources/guis.yml index ed9aa5f6..c6447625 100644 --- a/core/src/main/resources/guis.yml +++ b/core/src/main/resources/guis.yml @@ -2949,12 +2949,12 @@ GUIS: - "&7Active Pattern: %pattern%" - "&7Active Material: %material%" - "" - - "&eClick to customize" + - "&eLeft-Click to customize" + - "&cRight-Click to reset" TIER-TOGGLE-LORE: - "" - "&7Left-click: &cPrevious tier" - "&7Right-click: &aNext tier" - - "&7Required Permission: &e%tier_permission%" SUB-TITLE: "&8Customize %armor% Cosmetics" # Armor Piece Hub Configuration ARMOR-PIECE-HUB: @@ -2989,7 +2989,6 @@ GUIS: LORE: - "&7Status: %state%" - "&7Access: %access%" - - "&7Permission: &8%permission%" # Material Selection GUI Configuration MATERIAL-SELECTION: INVENTORY-ROWS: 5 @@ -3001,7 +3000,6 @@ GUIS: LORE: - "&7Status: %state%" - "&7Access: %access%" - - "&7Permission: &8%permission%" MATERIAL-ICONS: LAPIS: LAPIS_LAZULI AMETHYST: AMETHYST_SHARD From dbf2e46a2a2097f68ef6a6db38006c2bcf153401 Mon Sep 17 00:00:00 2001 From: Nandor Dukat Date: Wed, 18 Mar 2026 19:01:06 +0100 Subject: [PATCH 13/26] added soup bowl removal mechanic --- .../practice/listener/PlayerInteract.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/dev/nandi0813/practice/listener/PlayerInteract.java b/core/src/main/java/dev/nandi0813/practice/listener/PlayerInteract.java index 89278d96..682c1d2b 100644 --- a/core/src/main/java/dev/nandi0813/practice/listener/PlayerInteract.java +++ b/core/src/main/java/dev/nandi0813/practice/listener/PlayerInteract.java @@ -12,6 +12,7 @@ import org.bukkit.event.block.Action; import org.bukkit.event.player.PlayerBedEnterEvent; import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.ItemStack; import java.util.Objects; @@ -74,10 +75,10 @@ public void onSoup(PlayerInteractEvent e) { if (health == maxHealth) return; if ((health + regen) < maxHealth) { - player.getInventory().setItemInMainHand(new ItemStack(Material.BOWL)); + consumeUsedSoup(player, e.getHand()); player.setHealth(health + regen); } else if ((health + regen) >= maxHealth) { - player.getInventory().setItemInMainHand(new ItemStack(Material.BOWL)); + consumeUsedSoup(player, e.getHand()); player.setHealth(maxHealth); } player.updateInventory(); @@ -85,6 +86,15 @@ public void onSoup(PlayerInteractEvent e) { } } + private void consumeUsedSoup(Player player, EquipmentSlot hand) { + if (hand == EquipmentSlot.OFF_HAND) { + player.getInventory().setItemInOffHand(new ItemStack(Material.AIR)); + return; + } + + player.getInventory().setItemInMainHand(new ItemStack(Material.AIR)); + } + @EventHandler public void onPlayerSleep(PlayerInteractEvent e) { Player player = e.getPlayer(); From fc5ecec0a12d2fafbe36cb2c1fadc9fa6333b1cc Mon Sep 17 00:00:00 2001 From: Nandor Dukat Date: Wed, 18 Mar 2026 19:04:38 +0100 Subject: [PATCH 14/26] refactored single commands --- .../main/java/dev/nandi0813/practice/ZonePractice.java | 10 ---------- .../{accept => singlecommands}/AcceptCommand.java | 2 +- .../{division => singlecommands}/DivisionsCommand.java | 2 +- .../command/{duel => singlecommands}/DuelCommand.java | 2 +- .../{hologram => singlecommands}/HologramCommand.java | 2 +- .../MatchStatsCommand.java | 2 +- .../{preview => singlecommands}/PreviewCommand.java | 2 +- .../{settings => singlecommands}/SettingsCommand.java | 2 +- .../{setup => singlecommands}/SetupCommand.java | 2 +- .../{spectate => singlecommands}/SpectateCommand.java | 2 +- .../StatisticsCommand.java | 2 +- 11 files changed, 10 insertions(+), 20 deletions(-) rename core/src/main/java/dev/nandi0813/practice/command/{accept => singlecommands}/AcceptCommand.java (99%) rename core/src/main/java/dev/nandi0813/practice/command/{division => singlecommands}/DivisionsCommand.java (96%) rename core/src/main/java/dev/nandi0813/practice/command/{duel => singlecommands}/DuelCommand.java (98%) rename core/src/main/java/dev/nandi0813/practice/command/{hologram => singlecommands}/HologramCommand.java (99%) rename core/src/main/java/dev/nandi0813/practice/command/{matchstats => singlecommands}/MatchStatsCommand.java (97%) rename core/src/main/java/dev/nandi0813/practice/command/{preview => singlecommands}/PreviewCommand.java (98%) rename core/src/main/java/dev/nandi0813/practice/command/{settings => singlecommands}/SettingsCommand.java (96%) rename core/src/main/java/dev/nandi0813/practice/command/{setup => singlecommands}/SetupCommand.java (98%) rename core/src/main/java/dev/nandi0813/practice/command/{spectate => singlecommands}/SpectateCommand.java (98%) rename core/src/main/java/dev/nandi0813/practice/command/{statistics => singlecommands}/StatisticsCommand.java (98%) diff --git a/core/src/main/java/dev/nandi0813/practice/ZonePractice.java b/core/src/main/java/dev/nandi0813/practice/ZonePractice.java index dfdcd605..79c6866d 100644 --- a/core/src/main/java/dev/nandi0813/practice/ZonePractice.java +++ b/core/src/main/java/dev/nandi0813/practice/ZonePractice.java @@ -1,26 +1,16 @@ package dev.nandi0813.practice; import com.github.retrooper.packetevents.PacketEvents; -import dev.nandi0813.practice.command.accept.AcceptCommand; import dev.nandi0813.practice.command.arena.ArenaCommand; -import dev.nandi0813.practice.command.division.DivisionsCommand; -import dev.nandi0813.practice.command.duel.DuelCommand; import dev.nandi0813.practice.command.event.EventCommand; import dev.nandi0813.practice.command.ffa.FFACommand; -import dev.nandi0813.practice.command.hologram.HologramCommand; import dev.nandi0813.practice.command.ladder.LadderCommand; -import dev.nandi0813.practice.command.matchstats.MatchStatsCommand; import dev.nandi0813.practice.command.party.PartyCommand; import dev.nandi0813.practice.command.practice.PracticeCommand; -import dev.nandi0813.practice.command.preview.PreviewCommand; import dev.nandi0813.practice.command.privatemessage.MessageCommand; import dev.nandi0813.practice.command.privatemessage.ReplyCommand; -import dev.nandi0813.practice.command.settings.SettingsCommand; -import dev.nandi0813.practice.command.setup.SetupCommand; import dev.nandi0813.practice.command.singlecommands.*; -import dev.nandi0813.practice.command.spectate.SpectateCommand; import dev.nandi0813.practice.command.staff.StaffCommand; -import dev.nandi0813.practice.command.statistics.StatisticsCommand; import dev.nandi0813.practice.listener.*; import dev.nandi0813.practice.manager.arena.ArenaManager; import dev.nandi0813.practice.manager.arena.listener.ArenaCopyUtilListener; diff --git a/core/src/main/java/dev/nandi0813/practice/command/accept/AcceptCommand.java b/core/src/main/java/dev/nandi0813/practice/command/singlecommands/AcceptCommand.java similarity index 99% rename from core/src/main/java/dev/nandi0813/practice/command/accept/AcceptCommand.java rename to core/src/main/java/dev/nandi0813/practice/command/singlecommands/AcceptCommand.java index a180d1c6..d3d21265 100644 --- a/core/src/main/java/dev/nandi0813/practice/command/accept/AcceptCommand.java +++ b/core/src/main/java/dev/nandi0813/practice/command/singlecommands/AcceptCommand.java @@ -1,4 +1,4 @@ -package dev.nandi0813.practice.command.accept; +package dev.nandi0813.practice.command.singlecommands; import dev.nandi0813.practice.manager.backend.LanguageManager; import dev.nandi0813.practice.manager.duel.DuelManager; diff --git a/core/src/main/java/dev/nandi0813/practice/command/division/DivisionsCommand.java b/core/src/main/java/dev/nandi0813/practice/command/singlecommands/DivisionsCommand.java similarity index 96% rename from core/src/main/java/dev/nandi0813/practice/command/division/DivisionsCommand.java rename to core/src/main/java/dev/nandi0813/practice/command/singlecommands/DivisionsCommand.java index 9ac0c3e1..9c506be1 100644 --- a/core/src/main/java/dev/nandi0813/practice/command/division/DivisionsCommand.java +++ b/core/src/main/java/dev/nandi0813/practice/command/singlecommands/DivisionsCommand.java @@ -1,4 +1,4 @@ -package dev.nandi0813.practice.command.division; +package dev.nandi0813.practice.command.singlecommands; import dev.nandi0813.practice.manager.backend.LanguageManager; import dev.nandi0813.practice.manager.gui.guis.DivisionGui; diff --git a/core/src/main/java/dev/nandi0813/practice/command/duel/DuelCommand.java b/core/src/main/java/dev/nandi0813/practice/command/singlecommands/DuelCommand.java similarity index 98% rename from core/src/main/java/dev/nandi0813/practice/command/duel/DuelCommand.java rename to core/src/main/java/dev/nandi0813/practice/command/singlecommands/DuelCommand.java index a400edb9..6be14f15 100644 --- a/core/src/main/java/dev/nandi0813/practice/command/duel/DuelCommand.java +++ b/core/src/main/java/dev/nandi0813/practice/command/singlecommands/DuelCommand.java @@ -1,4 +1,4 @@ -package dev.nandi0813.practice.command.duel; +package dev.nandi0813.practice.command.singlecommands; import dev.nandi0813.practice.manager.backend.LanguageManager; import dev.nandi0813.practice.manager.duel.DuelManager; diff --git a/core/src/main/java/dev/nandi0813/practice/command/hologram/HologramCommand.java b/core/src/main/java/dev/nandi0813/practice/command/singlecommands/HologramCommand.java similarity index 99% rename from core/src/main/java/dev/nandi0813/practice/command/hologram/HologramCommand.java rename to core/src/main/java/dev/nandi0813/practice/command/singlecommands/HologramCommand.java index 9cda6ac9..72bebec2 100644 --- a/core/src/main/java/dev/nandi0813/practice/command/hologram/HologramCommand.java +++ b/core/src/main/java/dev/nandi0813/practice/command/singlecommands/HologramCommand.java @@ -1,4 +1,4 @@ -package dev.nandi0813.practice.command.hologram; +package dev.nandi0813.practice.command.singlecommands; import dev.nandi0813.practice.ZonePractice; import dev.nandi0813.practice.manager.backend.LanguageManager; diff --git a/core/src/main/java/dev/nandi0813/practice/command/matchstats/MatchStatsCommand.java b/core/src/main/java/dev/nandi0813/practice/command/singlecommands/MatchStatsCommand.java similarity index 97% rename from core/src/main/java/dev/nandi0813/practice/command/matchstats/MatchStatsCommand.java rename to core/src/main/java/dev/nandi0813/practice/command/singlecommands/MatchStatsCommand.java index 047743bb..38077d35 100644 --- a/core/src/main/java/dev/nandi0813/practice/command/matchstats/MatchStatsCommand.java +++ b/core/src/main/java/dev/nandi0813/practice/command/singlecommands/MatchStatsCommand.java @@ -1,4 +1,4 @@ -package dev.nandi0813.practice.command.matchstats; +package dev.nandi0813.practice.command.singlecommands; import dev.nandi0813.practice.manager.backend.LanguageManager; import dev.nandi0813.practice.manager.fight.match.Match; diff --git a/core/src/main/java/dev/nandi0813/practice/command/preview/PreviewCommand.java b/core/src/main/java/dev/nandi0813/practice/command/singlecommands/PreviewCommand.java similarity index 98% rename from core/src/main/java/dev/nandi0813/practice/command/preview/PreviewCommand.java rename to core/src/main/java/dev/nandi0813/practice/command/singlecommands/PreviewCommand.java index 4c70ec08..d5122017 100644 --- a/core/src/main/java/dev/nandi0813/practice/command/preview/PreviewCommand.java +++ b/core/src/main/java/dev/nandi0813/practice/command/singlecommands/PreviewCommand.java @@ -1,4 +1,4 @@ -package dev.nandi0813.practice.command.preview; +package dev.nandi0813.practice.command.singlecommands; import dev.nandi0813.practice.manager.backend.LanguageManager; import dev.nandi0813.practice.manager.ladder.LadderManager; diff --git a/core/src/main/java/dev/nandi0813/practice/command/settings/SettingsCommand.java b/core/src/main/java/dev/nandi0813/practice/command/singlecommands/SettingsCommand.java similarity index 96% rename from core/src/main/java/dev/nandi0813/practice/command/settings/SettingsCommand.java rename to core/src/main/java/dev/nandi0813/practice/command/singlecommands/SettingsCommand.java index f3bfa728..fc28553b 100644 --- a/core/src/main/java/dev/nandi0813/practice/command/settings/SettingsCommand.java +++ b/core/src/main/java/dev/nandi0813/practice/command/singlecommands/SettingsCommand.java @@ -1,4 +1,4 @@ -package dev.nandi0813.practice.command.settings; +package dev.nandi0813.practice.command.singlecommands; import dev.nandi0813.practice.manager.backend.LanguageManager; import dev.nandi0813.practice.manager.profile.Profile; diff --git a/core/src/main/java/dev/nandi0813/practice/command/setup/SetupCommand.java b/core/src/main/java/dev/nandi0813/practice/command/singlecommands/SetupCommand.java similarity index 98% rename from core/src/main/java/dev/nandi0813/practice/command/setup/SetupCommand.java rename to core/src/main/java/dev/nandi0813/practice/command/singlecommands/SetupCommand.java index 6f7e91ae..d5663169 100644 --- a/core/src/main/java/dev/nandi0813/practice/command/setup/SetupCommand.java +++ b/core/src/main/java/dev/nandi0813/practice/command/singlecommands/SetupCommand.java @@ -1,4 +1,4 @@ -package dev.nandi0813.practice.command.setup; +package dev.nandi0813.practice.command.singlecommands; import dev.nandi0813.practice.manager.backend.LanguageManager; import dev.nandi0813.practice.manager.gui.GUIManager; diff --git a/core/src/main/java/dev/nandi0813/practice/command/spectate/SpectateCommand.java b/core/src/main/java/dev/nandi0813/practice/command/singlecommands/SpectateCommand.java similarity index 98% rename from core/src/main/java/dev/nandi0813/practice/command/spectate/SpectateCommand.java rename to core/src/main/java/dev/nandi0813/practice/command/singlecommands/SpectateCommand.java index fa3fbd50..462c3363 100644 --- a/core/src/main/java/dev/nandi0813/practice/command/spectate/SpectateCommand.java +++ b/core/src/main/java/dev/nandi0813/practice/command/singlecommands/SpectateCommand.java @@ -1,4 +1,4 @@ -package dev.nandi0813.practice.command.spectate; +package dev.nandi0813.practice.command.singlecommands; import dev.nandi0813.practice.manager.backend.LanguageManager; import dev.nandi0813.practice.manager.fight.event.EventManager; diff --git a/core/src/main/java/dev/nandi0813/practice/command/statistics/StatisticsCommand.java b/core/src/main/java/dev/nandi0813/practice/command/singlecommands/StatisticsCommand.java similarity index 98% rename from core/src/main/java/dev/nandi0813/practice/command/statistics/StatisticsCommand.java rename to core/src/main/java/dev/nandi0813/practice/command/singlecommands/StatisticsCommand.java index d3d72cb2..882ab6a1 100644 --- a/core/src/main/java/dev/nandi0813/practice/command/statistics/StatisticsCommand.java +++ b/core/src/main/java/dev/nandi0813/practice/command/singlecommands/StatisticsCommand.java @@ -1,4 +1,4 @@ -package dev.nandi0813.practice.command.statistics; +package dev.nandi0813.practice.command.singlecommands; import dev.nandi0813.practice.manager.backend.LanguageManager; import dev.nandi0813.practice.manager.gui.guis.leaderboard.LbSelectorGui; From 8c1b84e3823305c5f7a86e59757808c237755195 Mon Sep 17 00:00:00 2001 From: Nandor Dukat Date: Wed, 18 Mar 2026 19:08:17 +0100 Subject: [PATCH 15/26] added protection rules to config, making lobby interactions toggleable --- .../command/singlecommands/DuelCommand.java | 9 ++- .../manager/inventory/InventoryListener.java | 79 ++++++++++++++----- core/src/main/resources/config.yml | 9 ++- 3 files changed, 75 insertions(+), 22 deletions(-) diff --git a/core/src/main/java/dev/nandi0813/practice/command/singlecommands/DuelCommand.java b/core/src/main/java/dev/nandi0813/practice/command/singlecommands/DuelCommand.java index 6be14f15..50ca212a 100644 --- a/core/src/main/java/dev/nandi0813/practice/command/singlecommands/DuelCommand.java +++ b/core/src/main/java/dev/nandi0813/practice/command/singlecommands/DuelCommand.java @@ -96,7 +96,14 @@ public List onTabComplete(CommandSender sender, Command command, String if (args.length == 1) { for (Player target : Bukkit.getOnlinePlayers()) { - if (player.equals(target)) continue; + if (player.equals(target)) { + continue; + } + + Profile targetProfile = ProfileManager.getInstance().getProfile(target); + if (!targetProfile.getStatus().equals(ProfileStatus.LOBBY) && !targetProfile.getStatus().equals(ProfileStatus.EDITOR) && !targetProfile.getStatus().equals(ProfileStatus.SPECTATE)) { + continue; + } arguments.add(target.getName()); } diff --git a/core/src/main/java/dev/nandi0813/practice/manager/inventory/InventoryListener.java b/core/src/main/java/dev/nandi0813/practice/manager/inventory/InventoryListener.java index 65ec561a..41cd5b05 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/inventory/InventoryListener.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/inventory/InventoryListener.java @@ -31,6 +31,8 @@ public class InventoryListener implements Listener { + private static final String LOBBY_PROTECTION_PATH = "PLAYER.LOBBY-PROTECTION."; + @EventHandler public void onPlayerInteractWithInvItem(PlayerInteractEvent e) { Player player = e.getPlayer(); @@ -165,15 +167,18 @@ public void onInventoryClick(InventoryClickEvent e) { Profile profile = ProfileManager.getInstance().getProfile(player); ProfileStatus profileStatus = profile.getStatus(); - if (!player.hasPermission("zpp.admin") && profileStatus.equals(ProfileStatus.LOBBY)) - e.setCancelled(true); - else { - switch (profileStatus) { - case QUEUE: - case STAFF_MODE: - e.setCancelled(true); - break; + if (isLobbyStatus(profileStatus)) { + if (!isLobbyProtectionAllowed("allow-inventory-interact") && !player.hasPermission("zpp.admin")) { + e.setCancelled(true); } + return; + } + + switch (profileStatus) { + case QUEUE: + case STAFF_MODE: + e.setCancelled(true); + break; } } @@ -185,6 +190,10 @@ public void onPlayerDropItem(PlayerDropItemEvent e) { switch (profileStatus) { case LOBBY: + if (!isLobbyProtectionAllowed("allow-item-drop")) { + e.setCancelled(!player.hasPermission("zpp.admin")); + } + break; case QUEUE: case STAFF_MODE: e.setCancelled(!player.hasPermission("zpp.admin")); @@ -207,17 +216,20 @@ public void onPlayerPickupItem(EntityPickupItemEvent e) { return; } - if (!player.hasPermission("zpp.admin") && profileStatus.equals(ProfileStatus.LOBBY)) - e.setCancelled(true); - else { - switch (profileStatus) { - case QUEUE: - case STAFF_MODE: - case CUSTOM_EDITOR: - case EDITOR: - e.setCancelled(true); - break; + if (isLobbyStatus(profileStatus)) { + if (!isLobbyProtectionAllowed("allow-item-pickup") && !player.hasPermission("zpp.admin")) { + e.setCancelled(true); } + return; + } + + switch (profileStatus) { + case QUEUE: + case STAFF_MODE: + case CUSTOM_EDITOR: + case EDITOR: + e.setCancelled(true); + break; } } @@ -228,8 +240,15 @@ public void onHunger(FoodLevelChangeEvent e) { Profile profile = ProfileManager.getInstance().getProfile(player); ProfileStatus profileStatus = profile.getStatus(); + if (isLobbyStatus(profileStatus)) { + if (!isLobbyProtectionAllowed("allow-hunger")) { + e.setCancelled(true); + e.setFoodLevel(20); + } + return; + } + switch (profileStatus) { - case LOBBY: case QUEUE: case STAFF_MODE: case CUSTOM_EDITOR: @@ -248,8 +267,20 @@ public void onEntityDamage(EntityDamageEvent e) { if (profile == null) return; ProfileStatus profileStatus = profile.getStatus(); + if (isLobbyStatus(profileStatus)) { + if (!isLobbyProtectionAllowed("allow-damage")) { + // Keep knockback from entity hits while preventing HP loss. + if (!isLobbyProtectionAllowed("allow-velocity") && e instanceof EntityDamageByEntityEvent) { + e.setDamage(0.0D); + return; + } + + e.setCancelled(true); + } + return; + } + switch (profileStatus) { - case LOBBY: case QUEUE: case STAFF_MODE: case CUSTOM_EDITOR: @@ -273,4 +304,12 @@ public void onItemSwitchHand(PlayerSwapHandItemsEvent e) { } } + private boolean isLobbyStatus(ProfileStatus profileStatus) { + return profileStatus == ProfileStatus.LOBBY; + } + + private boolean isLobbyProtectionAllowed(String setting) { + return ConfigManager.getConfig().getBoolean(LOBBY_PROTECTION_PATH + setting, false); + } + } diff --git a/core/src/main/resources/config.yml b/core/src/main/resources/config.yml index 557d3399..377dc8f4 100644 --- a/core/src/main/resources/config.yml +++ b/core/src/main/resources/config.yml @@ -1,4 +1,4 @@ -VERSION: 20 +VERSION: 21 # Mysql database setup. MYSQL-DATABASE: @@ -504,6 +504,13 @@ PLAYER: ENABLED: true DAYS: 30 # After this number of days, the inactive player profiles will be deleted. SETTINGS-DELAY: 3 # Sec between settings can be changed. It can be bypassed. + LOBBY-PROTECTION: + allow-velocity: false # player don't get damaged but they get knocked back + allow-damage: false + allow-inventory-interact: false + allow-item-drop: false + allow-item-pickup: false + allow-hunger: false LOBBY-NAMETAG: ENABLED: true NAMETAG-MANAGEMENT: From bc53924b9f5d5826677077f342b7396295c68889 Mon Sep 17 00:00:00 2001 From: Nandor Dukat Date: Wed, 18 Mar 2026 19:21:16 +0100 Subject: [PATCH 16/26] removed weightclass toggle when ladder doesn't support both --- .../customladder/premadecustom/CustomLadderEditorGui.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/customladder/premadecustom/CustomLadderEditorGui.java b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/customladder/premadecustom/CustomLadderEditorGui.java index 56ad4423..a67618d7 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/customladder/premadecustom/CustomLadderEditorGui.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/customladder/premadecustom/CustomLadderEditorGui.java @@ -243,9 +243,9 @@ public void open(Player player) { private @Nullable ItemStack getRankedItem() { switch (ladder.getWeightClass()) { case UNRANKED: - return GUIFile.getGuiItem("GUIS.KIT-EDITOR.KIT-EDITOR.ICONS.ONLY-UNRANKED").replace("%weightClass%", WeightClass.UNRANKED.getName()).get(); + return GUIManager.getFILLER_ITEM(); case RANKED: - return GUIFile.getGuiItem("GUIS.KIT-EDITOR.KIT-EDITOR.ICONS.ONLY-RANKED").replace("%weightClass%", WeightClass.RANKED.getName()).get(); + return GUIManager.getFILLER_ITEM(); case UNRANKED_AND_RANKED: if (this.ranked) return GUIFile.getGuiItem("GUIS.KIT-EDITOR.KIT-EDITOR.ICONS.SWITCH-TO-UNRANKED").replace("%weightClass%", WeightClass.UNRANKED.getName()).get(); From 91ce1d64bc24e7d3a0980a4386f13fd7b6eb72d7 Mon Sep 17 00:00:00 2001 From: MISHA <208148594+lokspel@users.noreply.github.com> Date: Thu, 19 Mar 2026 11:16:30 +0400 Subject: [PATCH 17/26] Fix lobby protection allow-velocity and more protection setting (#339) * Fix Lobby knockback * Add missing lobby protection * fix: prevent arrow hit message when damage event is cancelled --- .../manager/fight/ffa/FFAListener.java | 5 ++- .../match/listener/LadderTypeListener.java | 5 ++- .../manager/inventory/InventoryListener.java | 38 ++++++++++++++++--- core/src/main/resources/config.yml | 4 +- 4 files changed, 42 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/FFAListener.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/FFAListener.java index 55e9a65e..c9d97e57 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/FFAListener.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/FFAListener.java @@ -167,9 +167,10 @@ public void onPlayerQuit(PlayerQuitEvent e) { private static final boolean DISPLAY_ARROW_HIT = ConfigManager.getBoolean("FFA.DISPLAY-ARROW-HIT-HEALTH"); - protected static void arrowDisplayHearth(Player shooter, Player target, double finalDamage) { + protected static void arrowDisplayHearth(Player shooter, Player target, double finalDamage, EntityDamageByEntityEvent event) { if (!DISPLAY_ARROW_HIT) return; if (shooter == null || target == null) return; + if (event.isCancelled()) return; FFA ffa = FFAManager.getInstance().getFFAByPlayer(shooter); if (ffa == null) return; @@ -429,7 +430,7 @@ public void onEntityDamageByEntity(EntityDamageByEntityEvent e) { attacker = shooter; if (projectile instanceof Arrow) { - arrowDisplayHearth(shooter, target, e.getFinalDamage()); + arrowDisplayHearth(shooter, target, e.getFinalDamage(), e); } } } diff --git a/core/src/main/java/dev/nandi0813/practice/manager/fight/match/listener/LadderTypeListener.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/match/listener/LadderTypeListener.java index 6a83268f..7e34394c 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/fight/match/listener/LadderTypeListener.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/fight/match/listener/LadderTypeListener.java @@ -116,9 +116,10 @@ protected boolean delegateToLadderHandle(org.bukkit.event.Event event, Match mat // ========== EVENT HANDLERS ========== - protected static void arrowDisplayHearth(Player shooter, Player target, double finalDamage) { + protected static void arrowDisplayHearth(Player shooter, Player target, double finalDamage, EntityDamageByEntityEvent event) { if (!PermanentConfig.DISPLAY_ARROW_HIT) return; if (shooter == null || target == null) return; + if (event.isCancelled()) return; Match match = MatchManager.getInstance().getLiveMatchByPlayer(shooter); if (match == null) return; @@ -587,7 +588,7 @@ private static void onEntityDamageByEntity(EntityDamageByEntityEvent e) { attacker = (Player) projectile.getShooter(); if (projectile instanceof Arrow) { - arrowDisplayHearth(attacker, target, e.getFinalDamage()); + arrowDisplayHearth(attacker, target, e.getFinalDamage(), e); } } } diff --git a/core/src/main/java/dev/nandi0813/practice/manager/inventory/InventoryListener.java b/core/src/main/java/dev/nandi0813/practice/manager/inventory/InventoryListener.java index 41cd5b05..5995215d 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/inventory/InventoryListener.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/inventory/InventoryListener.java @@ -146,18 +146,46 @@ public void onPlayerAttackEntity(EntityDamageByEntityEvent e) { @EventHandler public void onBlockBreak(BlockBreakEvent e) { Player player = e.getPlayer(); + Profile profile = ProfileManager.getInstance().getProfile(player); + ProfileStatus profileStatus = profile.getStatus(); + + if (isLobbyStatus(profileStatus)) { + if (!isLobbyProtectionAllowed("allow-block-break") && !player.hasPermission("zpp.admin")) { + e.setCancelled(true); + } + return; + } - if (ServerManager.getInstance().getInWorld().containsKey(player) && ServerManager.getInstance().getInWorld().get(player).equals(WorldEnum.LOBBY)) { - e.setCancelled(!player.hasPermission("zpp.admin")); + switch (profileStatus) { + case QUEUE: + case STAFF_MODE: + case CUSTOM_EDITOR: + case EDITOR: + e.setCancelled(true); + break; } } @EventHandler public void onBlockPlace(BlockPlaceEvent e) { Player player = e.getPlayer(); + Profile profile = ProfileManager.getInstance().getProfile(player); + ProfileStatus profileStatus = profile.getStatus(); + + if (isLobbyStatus(profileStatus)) { + if (!isLobbyProtectionAllowed("allow-block-place") && !player.hasPermission("zpp.admin")) { + e.setCancelled(true); + } + return; + } - if (ServerManager.getInstance().getInWorld().containsKey(player) && ServerManager.getInstance().getInWorld().get(player).equals(WorldEnum.LOBBY)) { - e.setCancelled(!player.hasPermission("zpp.admin")); + switch (profileStatus) { + case QUEUE: + case STAFF_MODE: + case CUSTOM_EDITOR: + case EDITOR: + e.setCancelled(true); + break; } } @@ -270,7 +298,7 @@ public void onEntityDamage(EntityDamageEvent e) { if (isLobbyStatus(profileStatus)) { if (!isLobbyProtectionAllowed("allow-damage")) { // Keep knockback from entity hits while preventing HP loss. - if (!isLobbyProtectionAllowed("allow-velocity") && e instanceof EntityDamageByEntityEvent) { + if (isLobbyProtectionAllowed("allow-velocity") && e instanceof EntityDamageByEntityEvent) { e.setDamage(0.0D); return; } diff --git a/core/src/main/resources/config.yml b/core/src/main/resources/config.yml index 377dc8f4..2460e83c 100644 --- a/core/src/main/resources/config.yml +++ b/core/src/main/resources/config.yml @@ -1,4 +1,4 @@ -VERSION: 21 +VERSION: 22 # Mysql database setup. MYSQL-DATABASE: @@ -511,6 +511,8 @@ PLAYER: allow-item-drop: false allow-item-pickup: false allow-hunger: false + allow-block-break: false + allow-block-place: false LOBBY-NAMETAG: ENABLED: true NAMETAG-MANAGEMENT: From 7c228207ed8430afad9ab44432ac207a4d9bb8e1 Mon Sep 17 00:00:00 2001 From: Nandor Dukat Date: Thu, 19 Mar 2026 08:17:01 +0100 Subject: [PATCH 18/26] small refactor --- .../customladder/premadecustom/CustomLadderEditorGui.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/customladder/premadecustom/CustomLadderEditorGui.java b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/customladder/premadecustom/CustomLadderEditorGui.java index a67618d7..fa91f4d6 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/customladder/premadecustom/CustomLadderEditorGui.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/customladder/premadecustom/CustomLadderEditorGui.java @@ -239,12 +239,9 @@ public void open(Player player) { player.getInventory().setContents(ladder.getKitData().getStorage()); } - private @Nullable ItemStack getRankedItem() { switch (ladder.getWeightClass()) { - case UNRANKED: - return GUIManager.getFILLER_ITEM(); - case RANKED: + case UNRANKED, RANKED: return GUIManager.getFILLER_ITEM(); case UNRANKED_AND_RANKED: if (this.ranked) From e2e4b52d148b988a51aabcae18f2eed321475a5a Mon Sep 17 00:00:00 2001 From: Nandor Dukat Date: Thu, 19 Mar 2026 11:21:33 +0100 Subject: [PATCH 19/26] added death effects and shield customization added permission explanation of cosmetics to README.md added cosmetics icon to lobby inventory --- README.md | 73 ++++ .../dev/nandi0813/practice/ZonePractice.java | 4 +- .../singlecommands/CosmeticsCommand.java | 6 +- .../practice/listener/PlayerJoin.java | 2 +- .../practice/manager/fight/ffa/game/FFA.java | 29 ++ .../practice/manager/fight/match/Match.java | 27 ++ .../manager/fight/match/util/KitUtil.java | 37 +- .../fight/util/EntityHiderListener.java | 30 +- .../practice/manager/gui/GUIType.java | 10 +- .../gui/guis/cosmetics/CosmeticsHubGui.java | 156 ++++++++ .../{ => armortrim}/ArmorPieceHubGui.java | 12 +- .../ArmorTrimMainGui.java} | 32 +- .../{ => armortrim}/MaterialSelectionGui.java | 18 +- .../{ => armortrim}/PatternSelectionGui.java | 18 +- .../deatheffect/DeathEffectsGui.java | 202 +++++++++++ .../shield/ShieldColorPickerGui.java | 143 ++++++++ .../cosmetics/shield/ShieldCosmeticsGui.java | 33 ++ .../cosmetics/shield/ShieldCosmeticsUtil.java | 76 ++++ .../cosmetics/shield/ShieldEditorGui.java | 339 ++++++++++++++++++ .../cosmetics/shield/ShieldLayoutListGui.java | 286 +++++++++++++++ .../shield/ShieldPatternPickerGui.java | 245 +++++++++++++ .../inventory/inventories/LobbyInventory.java | 6 + .../lobbyitems/CosmeticsInvItem.java | 17 + .../practice/manager/profile/ProfileFile.java | 39 +- .../profile/cosmetics/CosmeticsData.java | 28 +- ...r.java => CosmeticsPermissionManager.java} | 100 +++++- .../cosmetics/{ => armortrim}/ArmorSlot.java | 2 +- .../{ => armortrim}/ArmorTrimTier.java | 4 +- .../CosmeticsPermissionSanitizer.java | 38 +- .../cosmetics/deatheffect/DeathEffect.java | 285 +++++++++++++++ .../cosmetics/shield/ShieldLayout.java | 124 +++++++ core/src/main/resources/guis.yml | 163 ++++++++- core/src/main/resources/inventories.yml | 18 +- core/src/main/resources/plugin.yml | 22 +- 34 files changed, 2525 insertions(+), 99 deletions(-) create mode 100644 core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/CosmeticsHubGui.java rename core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/{ => armortrim}/ArmorPieceHubGui.java (94%) rename core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/{CosmeticsGui.java => armortrim/ArmorTrimMainGui.java} (90%) rename core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/{ => armortrim}/MaterialSelectionGui.java (91%) rename core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/{ => armortrim}/PatternSelectionGui.java (89%) create mode 100644 core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/deatheffect/DeathEffectsGui.java create mode 100644 core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/shield/ShieldColorPickerGui.java create mode 100644 core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/shield/ShieldCosmeticsGui.java create mode 100644 core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/shield/ShieldCosmeticsUtil.java create mode 100644 core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/shield/ShieldEditorGui.java create mode 100644 core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/shield/ShieldLayoutListGui.java create mode 100644 core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/shield/ShieldPatternPickerGui.java create mode 100644 core/src/main/java/dev/nandi0813/practice/manager/inventory/inventoryitem/lobbyitems/CosmeticsInvItem.java rename core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/{ArmorTrimPermissionManager.java => CosmeticsPermissionManager.java} (60%) rename core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/{ => armortrim}/ArmorSlot.java (87%) rename core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/{ => armortrim}/ArmorTrimTier.java (96%) rename core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/{ => armortrim}/CosmeticsPermissionSanitizer.java (61%) create mode 100644 core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/deatheffect/DeathEffect.java create mode 100644 core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/shield/ShieldLayout.java diff --git a/README.md b/README.md index 3c4b3c14..168dcbdc 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,79 @@ Common commands include `/practice` (aliases: `/prac`, `/zonepractice`, `/zonepr Permissions follow the `zpp.*` namespace, such as `zpp.admin` (default: op), `zpp.practice.*`, `zpp.staffmode`, and many granular nodes. +### Cosmetics permissions + +Some cosmetics permissions are registered dynamically at startup by `CosmeticsPermissionManager`, so they are not fully listed in `plugin.yml`. + +#### Entry permission + +- `zpp.cosmetics.main` + - Required to run `/cosmetics` (`CosmeticsCommand`). + +#### Armor trim permissions + +- Tier access: + - `zpp.cosmetics.armortrim.base.leather` + - `zpp.cosmetics.armortrim.base.gold` + - `zpp.cosmetics.armortrim.base.iron` + - `zpp.cosmetics.armortrim.base.diamond` + - `zpp.cosmetics.armortrim.base.netherite` + - wildcard: `zpp.cosmetics.armortrim.base.*` +- Pattern access: + - `zpp.cosmetics.armortrim.pattern.` + - wildcard: `zpp.cosmetics.armortrim.pattern.*` +- Material access: + - `zpp.cosmetics.armortrim.material.` + - wildcard: `zpp.cosmetics.armortrim.material.*` + +`` values come from Mojang/Paper trim registries and are sanitized to lowercase alphanumeric/underscore (for example: `sentry`, `vex`, `amethyst`, `netherite`). + +#### Death effect permissions + +- Per effect: + - `zpp.cosmetics.deatheffect.none` + - `zpp.cosmetics.deatheffect.flame` + - `zpp.cosmetics.deatheffect.lightning` + - `zpp.cosmetics.deatheffect.firework` + - `zpp.cosmetics.deatheffect.explosion` + - `zpp.cosmetics.deatheffect.blood` + - `zpp.cosmetics.deatheffect.enchant` + - `zpp.cosmetics.deatheffect.ender` + - `zpp.cosmetics.deatheffect.hearts` + - `zpp.cosmetics.deatheffect.ice` +- Wildcard: + - `zpp.cosmetics.deatheffect.*` + +#### Shield layout permissions + +- Open/use shield cosmetics: + - `zpp.cosmetics.shield.use` + - wildcard: `zpp.cosmetics.shield.*` +- Layout count limits: + - `zpp.cosmetics.shield.layouts.1` ... `zpp.cosmetics.shield.layouts.21` + - wildcard: `zpp.cosmetics.shield.layouts.*` + - unlimited alias: `zpp.cosmetics.shield.layouts.unlimited` + +If none of the layout-count permissions are set, the code falls back to `1` max layout. + +### Groups and cosmetics permissions + +Player groups are configured in `core/src/main/resources/groups.yml` and selected by group permission: + +- `zpp.group.default` +- `zpp.group.premium` +- `zpp.group.supreme` +- `zpp.group.staff` +- `zpp.group.admin` + +The active group controls limits like custom kits and party capacity. You can also use your permission plugin (LuckPerms, etc.) to attach cosmetics permissions per group. + +Example bundle strategy: + +- `DEFAULT`: `zpp.cosmetics.main`, `zpp.cosmetics.armortrim.base.leather`, `zpp.cosmetics.deatheffect.none`, `zpp.cosmetics.shield.use`, `zpp.cosmetics.shield.layouts.1` +- `PREMIUM`: add `zpp.cosmetics.armortrim.base.gold`, selected trim/material nodes, `zpp.cosmetics.deatheffect.flame`, `zpp.cosmetics.shield.layouts.3` +- `SUPREME+`: grant broader wildcards (`zpp.cosmetics.armortrim.pattern.*`, `zpp.cosmetics.armortrim.material.*`, `zpp.cosmetics.deatheffect.*`, `zpp.cosmetics.shield.layouts.unlimited`) + ## Developer API ZonePractice Pro provides a comprehensive API for developers to interact with the core systems, retrieve player statistics, and listen to custom events. diff --git a/core/src/main/java/dev/nandi0813/practice/ZonePractice.java b/core/src/main/java/dev/nandi0813/practice/ZonePractice.java index 79c6866d..ab446651 100644 --- a/core/src/main/java/dev/nandi0813/practice/ZonePractice.java +++ b/core/src/main/java/dev/nandi0813/practice/ZonePractice.java @@ -37,7 +37,7 @@ import dev.nandi0813.practice.manager.nametag.NametagManager; import dev.nandi0813.practice.manager.playerkit.PlayerKitManager; import dev.nandi0813.practice.manager.profile.ProfileManager; -import dev.nandi0813.practice.manager.profile.cosmetics.ArmorTrimPermissionManager; +import dev.nandi0813.practice.manager.profile.cosmetics.CosmeticsPermissionManager; import dev.nandi0813.practice.manager.server.ServerManager; import dev.nandi0813.practice.manager.sidebar.SidebarManager; import dev.nandi0813.practice.util.*; @@ -109,7 +109,7 @@ public void onEnable() { DivisionManager.getInstance().getData(); ArenaWorldUtil.createArenaWorld(); BackendManager.createFile(this); - ArmorTrimPermissionManager.registerAllPermissions(); + CosmeticsPermissionManager.registerAllPermissions(); ZonePracticeApiImpl.setup(); StartUpUtil.loadStartUpProgressMap(); diff --git a/core/src/main/java/dev/nandi0813/practice/command/singlecommands/CosmeticsCommand.java b/core/src/main/java/dev/nandi0813/practice/command/singlecommands/CosmeticsCommand.java index e4671840..ce947281 100644 --- a/core/src/main/java/dev/nandi0813/practice/command/singlecommands/CosmeticsCommand.java +++ b/core/src/main/java/dev/nandi0813/practice/command/singlecommands/CosmeticsCommand.java @@ -1,6 +1,6 @@ package dev.nandi0813.practice.command.singlecommands; -import dev.nandi0813.practice.manager.gui.guis.cosmetics.CosmeticsGui; +import dev.nandi0813.practice.manager.gui.guis.cosmetics.CosmeticsHubGui; import dev.nandi0813.practice.manager.profile.Profile; import dev.nandi0813.practice.manager.profile.ProfileManager; import dev.nandi0813.practice.manager.profile.enums.ProfileStatus; @@ -39,8 +39,8 @@ public boolean onCommand(@NonNull CommandSender sender, @NonNull Command cmd, @N } // Open the main cosmetics GUI with no parent GUI (player can close it normally) - CosmeticsGui cosmeticsGui = new CosmeticsGui(profile, null); - cosmeticsGui.open(player); + CosmeticsHubGui cosmeticsHubGui = new CosmeticsHubGui(profile); + cosmeticsHubGui.open(player); return true; } diff --git a/core/src/main/java/dev/nandi0813/practice/listener/PlayerJoin.java b/core/src/main/java/dev/nandi0813/practice/listener/PlayerJoin.java index 3d8f22b5..48ce8267 100644 --- a/core/src/main/java/dev/nandi0813/practice/listener/PlayerJoin.java +++ b/core/src/main/java/dev/nandi0813/practice/listener/PlayerJoin.java @@ -6,7 +6,7 @@ import dev.nandi0813.practice.manager.nametag.NametagManager; import dev.nandi0813.practice.manager.profile.Profile; import dev.nandi0813.practice.manager.profile.ProfileManager; -import dev.nandi0813.practice.manager.profile.cosmetics.CosmeticsPermissionSanitizer; +import dev.nandi0813.practice.manager.profile.cosmetics.armortrim.CosmeticsPermissionSanitizer; import dev.nandi0813.practice.manager.profile.enums.ProfileStatus; import dev.nandi0813.practice.manager.sidebar.SidebarManager; import dev.nandi0813.practice.util.PermanentConfig; diff --git a/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/game/FFA.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/game/FFA.java index 63bd4f8d..7f77bf2a 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/game/FFA.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/game/FFA.java @@ -203,6 +203,8 @@ public void killPlayer(Player player, Player killer, String deathMessage) { if (killer != null) { fightPlayers.get(killer).getProfile().getStats().getLadderStat(players.get(killer)).increaseKills(); + playDeathEffect(killer, player); + if (arena.isReKitAfterKill()) { KitUtil.loadDefaultLadderKit(killer, TeamEnum.FFA, players.get(killer)); } @@ -223,6 +225,33 @@ public void killPlayer(Player player, Player killer, String deathMessage) { } } + private void playDeathEffect(Player killer, Player victim) { + if (killer == null || victim == null) { + return; + } + + try { + Profile killerProfile = fightPlayers.containsKey(killer) + ? fightPlayers.get(killer).getProfile() + : ProfileManager.getInstance().getProfile(killer); + + if (killerProfile == null || killerProfile.getCosmeticsData() == null) { + return; + } + + var deathEffect = killerProfile.getCosmeticsData().getDeathEffect(); + if (deathEffect == null) { + return; + } + + List viewers = new ArrayList<>(players.keySet()); + viewers.addAll(spectators); + deathEffect.play(victim.getLocation(), viewers); + } catch (Exception ignored) { + // Cosmetic effects should never break FFA kill handling. + } + } + private void applyHealthResetOnKill(Player killer) { AttributeInstance maxHealth = killer.getAttribute(Attribute.MAX_HEALTH); double maxHealthValue = maxHealth != null ? maxHealth.getValue() : 20.0D; diff --git a/core/src/main/java/dev/nandi0813/practice/manager/fight/match/Match.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/match/Match.java index c2b8225a..b10ae893 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/fight/match/Match.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/fight/match/Match.java @@ -238,9 +238,36 @@ public void killPlayer(Player player, Player killer, String deathMessage) { matchPlayers.get(player).getProfile().getStats().getLadderStat((NormalLadder) ladder).increaseDeaths(); } + playDeathEffect(killer, player); + killPlayer(player, deathMessage); } + private void playDeathEffect(Player killer, Player victim) { + if (killer == null || victim == null) { + return; + } + + try { + Profile killerProfile = matchPlayers.containsKey(killer) + ? matchPlayers.get(killer).getProfile() + : ProfileManager.getInstance().getProfile(killer); + + if (killerProfile == null || killerProfile.getCosmeticsData() == null) { + return; + } + + var deathEffect = killerProfile.getCosmeticsData().getDeathEffect(); + if (deathEffect == null) { + return; + } + + deathEffect.play(victim.getLocation(), getPeople()); + } catch (Exception ignored) { + // Cosmetic effects should never break combat flow. + } + } + protected abstract void killPlayer(Player player, String deathMessage); public abstract void removePlayer(Player player, boolean quit); diff --git a/core/src/main/java/dev/nandi0813/practice/manager/fight/match/util/KitUtil.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/match/util/KitUtil.java index d9582419..df234241 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/fight/match/util/KitUtil.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/fight/match/util/KitUtil.java @@ -2,13 +2,14 @@ import dev.nandi0813.practice.manager.fight.match.enums.TeamEnum; import dev.nandi0813.practice.manager.fight.util.PlayerUtil; +import dev.nandi0813.practice.manager.gui.guis.cosmetics.shield.ShieldCosmeticsUtil; import dev.nandi0813.practice.manager.ladder.abstraction.Ladder; import dev.nandi0813.practice.manager.ladder.util.LadderUtil; import dev.nandi0813.practice.manager.profile.Profile; import dev.nandi0813.practice.manager.profile.ProfileManager; -import dev.nandi0813.practice.manager.profile.cosmetics.ArmorSlot; -import dev.nandi0813.practice.manager.profile.cosmetics.ArmorTrimPermissionManager; -import dev.nandi0813.practice.manager.profile.cosmetics.ArmorTrimTier; +import dev.nandi0813.practice.manager.profile.cosmetics.CosmeticsPermissionManager; +import dev.nandi0813.practice.manager.profile.cosmetics.armortrim.ArmorSlot; +import dev.nandi0813.practice.manager.profile.cosmetics.armortrim.ArmorTrimTier; import dev.nandi0813.practice.util.KitData; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; @@ -74,6 +75,7 @@ public static void loadKit(Player player, TeamEnum team, ItemStack[] armor, Item } applyArmorTrimCosmetics(player); + applyShieldCosmetics(player); player.updateInventory(); } @@ -123,6 +125,27 @@ private static void applyArmorTrimCosmetics(Player player) { } } + private static void applyShieldCosmetics(Player player) { + try { + Profile profile = ProfileManager.getInstance().getProfile(player); + if (profile == null || profile.getCosmeticsData() == null) { + return; + } + + if (!CosmeticsPermissionManager.hasShieldPermission(player)) { + return; + } + + if (profile.getCosmeticsData().getActiveShieldLayout() == null) { + return; + } + + ShieldCosmeticsUtil.applyShieldToPlayer(player); + } catch (Exception e) { + // Silently fail - if shield cosmetics cannot be applied, continue with kit distribution + } + } + /** * Apply a trim pattern and material to an armor piece if both are set. */ @@ -138,8 +161,8 @@ private static void applyTrimToArmor(Player player, ItemStack item, } // Verify permission before applying - if (!player.hasPermission("zpp.cosmetics.pattern." + getTrimId(pattern)) || - !player.hasPermission("zpp.cosmetics.material." + getTrimId(material))) { + if (!player.hasPermission("zpp.cosmetics.armortrim.pattern." + getTrimId(pattern)) || + !player.hasPermission("zpp.cosmetics.armortrim.material." + getTrimId(material))) { return; } @@ -159,11 +182,11 @@ private static void applyTrimToArmor(Player player, ItemStack item, } private static String getTrimId(TrimPattern pattern) { - return ArmorTrimPermissionManager.getTrimId(pattern); + return CosmeticsPermissionManager.getTrimId(pattern); } private static String getTrimId(TrimMaterial material) { - return ArmorTrimPermissionManager.getTrimId(material); + return CosmeticsPermissionManager.getTrimId(material); } } diff --git a/core/src/main/java/dev/nandi0813/practice/manager/fight/util/EntityHiderListener.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/util/EntityHiderListener.java index 21406a39..344b1a5b 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/fight/util/EntityHiderListener.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/fight/util/EntityHiderListener.java @@ -41,6 +41,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; public class EntityHiderListener implements PacketListener, Listener { @@ -59,9 +60,33 @@ protected EntityHiderListener() { } protected final Set effectTo = new HashSet<>(); + private final ConcurrentHashMap allowedParticlePackets = new ConcurrentHashMap<>(); private final ConcurrentHashMap entityLocations = new ConcurrentHashMap<>(); + public void allowNextParticlePackets(Player player, int packetCount) { + if (player == null || packetCount <= 0) { + return; + } + + allowedParticlePackets + .computeIfAbsent(player.getUniqueId(), ignored -> new AtomicInteger()) + .addAndGet(packetCount); + } + + private boolean consumeAllowedParticlePacket(Player player) { + AtomicInteger allowance = allowedParticlePackets.get(player.getUniqueId()); + if (allowance == null) { + return false; + } + + int left = allowance.decrementAndGet(); + if (left <= 0) { + allowedParticlePackets.remove(player.getUniqueId()); + } + return true; + } + private boolean checkPlayer(Player player) { if (!ServerManager.getInstance().getInWorld().containsKey(player)) { return false; @@ -130,6 +155,7 @@ public void onPacketSend(PacketSendEvent e) { if (!this.checkPlayer(player)) { effectTo.remove(player); entityLocations.remove(player.getEntityId()); + allowedParticlePackets.remove(player.getUniqueId()); return; } @@ -152,7 +178,9 @@ public void onPacketSend(PacketSendEvent e) { effectTo.remove(player); } else if (e.getPacketType() == PacketType.Play.Server.PARTICLE) { - e.setCancelled(true); + if (!consumeAllowedParticlePacket(player)) { + e.setCancelled(true); + } } else if (e.getPacketType() == PacketType.Play.Server.SOUND_EFFECT) { WrapperPlayServerSoundEffect soundWrapper = new WrapperPlayServerSoundEffect(e); Vector3i pos = soundWrapper.getEffectPosition(); diff --git a/core/src/main/java/dev/nandi0813/practice/manager/gui/GUIType.java b/core/src/main/java/dev/nandi0813/practice/manager/gui/GUIType.java index e5ff60dc..ed606f29 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/gui/GUIType.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/gui/GUIType.java @@ -97,13 +97,21 @@ public enum GUIType { FFA_Ladder_Selector, // Cosmetics GUIs - Cosmetics_Main, + Cosmetics_Hub, + + ArmorTrimMainGui, Cosmetics_Helmet, Cosmetics_Chestplate, Cosmetics_Leggings, Cosmetics_Boots, Cosmetics_Shield, + Cosmetics_Shield_Layouts, + Cosmetics_Shield_Editor, + Cosmetics_Shield_ColorPicker, + Cosmetics_Shield_PatternPicker, Cosmetics_Pattern_Selection, Cosmetics_Material_Selection, + Cosmetics_DeathEffects, + } diff --git a/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/CosmeticsHubGui.java b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/CosmeticsHubGui.java new file mode 100644 index 00000000..efd5226d --- /dev/null +++ b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/CosmeticsHubGui.java @@ -0,0 +1,156 @@ +package dev.nandi0813.practice.manager.gui.guis.cosmetics; + +import dev.nandi0813.practice.manager.backend.GUIFile; +import dev.nandi0813.practice.manager.gui.GUI; +import dev.nandi0813.practice.manager.gui.GUIItem; +import dev.nandi0813.practice.manager.gui.GUIType; +import dev.nandi0813.practice.manager.gui.guis.cosmetics.armortrim.ArmorTrimMainGui; +import dev.nandi0813.practice.manager.gui.guis.cosmetics.deatheffect.DeathEffectsGui; +import dev.nandi0813.practice.manager.gui.guis.cosmetics.shield.ShieldLayoutListGui; +import dev.nandi0813.practice.manager.profile.Profile; +import dev.nandi0813.practice.manager.profile.cosmetics.deatheffect.DeathEffect; +import dev.nandi0813.practice.manager.profile.cosmetics.shield.ShieldLayout; +import dev.nandi0813.practice.util.InventoryUtil; +import dev.nandi0813.practice.util.ItemCreateUtil; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.List; + +/** + * Main cosmetics hub. Opens from the lobby hotbar item or /cosmetics command. + * Three navigation buttons lead to sub-GUIs: + * • Armor Trims (existing CosmeticsGui) + * • Shield (new ShieldCosmeticsGui) + * • Kill Effects (new KillEffectGui) + */ +public class CosmeticsHubGui extends GUI { + + private static final ItemStack FILLER_ITEM = GUIFile.getGuiItem("GENERAL-FILLER-ITEM").get(); + + private static final int ROWS = 3; + private static final int TRIMS_SLOT = 11; + private static final int SHIELD_SLOT = 13; + private static final int KILL_EFF_SLOT = 15; + + private final Profile profile; + + public CosmeticsHubGui(Profile profile) { + super(GUIType.Cosmetics_Hub); + this.profile = profile; + + String title = GUIFile.getConfig().getString( + "GUIS.COSMETICS.HUB.TITLE", "&8✦ Cosmetics"); + this.gui.put(1, InventoryUtil.createInventory(title, ROWS)); + build(); + } + + @Override public void build() { update(); } + + @Override + public void update() { + Inventory inv = gui.get(1); + inv.clear(); + for (int i = 0; i < inv.getSize(); i++) inv.setItem(i, FILLER_ITEM); + + inv.setItem(TRIMS_SLOT, buildTrimsButton()); + inv.setItem(SHIELD_SLOT, buildShieldButton()); + inv.setItem(KILL_EFF_SLOT, buildKillEffectButton()); + + updatePlayers(); + } + + @Override + public void handleClickEvent(InventoryClickEvent e) { + e.setCancelled(true); + Player player = (Player) e.getWhoClicked(); + int slot = e.getRawSlot(); + + switch (slot) { + case TRIMS_SLOT -> new ArmorTrimMainGui(profile, this).open(player); + case SHIELD_SLOT -> new ShieldLayoutListGui(profile, this).open(player); + case KILL_EFF_SLOT -> new DeathEffectsGui(profile, this).open(player); + } + } + + // ── Button builders ────────────────────────────────────────────── + + private ItemStack buildTrimsButton() { + String name = GUIFile.getConfig().getString( + "GUIS.COSMETICS.HUB.BUTTONS.ARMOR-TRIMS.NAME", "&6✦ Armor Trims"); + Material mat = safeMaterial( + GUIFile.getConfig().getString("GUIS.COSMETICS.HUB.BUTTONS.ARMOR-TRIMS.MATERIAL"), + Material.DIAMOND_CHESTPLATE); + List lore = getOrDefaultLore("GUIS.COSMETICS.HUB.BUTTONS.ARMOR-TRIMS.LORE", + List.of("&7Customize your armor tier,", "&7trim patterns and materials.")); + + GUIItem item = new GUIItem(name, mat, lore); + item.setGlowing(GUIFile.getConfig().getBoolean( + "GUIS.COSMETICS.HUB.BUTTONS.ARMOR-TRIMS.GLOW", true)); + return ItemCreateUtil.hideItemFlags(item.get()); + } + + private ItemStack buildShieldButton() { + String name = GUIFile.getConfig().getString( + "GUIS.COSMETICS.HUB.BUTTONS.SHIELD.NAME", "&9✦ Shield"); + Material mat = safeMaterial( + GUIFile.getConfig().getString("GUIS.COSMETICS.HUB.BUTTONS.SHIELD.MATERIAL"), + Material.SHIELD); + List lore = getOrDefaultLore("GUIS.COSMETICS.HUB.BUTTONS.SHIELD.LORE", + List.of("&7Design your shield with any", "&7color and pattern combination.", "&7Save multiple layouts.")); + + ShieldLayout active = profile.getCosmeticsData().getActiveShieldLayout(); + List finalLore = new ArrayList<>(lore); + finalLore.add(""); + finalLore.add("&7Active layout: &e" + (active != null ? active.getName() : "&cNone")); + finalLore.add("&7Saved layouts: &e" + + profile.getCosmeticsData().getShieldLayouts().size()); + + GUIItem item = new GUIItem(name, mat, finalLore); + item.setGlowing(GUIFile.getConfig().getBoolean( + "GUIS.COSMETICS.HUB.BUTTONS.SHIELD.GLOW", false)); + return item.get(); + } + + private ItemStack buildKillEffectButton() { + String name = GUIFile.getConfig().getString( + "GUIS.COSMETICS.HUB.BUTTONS.KILL-EFFECTS.NAME", "&c✦ Death Effects"); + Material mat = safeMaterial( + GUIFile.getConfig().getString("GUIS.COSMETICS.HUB.BUTTONS.KILL-EFFECTS.MATERIAL"), + Material.BLAZE_POWDER); + List lore = getOrDefaultLore("GUIS.COSMETICS.HUB.BUTTONS.KILL-EFFECTS.LORE", + List.of("&7Choose a particle effect", "&7that plays when you kill someone.")); + + DeathEffect active = profile.getCosmeticsData().getDeathEffect(); + List finalLore = new ArrayList<>(lore); + finalLore.add(""); + finalLore.add("&7Active effect: &e" + (active != null ? active.getDisplayName() : "&cNone")); + + GUIItem item = new GUIItem(name, mat, finalLore); + item.setGlowing(GUIFile.getConfig().getBoolean( + "GUIS.COSMETICS.HUB.BUTTONS.KILL-EFFECTS.GLOW", false)); + return item.get(); + } + + // ── Helpers ────────────────────────────────────────────────────── + + private List getOrDefaultLore(String key, List defaults) { + List lore = GUIFile.getConfig().getStringList(key); + return lore.isEmpty() ? defaults : lore; + } + + private static Material safeMaterial(String name, Material fallback) { + if (name == null || name.isBlank()) return fallback; + try { return Material.valueOf(name.toUpperCase()); } catch (Exception ignored) { return fallback; } + } + + private static String formatName(String raw) { + if (raw == null) return "None"; + String lower = raw.replace('_', ' ').toLowerCase(); + return Character.toUpperCase(lower.charAt(0)) + lower.substring(1); + } +} \ No newline at end of file diff --git a/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/ArmorPieceHubGui.java b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/armortrim/ArmorPieceHubGui.java similarity index 94% rename from core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/ArmorPieceHubGui.java rename to core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/armortrim/ArmorPieceHubGui.java index d8ffa052..69699503 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/ArmorPieceHubGui.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/armortrim/ArmorPieceHubGui.java @@ -1,13 +1,13 @@ -package dev.nandi0813.practice.manager.gui.guis.cosmetics; +package dev.nandi0813.practice.manager.gui.guis.cosmetics.armortrim; import dev.nandi0813.practice.manager.backend.GUIFile; import dev.nandi0813.practice.manager.gui.GUI; import dev.nandi0813.practice.manager.gui.GUIItem; import dev.nandi0813.practice.manager.gui.GUIType; import dev.nandi0813.practice.manager.profile.Profile; -import dev.nandi0813.practice.manager.profile.cosmetics.ArmorSlot; -import dev.nandi0813.practice.manager.profile.cosmetics.ArmorTrimPermissionManager; -import dev.nandi0813.practice.manager.profile.cosmetics.ArmorTrimTier; +import dev.nandi0813.practice.manager.profile.cosmetics.CosmeticsPermissionManager; +import dev.nandi0813.practice.manager.profile.cosmetics.armortrim.ArmorSlot; +import dev.nandi0813.practice.manager.profile.cosmetics.armortrim.ArmorTrimTier; import dev.nandi0813.practice.util.InventoryUtil; import org.bukkit.Material; import org.bukkit.entity.Player; @@ -132,7 +132,7 @@ private ItemStack buildNavigationItem(Material material, String name, String lor private ItemStack buildPatternSelectionItem(TrimPattern activePattern) { Material buttonMaterial = DEFAULT_PATTERN_BUTTON_MATERIAL; if (activePattern != null) { - String patternId = ArmorTrimPermissionManager.getTrimId(activePattern); + String patternId = CosmeticsPermissionManager.getTrimId(activePattern); Material activePatternMaterial = resolveMaterial(patternId.toUpperCase(Locale.ROOT) + "_ARMOR_TRIM_SMITHING_TEMPLATE"); if (activePatternMaterial != null) { buttonMaterial = activePatternMaterial; @@ -147,7 +147,7 @@ private ItemStack buildPatternSelectionItem(TrimPattern activePattern) { private ItemStack buildMaterialSelectionItem(TrimMaterial activeMaterial) { Material buttonMaterial = DEFAULT_MATERIAL_BUTTON_MATERIAL; if (activeMaterial != null) { - Material activeMaterialIcon = resolveTrimMaterialIcon(ArmorTrimPermissionManager.getTrimId(activeMaterial)); + Material activeMaterialIcon = resolveTrimMaterialIcon(CosmeticsPermissionManager.getTrimId(activeMaterial)); if (activeMaterialIcon != null) { buttonMaterial = activeMaterialIcon; } diff --git a/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/CosmeticsGui.java b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/armortrim/ArmorTrimMainGui.java similarity index 90% rename from core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/CosmeticsGui.java rename to core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/armortrim/ArmorTrimMainGui.java index 3e2c40c6..58298a4e 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/CosmeticsGui.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/armortrim/ArmorTrimMainGui.java @@ -1,13 +1,13 @@ -package dev.nandi0813.practice.manager.gui.guis.cosmetics; +package dev.nandi0813.practice.manager.gui.guis.cosmetics.armortrim; import dev.nandi0813.practice.manager.backend.GUIFile; import dev.nandi0813.practice.manager.gui.GUI; import dev.nandi0813.practice.manager.gui.GUIItem; import dev.nandi0813.practice.manager.gui.GUIType; import dev.nandi0813.practice.manager.profile.Profile; -import dev.nandi0813.practice.manager.profile.cosmetics.ArmorSlot; -import dev.nandi0813.practice.manager.profile.cosmetics.ArmorTrimPermissionManager; -import dev.nandi0813.practice.manager.profile.cosmetics.ArmorTrimTier; +import dev.nandi0813.practice.manager.profile.cosmetics.CosmeticsPermissionManager; +import dev.nandi0813.practice.manager.profile.cosmetics.armortrim.ArmorSlot; +import dev.nandi0813.practice.manager.profile.cosmetics.armortrim.ArmorTrimTier; import dev.nandi0813.practice.util.Common; import dev.nandi0813.practice.util.InventoryUtil; import dev.nandi0813.practice.util.StringUtil; @@ -28,10 +28,10 @@ /** * Main cosmetics GUI that displays armor pieces for the player to customize. */ -public class CosmeticsGui extends GUI { +public class ArmorTrimMainGui extends GUI { private static final int MAIN_ROWS = 5; - private static final int BACK_SLOT = 0; + private static final int BACK_SLOT = 36; private static final int HELMET_SLOT = 10; private static final int CHESTPLATE_SLOT = 12; private static final int LEGGINGS_SLOT = 14; @@ -49,8 +49,8 @@ public class CosmeticsGui extends GUI { private final Profile profile; private final GUI backToGui; - public CosmeticsGui(Profile profile, GUI backToGui) { - super(GUIType.Cosmetics_Main); + public ArmorTrimMainGui(Profile profile, GUI backToGui) { + super(GUIType.ArmorTrimMainGui); this.profile = profile; this.backToGui = backToGui; @@ -186,20 +186,20 @@ private ItemStack buildTierToggleItem(ArmorTrimTier activeTier) { guiItem.setMaterial(guiItem.getMaterial() == null ? Material.NETHER_STAR : guiItem.getMaterial()); Player player = profile.getPlayer().getPlayer(); - int totalPatternPermissions = ArmorTrimPermissionManager.getRegisteredPatterns().size(); - int totalMaterialPermissions = ArmorTrimPermissionManager.getRegisteredMaterials().size(); + int totalPatternPermissions = CosmeticsPermissionManager.getRegisteredPatterns().size(); + int totalMaterialPermissions = CosmeticsPermissionManager.getRegisteredMaterials().size(); int playerPatternPermissions = 0; int playerMaterialPermissions = 0; if (player != null) { - for (TrimPattern pattern : ArmorTrimPermissionManager.getRegisteredPatterns()) { - if (player.hasPermission("zpp.cosmetics.pattern." + getPermissionId(pattern))) { + for (TrimPattern pattern : CosmeticsPermissionManager.getRegisteredPatterns()) { + if (player.hasPermission("zpp.cosmetics.armortrim.pattern." + getPermissionId(pattern))) { playerPatternPermissions++; } } - for (TrimMaterial material : ArmorTrimPermissionManager.getRegisteredMaterials()) { - if (player.hasPermission("zpp.cosmetics.material." + getPermissionId(material))) { + for (TrimMaterial material : CosmeticsPermissionManager.getRegisteredMaterials()) { + if (player.hasPermission("zpp.cosmetics.armortrim.material." + getPermissionId(material))) { playerMaterialPermissions++; } } @@ -269,11 +269,11 @@ private static void applyTrimPreview(ItemStack item, TrimPattern pattern, TrimMa private static String getPermissionId(Object trimValue) { if (trimValue instanceof TrimPattern trimPattern) { - return ArmorTrimPermissionManager.getTrimId(trimPattern); + return CosmeticsPermissionManager.getTrimId(trimPattern); } if (trimValue instanceof TrimMaterial trimMaterial) { - return ArmorTrimPermissionManager.getTrimId(trimMaterial); + return CosmeticsPermissionManager.getTrimId(trimMaterial); } return "unknown"; diff --git a/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/MaterialSelectionGui.java b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/armortrim/MaterialSelectionGui.java similarity index 91% rename from core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/MaterialSelectionGui.java rename to core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/armortrim/MaterialSelectionGui.java index 4d7fee35..dd303cc6 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/MaterialSelectionGui.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/armortrim/MaterialSelectionGui.java @@ -1,13 +1,13 @@ -package dev.nandi0813.practice.manager.gui.guis.cosmetics; +package dev.nandi0813.practice.manager.gui.guis.cosmetics.armortrim; import dev.nandi0813.practice.manager.backend.GUIFile; import dev.nandi0813.practice.manager.gui.GUI; import dev.nandi0813.practice.manager.gui.GUIItem; import dev.nandi0813.practice.manager.gui.GUIType; import dev.nandi0813.practice.manager.profile.Profile; -import dev.nandi0813.practice.manager.profile.cosmetics.ArmorSlot; -import dev.nandi0813.practice.manager.profile.cosmetics.ArmorTrimPermissionManager; -import dev.nandi0813.practice.manager.profile.cosmetics.ArmorTrimTier; +import dev.nandi0813.practice.manager.profile.cosmetics.CosmeticsPermissionManager; +import dev.nandi0813.practice.manager.profile.cosmetics.armortrim.ArmorSlot; +import dev.nandi0813.practice.manager.profile.cosmetics.armortrim.ArmorTrimTier; import dev.nandi0813.practice.util.Common; import dev.nandi0813.practice.util.InventoryUtil; import dev.nandi0813.practice.util.StringUtil; @@ -67,7 +67,7 @@ public void update() { ArmorTrimTier tier = profile.getCosmeticsData().getActiveTier(); int slot = START_SLOT; - for (TrimMaterial material : ArmorTrimPermissionManager.getRegisteredMaterials()) { + for (TrimMaterial material : CosmeticsPermissionManager.getRegisteredMaterials()) { if (slot >= inventory.getSize()) { break; } @@ -113,7 +113,7 @@ public void handleClickEvent(InventoryClickEvent e) { return; } - String permissionNode = "zpp.cosmetics.material." + ArmorTrimPermissionManager.getTrimId(material); + String permissionNode = "zpp.cosmetics.armortrim.material." + CosmeticsPermissionManager.getTrimId(material); if (!player.hasPermission(permissionNode)) { String permDeniedMessage = GUIFile.getConfig().getString("GUIS.COSMETICS.MATERIAL-PERMISSION-DENIED-MESSAGE", "You do not have permission to use this trim material."); Common.sendMMMessage(player, permDeniedMessage); @@ -126,13 +126,13 @@ public void handleClickEvent(InventoryClickEvent e) { } private ItemStack buildMaterialItem(Player player, ArmorTrimTier tier, TrimMaterial material) { - String materialId = ArmorTrimPermissionManager.getTrimId(material); - String permissionNode = "zpp.cosmetics.material." + materialId; + String materialId = CosmeticsPermissionManager.getTrimId(material); + String permissionNode = "zpp.cosmetics.armortrim.material." + materialId; boolean hasPermission = player != null && player.hasPermission(permissionNode); TrimMaterial activeMaterial = profile.getCosmeticsData().getMaterial(tier, armorSlot); boolean active = activeMaterial != null - && ArmorTrimPermissionManager.getTrimId(activeMaterial).equalsIgnoreCase(materialId); + && CosmeticsPermissionManager.getTrimId(activeMaterial).equalsIgnoreCase(materialId); GUIItem item = new GUIItem(resolveIcon(materialId)); diff --git a/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/PatternSelectionGui.java b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/armortrim/PatternSelectionGui.java similarity index 89% rename from core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/PatternSelectionGui.java rename to core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/armortrim/PatternSelectionGui.java index 30577172..a3a1ec22 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/PatternSelectionGui.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/armortrim/PatternSelectionGui.java @@ -1,13 +1,13 @@ -package dev.nandi0813.practice.manager.gui.guis.cosmetics; +package dev.nandi0813.practice.manager.gui.guis.cosmetics.armortrim; import dev.nandi0813.practice.manager.backend.GUIFile; import dev.nandi0813.practice.manager.gui.GUI; import dev.nandi0813.practice.manager.gui.GUIItem; import dev.nandi0813.practice.manager.gui.GUIType; import dev.nandi0813.practice.manager.profile.Profile; -import dev.nandi0813.practice.manager.profile.cosmetics.ArmorSlot; -import dev.nandi0813.practice.manager.profile.cosmetics.ArmorTrimPermissionManager; -import dev.nandi0813.practice.manager.profile.cosmetics.ArmorTrimTier; +import dev.nandi0813.practice.manager.profile.cosmetics.CosmeticsPermissionManager; +import dev.nandi0813.practice.manager.profile.cosmetics.armortrim.ArmorSlot; +import dev.nandi0813.practice.manager.profile.cosmetics.armortrim.ArmorTrimTier; import dev.nandi0813.practice.util.Common; import dev.nandi0813.practice.util.InventoryUtil; import dev.nandi0813.practice.util.StringUtil; @@ -64,7 +64,7 @@ public void update() { ArmorTrimTier tier = profile.getCosmeticsData().getActiveTier(); int slot = START_SLOT; - for (TrimPattern pattern : ArmorTrimPermissionManager.getRegisteredPatterns()) { + for (TrimPattern pattern : CosmeticsPermissionManager.getRegisteredPatterns()) { if (slot >= inventory.getSize()) { break; } @@ -110,7 +110,7 @@ public void handleClickEvent(InventoryClickEvent e) { return; } - String permissionNode = "zpp.cosmetics.pattern." + ArmorTrimPermissionManager.getTrimId(pattern); + String permissionNode = "zpp.cosmetics.armortrim.pattern." + CosmeticsPermissionManager.getTrimId(pattern); if (!player.hasPermission(permissionNode)) { String permDeniedMessage = GUIFile.getConfig().getString("GUIS.COSMETICS.PATTERN-PERMISSION-DENIED-MESSAGE", "You do not have permission to use this trim pattern."); Common.sendMMMessage(player, permDeniedMessage); @@ -123,13 +123,13 @@ public void handleClickEvent(InventoryClickEvent e) { } private ItemStack buildPatternItem(Player player, ArmorTrimTier tier, TrimPattern pattern) { - String patternId = ArmorTrimPermissionManager.getTrimId(pattern); - String permissionNode = "zpp.cosmetics.pattern." + patternId; + String patternId = CosmeticsPermissionManager.getTrimId(pattern); + String permissionNode = "zpp.cosmetics.armortrim.pattern." + patternId; boolean hasPermission = player != null && player.hasPermission(permissionNode); TrimPattern activePattern = profile.getCosmeticsData().getPattern(tier, armorSlot); boolean active = activePattern != null - && ArmorTrimPermissionManager.getTrimId(activePattern).equalsIgnoreCase(patternId); + && CosmeticsPermissionManager.getTrimId(activePattern).equalsIgnoreCase(patternId); Material templateMaterial = resolveMaterial(patternId.toUpperCase(Locale.ROOT) + "_ARMOR_TRIM_SMITHING_TEMPLATE"); if (templateMaterial == null) { diff --git a/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/deatheffect/DeathEffectsGui.java b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/deatheffect/DeathEffectsGui.java new file mode 100644 index 00000000..da658cb4 --- /dev/null +++ b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/deatheffect/DeathEffectsGui.java @@ -0,0 +1,202 @@ +package dev.nandi0813.practice.manager.gui.guis.cosmetics.deatheffect; + +import dev.nandi0813.practice.manager.backend.GUIFile; +import dev.nandi0813.practice.manager.gui.GUI; +import dev.nandi0813.practice.manager.gui.GUIItem; +import dev.nandi0813.practice.manager.gui.GUIType; +import dev.nandi0813.practice.manager.profile.Profile; +import dev.nandi0813.practice.manager.profile.cosmetics.CosmeticsPermissionManager; +import dev.nandi0813.practice.manager.profile.cosmetics.deatheffect.DeathEffect; +import dev.nandi0813.practice.util.Common; +import dev.nandi0813.practice.util.InventoryUtil; +import dev.nandi0813.practice.util.ItemCreateUtil; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.ArrayList; +import java.util.List; + +/** + * GUI for selecting a kill effect cosmetic. + * Opens via the cosmetics hub → Kill Effects section. + */ +public class DeathEffectsGui extends GUI { + + private static final ItemStack FILLER_ITEM = GUIFile.getGuiItem("GENERAL-FILLER-ITEM").get(); + private static final ItemStack BACK_ITEM = GUIFile.getGuiItem("GUIS.COSMETICS.ICONS.BACK-TO").get(); + + /* layout: 5 rows, last row = back + filler */ + private static final int ROWS = 4; + private static final int BACK_SLOT = 27; + + /* effect slots — 3 rows of 7 centered (cols 1-7 of rows 1-3) */ + private static final int[] EFFECT_SLOTS = {10, 11, 12, 13, 14, 15, 16, + 19, 20, 21, 22, 23, 24, 25}; + + /* border around effect area: top row, bottom row, and left/right sides (rows 1-4) */ + private static final int[] FRAME_SLOTS = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, + 9, 17, + 18, 26, + 27, 28, 29, 30, 31, 32, 33, 34, 35 + }; + + private final Profile profile; + private final GUI backToGui; + + public DeathEffectsGui(Profile profile, GUI backToGui) { + super(GUIType.Cosmetics_DeathEffects); + this.profile = profile; + this.backToGui = backToGui; + + String title = GUIFile.getConfig().getString( + "GUIS.COSMETICS.DEATH-EFFECTS.TITLE", "&8Death Effects"); + this.gui.put(1, InventoryUtil.createInventory(title, ROWS)); + build(); + } + + @Override public void build() { update(); } + + @Override + public void update() { + Inventory inv = gui.get(1); + inv.clear(); + + for (int frameSlot : FRAME_SLOTS) { + inv.setItem(frameSlot, FILLER_ITEM); + } + + DeathEffect active = profile.getCosmeticsData().getDeathEffect(); + DeathEffect[] effects = DeathEffect.values(); + Player profilePlayer = profile.getPlayer().getPlayer(); + + for (int i = 0; i < EFFECT_SLOTS.length && i < effects.length; i++) { + DeathEffect deathEffect = effects[i]; + inv.setItem(EFFECT_SLOTS[i], buildEffectItem(deathEffect, active, profilePlayer)); + } + + inv.setItem(BACK_SLOT, BACK_ITEM != null ? BACK_ITEM : new ItemStack(Material.ARROW)); + updatePlayers(); + } + + @Override + public void handleClickEvent(InventoryClickEvent e) { + e.setCancelled(true); + Player player = (Player) e.getWhoClicked(); + int slot = e.getRawSlot(); + + if (slot == BACK_SLOT) { + backToGui.update(true); + backToGui.open(player); + return; + } + + // Identify which effect was clicked + for (int i = 0; i < EFFECT_SLOTS.length && i < DeathEffect.values().length; i++) { + if (slot == EFFECT_SLOTS[i]) { + DeathEffect deathEffect = DeathEffect.values()[i]; + handleEffectClick(player, deathEffect); + return; + } + } + } + + private void handleEffectClick(Player player, DeathEffect deathEffect) { + // Permission check + if (deathEffect != DeathEffect.NONE && !player.isOp() + && !player.hasPermission("zpp.cosmetics.killeffect.*") + && !CosmeticsPermissionManager.hasDeathEffectPermission(player, deathEffect)) { + String msg = GUIFile.getConfig().getString( + "GUIS.COSMETICS.DEATH-EFFECTS.NO-PERMISSION-MESSAGE", + "You don't have permission for this kill effect!"); + Common.sendMMMessage(player, msg); + return; + } + + // Toggle off if already selected + if (profile.getCosmeticsData().getDeathEffect() == deathEffect) { + profile.getCosmeticsData().setDeathEffect(DeathEffect.NONE); + } else { + profile.getCosmeticsData().setDeathEffect(deathEffect); + + try { + deathEffect.play(player.getLocation(), List.of(player)); + } catch (Exception ignored) {} + } + + profile.saveData(); + update(true); + } + + private ItemStack buildEffectItem(DeathEffect deathEffect, DeathEffect active, Player player) { + Material mat = deathEffect.getConfiguredIcon(); + GUIItem item = new GUIItem(mat); + + boolean isActive = (deathEffect == active); + boolean hasPerms = player == null || player.isOp() + || player.hasPermission("zpp.cosmetics.killeffect.*") + || CosmeticsPermissionManager.hasDeathEffectPermission(player, deathEffect) + || deathEffect == DeathEffect.NONE; + + String nameTemplate = GUIFile.getConfig().getString( + "GUIS.COSMETICS.DEATH-EFFECTS.ENTRIES." + deathEffect.name() + ".DISPLAY-NAME", + deathEffect.getDefaultDisplayName()); + + String statusPrefix; + if (isActive) { + statusPrefix = "&a&lSelected &8| &f"; + } else if (hasPerms) { + statusPrefix = "&e&lUnlocked &8| &f"; + } else { + statusPrefix = "&c&lLocked &8| &f"; + } + + item.setName(statusPrefix + nameTemplate); + + List lore = new ArrayList<>(); + List loreTemplate = GUIFile.getConfig().getStringList( + "GUIS.COSMETICS.DEATH-EFFECTS.ENTRIES." + deathEffect.name() + ".LORE"); + if (loreTemplate.isEmpty()) { + loreTemplate = GUIFile.getConfig().getStringList( + "GUIS.COSMETICS.DEATH-EFFECTS.DEFAULT-LORE"); + } + for (String line : loreTemplate) { + lore.add(line.replace("%status%", isActive ? "&aSelected" : (hasPerms ? "&eUnlocked" : "&cLocked"))); + } + + lore.add(0, "&8Status: " + (isActive ? "&aSelected" : (hasPerms ? "&eUnlocked" : "&cLocked"))); + lore.add(1, ""); + + if (isActive) { + lore.add(""); + lore.add(GUIFile.getConfig().getString( + "GUIS.COSMETICS.DEATH-EFFECTS.CLICK-TO-DESELECT", "&7Click to deselect this effect.")); + } else if (hasPerms) { + lore.add(""); + lore.add(GUIFile.getConfig().getString( + "GUIS.COSMETICS.DEATH-EFFECTS.CLICK-TO-SELECT", "&eClick to select this effect.")); + lore.add("&7A preview will play for you."); + } else { + lore.add(""); + lore.add("&cYou do not have permission for this effect."); + } + + item.setLore(lore); + item.setGlowing(isActive); + + ItemStack stack = item.get(); + + if (isActive && stack.hasItemMeta()) { + ItemMeta meta = stack.getItemMeta(); + meta.addItemFlags(org.bukkit.inventory.ItemFlag.HIDE_ENCHANTS, + org.bukkit.inventory.ItemFlag.HIDE_ATTRIBUTES); + stack.setItemMeta(meta); + } + + return ItemCreateUtil.hideItemFlags(stack); + } +} diff --git a/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/shield/ShieldColorPickerGui.java b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/shield/ShieldColorPickerGui.java new file mode 100644 index 00000000..bfcd99c2 --- /dev/null +++ b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/shield/ShieldColorPickerGui.java @@ -0,0 +1,143 @@ +package dev.nandi0813.practice.manager.gui.guis.cosmetics.shield; + +import dev.nandi0813.practice.manager.backend.GUIFile; +import dev.nandi0813.practice.manager.gui.GUI; +import dev.nandi0813.practice.manager.gui.GUIItem; +import dev.nandi0813.practice.manager.gui.GUIType; +import dev.nandi0813.practice.manager.profile.Profile; +import dev.nandi0813.practice.manager.profile.cosmetics.shield.ShieldLayout; +import dev.nandi0813.practice.util.InventoryUtil; +import org.bukkit.DyeColor; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.List; + +/** + * Color picker for either: + * - Base color of a shield layout (layerIndex == -1) + * - Color of a specific pattern layer (layerIndex >= 0, can be a new layer being added) + * After picking a color → if base color: save and return to editor. + * if layer: open ShieldPatternPickerGui with the chosen color. + */ +public class ShieldColorPickerGui extends GUI { + + private static final ItemStack FILLER = GUIFile.getGuiItem("GENERAL-FILLER-ITEM").get(); + private static final ItemStack BACK = GUIFile.getGuiItem("GUIS.COSMETICS.ICONS.BACK-TO").get(); + + private static final int ROWS = 5; + private static final int BACK_SLOT = 36; + + /* 16 colors in a 4×4 block, rows 1-2, centred */ + private static final int[] COLOR_SLOTS = { + 10, 11, 12, 13, 14, 15, 16, + 19, 20, 21, 22, 23, 24, 25, + 28, 29 + }; + private static final DyeColor[] DYE_COLORS = DyeColor.values(); // exactly 16 + + private final Profile profile; + private final int layoutIndex; + /** Pre-selected color when editing an existing layer. Null when creating new. */ + private final DyeColor preselected; + /** -1 = editing base color. >=0 = editing/adding this layer index. */ + private final int layerIndex; + private final GUI backToGui; + + public ShieldColorPickerGui(Profile profile, int layoutIndex, + DyeColor preselected, int layerIndex, GUI backToGui) { + super(GUIType.Cosmetics_Shield_ColorPicker); + this.profile = profile; + this.layoutIndex = layoutIndex; + this.preselected = preselected; + this.layerIndex = layerIndex; + this.backToGui = backToGui; + + boolean isBase = (layerIndex == -1); + String title = isBase + ? GUIFile.getConfig().getString("GUIS.COSMETICS.SHIELD.COLOR-PICKER.BASE-TITLE", "&8Pick Base Color") + : GUIFile.getConfig().getString("GUIS.COSMETICS.SHIELD.COLOR-PICKER.LAYER-TITLE", "&8Pick Layer Color"); + this.gui.put(1, InventoryUtil.createInventory(title, ROWS)); + build(); + } + + @Override public void build() { update(); } + + @Override + public void update() { + Inventory inv = gui.get(1); + inv.clear(); + for (int i = 0; i < inv.getSize(); i++) inv.setItem(i, FILLER); + + for (int i = 0; i < COLOR_SLOTS.length && i < DYE_COLORS.length; i++) { + inv.setItem(COLOR_SLOTS[i], buildColorItem(DYE_COLORS[i])); + } + + inv.setItem(BACK_SLOT, BACK != null ? BACK : new ItemStack(Material.ARROW)); + updatePlayers(); + } + + @Override + public void handleClickEvent(InventoryClickEvent e) { + e.setCancelled(true); + Player player = (Player) e.getWhoClicked(); + int slot = e.getRawSlot(); + + if (slot == BACK_SLOT) { backToGui.update(true); backToGui.open(player); return; } + + for (int i = 0; i < COLOR_SLOTS.length && i < DYE_COLORS.length; i++) { + if (slot == COLOR_SLOTS[i]) { + handleColorPick(player, DYE_COLORS[i]); + return; + } + } + } + + private void handleColorPick(Player player, DyeColor color) { + ShieldLayout layout = getLayout(); + if (layout == null) return; + + if (layerIndex == -1) { + // Base color + layout.setBaseColor(color); + profile.saveData(); + if (profile.getCosmeticsData().getActiveShieldLayoutIndex() == layoutIndex) { + ShieldCosmeticsUtil.applyShieldToPlayer(player); + } + backToGui.update(true); + backToGui.open(player); + } else { + // Layer color picked — now pick pattern + new ShieldPatternPickerGui(profile, layoutIndex, color, layerIndex, backToGui).open(player); + } + } + + private ItemStack buildColorItem(DyeColor color) { + Material wool = ShieldEditorGui.dyeToWool(color); + GUIItem item = new GUIItem(wool); + boolean active = (color == preselected); + + String prefix = active ? "&a✔ " : "&f"; + item.setName(prefix + fmt(color.name())); + List lore = new ArrayList<>(); + lore.add(active ? "&7Currently selected." : "&eClick to select."); + item.setLore(lore); + if (active) item.setGlowing(true); + return item.get(); + } + + private ShieldLayout getLayout() { + List layouts = profile.getCosmeticsData().getShieldLayouts(); + if (layoutIndex < 0 || layoutIndex >= layouts.size()) return null; + return layouts.get(layoutIndex); + } + + private static String fmt(String raw) { + String lower = raw.replace('_', ' ').toLowerCase(); + return Character.toUpperCase(lower.charAt(0)) + lower.substring(1); + } +} diff --git a/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/shield/ShieldCosmeticsGui.java b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/shield/ShieldCosmeticsGui.java new file mode 100644 index 00000000..db0e570d --- /dev/null +++ b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/shield/ShieldCosmeticsGui.java @@ -0,0 +1,33 @@ +package dev.nandi0813.practice.manager.gui.guis.cosmetics.shield; + +import dev.nandi0813.practice.manager.gui.GUI; +import dev.nandi0813.practice.manager.gui.GUIType; +import dev.nandi0813.practice.manager.profile.Profile; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; + +/** + * Entry point for the shield cosmetics flow. + * Immediately delegates to ShieldLayoutListGui. + * Kept as a named class so CosmeticsHubGui can still reference "Shield" by this name. + */ +public class ShieldCosmeticsGui extends GUI { + + private final ShieldLayoutListGui delegate; + + public ShieldCosmeticsGui(Profile profile, GUI backToGui) { + super(GUIType.Cosmetics_Shield); + this.delegate = new ShieldLayoutListGui(profile, backToGui); + // Share the delegate's inventory map so open() works correctly + this.gui.putAll(delegate.getGui()); + } + + @Override public void build() { delegate.build(); } + @Override public void update() { delegate.update(); } + @Override public void handleClickEvent(InventoryClickEvent e) { delegate.handleClickEvent(e); } + + /** Static helper kept for backwards compatibility (PlayerJoin calls this). */ + public static void applyShieldToPlayer(Player player) { + ShieldCosmeticsUtil.applyShieldToPlayer(player); + } +} diff --git a/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/shield/ShieldCosmeticsUtil.java b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/shield/ShieldCosmeticsUtil.java new file mode 100644 index 00000000..cb49b1fc --- /dev/null +++ b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/shield/ShieldCosmeticsUtil.java @@ -0,0 +1,76 @@ +package dev.nandi0813.practice.manager.gui.guis.cosmetics.shield; + +import dev.nandi0813.practice.manager.profile.Profile; +import dev.nandi0813.practice.manager.profile.ProfileManager; +import dev.nandi0813.practice.manager.profile.cosmetics.shield.ShieldLayout; +import org.bukkit.DyeColor; +import org.bukkit.Material; +import org.bukkit.block.Banner; +import org.bukkit.block.BlockState; +import org.bukkit.block.banner.Pattern; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.BlockStateMeta; + +import java.util.ArrayList; + +/** + * Static utility for applying shield layout designs to ItemStacks and player inventories. + */ +public final class ShieldCosmeticsUtil { + + private ShieldCosmeticsUtil() {} + + /** + * Applies the active layout of the player's profile to every SHIELD item in their inventory. + * Called on join and whenever the active layout changes. + */ + public static void applyShieldToPlayer(Player player) { + if (player == null) return; + Profile profile = ProfileManager.getInstance().getProfile(player); + if (profile == null || profile.getCosmeticsData() == null) return; + + ShieldLayout active = profile.getCosmeticsData().getActiveShieldLayout(); + + for (ItemStack item : player.getInventory().getContents()) { + if (item == null || item.getType() != Material.SHIELD) continue; + if (active != null) { + applyLayoutToItem(item, active); + } + // No active layout → leave the shield completely untouched (plain default look) + } + player.updateInventory(); + } + + /** Applies a ShieldLayout to a shield ItemStack using the BlockStateMeta API. */ + public static void applyLayoutToItem(ItemStack shield, ShieldLayout layout) { + if (shield == null || shield.getType() != Material.SHIELD || layout == null) return; + + var meta = shield.getItemMeta(); + if (!(meta instanceof BlockStateMeta bsm)) return; + + BlockState bs = bsm.getBlockState(); + if (!(bs instanceof Banner banner)) return; + + banner.setBaseColor(layout.getBaseColor() != null ? layout.getBaseColor() : DyeColor.WHITE); + banner.setPatterns(new ArrayList<>()); + for (ShieldLayout.PatternLayer layer : layout.getLayers()) { + banner.addPattern(new Pattern(layer.color(), layer.pattern())); + } + bsm.setBlockState(banner); + shield.setItemMeta(bsm); + } + + /** Removes all banner data from a shield (resets to blank). */ + public static void clearShield(ItemStack shield) { + if (shield == null || shield.getType() != Material.SHIELD) return; + var meta = shield.getItemMeta(); + if (!(meta instanceof BlockStateMeta bsm)) return; + BlockState bs = bsm.getBlockState(); + if (!(bs instanceof Banner banner)) return; + banner.setBaseColor(DyeColor.WHITE); + banner.setPatterns(new ArrayList<>()); + bsm.setBlockState(banner); + shield.setItemMeta(bsm); + } +} diff --git a/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/shield/ShieldEditorGui.java b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/shield/ShieldEditorGui.java new file mode 100644 index 00000000..7788eb0e --- /dev/null +++ b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/shield/ShieldEditorGui.java @@ -0,0 +1,339 @@ +package dev.nandi0813.practice.manager.gui.guis.cosmetics.shield; + +import dev.nandi0813.practice.manager.backend.GUIFile; +import dev.nandi0813.practice.manager.gui.GUI; +import dev.nandi0813.practice.manager.gui.GUIItem; +import dev.nandi0813.practice.manager.gui.GUIType; +import dev.nandi0813.practice.manager.profile.Profile; +import dev.nandi0813.practice.manager.profile.cosmetics.shield.ShieldLayout; +import dev.nandi0813.practice.util.Common; +import dev.nandi0813.practice.util.InventoryUtil; +import dev.nandi0813.practice.util.StringUtil; +import io.papermc.paper.registry.RegistryAccess; +import io.papermc.paper.registry.RegistryKey; +import org.bukkit.DyeColor; +import org.bukkit.Material; +import org.bukkit.block.banner.PatternType; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.ArrayList; +import java.util.List; + +/** + * Shield editor with a clear layout: preview + controls on top, 6 layer slots centered. + */ +public class ShieldEditorGui extends GUI { + + /* ── Slot map ─────────────────────────────────────────────────── */ + private static final int ROWS = 6; + private static final int PREVIEW_SLOT = 4; // row 1 center + private static final int BASE_COLOR_SLOT = 2; // row 1 left-side control + private static final int APPLY_SLOT = 6; // row 1 right-side control + private static final int ADD_LAYER_SLOT = 24; // row 3 right utility + private static final int REMOVE_LAYER_SLOT = 33; // row 4 right utility + private static final int BACK_SLOT = 45; // row 6 + + /* Two rows of three layer slots (max 6) */ + private static final int[] LAYER_SLOTS = {19, 20, 21, 28, 29, 30}; + + private static final ItemStack FILLER = GUIFile.getGuiItem("GENERAL-FILLER-ITEM").get(); + private static final ItemStack BACK = GUIFile.getGuiItem("GUIS.COSMETICS.ICONS.BACK-TO").get(); + + private final Profile profile; + /** Index into profile.getCosmeticsData().getShieldLayouts() */ + private final int layoutIndex; + private final GUI backToGui; + + public ShieldEditorGui(Profile profile, int layoutIndex, GUI backToGui) { + super(GUIType.Cosmetics_Shield_Editor); + this.profile = profile; + this.layoutIndex = layoutIndex; + this.backToGui = backToGui; + + ShieldLayout layout = getLayout(); + String layoutName = layout != null ? layout.getName() : "Shield"; + String title = GUIFile.getConfig().getString("GUIS.COSMETICS.SHIELD.EDITOR.TITLE", + "&8Editing: &e%name%").replace("%name%", layoutName); + this.gui.put(1, InventoryUtil.createInventory(title, ROWS)); + build(); + } + + @Override public void build() { update(); } + + @Override + public void update() { + Inventory inv = gui.get(1); + inv.clear(); + for (int i = 0; i < inv.getSize(); i++) { + if (isLayerSlot(i)) { + continue; + } + inv.setItem(i, FILLER); + } + + ShieldLayout layout = getLayout(); + if (layout == null) { backToGui.open(null); return; } + + boolean isActive = (profile.getCosmeticsData().getActiveShieldLayoutIndex() == layoutIndex); + + // Preview shield + inv.setItem(PREVIEW_SLOT, buildPreviewShield(layout)); + + // Base color button + inv.setItem(BASE_COLOR_SLOT, buildBaseColorButton(layout)); + + // Apply / Unapply button + inv.setItem(APPLY_SLOT, buildApplyButton(isActive)); + + // Layer slots (unused ones intentionally remain empty for readability) + for (int i = 0; i < LAYER_SLOTS.length && i < layout.getLayers().size(); i++) { + inv.setItem(LAYER_SLOTS[i], buildLayerItem(layout.getLayers().get(i), i)); + } + + // Add layer button (only if below max) + boolean canAdd = layout.getLayers().size() < ShieldLayout.MAX_LAYERS; + GUIItem addBtn = new GUIItem(canAdd ? Material.LIME_STAINED_GLASS_PANE : Material.RED_STAINED_GLASS_PANE); + addBtn.setName(canAdd + ? GUIFile.getConfig().getString("GUIS.COSMETICS.SHIELD.EDITOR.ADD-LAYER.NAME", "&aAdd Layer") + : GUIFile.getConfig().getString("GUIS.COSMETICS.SHIELD.EDITOR.MAX-LAYERS.NAME", "&cMax Layers Reached")); + List addLore = new ArrayList<>(); + addLore.add("&7Layers: &f" + layout.getLayers().size() + "&7/&f" + ShieldLayout.MAX_LAYERS); + if (canAdd) { + addLore.add("&eClick to add a new layer."); + addLore.add("&7Step 1: choose color, Step 2: choose pattern."); + } + addBtn.setLore(addLore); + inv.setItem(ADD_LAYER_SLOT, addBtn.get()); + + // Remove top layer button + boolean canRemove = !layout.getLayers().isEmpty(); + GUIItem removeBtn = new GUIItem(canRemove ? Material.ORANGE_STAINED_GLASS_PANE : Material.GRAY_STAINED_GLASS_PANE); + removeBtn.setName(canRemove + ? GUIFile.getConfig().getString("GUIS.COSMETICS.SHIELD.EDITOR.REMOVE-LAYER.NAME", "&cRemove Top Layer") + : "&8No layers to remove"); + if (canRemove) { + List remLore = new ArrayList<>(); + remLore.add("&7Removes the most recently added layer."); + remLore.add("&cClick to remove."); + removeBtn.setLore(remLore); + } + inv.setItem(REMOVE_LAYER_SLOT, removeBtn.get()); + + // Back + inv.setItem(BACK_SLOT, BACK != null ? BACK : new ItemStack(Material.ARROW)); + + updatePlayers(); + } + + @Override + public void handleClickEvent(InventoryClickEvent e) { + e.setCancelled(true); + Player player = (Player) e.getWhoClicked(); + int slot = e.getRawSlot(); + + ShieldLayout layout = getLayout(); + if (layout == null) return; + + if (slot == BACK_SLOT) { backToGui.update(true); backToGui.open(player); return; } + + if (slot == APPLY_SLOT) { + handleApply(player); + return; + } + + if (slot == BASE_COLOR_SLOT) { + new ShieldColorPickerGui(profile, layoutIndex, null, -1, this).open(player); + return; + } + + if (slot == ADD_LAYER_SLOT) { + if (layout.getLayers().size() >= ShieldLayout.MAX_LAYERS) { + Common.sendMMMessage(player, GUIFile.getConfig().getString( + "GUIS.COSMETICS.SHIELD.EDITOR.MAX-LAYERS.MESSAGE", "Maximum layers reached!")); + return; + } + // Open color picker for new layer (layer index = layers.size() = about to be added) + new ShieldColorPickerGui(profile, layoutIndex, null, layout.getLayers().size(), this).open(player); + return; + } + + if (slot == REMOVE_LAYER_SLOT) { + if (layout.removeTopLayer()) { + profile.saveData(); + if (profile.getCosmeticsData().getActiveShieldLayoutIndex() == layoutIndex) { + ShieldCosmeticsUtil.applyShieldToPlayer(player); + } + update(true); + } + return; + } + + // Layer slot clicked → open layer editor (color+pattern picker for that index) + for (int i = 0; i < LAYER_SLOTS.length && i < layout.getLayers().size(); i++) { + if (slot == LAYER_SLOTS[i]) { + new ShieldColorPickerGui(profile, layoutIndex, + layout.getLayers().get(i).color(), i, this).open(player); + return; + } + } + } + + // ── Apply / unapply ────────────────────────────────────────────── + + private void handleApply(Player player) { + int current = profile.getCosmeticsData().getActiveShieldLayoutIndex(); + if (current == layoutIndex) { + // Unapply + profile.getCosmeticsData().setActiveShieldLayoutIndex(-1); + ShieldCosmeticsUtil.applyShieldToPlayer(player); + Common.sendMMMessage(player, GUIFile.getConfig().getString( + "GUIS.COSMETICS.SHIELD.EDITOR.UNAPPLIED-MESSAGE", "Shield cosmetic removed.")); + } else { + profile.getCosmeticsData().setActiveShieldLayoutIndex(layoutIndex); + ShieldCosmeticsUtil.applyShieldToPlayer(player); + Common.sendMMMessage(player, GUIFile.getConfig().getString( + "GUIS.COSMETICS.SHIELD.EDITOR.APPLIED-MESSAGE", + "Shield layout applied!")); + } + profile.saveData(); + update(true); + } + + // ── Item builders ──────────────────────────────────────────────── + + private ItemStack buildPreviewShield(ShieldLayout layout) { + ItemStack shield = new ItemStack(Material.SHIELD); + ShieldCosmeticsUtil.applyLayoutToItem(shield, layout); + ItemMeta meta = shield.getItemMeta(); + if (meta != null) { + meta.displayName(tc("&eShield Preview")); + List lore = new ArrayList<>(); + lore.add(tc("&7Base: &f" + (layout.getBaseColor() != null ? fmt(layout.getBaseColor().name()) : "White"))); + lore.add(tc("&7Layers: &f" + layout.getLayers().size() + "&7/&f" + ShieldLayout.MAX_LAYERS)); + meta.lore(lore); + meta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES, ItemFlag.HIDE_ENCHANTS); + shield.setItemMeta(meta); + } + return shield; + } + + private ItemStack buildBaseColorButton(ShieldLayout layout) { + DyeColor base = layout.getBaseColor() != null ? layout.getBaseColor() : DyeColor.WHITE; + Material wool = dyeToWool(base); + GUIItem item = new GUIItem(wool); + item.setName("&bBase Color: &f" + fmt(base.name())); + List lore = new ArrayList<>(); + lore.add("&7The background color of your shield."); + lore.add("&eClick to change."); + item.setLore(lore); + return item.get(); + } + + private ItemStack buildApplyButton(boolean isActive) { + Material mat = isActive ? Material.LIME_WOOL : Material.GRAY_WOOL; + GUIItem item = new GUIItem(mat); + item.setName(isActive ? "&a&lLayout Active &7(Click to unapply)" : "&eApply This Layout"); + List lore = new ArrayList<>(); + lore.add(isActive ? "&7This design is on your shield." : "&7Apply this design to your shield."); + item.setLore(lore); + if (isActive) item.setGlowing(true); + return item.get(); + } + + private ItemStack buildLayerItem(ShieldLayout.PatternLayer layer, int index) { + Material banner = dyeToBanner(layer.color()); + GUIItem item = new GUIItem(banner); + item.setName("&fLayer " + (index + 1) + ": &e" + getPatternDisplayName(layer.pattern())); + + // Apply pattern to the banner icon + ItemStack stack = item.get(); + if (stack.getItemMeta() instanceof org.bukkit.inventory.meta.BannerMeta bm) { + bm.addPattern(new org.bukkit.block.banner.Pattern(layer.color(), layer.pattern())); + stack.setItemMeta(bm); + } + + ItemMeta meta = stack.getItemMeta(); + if (meta != null) { + List lore = new ArrayList<>(); + lore.add(tc("&7Color: &f" + fmt(layer.color().name()))); + lore.add(tc("&7Pattern: &f" + getPatternDisplayName(layer.pattern()))); + lore.add(tc("")); + lore.add(tc("&eClick to edit this layer.")); + lore.add(tc("&7Use Add Layer for a new slot.")); + meta.lore(lore); + meta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES, ItemFlag.HIDE_ENCHANTS); + stack.setItemMeta(meta); + } + return stack; + } + + // ── Helpers ────────────────────────────────────────────────────── + + private ShieldLayout getLayout() { + List layouts = profile.getCosmeticsData().getShieldLayouts(); + if (layoutIndex < 0 || layoutIndex >= layouts.size()) return null; + return layouts.get(layoutIndex); + } + + private static net.kyori.adventure.text.Component tc(String legacy) { + return net.kyori.adventure.text.Component.text(StringUtil.CC(legacy)); + } + + private static String fmt(String raw) { + String lower = raw.replace('_', ' ').toLowerCase(); + return Character.toUpperCase(lower.charAt(0)) + lower.substring(1); + } + + private static String getPatternDisplayName(PatternType pattern) { + if (pattern == null) { + return "Unknown"; + } + + var key = RegistryAccess.registryAccess().getRegistry(RegistryKey.BANNER_PATTERN).getKey(pattern); + if (key != null) { + return fmt(key.getKey()); + } + + return fmt(String.valueOf(pattern)); + } + + private static boolean isLayerSlot(int slot) { + for (int layerSlot : LAYER_SLOTS) { + if (layerSlot == slot) { + return true; + } + } + return false; + } + + static Material dyeToWool(DyeColor c) { + return switch (c) { + case WHITE -> Material.WHITE_WOOL; case ORANGE -> Material.ORANGE_WOOL; + case MAGENTA -> Material.MAGENTA_WOOL; case LIGHT_BLUE -> Material.LIGHT_BLUE_WOOL; + case YELLOW -> Material.YELLOW_WOOL; case LIME -> Material.LIME_WOOL; + case PINK -> Material.PINK_WOOL; case GRAY -> Material.GRAY_WOOL; + case LIGHT_GRAY -> Material.LIGHT_GRAY_WOOL; case CYAN -> Material.CYAN_WOOL; + case PURPLE -> Material.PURPLE_WOOL; case BLUE -> Material.BLUE_WOOL; + case BROWN -> Material.BROWN_WOOL; case GREEN -> Material.GREEN_WOOL; + case RED -> Material.RED_WOOL; case BLACK -> Material.BLACK_WOOL; + }; + } + + static Material dyeToBanner(DyeColor c) { + return switch (c) { + case WHITE -> Material.WHITE_BANNER; case ORANGE -> Material.ORANGE_BANNER; + case MAGENTA -> Material.MAGENTA_BANNER; case LIGHT_BLUE -> Material.LIGHT_BLUE_BANNER; + case YELLOW -> Material.YELLOW_BANNER; case LIME -> Material.LIME_BANNER; + case PINK -> Material.PINK_BANNER; case GRAY -> Material.GRAY_BANNER; + case LIGHT_GRAY -> Material.LIGHT_GRAY_BANNER; case CYAN -> Material.CYAN_BANNER; + case PURPLE -> Material.PURPLE_BANNER; case BLUE -> Material.BLUE_BANNER; + case BROWN -> Material.BROWN_BANNER; case GREEN -> Material.GREEN_BANNER; + case RED -> Material.RED_BANNER; case BLACK -> Material.BLACK_BANNER; + }; + } +} diff --git a/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/shield/ShieldLayoutListGui.java b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/shield/ShieldLayoutListGui.java new file mode 100644 index 00000000..a4012795 --- /dev/null +++ b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/shield/ShieldLayoutListGui.java @@ -0,0 +1,286 @@ +package dev.nandi0813.practice.manager.gui.guis.cosmetics.shield; + +import dev.nandi0813.practice.ZonePractice; +import dev.nandi0813.practice.manager.backend.GUIFile; +import dev.nandi0813.practice.manager.gui.GUI; +import dev.nandi0813.practice.manager.gui.GUIItem; +import dev.nandi0813.practice.manager.gui.GUIType; +import dev.nandi0813.practice.manager.profile.Profile; +import dev.nandi0813.practice.manager.profile.cosmetics.CosmeticsPermissionManager; +import dev.nandi0813.practice.manager.profile.cosmetics.shield.ShieldLayout; +import dev.nandi0813.practice.util.Common; +import dev.nandi0813.practice.util.InventoryUtil; +import dev.nandi0813.practice.util.ItemCreateUtil; +import io.papermc.paper.registry.RegistryAccess; +import io.papermc.paper.registry.RegistryKey; +import net.wesjd.anvilgui.AnvilGUI; +import org.bukkit.DyeColor; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.block.banner.PatternType; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.List; + +/** + * Lists all of a player's saved shield layouts. + * Layout slots: 10-16, 19-25, 28-34 (up to 21 layouts). + * Bottom row: back (slot 45), create-new (slot 49). + */ +public class ShieldLayoutListGui extends GUI { + + private static final ItemStack FILLER = GUIFile.getGuiItem("GENERAL-FILLER-ITEM").get(); + private static final ItemStack BACK = GUIFile.getGuiItem("GUIS.COSMETICS.ICONS.BACK-TO").get(); + + private static final int ROWS = 6; + private static final int BACK_SLOT = 45; + private static final int NEW_SLOT = 49; + + private static final int[] LAYOUT_SLOTS = { + 10, 11, 12, 13, 14, 15, 16, + 19, 20, 21, 22, 23, 24, 25, + 28, 29, 30, 31, 32, 33, 34 + }; + + private final Profile profile; + private final GUI backToGui; + + public ShieldLayoutListGui(Profile profile, GUI backToGui) { + super(GUIType.Cosmetics_Shield_Layouts); + this.profile = profile; + this.backToGui = backToGui; + String title = GUIFile.getConfig().getString("GUIS.COSMETICS.SHIELD.LAYOUTS.TITLE", "&8Shield Layouts"); + this.gui.put(1, InventoryUtil.createInventory(title, ROWS)); + build(); + } + + @Override public void build() { update(); } + + @Override + public void update() { + Inventory inv = gui.get(1); + inv.clear(); + for (int i = 0; i < inv.getSize(); i++) { + if (isLayoutSlot(i)) { + continue; + } + inv.setItem(i, FILLER); + } + + List layouts = profile.getCosmeticsData().getShieldLayouts(); + int active = profile.getCosmeticsData().getActiveShieldLayoutIndex(); + + for (int i = 0; i < LAYOUT_SLOTS.length && i < layouts.size(); i++) { + inv.setItem(LAYOUT_SLOTS[i], buildLayoutItem(layouts.get(i), i, i == active)); + } + + // "New layout" button + int maxLayouts = getMaxLayouts(profile.getPlayer().getPlayer()); + boolean canCreate = layouts.size() < maxLayouts; + GUIItem newItem = new GUIItem( + GUIFile.getConfig().getString("GUIS.COSMETICS.SHIELD.LAYOUTS.NEW-BUTTON.NAME", + canCreate ? "&aNew Layout" : "&cLayout limit reached"), + canCreate ? Material.LIME_DYE : Material.RED_DYE); + List newLore = new ArrayList<>(); + newLore.add("&7Saved: &e" + layouts.size() + "&7/&e" + maxLayouts); + if (canCreate) newLore.add("&eClick to create a new layout."); + else newLore.add("&cGet a higher rank for more slots."); + newItem.setLore(newLore); + inv.setItem(NEW_SLOT, newItem.get()); + + inv.setItem(BACK_SLOT, BACK != null ? BACK : new ItemStack(Material.ARROW)); + updatePlayers(); + } + + private static boolean isLayoutSlot(int slot) { + for (int layoutSlot : LAYOUT_SLOTS) { + if (layoutSlot == slot) { + return true; + } + } + return false; + } + + @Override + public void handleClickEvent(InventoryClickEvent e) { + e.setCancelled(true); + Player player = (Player) e.getWhoClicked(); + int slot = e.getRawSlot(); + + if (slot == BACK_SLOT) { backToGui.update(true); backToGui.open(player); return; } + + if (!CosmeticsPermissionManager.hasShieldPermission(player)) { + Common.sendMMMessage(player, GUIFile.getConfig().getString( + "GUIS.COSMETICS.SHIELD.NO-PERMISSION-MESSAGE", "You do not have permission to use shield cosmetics.")); + return; + } + + if (slot == NEW_SLOT) { handleCreateNew(player); return; } + + // Layout slot clicked + List layouts = profile.getCosmeticsData().getShieldLayouts(); + for (int i = 0; i < LAYOUT_SLOTS.length && i < layouts.size(); i++) { + if (slot == LAYOUT_SLOTS[i]) { + if (e.isRightClick()) { + // Right-click = delete + handleDelete(player, i); + } else if (e.isShiftClick()) { + // Shift-click = rename + handleRename(player, i); + } else { + // Left-click = open editor OR apply if already active + new ShieldEditorGui(profile, i, this).open(player); + } + return; + } + } + } + + private void handleCreateNew(Player player) { + List layouts = profile.getCosmeticsData().getShieldLayouts(); + int max = getMaxLayouts(player); + if (layouts.size() >= max) { + Common.sendMMMessage(player, GUIFile.getConfig().getString( + "GUIS.COSMETICS.SHIELD.LAYOUTS.LIMIT-REACHED", "You've reached your layout limit!")); + return; + } + + // Ask for a name via AnvilGUI + new AnvilGUI.Builder() + .onClose(state -> {}) + .onClick((anvilSlot, state) -> { + if (anvilSlot != AnvilGUI.Slot.OUTPUT) return List.of(); + String name = state.getText().trim(); + if (name.isEmpty()) name = "Layout " + (layouts.size() + 1); + if (name.length() > 24) name = name.substring(0, 24); + ShieldLayout newLayout = new ShieldLayout(name, DyeColor.WHITE); + profile.getCosmeticsData().getShieldLayouts().add(newLayout); + int newIndex = layouts.size() - 1; + profile.saveData(); + final int finalIndex = newIndex; + org.bukkit.Bukkit.getScheduler().runTask(ZonePractice.getInstance(), + () -> new ShieldEditorGui(profile, finalIndex, this).open(player)); + return List.of(AnvilGUI.ResponseAction.close()); + }) + .text("My Layout") + .title(GUIFile.getConfig().getString("GUIS.COSMETICS.SHIELD.LAYOUTS.NAME-TITLE", "Layout Name")) + .plugin(ZonePractice.getInstance()) + .open(player); + } + + private void handleDelete(Player player, int index) { + List layouts = profile.getCosmeticsData().getShieldLayouts(); + if (index < 0 || index >= layouts.size()) return; + layouts.remove(index); + // Fix active index + int active = profile.getCosmeticsData().getActiveShieldLayoutIndex(); + if (active == index) { + profile.getCosmeticsData().setActiveShieldLayoutIndex(-1); + ShieldCosmeticsUtil.applyShieldToPlayer(player); // clear shield + } else if (active > index) { + profile.getCosmeticsData().setActiveShieldLayoutIndex(active - 1); + } + profile.saveData(); + Common.sendMMMessage(player, GUIFile.getConfig().getString( + "GUIS.COSMETICS.SHIELD.LAYOUTS.DELETED-MESSAGE", "Layout deleted.")); + update(true); + } + + private void handleRename(Player player, int index) { + List layouts = profile.getCosmeticsData().getShieldLayouts(); + if (index < 0 || index >= layouts.size()) return; + String currentName = layouts.get(index).getName(); + + new AnvilGUI.Builder() + .onClose(state -> {}) + .onClick((anvilSlot, state) -> { + if (anvilSlot != AnvilGUI.Slot.OUTPUT) return List.of(); + String name = state.getText().trim(); + if (name.isEmpty()) name = currentName; + if (name.length() > 24) name = name.substring(0, 24); + layouts.get(index).setName(name); + profile.saveData(); + org.bukkit.Bukkit.getScheduler().runTask(ZonePractice.getInstance(), + () -> update(true)); + return List.of(AnvilGUI.ResponseAction.close()); + }) + .text(currentName) + .title(GUIFile.getConfig().getString("GUIS.COSMETICS.SHIELD.LAYOUTS.RENAME-TITLE", "Rename Layout")) + .plugin(ZonePractice.getInstance()) + .open(player); + } + + // ── Builders ───────────────────────────────────────────────────── + + private ItemStack buildLayoutItem(ShieldLayout layout, int index, boolean active) { + ItemStack shield = new ItemStack(Material.SHIELD); + ShieldCosmeticsUtil.applyLayoutToItem(shield, layout); + + var meta = shield.getItemMeta(); + if (meta != null) { + String nameColor = active ? "&a✔ " : "&e"; + meta.displayName(net.kyori.adventure.text.Component.text( + dev.nandi0813.practice.util.StringUtil.CC(nameColor + layout.getName()))); + + List lore = new ArrayList<>(); + String base = layout.getBaseColor() != null ? formatName(layout.getBaseColor().name()) : "White"; + lore.add(tc("&7Base color: &f" + base)); + lore.add(tc("&7Layers: &f" + layout.getLayers().size() + "&7/&f" + ShieldLayout.MAX_LAYERS)); + if (!layout.getLayers().isEmpty()) { + lore.add(tc("&8─────────────────")); + for (int i = 0; i < layout.getLayers().size(); i++) { + ShieldLayout.PatternLayer layer = layout.getLayers().get(i); + lore.add(tc("&7" + (i + 1) + ". &f" + formatName(layer.color().name()) + + " &8│ &f" + getPatternDisplayName(layer.pattern()))); + } + } + lore.add(tc("&8─────────────────")); + if (active) { + lore.add(tc("&a&lCurrently Active")); + } + lore.add(tc("&eLeft-click &7to edit")); + lore.add(tc("&bShift-click &7to rename")); + lore.add(tc("&cRight-click &7to delete")); + meta.lore(lore); + meta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES, ItemFlag.HIDE_ENCHANTS); + shield.setItemMeta(meta); + } + return ItemCreateUtil.hideItemFlags(shield); + } + + /** Returns how many layouts the player is allowed to have. */ + public static int getMaxLayouts(Player player) { + return CosmeticsPermissionManager.getMaxShieldLayouts(player); + } + + private static net.kyori.adventure.text.Component tc(String legacy) { + return net.kyori.adventure.text.Component.text( + dev.nandi0813.practice.util.StringUtil.CC(legacy)); + } + + private static String formatName(String raw) { + String lower = raw.replace('_', ' ').toLowerCase(); + return Character.toUpperCase(lower.charAt(0)) + lower.substring(1); + } + + private static String getPatternDisplayName(PatternType pattern) { + if (pattern == null) { + return "Unknown"; + } + + NamespacedKey key = RegistryAccess.registryAccess() + .getRegistry(RegistryKey.BANNER_PATTERN) + .getKey(pattern); + + if (key != null) { + return formatName(key.getKey()); + } + + return formatName(String.valueOf(pattern)); + } +} diff --git a/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/shield/ShieldPatternPickerGui.java b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/shield/ShieldPatternPickerGui.java new file mode 100644 index 00000000..658c183e --- /dev/null +++ b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/shield/ShieldPatternPickerGui.java @@ -0,0 +1,245 @@ +package dev.nandi0813.practice.manager.gui.guis.cosmetics.shield; + +import dev.nandi0813.practice.manager.backend.GUIFile; +import dev.nandi0813.practice.manager.gui.GUI; +import dev.nandi0813.practice.manager.gui.GUIItem; +import dev.nandi0813.practice.manager.gui.GUIType; +import dev.nandi0813.practice.manager.profile.Profile; +import dev.nandi0813.practice.manager.profile.cosmetics.shield.ShieldLayout; +import dev.nandi0813.practice.util.Common; +import dev.nandi0813.practice.util.InventoryUtil; +import dev.nandi0813.practice.util.StringUtil; +import io.papermc.paper.registry.RegistryAccess; +import io.papermc.paper.registry.RegistryKey; +import org.bukkit.DyeColor; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.block.banner.Pattern; +import org.bukkit.block.banner.PatternType; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.BannerMeta; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * Shows ALL available PatternType values with live banner previews. + * Paginated: 28 patterns per page (4 rows × 7 cols, slots 10-16, 19-25, 28-34, 37-43). + * Bottom row: prev page (slot 45), back (slot 49), next page (slot 53). + */ +public class ShieldPatternPickerGui extends GUI { + + private static final int ROWS = 6; + private static final int PREV_SLOT = 45; + private static final int BACK_SLOT = 49; + private static final int NEXT_SLOT = 53; + private static final int[] PATTERN_SLOTS = { + 10, 11, 12, 13, 14, 15, 16, + 19, 20, 21, 22, 23, 24, 25, + 28, 29, 30, 31, 32, 33, 34, + 37, 38, 39, 40, 41, 42, 43 + }; + private static final int PER_PAGE = PATTERN_SLOTS.length; // 28 + + private static final ItemStack FILLER = GUIFile.getGuiItem("GENERAL-FILLER-ITEM").get(); + private static final ItemStack BACK = GUIFile.getGuiItem("GUIS.COSMETICS.ICONS.BACK-TO").get(); + + // All banner patterns discovered from the registry (sorted for stable paging). + private static final List ALL_PATTERNS = loadAllPatterns(); + + private final Profile profile; + private final int layoutIndex; + private final DyeColor chosenColor; // colour picked in the previous step + /** -1 = editing base (should not happen here). >=0 = layer index (or size = new layer). */ + private final int layerIndex; + private final GUI backToGui; // editor GUI to return to + + private int page = 0; + + public ShieldPatternPickerGui(Profile profile, int layoutIndex, + DyeColor chosenColor, int layerIndex, GUI backToGui) { + super(GUIType.Cosmetics_Shield_PatternPicker); + this.profile = profile; + this.layoutIndex = layoutIndex; + this.chosenColor = chosenColor; + this.layerIndex = layerIndex; + this.backToGui = backToGui; + + String title = GUIFile.getConfig().getString( + "GUIS.COSMETICS.SHIELD.PATTERN-PICKER.TITLE", "&8Pick Pattern"); + this.gui.put(1, InventoryUtil.createInventory(title, ROWS)); + build(); + } + + @Override public void build() { update(); } + + @Override + public void update() { + Inventory inv = gui.get(1); + inv.clear(); + for (int i = 0; i < inv.getSize(); i++) inv.setItem(i, FILLER); + + int totalPages = (int) Math.ceil((double) ALL_PATTERNS.size() / PER_PAGE); + int startIndex = page * PER_PAGE; + + for (int i = 0; i < PATTERN_SLOTS.length; i++) { + int ptIndex = startIndex + i; + if (ptIndex >= ALL_PATTERNS.size()) break; + inv.setItem(PATTERN_SLOTS[i], buildPatternItem(ALL_PATTERNS.get(ptIndex))); + } + + // Navigation + if (page > 0) { + GUIItem prev = new GUIItem("&ePrevious Page &8(" + page + "/" + totalPages + ")", Material.ARROW); + inv.setItem(PREV_SLOT, prev.get()); + } + if (page < totalPages - 1) { + GUIItem next = new GUIItem("&eNext Page &8(" + (page + 2) + "/" + totalPages + ")", Material.ARROW); + inv.setItem(NEXT_SLOT, next.get()); + } + + inv.setItem(BACK_SLOT, BACK != null ? BACK : new ItemStack(Material.ARROW)); + updatePlayers(); + } + + @Override + public void handleClickEvent(InventoryClickEvent e) { + e.setCancelled(true); + Player player = (Player) e.getWhoClicked(); + int slot = e.getRawSlot(); + + if (slot == BACK_SLOT) { + // Go back to color picker with current preselected color + new ShieldColorPickerGui(profile, layoutIndex, chosenColor, layerIndex, backToGui).open(player); + return; + } + + if (slot == PREV_SLOT && page > 0) { page--; update(true); return; } + if (slot == NEXT_SLOT && page < (int) Math.ceil((double) ALL_PATTERNS.size() / PER_PAGE) - 1) { + page++; update(true); return; + } + + // Pattern slot clicked + for (int i = 0; i < PATTERN_SLOTS.length; i++) { + if (slot == PATTERN_SLOTS[i]) { + int ptIndex = page * PER_PAGE + i; + if (ptIndex < ALL_PATTERNS.size()) { + handlePatternPick(player, ALL_PATTERNS.get(ptIndex)); + } + return; + } + } + } + + private void handlePatternPick(Player player, PatternType pattern) { + ShieldLayout layout = getLayout(); + if (layout == null) return; + + List layers = layout.getLayers(); + + if (layerIndex < layers.size()) { + // Edit existing layer + layers.set(layerIndex, new ShieldLayout.PatternLayer(chosenColor, pattern)); + } else { + // Add new layer + if (!layout.addLayer(chosenColor, pattern)) { + Common.sendMMMessage(player, GUIFile.getConfig().getString( + "GUIS.COSMETICS.SHIELD.EDITOR.MAX-LAYERS.MESSAGE", "Maximum layers reached!")); + return; + } + } + + profile.saveData(); + + // Apply live if this layout is active + if (profile.getCosmeticsData().getActiveShieldLayoutIndex() == layoutIndex) { + ShieldCosmeticsUtil.applyShieldToPlayer(player); + } + + Common.sendMMMessage(player, GUIFile.getConfig().getString( + "GUIS.COSMETICS.SHIELD.PATTERN-PICKER.APPLIED-MESSAGE", "Layer applied!")); + + // Go back to editor + backToGui.update(true); + backToGui.open(player); + } + + // ── Item builder ───────────────────────────────────────────────── + + private ItemStack buildPatternItem(PatternType pattern) { + // Material already encodes the base colour (e.g. RED_BANNER for RED). + // In 1.21.1 BannerMeta no longer has setBaseColor(); the colour is the Material. + Material bannerMat = ShieldEditorGui.dyeToBanner(chosenColor); + GUIItem item = new GUIItem(bannerMat); + item.setName("&f" + getPatternDisplayName(pattern)); + + ItemStack stack = item.get(); + // Apply the pattern using a contrasting colour so it is visible + DyeColor contrast = (chosenColor == DyeColor.WHITE || chosenColor == DyeColor.LIGHT_GRAY) + ? DyeColor.BLACK : DyeColor.WHITE; + if (stack.getItemMeta() instanceof BannerMeta bm) { + bm.addPattern(new Pattern(contrast, pattern)); + bm.addItemFlags(ItemFlag.HIDE_ATTRIBUTES, ItemFlag.HIDE_ENCHANTS); + List lore = new ArrayList<>(); + lore.add(tc("&7Color: &f" + fmt(chosenColor.name()))); + lore.add(tc("&eClick to apply.")); + bm.lore(lore); + stack.setItemMeta(bm); + } else if (stack.hasItemMeta()) { + ItemMeta meta = stack.getItemMeta(); + List lore = new ArrayList<>(); + lore.add(tc("&7Color: &f" + fmt(chosenColor.name()))); + lore.add(tc("&eClick to apply.")); + meta.lore(lore); + stack.setItemMeta(meta); + } + return stack; + } + + private ShieldLayout getLayout() { + List layouts = profile.getCosmeticsData().getShieldLayouts(); + if (layoutIndex < 0 || layoutIndex >= layouts.size()) return null; + return layouts.get(layoutIndex); + } + + private static net.kyori.adventure.text.Component tc(String legacy) { + return net.kyori.adventure.text.Component.text(StringUtil.CC(legacy)); + } + + private static String fmt(String raw) { + String lower = raw.replace('_', ' ').toLowerCase(); + return Character.toUpperCase(lower.charAt(0)) + lower.substring(1); + } + + private static List loadAllPatterns() { + var registry = RegistryAccess.registryAccess().getRegistry(RegistryKey.BANNER_PATTERN); + return registry.stream() + .sorted(Comparator.comparing(pattern -> { + NamespacedKey key = registry.getKey(pattern); + return key == null ? "" : key.toString(); + })) + .toList(); + } + + private static String getPatternDisplayName(PatternType pattern) { + if (pattern == null) { + return "Unknown"; + } + + NamespacedKey key = RegistryAccess.registryAccess() + .getRegistry(RegistryKey.BANNER_PATTERN) + .getKey(pattern); + + if (key != null) { + return fmt(key.getKey()); + } + + return fmt(String.valueOf(pattern)); + } +} diff --git a/core/src/main/java/dev/nandi0813/practice/manager/inventory/inventories/LobbyInventory.java b/core/src/main/java/dev/nandi0813/practice/manager/inventory/inventories/LobbyInventory.java index 16b214ab..a0a089b5 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/inventory/inventories/LobbyInventory.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/inventory/inventories/LobbyInventory.java @@ -19,6 +19,7 @@ public LobbyInventory() { this.invItems.add(new RankedInvItem()); this.invItems.add(new RematchInvItem()); this.invItems.add(new SettingsInvItem()); + this.invItems.add(new CosmeticsInvItem()); this.invItems.add(new SpectateModeInvItem()); this.invItems.add(new StaffMode()); this.invItems.add(new SetupInvItem()); @@ -56,6 +57,11 @@ protected void set(Player player) { case RematchInvItem rematchInvItem -> { continue; } + case CosmeticsInvItem cosmeticsInvItem -> { + if (!player.hasPermission("zpp.cosmetics.main")) { + continue; + } + } default -> { } } diff --git a/core/src/main/java/dev/nandi0813/practice/manager/inventory/inventoryitem/lobbyitems/CosmeticsInvItem.java b/core/src/main/java/dev/nandi0813/practice/manager/inventory/inventoryitem/lobbyitems/CosmeticsInvItem.java new file mode 100644 index 00000000..e617c8ad --- /dev/null +++ b/core/src/main/java/dev/nandi0813/practice/manager/inventory/inventoryitem/lobbyitems/CosmeticsInvItem.java @@ -0,0 +1,17 @@ +package dev.nandi0813.practice.manager.inventory.inventoryitem.lobbyitems; + +import dev.nandi0813.practice.manager.inventory.inventoryitem.InvItem; +import org.bukkit.entity.Player; + +public class CosmeticsInvItem extends InvItem { + + public CosmeticsInvItem() { + super(getItemStack("LOBBY-BASIC.NORMAL.COSMETICS.ITEM"), getInt("LOBBY-BASIC.NORMAL.COSMETICS.SLOT")); + } + + @Override + public void handleClickEvent(Player player) { + player.performCommand("cosmetics"); + } +} + diff --git a/core/src/main/java/dev/nandi0813/practice/manager/profile/ProfileFile.java b/core/src/main/java/dev/nandi0813/practice/manager/profile/ProfileFile.java index 1f0f0f2d..0d72bd76 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/profile/ProfileFile.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/profile/ProfileFile.java @@ -6,9 +6,11 @@ import dev.nandi0813.practice.manager.ladder.LadderManager; import dev.nandi0813.practice.manager.ladder.abstraction.Ladder; import dev.nandi0813.practice.manager.ladder.abstraction.normal.NormalLadder; -import dev.nandi0813.practice.manager.profile.cosmetics.ArmorSlot; -import dev.nandi0813.practice.manager.profile.cosmetics.ArmorTrimPermissionManager; -import dev.nandi0813.practice.manager.profile.cosmetics.ArmorTrimTier; +import dev.nandi0813.practice.manager.profile.cosmetics.CosmeticsPermissionManager; +import dev.nandi0813.practice.manager.profile.cosmetics.armortrim.ArmorSlot; +import dev.nandi0813.practice.manager.profile.cosmetics.armortrim.ArmorTrimTier; +import dev.nandi0813.practice.manager.profile.cosmetics.deatheffect.DeathEffect; +import dev.nandi0813.practice.manager.profile.cosmetics.shield.ShieldLayout; import dev.nandi0813.practice.manager.profile.enums.ProfileWorldTime; import dev.nandi0813.practice.manager.profile.group.Group; import dev.nandi0813.practice.manager.profile.group.GroupManager; @@ -22,9 +24,7 @@ import org.bukkit.inventory.meta.trim.TrimMaterial; import org.bukkit.inventory.meta.trim.TrimPattern; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; +import java.util.*; public class ProfileFile extends ConfigFile { @@ -73,6 +73,13 @@ public void setData() { // Cosmetics data for armor trims if (profile.getCosmeticsData() != null) { config.set("cosmetics.active-tier", profile.getCosmeticsData().getActiveTier().getId()); + config.set("cosmetics.death-effect", profile.getCosmeticsData().getDeathEffect().getId()); + config.set("cosmetics.shield.active-layout-index", profile.getCosmeticsData().getActiveShieldLayoutIndex()); + + List serializedShieldLayouts = profile.getCosmeticsData().getShieldLayouts().stream() + .map(ShieldLayout::serialise) + .toList(); + config.set("cosmetics.shield.layouts", serializedShieldLayouts); for (ArmorTrimTier tier : ArmorTrimTier.values()) { for (ArmorSlot slot : ArmorSlot.values()) { @@ -80,14 +87,14 @@ public void setData() { TrimPattern pattern = profile.getCosmeticsData().getPattern(tier, slot); if (pattern != null) { - config.set(basePath + ".pattern", "minecraft:" + ArmorTrimPermissionManager.getTrimId(pattern)); + config.set(basePath + ".pattern", "minecraft:" + CosmeticsPermissionManager.getTrimId(pattern)); } else { config.set(basePath + ".pattern", null); } TrimMaterial material = profile.getCosmeticsData().getMaterial(tier, slot); if (material != null) { - config.set(basePath + ".material", "minecraft:" + ArmorTrimPermissionManager.getTrimId(material)); + config.set(basePath + ".material", "minecraft:" + CosmeticsPermissionManager.getTrimId(material)); } else { config.set(basePath + ".material", null); } @@ -192,6 +199,22 @@ public void getData() { try { ArmorTrimTier activeTier = ArmorTrimTier.fromId(config.getString("cosmetics.active-tier", "leather")); profile.getCosmeticsData().setActiveTier(activeTier); + profile.getCosmeticsData().setDeathEffect(DeathEffect.fromId(config.getString("cosmetics.death-effect", "none"))); + + List shieldLayouts = new ArrayList<>(); + for (String serializedLayout : config.getStringList("cosmetics.shield.layouts")) { + ShieldLayout layout = ShieldLayout.deserialise(serializedLayout); + if (layout != null) { + shieldLayouts.add(layout); + } + } + profile.getCosmeticsData().setShieldLayouts(shieldLayouts); + + int activeShieldLayoutIndex = config.getInt("cosmetics.shield.active-layout-index", -1); + if (activeShieldLayoutIndex < -1 || activeShieldLayoutIndex >= shieldLayouts.size()) { + activeShieldLayoutIndex = -1; + } + profile.getCosmeticsData().setActiveShieldLayoutIndex(activeShieldLayoutIndex); boolean loadedTierData = false; for (ArmorTrimTier tier : ArmorTrimTier.values()) { diff --git a/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/CosmeticsData.java b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/CosmeticsData.java index 5d8fffcc..7f9832a0 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/CosmeticsData.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/CosmeticsData.java @@ -1,11 +1,17 @@ package dev.nandi0813.practice.manager.profile.cosmetics; +import dev.nandi0813.practice.manager.profile.cosmetics.armortrim.ArmorSlot; +import dev.nandi0813.practice.manager.profile.cosmetics.armortrim.ArmorTrimTier; +import dev.nandi0813.practice.manager.profile.cosmetics.deatheffect.DeathEffect; +import dev.nandi0813.practice.manager.profile.cosmetics.shield.ShieldLayout; import lombok.Getter; import lombok.Setter; import org.bukkit.inventory.meta.trim.TrimMaterial; import org.bukkit.inventory.meta.trim.TrimPattern; +import java.util.ArrayList; import java.util.EnumMap; +import java.util.List; import java.util.Map; @Getter @@ -13,6 +19,10 @@ public class CosmeticsData { private ArmorTrimTier activeTier = ArmorTrimTier.LEATHER; + private DeathEffect deathEffect = DeathEffect.NONE; + + private final List shieldLayouts = new ArrayList<>(); + private int activeShieldLayoutIndex = -1; private final Map> tierData = new EnumMap<>(ArmorTrimTier.class); @@ -77,10 +87,24 @@ private SlotData getSlotData(ArmorTrimTier tier, ArmorSlot slot) { return bySlot.computeIfAbsent(slot, k -> new SlotData()); } + public ShieldLayout getActiveShieldLayout() { + if (activeShieldLayoutIndex < 0 || activeShieldLayoutIndex >= shieldLayouts.size()) return null; + return shieldLayouts.get(activeShieldLayoutIndex); + } + + public void setDeathEffect(DeathEffect deathEffect) { + this.deathEffect = deathEffect == null ? DeathEffect.NONE : deathEffect; + } + + public void setShieldLayouts(List layouts) { + this.shieldLayouts.clear(); + if (layouts != null) { + this.shieldLayouts.addAll(layouts); + } + } + private static final class SlotData { private TrimPattern pattern; private TrimMaterial material; } } - - diff --git a/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/ArmorTrimPermissionManager.java b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/CosmeticsPermissionManager.java similarity index 60% rename from core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/ArmorTrimPermissionManager.java rename to core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/CosmeticsPermissionManager.java index f23751c7..bab21522 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/ArmorTrimPermissionManager.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/CosmeticsPermissionManager.java @@ -1,5 +1,7 @@ package dev.nandi0813.practice.manager.profile.cosmetics; +import dev.nandi0813.practice.manager.profile.cosmetics.armortrim.ArmorTrimTier; +import dev.nandi0813.practice.manager.profile.cosmetics.deatheffect.DeathEffect; import io.papermc.paper.registry.RegistryAccess; import io.papermc.paper.registry.RegistryKey; import org.bukkit.Bukkit; @@ -14,11 +16,14 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -public enum ArmorTrimPermissionManager { +public enum CosmeticsPermissionManager { ; + private static final int MAX_SHIELD_LAYOUTS = 21; + private static final List REGISTERED_PATTERNS = new ArrayList<>(); private static final List REGISTERED_MATERIALS = new ArrayList<>(); + private static final List REGISTERED_DEATH_EFFECTS = new ArrayList<>(); private static final Map PATTERN_IDS = new HashMap<>(); private static final Map MATERIAL_IDS = new HashMap<>(); private static final Pattern NAMESPACE_PATTERN = Pattern.compile("([a-z0-9_.-]+):([a-z0-9_./-]+)"); @@ -26,6 +31,15 @@ public enum ArmorTrimPermissionManager { public static void registerAllPermissions() { PluginManager pluginManager = Bukkit.getPluginManager(); + registerPermission(pluginManager, "zpp.cosmetics.shield.use", "Use shield cosmetics."); + registerPermission(pluginManager, "zpp.cosmetics.shield.layouts.*", "Use all shield layout slots."); + registerPermission(pluginManager, "zpp.cosmetics.shield.layouts.unlimited", "Use unlimited shield layouts."); + for (int layouts = 1; layouts <= MAX_SHIELD_LAYOUTS; layouts++) { + registerPermission(pluginManager, + "zpp.cosmetics.shield.layouts." + layouts, + "Use up to " + layouts + " shield layouts."); + } + for (ArmorTrimTier tier : ArmorTrimTier.values()) { registerPermission(pluginManager, tier.getPermissionNode(), "Use " + tier.getDisplayName() + " armor tier cosmetics."); } @@ -43,7 +57,7 @@ public static void registerAllPermissions() { REGISTERED_PATTERNS.add(pattern); PATTERN_IDS.put(pattern, id); registerPermission(pluginManager, - "zpp.cosmetics.pattern." + id, + "zpp.cosmetics.armortrim.pattern." + id, "Use armor trim pattern " + id + "."); }); @@ -60,12 +74,19 @@ public static void registerAllPermissions() { REGISTERED_MATERIALS.add(material); MATERIAL_IDS.put(material, id); registerPermission(pluginManager, - "zpp.cosmetics.material." + id, + "zpp.cosmetics.armortrim.material." + id, "Use armor trim material " + id + "."); }); - REGISTERED_PATTERNS.sort(Comparator.comparing(ArmorTrimPermissionManager::getTrimId)); - REGISTERED_MATERIALS.sort(Comparator.comparing(ArmorTrimPermissionManager::getTrimId)); + REGISTERED_PATTERNS.sort(Comparator.comparing(CosmeticsPermissionManager::getTrimId)); + REGISTERED_MATERIALS.sort(Comparator.comparing(CosmeticsPermissionManager::getTrimId)); + + REGISTERED_DEATH_EFFECTS.clear(); + for (DeathEffect deathEffect : DeathEffect.values()) { + String node = getDeathEffectPermissionNode(deathEffect); + REGISTERED_DEATH_EFFECTS.add(deathEffect); + registerPermission(pluginManager, node, "Use death effect " + deathEffect.getId() + "."); + } } public static List getRegisteredPatterns() { @@ -76,13 +97,17 @@ public static List getRegisteredMaterials() { return Collections.unmodifiableList(REGISTERED_MATERIALS); } + public static List getRegisteredDeathEffects() { + return Collections.unmodifiableList(REGISTERED_DEATH_EFFECTS); + } + public static boolean hasBasePermission(Player player, ArmorTrimTier tier) { if (player == null || tier == null) { return false; } return player.isOp() - || player.hasPermission("zpp.cosmetics.base.*") + || player.hasPermission("zpp.cosmetics.armortrim.base.*") || player.hasPermission(tier.getPermissionNode()); } @@ -91,7 +116,7 @@ public static boolean hasPatternPermission(Player player, TrimPattern pattern) { return false; } - return hasPatternPermission(player, "zpp.cosmetics.pattern." + getTrimId(pattern)); + return hasPatternPermission(player, "zpp.cosmetics.armortrim.pattern." + getTrimId(pattern)); } public static boolean hasPatternPermission(Player player, String node) { @@ -100,7 +125,7 @@ public static boolean hasPatternPermission(Player player, String node) { } return player.isOp() - || player.hasPermission("zpp.cosmetics.pattern.*") + || player.hasPermission("zpp.cosmetics.armortrim.pattern.*") || player.hasPermission(node); } @@ -109,7 +134,7 @@ public static boolean hasMaterialPermission(Player player, TrimMaterial material return false; } - return hasMaterialPermission(player, "zpp.cosmetics.material." + getTrimId(material)); + return hasMaterialPermission(player, "zpp.cosmetics.armortrim.material." + getTrimId(material)); } public static boolean hasMaterialPermission(Player player, String node) { @@ -118,10 +143,60 @@ public static boolean hasMaterialPermission(Player player, String node) { } return player.isOp() - || player.hasPermission("zpp.cosmetics.material.*") + || player.hasPermission("zpp.cosmetics.armortrim.material.*") || player.hasPermission(node); } + public static String getDeathEffectPermissionNode(DeathEffect deathEffect) { + String id = deathEffect == null ? "none" : deathEffect.getId(); + return DeathEffect.getPermissionNode(sanitizeId(id)); + } + + public static boolean hasDeathEffectPermission(Player player, DeathEffect deathEffect) { + if (player == null || deathEffect == null) { + return false; + } + + if (deathEffect == DeathEffect.NONE) { + return true; + } + + return player.isOp() + || player.hasPermission(DeathEffect.getPermissionNode("*")) + || player.hasPermission(getDeathEffectPermissionNode(deathEffect)); + } + + public static boolean hasShieldPermission(Player player) { + if (player == null) { + return false; + } + + return player.isOp() + || player.hasPermission("zpp.cosmetics.shield.*") + || player.hasPermission("zpp.cosmetics.shield.use"); + } + + public static int getMaxShieldLayouts(Player player) { + if (player == null) { + return 1; + } + + if (player.isOp() + || player.hasPermission("zpp.cosmetics.shield.*") + || player.hasPermission("zpp.cosmetics.shield.layouts.*") + || player.hasPermission("zpp.cosmetics.shield.layouts.unlimited")) { + return MAX_SHIELD_LAYOUTS; + } + + for (int layouts = MAX_SHIELD_LAYOUTS; layouts >= 1; layouts--) { + if (player.hasPermission("zpp.cosmetics.shield.layouts." + layouts)) { + return layouts; + } + } + + return 1; + } + public static String getTrimId(TrimPattern pattern) { if (pattern == null) { return "unknown"; @@ -171,6 +246,9 @@ private static String resolveTrimIdFallback(Object trimValue) { } private static String sanitizeId(String value) { + if (value == null || value.isBlank()) { + return "unknown"; + } return value.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9_]+", ""); } @@ -184,3 +262,5 @@ private static void registerPermission(PluginManager pluginManager, String node, } + + diff --git a/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/ArmorSlot.java b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/armortrim/ArmorSlot.java similarity index 87% rename from core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/ArmorSlot.java rename to core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/armortrim/ArmorSlot.java index 085987a0..57f927f8 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/ArmorSlot.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/armortrim/ArmorSlot.java @@ -1,4 +1,4 @@ -package dev.nandi0813.practice.manager.profile.cosmetics; +package dev.nandi0813.practice.manager.profile.cosmetics.armortrim; import lombok.Getter; diff --git a/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/ArmorTrimTier.java b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/armortrim/ArmorTrimTier.java similarity index 96% rename from core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/ArmorTrimTier.java rename to core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/armortrim/ArmorTrimTier.java index 244e2137..f9b3055c 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/ArmorTrimTier.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/armortrim/ArmorTrimTier.java @@ -1,4 +1,4 @@ -package dev.nandi0813.practice.manager.profile.cosmetics; +package dev.nandi0813.practice.manager.profile.cosmetics.armortrim; import dev.nandi0813.practice.manager.backend.GUIFile; import lombok.Getter; @@ -40,7 +40,7 @@ public String getDisplayName() { } public String getPermissionNode() { - return "zpp.cosmetics.base." + id; + return "zpp.cosmetics.armortrim.base." + id; } public Material getMaterial(ArmorSlot slot) { diff --git a/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/CosmeticsPermissionSanitizer.java b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/armortrim/CosmeticsPermissionSanitizer.java similarity index 61% rename from core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/CosmeticsPermissionSanitizer.java rename to core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/armortrim/CosmeticsPermissionSanitizer.java index dd6c3a5c..1b306906 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/CosmeticsPermissionSanitizer.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/armortrim/CosmeticsPermissionSanitizer.java @@ -1,6 +1,8 @@ -package dev.nandi0813.practice.manager.profile.cosmetics; +package dev.nandi0813.practice.manager.profile.cosmetics.armortrim; import dev.nandi0813.practice.manager.profile.Profile; +import dev.nandi0813.practice.manager.profile.cosmetics.CosmeticsPermissionManager; +import dev.nandi0813.practice.manager.profile.cosmetics.deatheffect.DeathEffect; import org.bukkit.entity.Player; import org.bukkit.inventory.meta.trim.TrimMaterial; import org.bukkit.inventory.meta.trim.TrimPattern; @@ -17,7 +19,6 @@ public static boolean sanitize(Player player, Profile profile) { boolean changed = false; - // Shield customization is intentionally skipped until its dedicated rework. EnumSet supportedSlots = EnumSet.of( ArmorSlot.HELMET, ArmorSlot.CHESTPLATE, @@ -26,7 +27,7 @@ public static boolean sanitize(Player player, Profile profile) { ); for (ArmorTrimTier tier : ArmorTrimTier.values()) { - boolean hasTierPermission = ArmorTrimPermissionManager.hasBasePermission(player, tier); + boolean hasTierPermission = CosmeticsPermissionManager.hasBasePermission(player, tier); for (ArmorSlot slot : supportedSlots) { TrimPattern pattern = profile.getCosmeticsData().getPattern(tier, slot); @@ -46,12 +47,12 @@ public static boolean sanitize(Player player, Profile profile) { continue; } - if (pattern != null && !ArmorTrimPermissionManager.hasPatternPermission(player, pattern)) { + if (pattern != null && !CosmeticsPermissionManager.hasPatternPermission(player, pattern)) { profile.getCosmeticsData().setPattern(tier, slot, null); changed = true; } - if (material != null && !ArmorTrimPermissionManager.hasMaterialPermission(player, material)) { + if (material != null && !CosmeticsPermissionManager.hasMaterialPermission(player, material)) { profile.getCosmeticsData().setMaterial(tier, slot, null); changed = true; } @@ -59,10 +60,10 @@ public static boolean sanitize(Player player, Profile profile) { } ArmorTrimTier activeTier = profile.getCosmeticsData().getActiveTier(); - if (!ArmorTrimPermissionManager.hasBasePermission(player, activeTier)) { + if (!CosmeticsPermissionManager.hasBasePermission(player, activeTier)) { ArmorTrimTier replacement = null; for (ArmorTrimTier tier : ArmorTrimTier.values()) { - if (ArmorTrimPermissionManager.hasBasePermission(player, tier)) { + if (CosmeticsPermissionManager.hasBasePermission(player, tier)) { replacement = tier; break; } @@ -78,6 +79,29 @@ public static boolean sanitize(Player player, Profile profile) { } } + DeathEffect deathEffect = profile.getCosmeticsData().getDeathEffect(); + if (deathEffect != null && !CosmeticsPermissionManager.hasDeathEffectPermission(player, deathEffect)) { + profile.getCosmeticsData().setDeathEffect(DeathEffect.NONE); + changed = true; + } + + int maxShieldLayouts = CosmeticsPermissionManager.getMaxShieldLayouts(player); + var shieldLayouts = profile.getCosmeticsData().getShieldLayouts(); + while (shieldLayouts.size() > maxShieldLayouts) { + shieldLayouts.remove(shieldLayouts.size() - 1); + changed = true; + } + + int activeShieldLayoutIndex = profile.getCosmeticsData().getActiveShieldLayoutIndex(); + if (!CosmeticsPermissionManager.hasShieldPermission(player) + || activeShieldLayoutIndex < -1 + || activeShieldLayoutIndex >= shieldLayouts.size()) { + if (activeShieldLayoutIndex != -1) { + profile.getCosmeticsData().setActiveShieldLayoutIndex(-1); + changed = true; + } + } + if (changed) { profile.saveData(); } diff --git a/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/deatheffect/DeathEffect.java b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/deatheffect/DeathEffect.java new file mode 100644 index 00000000..7fea7e70 --- /dev/null +++ b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/deatheffect/DeathEffect.java @@ -0,0 +1,285 @@ +package dev.nandi0813.practice.manager.profile.cosmetics.deatheffect; + +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.protocol.particle.Particle; +import com.github.retrooper.packetevents.protocol.particle.data.ParticleDustData; +import com.github.retrooper.packetevents.protocol.particle.type.ParticleTypes; +import com.github.retrooper.packetevents.util.Vector3d; +import com.github.retrooper.packetevents.util.Vector3f; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerParticle; +import dev.nandi0813.practice.manager.backend.GUIFile; +import dev.nandi0813.practice.manager.fight.util.EntityHiderListener; +import lombok.Getter; +import org.bukkit.Color; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.entity.Player; + +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; + +/** + * Represents a kill effect cosmetic that plays at the victim's location on death. + * Every effect is fully configurable via guis.yml under GUIS.COSMETICS.DEATH-EFFECTS. + */ +@Getter +public enum DeathEffect { + + NONE( + "none", + "None", + Material.BARRIER + ), + FLAME( + "flame", + "Flame", + Material.BLAZE_POWDER + ), + LIGHTNING( + "lightning", + "Lightning", + Material.LIGHTNING_ROD + ), + FIREWORK( + "firework", + "Firework", + Material.FIREWORK_ROCKET + ), + EXPLOSION( + "explosion", + "Explosion", + Material.TNT + ), + BLOOD( + "blood", + "Blood", + Material.REDSTONE + ), + ENCHANT( + "enchant", + "Enchant", + Material.ENCHANTING_TABLE + ), + ENDER( + "ender", + "Ender", + Material.ENDER_PEARL + ), + HEARTS( + "hearts", + "Hearts", + Material.PINK_DYE + ), + ICE( + "ice", + "Ice", + Material.PACKED_ICE + ); + + private final String id; + private final String defaultDisplayName; + private final Material icon; + + DeathEffect(String id, String defaultDisplayName, Material icon) { + this.id = id; + this.defaultDisplayName = defaultDisplayName; + this.icon = icon; + } + + public String getDefaultDisplayName() { return defaultDisplayName; } + + /** Display name read from guis.yml with fallback to default. */ + public String getDisplayName() { + String key = "GUIS.COSMETICS.DEATH-EFFECTS.ENTRIES." + this.name() + ".DISPLAY-NAME"; + String val = GUIFile.getConfig().getString(key); + return (val != null && !val.isBlank()) ? val : defaultDisplayName; + } + + /** Icon material read from guis.yml with fallback. */ + public Material getConfiguredIcon() { + String key = "GUIS.COSMETICS.DEATH-EFFECTS.ENTRIES." + this.name() + ".ICON"; + String val = GUIFile.getConfig().getString(key); + if (val != null && !val.isBlank()) { + try { + return Material.valueOf(val.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException ignored) {} + } + return icon; + } + + public static String getPermissionNode(String id) { + return "zpp.cosmetics.deatheffect." + id; + } + + /** + * Plays this kill effect at the given location. + * Called from Match.killPlayer and FFA.killPlayer after a kill is confirmed. + */ + public void play(Location location) { + if (location == null || location.getWorld() == null) { + return; + } + play(location, location.getWorld().getPlayers()); + } + + public void play(Location location, Collection recipients) { + if (location == null || location.getWorld() == null || recipients == null || recipients.isEmpty()) { + return; + } + + List viewers = recipients.stream() + .filter(player -> player != null && player.isOnline()) + .filter(player -> player.getWorld().equals(location.getWorld())) + .collect(Collectors.toList()); + + if (viewers.isEmpty()) { + return; + } + + List particles = buildParticles(location); + if (!particles.isEmpty()) { + EntityHiderListener listener = EntityHiderListener.getInstance(); + for (Player viewer : viewers) { + listener.allowNextParticlePackets(viewer, particles.size()); + } + + for (ParticleSpec particleSpec : particles) { + WrapperPlayServerParticle packet = new WrapperPlayServerParticle( + particleSpec.particle, + false, + particleSpec.position, + particleSpec.offset, + particleSpec.speed, + particleSpec.count, + true + ); + + for (Player viewer : viewers) { + PacketEvents.getAPI().getPlayerManager().sendPacket(viewer, packet); + } + } + } + + playScopedSounds(location, viewers); + } + + private List buildParticles(Location location) { + Vector3d position = toVector3d(location); + switch (this) { + case NONE -> { + return List.of(); + } + + case FLAME -> { + return List.of( + spec(new Particle<>(ParticleTypes.FLAME), position, 60, 0.4f, 0.4f, 0.4f, 0.05f), + spec(new Particle<>(ParticleTypes.LAVA), position, 15, 0.3f, 0.3f, 0.3f, 0.0f) + ); + } + + case LIGHTNING -> { + return List.of( + spec(new Particle<>(ParticleTypes.ELECTRIC_SPARK), position, 80, 0.5f, 0.5f, 0.5f, 0.1f) + ); + } + + case FIREWORK -> { + return List.of( + spec(new Particle<>(ParticleTypes.FIREWORK), position, 90, 0.4f, 0.4f, 0.4f, 0.2f), + spec(new Particle<>(ParticleTypes.EXPLOSION), position, 2, 0.2f, 0.2f, 0.2f, 0.0f) + ); + } + + case EXPLOSION -> { + return List.of( + spec(new Particle<>(ParticleTypes.EXPLOSION), position, 5, 0.3f, 0.3f, 0.3f, 0.0f), + spec(new Particle<>(ParticleTypes.SMOKE), position, 40, 0.5f, 0.5f, 0.5f, 0.08f), + spec(new Particle<>(ParticleTypes.LARGE_SMOKE), position, 20, 0.4f, 0.4f, 0.4f, 0.04f) + ); + } + + case BLOOD -> { + return List.of( + spec(dust(1.5f, Color.RED), position, 80, 0.4f, 0.4f, 0.4f, 0.0f), + spec(dust(2.0f, Color.fromRGB(139, 0, 0)), position, 30, 0.2f, 0.2f, 0.2f, 0.0f) + ); + } + + case ENCHANT -> { + return List.of( + spec(new Particle<>(ParticleTypes.ENCHANT), position, 200, 0.5f, 0.5f, 0.5f, 0.5f), + spec(new Particle<>(ParticleTypes.ENCHANTED_HIT), position, 60, 0.4f, 0.4f, 0.4f, 0.3f), + spec(new Particle<>(ParticleTypes.WITCH), position, 30, 0.4f, 0.4f, 0.4f, 0.0f) + ); + } + + case ENDER -> { + return List.of( + spec(new Particle<>(ParticleTypes.PORTAL), position, 150, 0.5f, 0.5f, 0.5f, 1.0f), + spec(new Particle<>(ParticleTypes.SMOKE), position, 30, 0.4f, 0.4f, 0.4f, 0.05f), + spec(new Particle<>(ParticleTypes.WITCH), position, 20, 0.4f, 0.4f, 0.4f, 0.0f) + ); + } + + case HEARTS -> { + return List.of( + spec(new Particle<>(ParticleTypes.HEART), position, 25, 0.5f, 0.5f, 0.5f, 0.1f), + spec(dust(1.5f, Color.fromRGB(255, 105, 180)), position, 40, 0.4f, 0.4f, 0.4f, 0.0f) + ); + } + + case ICE -> { + return List.of( + spec(new Particle<>(ParticleTypes.SNOWFLAKE), position, 60, 0.5f, 0.5f, 0.5f, 0.1f), + spec(dust(1.5f, Color.fromRGB(173, 216, 230)), position, 30, 0.4f, 0.4f, 0.4f, 0.0f), + spec(new Particle<>(ParticleTypes.ITEM_SNOWBALL), position, 20, 0.4f, 0.3f, 0.4f, 0.05f) + ); + } + } + + return List.of(); + } + + private void playScopedSounds(Location location, List viewers) { + if (viewers.isEmpty()) { + return; + } + + switch (this) { + case LIGHTNING -> viewers.forEach(player -> player.playSound(location, Sound.ENTITY_LIGHTNING_BOLT_THUNDER, 1.0f, 1.0f)); + case FIREWORK -> viewers.forEach(player -> player.playSound(location, Sound.ENTITY_FIREWORK_ROCKET_BLAST, 1.0f, 1.0f)); + case EXPLOSION -> viewers.forEach(player -> player.playSound(location, Sound.ENTITY_GENERIC_EXPLODE, 0.8f, 1.0f)); + default -> { + } + } + } + + private static Particle dust(float scale, Color color) { + return new Particle<>(ParticleTypes.DUST, + new ParticleDustData(scale, color.getRed(), color.getGreen(), color.getBlue())); + } + + private static Vector3d toVector3d(Location location) { + return new Vector3d(location.getX(), location.getY(), location.getZ()); + } + + private static ParticleSpec spec(Particle particle, Vector3d position, int count, + float offsetX, float offsetY, float offsetZ, float speed) { + return new ParticleSpec(particle, position, new Vector3f(offsetX, offsetY, offsetZ), speed, count); + } + + private record ParticleSpec(Particle particle, Vector3d position, Vector3f offset, float speed, int count) { + } + + public static DeathEffect fromId(String id) { + if (id == null || id.isBlank()) return NONE; + String normalized = id.toLowerCase(Locale.ROOT); + for (DeathEffect ke : values()) { + if (ke.id.equals(normalized)) return ke; + } + return NONE; + } +} \ No newline at end of file diff --git a/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/shield/ShieldLayout.java b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/shield/ShieldLayout.java new file mode 100644 index 00000000..79f082fe --- /dev/null +++ b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/shield/ShieldLayout.java @@ -0,0 +1,124 @@ +package dev.nandi0813.practice.manager.profile.cosmetics.shield; + +import io.papermc.paper.registry.RegistryAccess; +import io.papermc.paper.registry.RegistryKey; +import lombok.Getter; +import lombok.Setter; +import org.bukkit.DyeColor; +import org.bukkit.NamespacedKey; +import org.bukkit.block.banner.PatternType; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +/** + * A named shield design saved by a player. + * Stores a base colour plus up to MAX_LAYERS banner pattern layers (colour + pattern). + */ +@Getter +@Setter +public class ShieldLayout { + + public static final int MAX_LAYERS = 6; + + /** Display name shown in the layout list GUI. */ + private String name; + + /** Base banner colour (null = white). */ + private DyeColor baseColor; + + /** Ordered list of pattern layers (like a real banner – bottom → top). */ + private final List layers; + + public ShieldLayout(String name, DyeColor baseColor) { + this.name = name; + this.baseColor = baseColor; + this.layers = new ArrayList<>(); + } + + /** Add a layer if MAX_LAYERS not reached. Returns true on success. */ + public boolean addLayer(DyeColor color, PatternType pattern) { + if (layers.size() >= MAX_LAYERS) return false; + layers.add(new PatternLayer(color, pattern)); + return true; + } + + /** Remove the topmost layer. Returns true if something was removed. */ + public boolean removeTopLayer() { + if (layers.isEmpty()) return false; + layers.removeLast(); + return true; + } + + // ── Serialisation helpers ──────────────────────────────────────── + + /** Serialise to a single string for YAML storage: "name|BASE_COLOR|COLOR:PATTERN,COLOR:PATTERN,..." */ + public String serialise() { + var bannerPatternRegistry = RegistryAccess.registryAccess().getRegistry(RegistryKey.BANNER_PATTERN); + StringBuilder sb = new StringBuilder(); + sb.append(escapePipe(name)).append("|"); + sb.append(baseColor != null ? baseColor.name() : "WHITE"); + for (PatternLayer l : layers) { + sb.append("|").append(l.color().name()).append(":").append(bannerPatternRegistry.getKeyOrThrow(l.pattern())); + } + return sb.toString(); + } + + /** Deserialise from the format produced by {@link #serialise()}. Returns null on error. */ + public static ShieldLayout deserialise(String raw) { + if (raw == null || raw.isBlank()) return null; + String[] parts = raw.split("\\|", -1); + if (parts.length < 2) return null; + try { + String layoutName = unescapePipe(parts[0]); + DyeColor base = DyeColor.valueOf(parts[1]); + ShieldLayout layout = new ShieldLayout(layoutName, base); + for (int i = 2; i < parts.length; i++) { + String[] lp = parts[i].split(":", 2); + if (lp.length == 2) { + DyeColor lc = DyeColor.valueOf(lp[0]); + PatternType pt = parsePatternType(lp[1]); + if (pt != null) { + layout.addLayer(lc, pt); + } + } + } + return layout; + } catch (Exception e) { + return null; + } + } + + private static PatternType parsePatternType(String raw) { + if (raw == null || raw.isBlank()) return null; + + String normalized = raw.trim().toLowerCase(Locale.ROOT); + if (!normalized.contains(":")) { + normalized = "minecraft:" + normalized; + } + + NamespacedKey key = NamespacedKey.fromString(normalized); + if (key == null) { + return null; + } + + return RegistryAccess.registryAccess() + .getRegistry(RegistryKey.BANNER_PATTERN) + .get(key); + } + + private static String escapePipe(String s) { return s.replace("|", "\\|"); } + private static String unescapePipe(String s) { return s.replace("\\|", "|"); } + + // ── PatternLayer record ────────────────────────────────────────── + + public record PatternLayer(DyeColor color, PatternType pattern) { + @Override + public boolean equals(Object obj) { + if (!(obj instanceof PatternLayer(DyeColor color1, PatternType pattern1))) return false; + return Objects.equals(color, color1) && Objects.equals(pattern, pattern1); + } + } +} diff --git a/core/src/main/resources/guis.yml b/core/src/main/resources/guis.yml index c6447625..0fab0c2a 100644 --- a/core/src/main/resources/guis.yml +++ b/core/src/main/resources/guis.yml @@ -1,4 +1,4 @@ -VERSION: 16 +VERSION: 17 GENERAL-FILLER-ITEM: NAME: " " @@ -3004,4 +3004,163 @@ GUIS: LAPIS: LAPIS_LAZULI AMETHYST: AMETHYST_SHARD RESIN: RESIN_BRICK - + HUB: + TITLE: "&8✦ Cosmetics" + BUTTONS: + ARMOR-TRIMS: + NAME: "&6✦ Armor Trims" + MATERIAL: DIAMOND_CHESTPLATE + GLOW: true + LORE: + - "" + - "&7Customize your armor tier," + - "&7trim patterns and materials." + - "" + - "&eClick to open." + SHIELD: + NAME: "&9✦ Shield" + MATERIAL: SHIELD + GLOW: false + LORE: + - "" + - "&7Design your shield with any" + - "&7color and pattern combination." + - "&7Save multiple layouts." + - "" + - "&eClick to open." + KILL-EFFECTS: + NAME: "&c✦ Death Effects" + MATERIAL: BLAZE_POWDER + GLOW: false + LORE: + - "" + - "&7Choose a particle effect that" + - "&7plays when you kill someone." + - "" + - "&eClick to open." + DEATH-EFFECTS: + TITLE: "&8✦ Death Effects" + NO-PERMISSION-MESSAGE: "You don't have permission for this death effect!" + SELECTED-PREFIX: "&a✔ " + UNLOCKED-PREFIX: "&e" + LOCKED-PREFIX: "&c🔒 " + CLICK-TO-SELECT: "&eClick to select." + CLICK-TO-DESELECT: "&7Click to deselect." + NO-PERMISSION-LORE: "&cRequires: &7%permission%" + DEFAULT-LORE: + - "" + - "&7Status: %status%" + - "" + ENTRIES: + NONE: + DISPLAY-NAME: "None" + ICON: BARRIER + LORE: + - "" + - "&7No kill effect." + - "&7Status: %status%" + - "" + FLAME: + DISPLAY-NAME: "Flame" + ICON: BLAZE_POWDER + LORE: + - "" + - "&7Bursts of fire on kill." + - "&7Status: %status%" + - "" + LIGHTNING: + DISPLAY-NAME: "Lightning" + ICON: LIGHTNING_ROD + LORE: + - "" + - "&7A lightning strike on kill." + - "&7Status: %status%" + - "" + FIREWORK: + DISPLAY-NAME: "Firework" + ICON: FIREWORK_ROCKET + LORE: + - "" + - "&7Fireworks burst on kill." + - "&7Status: %status%" + - "" + EXPLOSION: + DISPLAY-NAME: "Explosion" + ICON: TNT + LORE: + - "" + - "&7Smoke & fire explosion on kill." + - "&7Status: %status%" + - "" + BLOOD: + DISPLAY-NAME: "Blood" + ICON: REDSTONE + LORE: + - "" + - "&7Red dust particles on kill." + - "&7Status: %status%" + - "" + ENCHANT: + DISPLAY-NAME: "Enchant" + ICON: ENCHANTING_TABLE + LORE: + - "" + - "&7Magic enchant particles on kill." + - "&7Status: %status%" + - "" + ENDER: + DISPLAY-NAME: "Ender" + ICON: ENDER_PEARL + LORE: + - "" + - "&7Ender portal particles on kill." + - "&7Status: %status%" + - "" + HEARTS: + DISPLAY-NAME: "Hearts" + ICON: PINK_DYE + LORE: + - "" + - "&7Floating hearts on kill." + - "&7Status: %status%" + - "" + ICE: + DISPLAY-NAME: "Ice" + ICON: PACKED_ICE + LORE: + - "" + - "&7Snowflake & ice particles on kill." + - "&7Status: %status%" + - "" + # ── Shield Cosmetics ────────────────────────────────────────────────────── + SHIELD: + NO-PERMISSION-MESSAGE: "You don't have permission to use shield cosmetics!" + # Layout list GUI + LAYOUTS: + TITLE: "&8\u2756 Shield Layouts" + NAME-TITLE: "Layout Name" + RENAME-TITLE: "Rename Layout" + LIMIT-REACHED: "You've reached your layout limit! Get a higher rank for more." + DELETED-MESSAGE: "Layout deleted." + NEW-BUTTON: + NAME: "&aNew Layout" + # Editor GUI + EDITOR: + TITLE: "&8Editing: &e%name%" + APPLIED-MESSAGE: "Shield layout applied!" + UNAPPLIED-MESSAGE: "Shield cosmetic removed." + ADD-LAYER: + NAME: "&aAdd Layer" + MAX-LAYERS: + NAME: "&cMax Layers Reached" + MESSAGE: "Maximum of 6 layers reached!" + REMOVE-LAYER: + NAME: "&cRemove Top Layer" + # Color picker GUI + COLOR-PICKER: + BASE-TITLE: "&8Pick Base Color" + LAYER-TITLE: "&8Pick Layer Color" + # Pattern picker GUI + PATTERN-PICKER: + TITLE: "&8Pick Pattern" + APPLIED-MESSAGE: "Layer applied!" \ No newline at end of file diff --git a/core/src/main/resources/inventories.yml b/core/src/main/resources/inventories.yml index 0c768da4..ce89d92c 100644 --- a/core/src/main/resources/inventories.yml +++ b/core/src/main/resources/inventories.yml @@ -1,4 +1,4 @@ -VERSION: 1 +VERSION: 2 # # Spawn inventories @@ -17,6 +17,22 @@ LOBBY-BASIC: ITEM: NAME: "&cRanked Queue &7(Right-Click)" MATERIAL: IRON_SWORD + COSMETICS: + SLOT: 2 + ITEM: + NAME: "&dCosmetics Hub &7(Right-Click)" + MATERIAL: NETHER_STAR + LORE: + - "" + - "&7Customize your style with" + - "&fArmor Trims&7, &fShield Layouts&7," + - "and &fDeath Effects&7." + - "" + - "&eOpen the cosmetics menu." + ENCHANTMENTS: + - "DURABILITY:1" + FLAGS: + - "HIDE_ENCHANTS" ENABLE-SPECTATE-MODE: SLOT: 3 ITEM: diff --git a/core/src/main/resources/plugin.yml b/core/src/main/resources/plugin.yml index e8a8a795..20582211 100644 --- a/core/src/main/resources/plugin.yml +++ b/core/src/main/resources/plugin.yml @@ -326,23 +326,23 @@ permissions: # Cosmetics zpp.cosmetics.main: description: Open the cosmetics GUI. - zpp.cosmetics.base.*: + zpp.cosmetics.armortrim.base.*: description: Allow access to all armor base tiers. children: - zpp.cosmetics.base.leather: true - zpp.cosmetics.base.gold: true - zpp.cosmetics.base.iron: true - zpp.cosmetics.base.diamond: true - zpp.cosmetics.base.netherite: true - zpp.cosmetics.base.leather: + zpp.cosmetics.armortrim.base.leather: true + zpp.cosmetics.armortrim.base.gold: true + zpp.cosmetics.armortrim.base.iron: true + zpp.cosmetics.armortrim.base.diamond: true + zpp.cosmetics.armortrim.base.netherite: true + zpp.cosmetics.armortrim.base.leather: description: Allow access to Leather armor tier. - zpp.cosmetics.base.gold: + zpp.cosmetics.armortrim.base.gold: description: Allow access to Gold armor tier. - zpp.cosmetics.base.iron: + zpp.cosmetics.armortrim.base.iron: description: Allow access to Iron armor tier. - zpp.cosmetics.base.diamond: + zpp.cosmetics.armortrim.base.diamond: description: Allow access to Diamond armor tier. - zpp.cosmetics.base.netherite: + zpp.cosmetics.armortrim.base.netherite: description: Allow access to Netherite armor tier. # Note: zpp.cosmetics.pattern.* and zpp.cosmetics.material.* permissions are From 1c6185a405f0eca24236ad2e840119d8df7841e6 Mon Sep 17 00:00:00 2001 From: Nandor Dukat Date: Thu, 19 Mar 2026 11:28:29 +0100 Subject: [PATCH 20/26] updated workflow steps --- .github/workflows/maven-build.yml | 8 +++++--- .github/workflows/release.yml | 6 ++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/maven-build.yml b/.github/workflows/maven-build.yml index 5112d034..83ed4669 100644 --- a/.github/workflows/maven-build.yml +++ b/.github/workflows/maven-build.yml @@ -9,15 +9,17 @@ on: jobs: build: runs-on: ubuntu-latest + env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: lfs: true - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: '21' distribution: 'temurin' @@ -27,7 +29,7 @@ jobs: run: mvn -B clean install --file pom.xml - name: Upload Build Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ZonePractice-Plugin path: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5284041f..0f0558b1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,18 +10,20 @@ jobs: release: name: Create Release runs-on: ubuntu-latest + env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true permissions: contents: write steps: - name: Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 lfs: true - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: '21' distribution: 'temurin' From 30dee3e322d660024c39fcf816302192b82e46c7 Mon Sep 17 00:00:00 2001 From: MISHA <208148594+lokspel@users.noreply.github.com> Date: Thu, 19 Mar 2026 16:55:56 +0400 Subject: [PATCH 21/26] Modernize default ladders (#340) --- core/src/main/resources/ladders/axe.yml | 68 +++--------------- core/src/main/resources/ladders/fireball.yml | 71 ++++--------------- core/src/main/resources/ladders/mace.yml | 37 ++++++++-- .../src/main/resources/ladders/pearlfight.yml | 47 +++--------- 4 files changed, 63 insertions(+), 160 deletions(-) diff --git a/core/src/main/resources/ladders/axe.yml b/core/src/main/resources/ladders/axe.yml index ec5bc618..79be6fb7 100644 --- a/core/src/main/resources/ladders/axe.yml +++ b/core/src/main/resources/ladders/axe.yml @@ -21,62 +21,12 @@ settings: effects: [] icon: ==: org.bukkit.inventory.ItemStack - v: 3337 - type: IRON_AXE - meta: - ==: ItemMeta - meta-type: UNSPECIFIC - display-name: '{"extra":[{"bold":false,"italic":false,"underlined":false,"strikethrough":false,"obfuscated":false,"color":"gray","text":"Axe"}],"text":""}' -armor: | - rO0ABXcEAAAABHNyABpvcmcuYnVra2l0LnV0aWwuaW8uV3JhcHBlcvJQR+zxEm8FAgABTAADbWFw - dAAPTGphdmEvdXRpbC9NYXA7eHBzcgA1Y29tLmdvb2dsZS5jb21tb24uY29sbGVjdC5JbW11dGFi - bGVNYXAkU2VyaWFsaXplZEZvcm0AAAAAAAAAAAIAAkwABGtleXN0ABJMamF2YS9sYW5nL09iamVj - dDtMAAZ2YWx1ZXNxAH4ABHhwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAA - A3QAAj09dAABdnQABHR5cGV1cQB+AAYAAAADdAAeb3JnLmJ1a2tpdC5pbnZlbnRvcnkuSXRlbVN0 - YWNrc3IAEWphdmEubGFuZy5JbnRlZ2VyEuKgpPeBhzgCAAFJAAV2YWx1ZXhyABBqYXZhLmxhbmcu - TnVtYmVyhqyVHQuU4IsCAAB4cAAADQl0AApJUk9OX0JPT1RTc3EAfgAAc3EAfgADdXEAfgAGAAAA - A3EAfgAIcQB+AAlxAH4ACnVxAH4ABgAAAANxAH4ADHNxAH4ADQAADQl0AA1JUk9OX0xFR0dJTkdT - c3EAfgAAc3EAfgADdXEAfgAGAAAAA3EAfgAIcQB+AAlxAH4ACnVxAH4ABgAAAANxAH4ADHNxAH4A - DQAADQl0AA9JUk9OX0NIRVNUUExBVEVzcQB+AABzcQB+AAN1cQB+AAYAAAADcQB+AAhxAH4ACXEA - fgAKdXEAfgAGAAAAA3EAfgAMc3EAfgANAAANCXQAC0lST05fSEVMTUVU -inventory: | - rO0ABXcEAAAAJHNyABpvcmcuYnVra2l0LnV0aWwuaW8uV3JhcHBlcvJQR+zxEm8FAgABTAADbWFw - dAAPTGphdmEvdXRpbC9NYXA7eHBzcgA1Y29tLmdvb2dsZS5jb21tb24uY29sbGVjdC5JbW11dGFi - bGVNYXAkU2VyaWFsaXplZEZvcm0AAAAAAAAAAAIAAkwABGtleXN0ABJMamF2YS9sYW5nL09iamVj - dDtMAAZ2YWx1ZXNxAH4ABHhwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAA - BHQAAj09dAABdnQABHR5cGV0AARtZXRhdXEAfgAGAAAABHQAHm9yZy5idWtraXQuaW52ZW50b3J5 - Lkl0ZW1TdGFja3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2 - YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAA0JdAAISVJPTl9BWEVzcQB+AABzcQB+AAN1cQB+ - AAYAAAADcQB+AAh0AAltZXRhLXR5cGV0AAhlbmNoYW50c3VxAH4ABgAAAAN0AAhJdGVtTWV0YXQA - ClVOU1BFQ0lGSUNzcgA3Y29tLmdvb2dsZS5jb21tb24uY29sbGVjdC5JbW11dGFibGVCaU1hcCRT - ZXJpYWxpemVkRm9ybQAAAAAAAAAAAgAAeHEAfgADdXEAfgAGAAAAAXQACkRBTUFHRV9BTEx1cQB+ - AAYAAAABc3EAfgAOAAAAAXNxAH4AAHNxAH4AA3VxAH4ABgAAAARxAH4ACHEAfgAJcQB+AApxAH4A - C3VxAH4ABgAAAARxAH4ADXNxAH4ADgAADQl0AAZQT1RJT05zcQB+AABzcQB+AAN1cQB+AAYAAAAD - cQB+AAhxAH4AFXQAC3BvdGlvbi10eXBldXEAfgAGAAAAA3EAfgAYcQB+ACV0ABptaW5lY3JhZnQ6 - c3Ryb25nX3N3aWZ0bmVzc3NxAH4AAHNxAH4AA3VxAH4ABgAAAARxAH4ACHEAfgAJcQB+AApxAH4A - C3VxAH4ABgAAAARxAH4ADXNxAH4ADgAADQl0AA1TUExBU0hfUE9USU9Oc3EAfgAAc3EAfgADdXEA - fgAGAAAAA3EAfgAIcQB+ABVxAH4AKXVxAH4ABgAAAANxAH4AGHEAfgAldAAYbWluZWNyYWZ0OnN0 - cm9uZ19oZWFsaW5nc3EAfgAAc3EAfgADdXEAfgAGAAAABHEAfgAIcQB+AAlxAH4ACnEAfgALdXEA - fgAGAAAABHEAfgANc3EAfgAOAAANCXEAfgAxc3EAfgAAc3EAfgADdXEAfgAGAAAAA3EAfgAIcQB+ - ABVxAH4AKXVxAH4ABgAAAANxAH4AGHEAfgAldAAYbWluZWNyYWZ0OnN0cm9uZ19oZWFsaW5nc3EA - fgAAc3EAfgADdXEAfgAGAAAABHEAfgAIcQB+AAlxAH4ACnEAfgALdXEAfgAGAAAABHEAfgANc3EA - fgAOAAANCXEAfgAxc3EAfgAAc3EAfgADdXEAfgAGAAAAA3EAfgAIcQB+ABVxAH4AKXVxAH4ABgAA - AANxAH4AGHEAfgAldAAYbWluZWNyYWZ0OnN0cm9uZ19oZWFsaW5nc3EAfgAAc3EAfgADdXEAfgAG - AAAABHEAfgAIcQB+AAlxAH4ACnEAfgALdXEAfgAGAAAABHEAfgANc3EAfgAOAAANCXEAfgAxc3EA - fgAAc3EAfgADdXEAfgAGAAAAA3EAfgAIcQB+ABVxAH4AKXVxAH4ABgAAAANxAH4AGHEAfgAldAAY - bWluZWNyYWZ0OnN0cm9uZ19oZWFsaW5nc3EAfgAAc3EAfgADdXEAfgAGAAAABHEAfgAIcQB+AAlx - AH4ACnEAfgALdXEAfgAGAAAABHEAfgANc3EAfgAOAAANCXEAfgAxc3EAfgAAc3EAfgADdXEAfgAG - AAAAA3EAfgAIcQB+ABVxAH4AKXVxAH4ABgAAAANxAH4AGHEAfgAldAAYbWluZWNyYWZ0OnN0cm9u - Z19oZWFsaW5nc3EAfgAAc3EAfgADdXEAfgAGAAAABHEAfgAIcQB+AAlxAH4ACnEAfgALdXEAfgAG - AAAABHEAfgANc3EAfgAOAAANCXEAfgAxc3EAfgAAc3EAfgADdXEAfgAGAAAAA3EAfgAIcQB+ABVx - AH4AKXVxAH4ABgAAAANxAH4AGHEAfgAldAAYbWluZWNyYWZ0OnN0cm9uZ19oZWFsaW5nc3EAfgAA - c3EAfgADdXEAfgAGAAAABHEAfgAIcQB+AAlxAH4ACnQABmFtb3VudHVxAH4ABgAAAARxAH4ADXNx - AH4ADgAADQl0AAxHT0xERU5fQVBQTEVzcQB+AA4AAAAMcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBw - cHNxAH4AAHNxAH4AA3VxAH4ABgAAAARxAH4ACHEAfgAJcQB+AApxAH4AC3VxAH4ABgAAAARxAH4A - DXNxAH4ADgAADQlxAH4AMXNxAH4AAHNxAH4AA3VxAH4ABgAAAANxAH4ACHEAfgAVcQB+ACl1cQB+ - AAYAAAADcQB+ABhxAH4AJXQAGG1pbmVjcmFmdDpzdHJvbmdfaGVhbGluZ3NxAH4AAHNxAH4AA3Vx - AH4ABgAAAARxAH4ACHEAfgAJcQB+AApxAH4AC3VxAH4ABgAAAARxAH4ADXNxAH4ADgAADQlxAH4A - JXNxAH4AAHNxAH4AA3VxAH4ABgAAAANxAH4ACHEAfgAVcQB+ACl1cQB+AAYAAAADcQB+ABhxAH4A - JXQAGm1pbmVjcmFmdDpzdHJvbmdfc3dpZnRuZXNz -extra: | - rO0ABXcEAAAAAXA= + DataVersion: 4671 + id: minecraft:diamond_axe + count: 1 + components: + minecraft:custom_name: '"§bAxe"' + schema_version: 1 +armor: rO0ABXcEAAAABHNyABpvcmcuYnVra2l0LnV0aWwuaW8uV3JhcHBlcvJQR+zxEm8FAgABTAADbWFwdAAPTGphdmEvdXRpbC9NYXA7eHBzcgA1Y29tLmdvb2dsZS5jb21tb24uY29sbGVjdC5JbW11dGFibGVNYXAkU2VyaWFsaXplZEZvcm0AAAAAAAAAAAIAAkwABGtleXN0ABJMamF2YS9sYW5nL09iamVjdDtMAAZ2YWx1ZXNxAH4ABHhwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAABXQAAj09dAALRGF0YVZlcnNpb250AAJpZHQABWNvdW50dAAOc2NoZW1hX3ZlcnNpb251cQB+AAYAAAAFdAAeb3JnLmJ1a2tpdC5pbnZlbnRvcnkuSXRlbVN0YWNrc3IAEWphdmEubGFuZy5JbnRlZ2VyEuKgpPeBhzgCAAFJAAV2YWx1ZXhyABBqYXZhLmxhbmcuTnVtYmVyhqyVHQuU4IsCAAB4cAAAEj90ABdtaW5lY3JhZnQ6ZGlhbW9uZF9ib290c3NxAH4ADwAAAAFxAH4AE3NxAH4AAHNxAH4AA3VxAH4ABgAAAAVxAH4ACHEAfgAJcQB+AApxAH4AC3EAfgAMdXEAfgAGAAAABXEAfgAOc3EAfgAPAAASP3QAGm1pbmVjcmFmdDpkaWFtb25kX2xlZ2dpbmdzcQB+ABNxAH4AE3NxAH4AAHNxAH4AA3VxAH4ABgAAAAVxAH4ACHEAfgAJcQB+AApxAH4AC3EAfgAMdXEAfgAGAAAABXEAfgAOc3EAfgAPAAASP3QAHG1pbmVjcmFmdDpkaWFtb25kX2NoZXN0cGxhdGVxAH4AE3EAfgATc3EAfgAAc3EAfgADdXEAfgAGAAAABXEAfgAIcQB+AAlxAH4ACnEAfgALcQB+AAx1cQB+AAYAAAAFcQB+AA5zcQB+AA8AABI/dAAYbWluZWNyYWZ0OmRpYW1vbmRfaGVsbWV0cQB+ABNxAH4AEw== +inventory: rO0ABXcEAAAAJHNyABpvcmcuYnVra2l0LnV0aWwuaW8uV3JhcHBlcvJQR+zxEm8FAgABTAADbWFwdAAPTGphdmEvdXRpbC9NYXA7eHBzcgA1Y29tLmdvb2dsZS5jb21tb24uY29sbGVjdC5JbW11dGFibGVNYXAkU2VyaWFsaXplZEZvcm0AAAAAAAAAAAIAAkwABGtleXN0ABJMamF2YS9sYW5nL09iamVjdDtMAAZ2YWx1ZXNxAH4ABHhwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAABXQAAj09dAALRGF0YVZlcnNpb250AAJpZHQABWNvdW50dAAOc2NoZW1hX3ZlcnNpb251cQB+AAYAAAAFdAAeb3JnLmJ1a2tpdC5pbnZlbnRvcnkuSXRlbVN0YWNrc3IAEWphdmEubGFuZy5JbnRlZ2VyEuKgpPeBhzgCAAFJAAV2YWx1ZXhyABBqYXZhLmxhbmcuTnVtYmVyhqyVHQuU4IsCAAB4cAAAEj90ABVtaW5lY3JhZnQ6ZGlhbW9uZF9heGVzcQB+AA8AAAABcQB+ABNzcQB+AABzcQB+AAN1cQB+AAYAAAAFcQB+AAhxAH4ACXEAfgAKcQB+AAtxAH4ADHVxAH4ABgAAAAVxAH4ADnNxAH4ADwAAEj90ABdtaW5lY3JhZnQ6ZGlhbW9uZF9zd29yZHEAfgATcQB+ABNzcQB+AABzcQB+AAN1cQB+AAYAAAAFcQB+AAhxAH4ACXEAfgAKcQB+AAtxAH4ADHVxAH4ABgAAAAVxAH4ADnNxAH4ADwAAEj90ABJtaW5lY3JhZnQ6Y3Jvc3Nib3dxAH4AE3EAfgATcHBwcHBzcQB+AABzcQB+AAN1cQB+AAYAAAAFcQB+AAhxAH4ACXEAfgAKcQB+AAtxAH4ADHVxAH4ABgAAAAVxAH4ADnNxAH4ADwAAEj90AA9taW5lY3JhZnQ6YXJyb3dzcQB+AA8AAAAFcQB+ABNwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHA= +extra: rO0ABXcEAAAAAXNyABpvcmcuYnVra2l0LnV0aWwuaW8uV3JhcHBlcvJQR+zxEm8FAgABTAADbWFwdAAPTGphdmEvdXRpbC9NYXA7eHBzcgA1Y29tLmdvb2dsZS5jb21tb24uY29sbGVjdC5JbW11dGFibGVNYXAkU2VyaWFsaXplZEZvcm0AAAAAAAAAAAIAAkwABGtleXN0ABJMamF2YS9sYW5nL09iamVjdDtMAAZ2YWx1ZXNxAH4ABHhwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAABXQAAj09dAALRGF0YVZlcnNpb250AAJpZHQABWNvdW50dAAOc2NoZW1hX3ZlcnNpb251cQB+AAYAAAAFdAAeb3JnLmJ1a2tpdC5pbnZlbnRvcnkuSXRlbVN0YWNrc3IAEWphdmEubGFuZy5JbnRlZ2VyEuKgpPeBhzgCAAFJAAV2YWx1ZXhyABBqYXZhLmxhbmcuTnVtYmVyhqyVHQuU4IsCAAB4cAAAEj90ABBtaW5lY3JhZnQ6c2hpZWxkc3EAfgAPAAAAAXEAfgAT diff --git a/core/src/main/resources/ladders/fireball.yml b/core/src/main/resources/ladders/fireball.yml index f871030b..8bbfd04b 100644 --- a/core/src/main/resources/ladders/fireball.yml +++ b/core/src/main/resources/ladders/fireball.yml @@ -23,62 +23,15 @@ bed-respawn: 3 fireball-cooldown: 1.0 icon: ==: org.bukkit.inventory.ItemStack - v: 3337 - type: FIRE_CHARGE - meta: - ==: ItemMeta - meta-type: UNSPECIFIC - display-name: '{"extra":[{"bold":false,"italic":false,"underlined":false,"strikethrough":false,"obfuscated":false,"color":"gold","text":"Fireball - "},{"italic":false,"color":"gray","text":"Fight"}],"text":""}' -armor: | - rO0ABXcEAAAABHNyABpvcmcuYnVra2l0LnV0aWwuaW8uV3JhcHBlcvJQR+zxEm8FAgABTAADbWFw - dAAPTGphdmEvdXRpbC9NYXA7eHBzcgA1Y29tLmdvb2dsZS5jb21tb24uY29sbGVjdC5JbW11dGFi - bGVNYXAkU2VyaWFsaXplZEZvcm0AAAAAAAAAAAIAAkwABGtleXN0ABJMamF2YS9sYW5nL09iamVj - dDtMAAZ2YWx1ZXNxAH4ABHhwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAA - BHQAAj09dAABdnQABHR5cGV0AARtZXRhdXEAfgAGAAAABHQAHm9yZy5idWtraXQuaW52ZW50b3J5 - Lkl0ZW1TdGFja3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2 - YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAA0JdAANTEVBVEhFUl9CT09UU3NxAH4AAHNxAH4A - A3VxAH4ABgAAAANxAH4ACHQACW1ldGEtdHlwZXQABWNvbG9ydXEAfgAGAAAAA3QACEl0ZW1NZXRh - dAAPQ09MT1JBQkxFX0FSTU9Sc3EAfgAAc3EAfgADdXEAfgAGAAAABXEAfgAIdAAFQUxQSEF0AANS - RUR0AARCTFVFdAAFR1JFRU51cQB+AAYAAAAFdAAFQ29sb3JzcQB+AA4AAAD/c3EAfgAOAAAA/3Nx - AH4ADgAAAABxAH4AJXNxAH4AAHNxAH4AA3VxAH4ABgAAAARxAH4ACHEAfgAJcQB+AApxAH4AC3Vx - AH4ABgAAAARxAH4ADXNxAH4ADgAADQl0ABBMRUFUSEVSX0xFR0dJTkdTc3EAfgAAc3EAfgADdXEA - fgAGAAAAA3EAfgAIcQB+ABVxAH4AFnVxAH4ABgAAAANxAH4AGHEAfgAZc3EAfgAAc3EAfgADdXEA - fgAGAAAABXEAfgAIcQB+AB1xAH4AHnEAfgAfcQB+ACB1cQB+AAYAAAAFcQB+ACJzcQB+AA4AAAD/ - c3EAfgAOAAAA/3EAfgAlcQB+ACVzcQB+AABzcQB+AAN1cQB+AAYAAAAEcQB+AAhxAH4ACXEAfgAK - cQB+AAt1cQB+AAYAAAAEcQB+AA1zcQB+AA4AAA0JdAASTEVBVEhFUl9DSEVTVFBMQVRFc3EAfgAA - c3EAfgADdXEAfgAGAAAAA3EAfgAIcQB+ABVxAH4AFnVxAH4ABgAAAANxAH4AGHEAfgAZc3EAfgAA - c3EAfgADdXEAfgAGAAAABXEAfgAIcQB+AB1xAH4AHnEAfgAfcQB+ACB1cQB+AAYAAAAFcQB+ACJz - cQB+AA4AAAD/c3EAfgAOAAAA/3EAfgAlcQB+ACVzcQB+AABzcQB+AAN1cQB+AAYAAAAEcQB+AAhx - AH4ACXEAfgAKcQB+AAt1cQB+AAYAAAAEcQB+AA1zcQB+AA4AAA0JdAAOTEVBVEhFUl9IRUxNRVRz - cQB+AABzcQB+AAN1cQB+AAYAAAADcQB+AAhxAH4AFXEAfgAWdXEAfgAGAAAAA3EAfgAYcQB+ABlz - cQB+AABzcQB+AAN1cQB+AAYAAAAFcQB+AAhxAH4AHXEAfgAecQB+AB9xAH4AIHVxAH4ABgAAAAVx - AH4AInNxAH4ADgAAAP9zcQB+AA4AAAD/cQB+ACVxAH4AJQ== -inventory: | - rO0ABXcEAAAAJHNyABpvcmcuYnVra2l0LnV0aWwuaW8uV3JhcHBlcvJQR+zxEm8FAgABTAADbWFw - dAAPTGphdmEvdXRpbC9NYXA7eHBzcgA1Y29tLmdvb2dsZS5jb21tb24uY29sbGVjdC5JbW11dGFi - bGVNYXAkU2VyaWFsaXplZEZvcm0AAAAAAAAAAAIAAkwABGtleXN0ABJMamF2YS9sYW5nL09iamVj - dDtMAAZ2YWx1ZXNxAH4ABHhwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAA - A3QAAj09dAABdnQABHR5cGV1cQB+AAYAAAADdAAeb3JnLmJ1a2tpdC5pbnZlbnRvcnkuSXRlbVN0 - YWNrc3IAEWphdmEubGFuZy5JbnRlZ2VyEuKgpPeBhzgCAAFJAAV2YWx1ZXhyABBqYXZhLmxhbmcu - TnVtYmVyhqyVHQuU4IsCAAB4cAAADQl0AAtTVE9ORV9TV09SRHNxAH4AAHNxAH4AA3VxAH4ABgAA - AARxAH4ACHEAfgAJcQB+AAp0AAZhbW91bnR1cQB+AAYAAAAEcQB+AAxzcQB+AA0AAA0JdAAJQkxV - RV9XT09Mc3EAfgANAAAAQHNxAH4AAHNxAH4AA3VxAH4ABgAAAARxAH4ACHEAfgAJcQB+AApxAH4A - FHVxAH4ABgAAAARxAH4ADHNxAH4ADQAADQl0AAlFTkRfU1RPTkVzcQB+AA0AAAAIc3EAfgAAc3EA - fgADdXEAfgAGAAAABHEAfgAIcQB+AAlxAH4ACnEAfgAUdXEAfgAGAAAABHEAfgAMc3EAfgANAAAN - CXQAC0ZJUkVfQ0hBUkdFc3EAfgANAAAABnNxAH4AAHNxAH4AA3VxAH4ABgAAAARxAH4ACHEAfgAJ - cQB+AApxAH4AFHVxAH4ABgAAAARxAH4ADHNxAH4ADQAADQl0AANUTlRzcQB+AA0AAAACc3EAfgAA - c3EAfgADdXEAfgAGAAAABHEAfgAIcQB+AAlxAH4ACnQABG1ldGF1cQB+AAYAAAAEcQB+AAxzcQB+ - AA0AAA0JdAAOV09PREVOX1BJQ0tBWEVzcQB+AABzcQB+AAN1cQB+AAYAAAADcQB+AAh0AAltZXRh - LXR5cGV0AAhlbmNoYW50c3VxAH4ABgAAAAN0AAhJdGVtTWV0YXQAClVOU1BFQ0lGSUNzcgA3Y29t - Lmdvb2dsZS5jb21tb24uY29sbGVjdC5JbW11dGFibGVCaU1hcCRTZXJpYWxpemVkRm9ybQAAAAAA - AAAAAgAAeHEAfgADdXEAfgAGAAAAAXQACURJR19TUEVFRHVxAH4ABgAAAAFzcQB+AA0AAAABc3EA - fgAAc3EAfgADdXEAfgAGAAAABHEAfgAIcQB+AAlxAH4ACnEAfgAxdXEAfgAGAAAABHEAfgAMc3EA - fgANAAANCXQACldPT0RFTl9BWEVzcQB+AABzcQB+AAN1cQB+AAYAAAADcQB+AAhxAH4AOHEAfgA5 - dXEAfgAGAAAAA3EAfgA7cQB+ADxzcQB+AD11cQB+AAYAAAABcQB+AEB1cQB+AAYAAAABcQB+AEJz - cQB+AABzcQB+AAN1cQB+AAYAAAADcQB+AAhxAH4ACXEAfgAKdXEAfgAGAAAAA3EAfgAMc3EAfgAN - AAANCXQABlNIRUFSU3NxAH4AAHNxAH4AA3VxAH4ABgAAAARxAH4ACHEAfgAJcQB+AApxAH4AFHVx - AH4ABgAAAARxAH4ADHNxAH4ADQAADQl0AAZMQURERVJxAH4AH3BwcHBwcHBwcHBwcHBwcHBwcHBw - cHBwcHBwcA== -extra: | - rO0ABXcEAAAAAXA= \ No newline at end of file + DataVersion: 4671 + id: minecraft:fire_charge + count: 1 + components: + minecraft:custom_name: '{extra:[{bold:0b,color:"gold",italic:0b,obfuscated:0b,strikethrough:0b,text:"Fireball + ",underlined:0b},{color:"gray",italic:0b,text:"Fight"}],text:""}' + schema_version: 1 +armor: rO0ABXcEAAAABHNyABpvcmcuYnVra2l0LnV0aWwuaW8uV3JhcHBlcvJQR+zxEm8FAgABTAADbWFwdAAPTGphdmEvdXRpbC9NYXA7eHBzcgA1Y29tLmdvb2dsZS5jb21tb24uY29sbGVjdC5JbW11dGFibGVNYXAkU2VyaWFsaXplZEZvcm0AAAAAAAAAAAIAAkwABGtleXN0ABJMamF2YS9sYW5nL09iamVjdDtMAAZ2YWx1ZXNxAH4ABHhwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAABnQAAj09dAALRGF0YVZlcnNpb250AAJpZHQABWNvdW50dAAKY29tcG9uZW50c3QADnNjaGVtYV92ZXJzaW9udXEAfgAGAAAABnQAHm9yZy5idWtraXQuaW52ZW50b3J5Lkl0ZW1TdGFja3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAABI/dAAXbWluZWNyYWZ0OmxlYXRoZXJfYm9vdHNzcQB+ABAAAAABc3IAF2phdmEudXRpbC5MaW5rZWRIYXNoTWFwNMBOXBBswPsCAAFaAAthY2Nlc3NPcmRlcnhyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABdAAUbWluZWNyYWZ0OmR5ZWRfY29sb3J0AAgxNjczMzUyNXgAcQB+ABRzcQB+AABzcQB+AAN1cQB+AAYAAAAGcQB+AAhxAH4ACXEAfgAKcQB+AAtxAH4ADHEAfgANdXEAfgAGAAAABnEAfgAPc3EAfgAQAAASP3QAGm1pbmVjcmFmdDpsZWF0aGVyX2xlZ2dpbmdzcQB+ABRzcQB+ABU/QAAAAAAADHcIAAAAEAAAAAF0ABRtaW5lY3JhZnQ6ZHllZF9jb2xvcnQACDE2NzMzNTI1eABxAH4AFHNxAH4AAHNxAH4AA3VxAH4ABgAAAAZxAH4ACHEAfgAJcQB+AApxAH4AC3EAfgAMcQB+AA11cQB+AAYAAAAGcQB+AA9zcQB+ABAAABI/dAAcbWluZWNyYWZ0OmxlYXRoZXJfY2hlc3RwbGF0ZXEAfgAUc3EAfgAVP0AAAAAAAAx3CAAAABAAAAABdAAUbWluZWNyYWZ0OmR5ZWRfY29sb3J0AAgxNjczMzUyNXgAcQB+ABRzcQB+AABzcQB+AAN1cQB+AAYAAAAGcQB+AAhxAH4ACXEAfgAKcQB+AAtxAH4ADHEAfgANdXEAfgAGAAAABnEAfgAPc3EAfgAQAAASP3QAGG1pbmVjcmFmdDpsZWF0aGVyX2hlbG1ldHEAfgAUc3EAfgAVP0AAAAAAAAx3CAAAABAAAAABdAAUbWluZWNyYWZ0OmR5ZWRfY29sb3J0AAgxNjczMzUyNXgAcQB+ABQ= +inventory: rO0ABXcEAAAAJHNyABpvcmcuYnVra2l0LnV0aWwuaW8uV3JhcHBlcvJQR+zxEm8FAgABTAADbWFwdAAPTGphdmEvdXRpbC9NYXA7eHBzcgA1Y29tLmdvb2dsZS5jb21tb24uY29sbGVjdC5JbW11dGFibGVNYXAkU2VyaWFsaXplZEZvcm0AAAAAAAAAAAIAAkwABGtleXN0ABJMamF2YS9sYW5nL09iamVjdDtMAAZ2YWx1ZXNxAH4ABHhwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAABXQAAj09dAALRGF0YVZlcnNpb250AAJpZHQABWNvdW50dAAOc2NoZW1hX3ZlcnNpb251cQB+AAYAAAAFdAAeb3JnLmJ1a2tpdC5pbnZlbnRvcnkuSXRlbVN0YWNrc3IAEWphdmEubGFuZy5JbnRlZ2VyEuKgpPeBhzgCAAFJAAV2YWx1ZXhyABBqYXZhLmxhbmcuTnVtYmVyhqyVHQuU4IsCAAB4cAAAEj90ABVtaW5lY3JhZnQ6c3RvbmVfc3dvcmRzcQB+AA8AAAABcQB+ABNzcQB+AABzcQB+AAN1cQB+AAYAAAAFcQB+AAhxAH4ACXEAfgAKcQB+AAtxAH4ADHVxAH4ABgAAAAVxAH4ADnNxAH4ADwAAEj90ABVtaW5lY3JhZnQ6ZmlyZV9jaGFyZ2VzcQB+AA8AAAAGcQB+ABNzcQB+AABzcQB+AAN1cQB+AAYAAAAFcQB+AAhxAH4ACXEAfgAKcQB+AAtxAH4ADHVxAH4ABgAAAAVxAH4ADnNxAH4ADwAAEj90ABNtaW5lY3JhZnQ6ZW5kX3N0b25lc3EAfgAPAAAACHEAfgATc3EAfgAAc3EAfgADdXEAfgAGAAAABXEAfgAIcQB+AAlxAH4ACnEAfgALcQB+AAx1cQB+AAYAAAAFcQB+AA5zcQB+AA8AABI/dAAQbWluZWNyYWZ0OnNoZWFyc3EAfgATcQB+ABNzcQB+AABzcQB+AAN1cQB+AAYAAAAFcQB+AAhxAH4ACXEAfgAKcQB+AAtxAH4ADHVxAH4ABgAAAAVxAH4ADnNxAH4ADwAAEj90AA1taW5lY3JhZnQ6dG50c3EAfgAPAAAAAnEAfgATc3EAfgAAc3EAfgADdXEAfgAGAAAABnEAfgAIcQB+AAlxAH4ACnEAfgALdAAKY29tcG9uZW50c3EAfgAMdXEAfgAGAAAABnEAfgAOc3EAfgAPAAASP3QAFG1pbmVjcmFmdDp3b29kZW5fYXhlcQB+ABNzcgAXamF2YS51dGlsLkxpbmtlZEhhc2hNYXA0wE5cEGzA+wIAAVoAC2FjY2Vzc09yZGVyeHIAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAADHcIAAAAEAAAAAF0ABZtaW5lY3JhZnQ6ZW5jaGFudG1lbnRzdAAaeyJtaW5lY3JhZnQ6ZWZmaWNpZW5jeSI6MX14AHEAfgATc3EAfgAAc3EAfgADdXEAfgAGAAAABnEAfgAIcQB+AAlxAH4ACnEAfgALcQB+ADJxAH4ADHVxAH4ABgAAAAZxAH4ADnNxAH4ADwAAEj90ABhtaW5lY3JhZnQ6d29vZGVuX3BpY2theGVxAH4AE3NxAH4ANj9AAAAAAAAMdwgAAAAQAAAAAXQAFm1pbmVjcmFmdDplbmNoYW50bWVudHN0ABp7Im1pbmVjcmFmdDplZmZpY2llbmN5IjoxfXgAcQB+ABNzcQB+AABzcQB+AAN1cQB+AAYAAAAFcQB+AAhxAH4ACXEAfgAKcQB+AAtxAH4ADHVxAH4ABgAAAAVxAH4ADnNxAH4ADwAAEj90ABBtaW5lY3JhZnQ6bGFkZGVycQB+ACFxAH4AE3BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHA= +extra: rO0ABXcEAAAAAXNyABpvcmcuYnVra2l0LnV0aWwuaW8uV3JhcHBlcvJQR+zxEm8FAgABTAADbWFwdAAPTGphdmEvdXRpbC9NYXA7eHBzcgA1Y29tLmdvb2dsZS5jb21tb24uY29sbGVjdC5JbW11dGFibGVNYXAkU2VyaWFsaXplZEZvcm0AAAAAAAAAAAIAAkwABGtleXN0ABJMamF2YS9sYW5nL09iamVjdDtMAAZ2YWx1ZXNxAH4ABHhwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAABXQAAj09dAALRGF0YVZlcnNpb250AAJpZHQABWNvdW50dAAOc2NoZW1hX3ZlcnNpb251cQB+AAYAAAAFdAAeb3JnLmJ1a2tpdC5pbnZlbnRvcnkuSXRlbVN0YWNrc3IAEWphdmEubGFuZy5JbnRlZ2VyEuKgpPeBhzgCAAFJAAV2YWx1ZXhyABBqYXZhLmxhbmcuTnVtYmVyhqyVHQuU4IsCAAB4cAAAEj90ABNtaW5lY3JhZnQ6Ymx1ZV93b29sc3EAfgAPAAAAQHNxAH4ADwAAAAE= +destroyable-blocks: [] +fireball-block-destroy: true \ No newline at end of file diff --git a/core/src/main/resources/ladders/mace.yml b/core/src/main/resources/ladders/mace.yml index 0689b940..6c06e59b 100644 --- a/core/src/main/resources/ladders/mace.yml +++ b/core/src/main/resources/ladders/mace.yml @@ -11,16 +11,21 @@ settings: hitdelay: 20 rounds: 1 maxduration: 600 - epcooldown: 13 + epcooldown: 1 gacooldown: 0 fireworkcooldown: 1 startcountdown: 5 startmove: true matchtypes: - DUEL + - PARTY_FFA + - PARTY_SPLIT + - PARTY_VS_PARTY knockback: DEFAULT tntfusetime: 4 - healthbelowname: false + healthbelowname: true + resetbuildafterround: false + breakallblocks: false icon: ==: org.bukkit.inventory.ItemStack DataVersion: 4671 @@ -29,6 +34,28 @@ icon: components: minecraft:custom_name: '{extra:["Mace"],text:""}' schema_version: 1 -inventory: rO0ABXcEAAAAJHNyABpvcmcuYnVra2l0LnV0aWwuaW8uV3JhcHBlcvJQR+zxEm8FAgABTAADbWFwdAAPTGphdmEvdXRpbC9NYXA7eHBzcgA1Y29tLmdvb2dsZS5jb21tb24uY29sbGVjdC5JbW11dGFibGVNYXAkU2VyaWFsaXplZEZvcm0AAAAAAAAAAAIAAkwABGtleXN0ABJMamF2YS9sYW5nL09iamVjdDtMAAZ2YWx1ZXNxAH4ABHhwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAABnQAAj09dAALRGF0YVZlcnNpb250AAJpZHQABWNvdW50dAAKY29tcG9uZW50c3QADnNjaGVtYV92ZXJzaW9udXEAfgAGAAAABnQAHm9yZy5idWtraXQuaW52ZW50b3J5Lkl0ZW1TdGFja3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAABI/dAAObWluZWNyYWZ0Om1hY2VzcQB+ABAAAAABc3IAF2phdmEudXRpbC5MaW5rZWRIYXNoTWFwNMBOXBBswPsCAAFaAAthY2Nlc3NPcmRlcnhyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAACdAAVbWluZWNyYWZ0OnJlcGFpcl9jb3N0dAABMXQAFm1pbmVjcmFmdDplbmNoYW50bWVudHN0ABp7Im1pbmVjcmFmdDp3aW5kX2J1cnN0IjoxfXgAcQB+ABRzcQB+AABzcQB+AAN1cQB+AAYAAAAGcQB+AAhxAH4ACXEAfgAKcQB+AAtxAH4ADHEAfgANdXEAfgAGAAAABnEAfgAPc3EAfgAQAAASP3QAEW1pbmVjcmFmdDp0cmlkZW50cQB+ABRzcQB+ABU/QAAAAAAADHcIAAAAEAAAAAJ0ABVtaW5lY3JhZnQ6cmVwYWlyX2Nvc3R0AAExdAAWbWluZWNyYWZ0OmVuY2hhbnRtZW50c3QAF3sibWluZWNyYWZ0OnJpcHRpZGUiOjJ9eABxAH4AFHNxAH4AAHNxAH4AA3VxAH4ABgAAAAVxAH4ACHEAfgAJcQB+AApxAH4AC3EAfgANdXEAfgAGAAAABXEAfgAPc3EAfgAQAAASP3QAFW1pbmVjcmFmdDplbmRlcl9wZWFybHNxAH4AEAAAAAVxAH4AFHNxAH4AAHNxAH4AA3VxAH4ABgAAAAVxAH4ACHEAfgAJcQB+AApxAH4AC3EAfgANdXEAfgAGAAAABXEAfgAPc3EAfgAQAAASP3QAEG1pbmVjcmFmdDplbHl0cmFxAH4AFHEAfgAUc3EAfgAAc3EAfgADdXEAfgAGAAAABXEAfgAIcQB+AAlxAH4ACnEAfgALcQB+AA11cQB+AAYAAAAFcQB+AA9zcQB+ABAAABI/dAAabWluZWNyYWZ0OnRvdGVtX29mX3VuZHlpbmdxAH4AFHEAfgAUc3EAfgAAc3EAfgADdXEAfgAGAAAABXEAfgAIcQB+AAlxAH4ACnEAfgALcQB+AA11cQB+AAYAAAAFcQB+AA9zcQB+ABAAABI/dAAabWluZWNyYWZ0OnRvdGVtX29mX3VuZHlpbmdxAH4AFHEAfgAUc3EAfgAAc3EAfgADdXEAfgAGAAAABXEAfgAIcQB+AAlxAH4ACnEAfgALcQB+AA11cQB+AAYAAAAFcQB+AA9zcQB+ABAAABI/dAAWbWluZWNyYWZ0OmdvbGRlbl9hcHBsZXNxAH4AEAAAAAxxAH4AFHNxAH4AAHNxAH4AA3VxAH4ABgAAAAVxAH4ACHEAfgAJcQB+AApxAH4AC3EAfgANdXEAfgAGAAAABXEAfgAPc3EAfgAQAAASP3QAFW1pbmVjcmFmdDp3aW5kX2NoYXJnZXNxAH4AEAAAAEBxAH4AFHNxAH4AAHNxAH4AA3VxAH4ABgAAAAVxAH4ACHEAfgAJcQB+AApxAH4AC3EAfgANdXEAfgAGAAAABXEAfgAPc3EAfgAQAAASP3QAFm1pbmVjcmFmdDp3YXRlcl9idWNrZXRxAH4AFHEAfgAUcHBwcHBwcHBzcQB+AABzcQB+AAN1cQB+AAYAAAAFcQB+AAhxAH4ACXEAfgAKcQB+AAtxAH4ADXVxAH4ABgAAAAVxAH4AD3NxAH4AEAAAEj90ABZtaW5lY3JhZnQ6d2F0ZXJfYnVja2V0cQB+ABRxAH4AFHBwcHBwcHBwc3EAfgAAc3EAfgADdXEAfgAGAAAABXEAfgAIcQB+AAlxAH4ACnEAfgALcQB+AA11cQB+AAYAAAAFcQB+AA9zcQB+ABAAABI/dAAWbWluZWNyYWZ0OndhdGVyX2J1Y2tldHEAfgAUcQB+ABRwcHBwcHBwcHNxAH4AAHNxAH4AA3VxAH4ABgAAAAVxAH4ACHEAfgAJcQB+AApxAH4AC3EAfgANdXEAfgAGAAAABXEAfgAPc3EAfgAQAAASP3QAFm1pbmVjcmFmdDp3YXRlcl9idWNrZXRxAH4AFHEAfgAU -armor: rO0ABXcEAAAABHNyABpvcmcuYnVra2l0LnV0aWwuaW8uV3JhcHBlcvJQR+zxEm8FAgABTAADbWFwdAAPTGphdmEvdXRpbC9NYXA7eHBzcgA1Y29tLmdvb2dsZS5jb21tb24uY29sbGVjdC5JbW11dGFibGVNYXAkU2VyaWFsaXplZEZvcm0AAAAAAAAAAAIAAkwABGtleXN0ABJMamF2YS9sYW5nL09iamVjdDtMAAZ2YWx1ZXNxAH4ABHhwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAABnQAAj09dAALRGF0YVZlcnNpb250AAJpZHQABWNvdW50dAAKY29tcG9uZW50c3QADnNjaGVtYV92ZXJzaW9udXEAfgAGAAAABnQAHm9yZy5idWtraXQuaW52ZW50b3J5Lkl0ZW1TdGFja3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAABI/dAAZbWluZWNyYWZ0Om5ldGhlcml0ZV9ib290c3NxAH4AEAAAAAFzcgAXamF2YS51dGlsLkxpbmtlZEhhc2hNYXA0wE5cEGzA+wIAAVoAC2FjY2Vzc09yZGVyeHIAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAADHcIAAAAEAAAAAJ0ABVtaW5lY3JhZnQ6cmVwYWlyX2Nvc3R0AAExdAAWbWluZWNyYWZ0OmVuY2hhbnRtZW50c3QAH3sibWluZWNyYWZ0OmZlYXRoZXJfZmFsbGluZyI6M314AHEAfgAUc3EAfgAAc3EAfgADdXEAfgAGAAAABXEAfgAIcQB+AAlxAH4ACnEAfgALcQB+AA11cQB+AAYAAAAFcQB+AA9zcQB+ABAAABI/dAAcbWluZWNyYWZ0Om5ldGhlcml0ZV9sZWdnaW5nc3EAfgAUcQB+ABRzcQB+AABzcQB+AAN1cQB+AAYAAAAFcQB+AAhxAH4ACXEAfgAKcQB+AAtxAH4ADXVxAH4ABgAAAAVxAH4AD3NxAH4AEAAAEj90AB5taW5lY3JhZnQ6bmV0aGVyaXRlX2NoZXN0cGxhdGVxAH4AFHEAfgAUc3EAfgAAc3EAfgADdXEAfgAGAAAABXEAfgAIcQB+AAlxAH4ACnEAfgALcQB+AA11cQB+AAYAAAAFcQB+AA9zcQB+ABAAABI/dAAabWluZWNyYWZ0Om5ldGhlcml0ZV9oZWxtZXRxAH4AFHEAfgAU -extra: rO0ABXcEAAAAAXNyABpvcmcuYnVra2l0LnV0aWwuaW8uV3JhcHBlcvJQR+zxEm8FAgABTAADbWFwdAAPTGphdmEvdXRpbC9NYXA7eHBzcgA1Y29tLmdvb2dsZS5jb21tb24uY29sbGVjdC5JbW11dGFibGVNYXAkU2VyaWFsaXplZEZvcm0AAAAAAAAAAAIAAkwABGtleXN0ABJMamF2YS9sYW5nL09iamVjdDtMAAZ2YWx1ZXNxAH4ABHhwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAABXQAAj09dAALRGF0YVZlcnNpb250AAJpZHQABWNvdW50dAAOc2NoZW1hX3ZlcnNpb251cQB+AAYAAAAFdAAeb3JnLmJ1a2tpdC5pbnZlbnRvcnkuSXRlbVN0YWNrc3IAEWphdmEubGFuZy5JbnRlZ2VyEuKgpPeBhzgCAAFJAAV2YWx1ZXhyABBqYXZhLmxhbmcuTnVtYmVyhqyVHQuU4IsCAAB4cAAAEj90ABVtaW5lY3JhZnQ6d2luZF9jaGFyZ2VzcQB+AA8AAABAc3EAfgAPAAAAAQ== +inventory: rO0ABXcEAAAAJHNyABpvcmcuYnVra2l0LnV0aWwuaW8uV3JhcHBlcvJQR+zxEm8FAgABTAADbWFwdAAPTGphdmEvdXRpbC9NYXA7eHBzcgA1Y29tLmdvb2dsZS5jb21tb24uY29sbGVjdC5JbW11dGFibGVNYXAkU2VyaWFsaXplZEZvcm0AAAAAAAAAAAIAAkwABGtleXN0ABJMamF2YS9sYW5nL09iamVjdDtMAAZ2YWx1ZXNxAH4ABHhwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAABnQAAj09dAALRGF0YVZlcnNpb250AAJpZHQABWNvdW50dAAKY29tcG9uZW50c3QADnNjaGVtYV92ZXJzaW9udXEAfgAGAAAABnQAHm9yZy5idWtraXQuaW52ZW50b3J5Lkl0ZW1TdGFja3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAABI/dAAZbWluZWNyYWZ0Om5ldGhlcml0ZV9zd29yZHNxAH4AEAAAAAFzcgAXamF2YS51dGlsLkxpbmtlZEhhc2hNYXA0wE5cEGzA+wIAAVoAC2FjY2Vzc09yZGVyeHIAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAADHcIAAAAEAAAAAJ0ABVtaW5lY3JhZnQ6cmVwYWlyX2Nvc3R0AAEzdAAWbWluZWNyYWZ0OmVuY2hhbnRtZW50c3QAMnsibWluZWNyYWZ0OnNoYXJwbmVzcyI6NSwibWluZWNyYWZ0OnVuYnJlYWtpbmciOjN9eABxAH4AFHNxAH4AAHNxAH4AA3VxAH4ABgAAAAZxAH4ACHEAfgAJcQB+AApxAH4AC3EAfgAMcQB+AA11cQB+AAYAAAAGcQB+AA9zcQB+ABAAABI/dAAXbWluZWNyYWZ0Om5ldGhlcml0ZV9heGVxAH4AFHNxAH4AFT9AAAAAAAAMdwgAAAAQAAAAAnQAFW1pbmVjcmFmdDpyZXBhaXJfY29zdHQAATN0ABZtaW5lY3JhZnQ6ZW5jaGFudG1lbnRzdAAyeyJtaW5lY3JhZnQ6c2hhcnBuZXNzIjo1LCJtaW5lY3JhZnQ6dW5icmVha2luZyI6M314AHEAfgAUc3EAfgAAc3EAfgADdXEAfgAGAAAABXEAfgAIcQB+AAlxAH4ACnEAfgALcQB+AA11cQB+AAYAAAAFcQB+AA9zcQB+ABAAABI/dAAVbWluZWNyYWZ0OmVuZGVyX3BlYXJsc3EAfgAQAAAAEHEAfgAUc3EAfgAAc3EAfgADdXEAfgAGAAAABXEAfgAIcQB+AAlxAH4ACnEAfgALcQB+AA11cQB+AAYAAAAFcQB+AA9zcQB+ABAAABI/dAAWbWluZWNyYWZ0OmdvbGRlbl9hcHBsZXNxAH4AEAAAAEBxAH4AFHNxAH4AAHNxAH4AA3VxAH4ABgAAAAVxAH4ACHEAfgAJcQB+AApxAH4AC3EAfgANdXEAfgAGAAAABXEAfgAPc3EAfgAQAAASP3QAEG1pbmVjcmFmdDplbHl0cmFxAH4AFHEAfgAUc3EAfgAAc3EAfgADdXEAfgAGAAAABXEAfgAIcQB+AAlxAH4ACnEAfgALcQB+AA11cQB+AAYAAAAFcQB+AA9zcQB+ABAAABI/dAAVbWluZWNyYWZ0OndpbmRfY2hhcmdlcQB+ADRxAH4AFHNxAH4AAHNxAH4AA3VxAH4ABgAAAAZxAH4ACHEAfgAJcQB+AApxAH4AC3EAfgAMcQB+AA11cQB+AAYAAAAGcQB+AA9zcQB+ABAAABI/dAAObWluZWNyYWZ0Om1hY2VxAH4AFHNxAH4AFT9AAAAAAAAMdwgAAAAQAAAAAnQAFW1pbmVjcmFmdDpyZXBhaXJfY29zdHQAATd0ABZtaW5lY3JhZnQ6ZW5jaGFudG1lbnRzdABJeyJtaW5lY3JhZnQ6ZGVuc2l0eSI6NSwibWluZWNyYWZ0OnVuYnJlYWtpbmciOjMsIm1pbmVjcmFmdDp3aW5kX2J1cnN0IjoxfXgAcQB+ABRzcQB+AABzcQB+AAN1cQB+AAYAAAAGcQB+AAhxAH4ACXEAfgAKcQB+AAtxAH4ADHEAfgANdXEAfgAGAAAABnEAfgAPc3EAfgAQAAASP3QADm1pbmVjcmFmdDptYWNlcQB+ABRzcQB+ABU/QAAAAAAADHcIAAAAEAAAAAJ0ABVtaW5lY3JhZnQ6cmVwYWlyX2Nvc3R0AAEzdAAWbWluZWNyYWZ0OmVuY2hhbnRtZW50c3QAL3sibWluZWNyYWZ0OmJyZWFjaCI6NCwibWluZWNyYWZ0OnVuYnJlYWtpbmciOjN9eABxAH4AFHNxAH4AAHNxAH4AA3VxAH4ABgAAAAZxAH4ACHEAfgAJcQB+AApxAH4AC3EAfgAMcQB+AA11cQB+AAYAAAAGcQB+AA9zcQB+ABAAABI/dAAQbWluZWNyYWZ0OnNoaWVsZHEAfgAUc3EAfgAVP0AAAAAAAAx3CAAAABAAAAACdAAVbWluZWNyYWZ0OnJlcGFpcl9jb3N0dAABM3QAFm1pbmVjcmFmdDplbmNoYW50bWVudHN0ADB7Im1pbmVjcmFmdDptZW5kaW5nIjoxLCJtaW5lY3JhZnQ6dW5icmVha2luZyI6M314AHEAfgAUc3EAfgAAc3EAfgADdXEAfgAGAAAABXEAfgAIcQB+AAlxAH4ACnEAfgALcQB+AA11cQB+AAYAAAAFcQB+AA9zcQB+ABAAABI/dAAVbWluZWNyYWZ0OmVuZGVyX3BlYXJscQB+AC1xAH4AFHNxAH4AAHNxAH4AA3VxAH4ABgAAAAZxAH4ACHEAfgAJcQB+AApxAH4AC3EAfgAMcQB+AA11cQB+AAYAAAAGcQB+AA9zcQB+ABAAABI/dAAXbWluZWNyYWZ0OnNwbGFzaF9wb3Rpb25xAH4AFHNxAH4AFT9AAAAAAAAMdwgAAAAQAAAAAXQAGW1pbmVjcmFmdDpwb3Rpb25fY29udGVudHN0ACR7cG90aW9uOiJtaW5lY3JhZnQ6c3Ryb25nX3N0cmVuZ3RoIn14AHEAfgAUc3EAfgAAc3EAfgADdXEAfgAGAAAABnEAfgAIcQB+AAlxAH4ACnEAfgALcQB+AAxxAH4ADXVxAH4ABgAAAAZxAH4AD3NxAH4AEAAAEj90ABdtaW5lY3JhZnQ6c3BsYXNoX3BvdGlvbnEAfgAUc3EAfgAVP0AAAAAAAAx3CAAAABAAAAABdAAZbWluZWNyYWZ0OnBvdGlvbl9jb250ZW50c3QAJHtwb3Rpb246Im1pbmVjcmFmdDpzdHJvbmdfc3RyZW5ndGgifXgAcQB+ABRzcQB+AABzcQB+AAN1cQB+AAYAAAAGcQB+AAhxAH4ACXEAfgAKcQB+AAtxAH4ADHEAfgANdXEAfgAGAAAABnEAfgAPc3EAfgAQAAASP3QAF21pbmVjcmFmdDpzcGxhc2hfcG90aW9ucQB+ABRzcQB+ABU/QAAAAAAADHcIAAAAEAAAAAF0ABltaW5lY3JhZnQ6cG90aW9uX2NvbnRlbnRzdAAke3BvdGlvbjoibWluZWNyYWZ0OnN0cm9uZ19zdHJlbmd0aCJ9eABxAH4AFHNxAH4AAHNxAH4AA3VxAH4ABgAAAAZxAH4ACHEAfgAJcQB+AApxAH4AC3EAfgAMcQB+AA11cQB+AAYAAAAGcQB+AA9zcQB+ABAAABI/dAAXbWluZWNyYWZ0OnNwbGFzaF9wb3Rpb25xAH4AFHNxAH4AFT9AAAAAAAAMdwgAAAAQAAAAAXQAGW1pbmVjcmFmdDpwb3Rpb25fY29udGVudHN0ACR7cG90aW9uOiJtaW5lY3JhZnQ6c3Ryb25nX3N0cmVuZ3RoIn14AHEAfgAUc3EAfgAAc3EAfgADdXEAfgAGAAAABnEAfgAIcQB+AAlxAH4ACnEAfgALcQB+AAxxAH4ADXVxAH4ABgAAAAZxAH4AD3NxAH4AEAAAEj90ABdtaW5lY3JhZnQ6c3BsYXNoX3BvdGlvbnEAfgAUc3EAfgAVP0AAAAAAAAx3CAAAABAAAAABdAAZbWluZWNyYWZ0OnBvdGlvbl9jb250ZW50c3QAJHtwb3Rpb246Im1pbmVjcmFmdDpzdHJvbmdfc3RyZW5ndGgifXgAcQB+ABRzcQB+AABzcQB+AAN1cQB+AAYAAAAGcQB+AAhxAH4ACXEAfgAKcQB+AAtxAH4ADHEAfgANdXEAfgAGAAAABnEAfgAPc3EAfgAQAAASP3QAF21pbmVjcmFmdDpzcGxhc2hfcG90aW9ucQB+ABRzcQB+ABU/QAAAAAAADHcIAAAAEAAAAAF0ABltaW5lY3JhZnQ6cG90aW9uX2NvbnRlbnRzdAAke3BvdGlvbjoibWluZWNyYWZ0OnN0cm9uZ19zdHJlbmd0aCJ9eABxAH4AFHNxAH4AAHNxAH4AA3VxAH4ABgAAAAZxAH4ACHEAfgAJcQB+AApxAH4AC3EAfgAMcQB+AA11cQB+AAYAAAAGcQB+AA9zcQB+ABAAABI/dAAXbWluZWNyYWZ0OnNwbGFzaF9wb3Rpb25xAH4AFHNxAH4AFT9AAAAAAAAMdwgAAAAQAAAAAXQAGW1pbmVjcmFmdDpwb3Rpb25fY29udGVudHN0ACR7cG90aW9uOiJtaW5lY3JhZnQ6c3Ryb25nX3N0cmVuZ3RoIn14AHEAfgAUc3EAfgAAc3EAfgADdXEAfgAGAAAABnEAfgAIcQB+AAlxAH4ACnEAfgALcQB+AAxxAH4ADXVxAH4ABgAAAAZxAH4AD3NxAH4AEAAAEj90ABVtaW5lY3JhZnQ6c2h1bGtlcl9ib3hxAH4AFHNxAH4AFT9AAAAAAAAMdwgAAAAQAAAAAXQAE21pbmVjcmFmdDpjb250YWluZXJ0DfFbe2l0ZW06e2NvbXBvbmVudHM6eyJtaW5lY3JhZnQ6cG90aW9uX2NvbnRlbnRzIjp7cG90aW9uOiJtaW5lY3JhZnQ6c3Ryb25nX3N0cmVuZ3RoIn19LGNvdW50OjEsaWQ6Im1pbmVjcmFmdDpzcGxhc2hfcG90aW9uIn0sc2xvdDowfSx7aXRlbTp7Y29tcG9uZW50czp7Im1pbmVjcmFmdDpwb3Rpb25fY29udGVudHMiOntwb3Rpb246Im1pbmVjcmFmdDpzdHJvbmdfc3RyZW5ndGgifX0sY291bnQ6MSxpZDoibWluZWNyYWZ0OnNwbGFzaF9wb3Rpb24ifSxzbG90OjF9LHtpdGVtOntjb21wb25lbnRzOnsibWluZWNyYWZ0OnBvdGlvbl9jb250ZW50cyI6e3BvdGlvbjoibWluZWNyYWZ0OnN0cm9uZ19zdHJlbmd0aCJ9fSxjb3VudDoxLGlkOiJtaW5lY3JhZnQ6c3BsYXNoX3BvdGlvbiJ9LHNsb3Q6Mn0se2l0ZW06e2NvbXBvbmVudHM6eyJtaW5lY3JhZnQ6cG90aW9uX2NvbnRlbnRzIjp7cG90aW9uOiJtaW5lY3JhZnQ6c3Ryb25nX3N0cmVuZ3RoIn19LGNvdW50OjEsaWQ6Im1pbmVjcmFmdDpzcGxhc2hfcG90aW9uIn0sc2xvdDozfSx7aXRlbTp7Y29tcG9uZW50czp7Im1pbmVjcmFmdDpwb3Rpb25fY29udGVudHMiOntwb3Rpb246Im1pbmVjcmFmdDpzdHJvbmdfc3RyZW5ndGgifX0sY291bnQ6MSxpZDoibWluZWNyYWZ0OnNwbGFzaF9wb3Rpb24ifSxzbG90OjR9LHtpdGVtOntjb21wb25lbnRzOnsibWluZWNyYWZ0OnBvdGlvbl9jb250ZW50cyI6e3BvdGlvbjoibWluZWNyYWZ0OnN0cm9uZ19zdHJlbmd0aCJ9fSxjb3VudDoxLGlkOiJtaW5lY3JhZnQ6c3BsYXNoX3BvdGlvbiJ9LHNsb3Q6NX0se2l0ZW06e2NvbXBvbmVudHM6eyJtaW5lY3JhZnQ6cG90aW9uX2NvbnRlbnRzIjp7cG90aW9uOiJtaW5lY3JhZnQ6c3Ryb25nX3N0cmVuZ3RoIn19LGNvdW50OjEsaWQ6Im1pbmVjcmFmdDpzcGxhc2hfcG90aW9uIn0sc2xvdDo2fSx7aXRlbTp7Y29tcG9uZW50czp7Im1pbmVjcmFmdDpwb3Rpb25fY29udGVudHMiOntwb3Rpb246Im1pbmVjcmFmdDpzdHJvbmdfc3RyZW5ndGgifX0sY291bnQ6MSxpZDoibWluZWNyYWZ0OnNwbGFzaF9wb3Rpb24ifSxzbG90Ojd9LHtpdGVtOntjb21wb25lbnRzOnsibWluZWNyYWZ0OnBvdGlvbl9jb250ZW50cyI6e3BvdGlvbjoibWluZWNyYWZ0OnN0cm9uZ19zdHJlbmd0aCJ9fSxjb3VudDoxLGlkOiJtaW5lY3JhZnQ6c3BsYXNoX3BvdGlvbiJ9LHNsb3Q6OH0se2l0ZW06e2NvbXBvbmVudHM6eyJtaW5lY3JhZnQ6cG90aW9uX2NvbnRlbnRzIjp7cG90aW9uOiJtaW5lY3JhZnQ6c3Ryb25nX3N3aWZ0bmVzcyJ9fSxjb3VudDoxLGlkOiJtaW5lY3JhZnQ6c3BsYXNoX3BvdGlvbiJ9LHNsb3Q6OX0se2l0ZW06e2NvbXBvbmVudHM6eyJtaW5lY3JhZnQ6cG90aW9uX2NvbnRlbnRzIjp7cG90aW9uOiJtaW5lY3JhZnQ6c3Ryb25nX3N3aWZ0bmVzcyJ9fSxjb3VudDoxLGlkOiJtaW5lY3JhZnQ6c3BsYXNoX3BvdGlvbiJ9LHNsb3Q6MTB9LHtpdGVtOntjb21wb25lbnRzOnsibWluZWNyYWZ0OnBvdGlvbl9jb250ZW50cyI6e3BvdGlvbjoibWluZWNyYWZ0OnN0cm9uZ19zdHJlbmd0aCJ9fSxjb3VudDoxLGlkOiJtaW5lY3JhZnQ6c3BsYXNoX3BvdGlvbiJ9LHNsb3Q6MTF9LHtpdGVtOntjb21wb25lbnRzOnsibWluZWNyYWZ0OnBvdGlvbl9jb250ZW50cyI6e3BvdGlvbjoibWluZWNyYWZ0OnN0cm9uZ19zdHJlbmd0aCJ9fSxjb3VudDoxLGlkOiJtaW5lY3JhZnQ6c3BsYXNoX3BvdGlvbiJ9LHNsb3Q6MTJ9LHtpdGVtOntjb21wb25lbnRzOnsibWluZWNyYWZ0OnBvdGlvbl9jb250ZW50cyI6e3BvdGlvbjoibWluZWNyYWZ0OnN0cm9uZ19zdHJlbmd0aCJ9fSxjb3VudDoxLGlkOiJtaW5lY3JhZnQ6c3BsYXNoX3BvdGlvbiJ9LHNsb3Q6MTN9LHtpdGVtOntjb21wb25lbnRzOnsibWluZWNyYWZ0OnBvdGlvbl9jb250ZW50cyI6e3BvdGlvbjoibWluZWNyYWZ0OnN0cm9uZ19zdHJlbmd0aCJ9fSxjb3VudDoxLGlkOiJtaW5lY3JhZnQ6c3BsYXNoX3BvdGlvbiJ9LHNsb3Q6MTR9LHtpdGVtOntjb21wb25lbnRzOnsibWluZWNyYWZ0OnBvdGlvbl9jb250ZW50cyI6e3BvdGlvbjoibWluZWNyYWZ0OnN0cm9uZ19zd2lmdG5lc3MifX0sY291bnQ6MSxpZDoibWluZWNyYWZ0OnNwbGFzaF9wb3Rpb24ifSxzbG90OjE1fSx7aXRlbTp7Y29tcG9uZW50czp7Im1pbmVjcmFmdDpwb3Rpb25fY29udGVudHMiOntwb3Rpb246Im1pbmVjcmFmdDpzdHJvbmdfc3dpZnRuZXNzIn19LGNvdW50OjEsaWQ6Im1pbmVjcmFmdDpzcGxhc2hfcG90aW9uIn0sc2xvdDoxNn0se2l0ZW06e2NvbXBvbmVudHM6eyJtaW5lY3JhZnQ6cG90aW9uX2NvbnRlbnRzIjp7cG90aW9uOiJtaW5lY3JhZnQ6c3Ryb25nX3N3aWZ0bmVzcyJ9fSxjb3VudDoxLGlkOiJtaW5lY3JhZnQ6c3BsYXNoX3BvdGlvbiJ9LHNsb3Q6MTd9LHtpdGVtOntjb21wb25lbnRzOnsibWluZWNyYWZ0OnBvdGlvbl9jb250ZW50cyI6e3BvdGlvbjoibWluZWNyYWZ0OnN0cm9uZ19zd2lmdG5lc3MifX0sY291bnQ6MSxpZDoibWluZWNyYWZ0OnNwbGFzaF9wb3Rpb24ifSxzbG90OjE4fSx7aXRlbTp7Y29tcG9uZW50czp7Im1pbmVjcmFmdDpwb3Rpb25fY29udGVudHMiOntwb3Rpb246Im1pbmVjcmFmdDpzdHJvbmdfc3dpZnRuZXNzIn19LGNvdW50OjEsaWQ6Im1pbmVjcmFmdDpzcGxhc2hfcG90aW9uIn0sc2xvdDoxOX0se2l0ZW06e2NvbXBvbmVudHM6eyJtaW5lY3JhZnQ6cG90aW9uX2NvbnRlbnRzIjp7cG90aW9uOiJtaW5lY3JhZnQ6c3Ryb25nX3N3aWZ0bmVzcyJ9fSxjb3VudDoxLGlkOiJtaW5lY3JhZnQ6c3BsYXNoX3BvdGlvbiJ9LHNsb3Q6MjB9LHtpdGVtOntjb21wb25lbnRzOnsibWluZWNyYWZ0OnBvdGlvbl9jb250ZW50cyI6e3BvdGlvbjoibWluZWNyYWZ0OnN0cm9uZ19zd2lmdG5lc3MifX0sY291bnQ6MSxpZDoibWluZWNyYWZ0OnNwbGFzaF9wb3Rpb24ifSxzbG90OjIxfSx7aXRlbTp7Y29tcG9uZW50czp7Im1pbmVjcmFmdDpwb3Rpb25fY29udGVudHMiOntwb3Rpb246Im1pbmVjcmFmdDpzdHJvbmdfc3dpZnRuZXNzIn19LGNvdW50OjEsaWQ6Im1pbmVjcmFmdDpzcGxhc2hfcG90aW9uIn0sc2xvdDoyMn0se2l0ZW06e2NvbXBvbmVudHM6eyJtaW5lY3JhZnQ6cG90aW9uX2NvbnRlbnRzIjp7cG90aW9uOiJtaW5lY3JhZnQ6c3Ryb25nX3N3aWZ0bmVzcyJ9fSxjb3VudDoxLGlkOiJtaW5lY3JhZnQ6c3BsYXNoX3BvdGlvbiJ9LHNsb3Q6MjN9LHtpdGVtOntjb21wb25lbnRzOnsibWluZWNyYWZ0OnBvdGlvbl9jb250ZW50cyI6e3BvdGlvbjoibWluZWNyYWZ0OnN0cm9uZ19zd2lmdG5lc3MifX0sY291bnQ6MSxpZDoibWluZWNyYWZ0OnNwbGFzaF9wb3Rpb24ifSxzbG90OjI0fSx7aXRlbTp7Y29tcG9uZW50czp7Im1pbmVjcmFmdDpwb3Rpb25fY29udGVudHMiOntwb3Rpb246Im1pbmVjcmFmdDpzdHJvbmdfc3dpZnRuZXNzIn19LGNvdW50OjEsaWQ6Im1pbmVjcmFmdDpzcGxhc2hfcG90aW9uIn0sc2xvdDoyNX0se2l0ZW06e2NvbXBvbmVudHM6eyJtaW5lY3JhZnQ6cG90aW9uX2NvbnRlbnRzIjp7cG90aW9uOiJtaW5lY3JhZnQ6c3Ryb25nX3N3aWZ0bmVzcyJ9fSxjb3VudDoxLGlkOiJtaW5lY3JhZnQ6c3BsYXNoX3BvdGlvbiJ9LHNsb3Q6MjZ9XXgAcQB+ABRzcQB+AABzcQB+AAN1cQB+AAYAAAAFcQB+AAhxAH4ACXEAfgAKcQB+AAtxAH4ADXVxAH4ABgAAAAVxAH4AD3NxAH4AEAAAEj90ABVtaW5lY3JhZnQ6ZW5kZXJfcGVhcmxxAH4ALXEAfgAUc3EAfgAAc3EAfgADdXEAfgAGAAAABnEAfgAIcQB+AAlxAH4ACnEAfgALcQB+AAxxAH4ADXVxAH4ABgAAAAZxAH4AD3NxAH4AEAAAEj90ABdtaW5lY3JhZnQ6c3BsYXNoX3BvdGlvbnEAfgAUc3EAfgAVP0AAAAAAAAx3CAAAABAAAAABdAAZbWluZWNyYWZ0OnBvdGlvbl9jb250ZW50c3QAJXtwb3Rpb246Im1pbmVjcmFmdDpzdHJvbmdfc3dpZnRuZXNzIn14AHEAfgAUc3EAfgAAc3EAfgADdXEAfgAGAAAABnEAfgAIcQB+AAlxAH4ACnEAfgALcQB+AAxxAH4ADXVxAH4ABgAAAAZxAH4AD3NxAH4AEAAAEj90ABdtaW5lY3JhZnQ6c3BsYXNoX3BvdGlvbnEAfgAUc3EAfgAVP0AAAAAAAAx3CAAAABAAAAABdAAZbWluZWNyYWZ0OnBvdGlvbl9jb250ZW50c3QAJXtwb3Rpb246Im1pbmVjcmFmdDpzdHJvbmdfc3dpZnRuZXNzIn14AHEAfgAUc3EAfgAAc3EAfgADdXEAfgAGAAAABnEAfgAIcQB+AAlxAH4ACnEAfgALcQB+AAxxAH4ADXVxAH4ABgAAAAZxAH4AD3NxAH4AEAAAEj90ABdtaW5lY3JhZnQ6c3BsYXNoX3BvdGlvbnEAfgAUc3EAfgAVP0AAAAAAAAx3CAAAABAAAAABdAAZbWluZWNyYWZ0OnBvdGlvbl9jb250ZW50c3QAJXtwb3Rpb246Im1pbmVjcmFmdDpzdHJvbmdfc3dpZnRuZXNzIn14AHEAfgAUc3EAfgAAc3EAfgADdXEAfgAGAAAABnEAfgAIcQB+AAlxAH4ACnEAfgALcQB+AAxxAH4ADXVxAH4ABgAAAAZxAH4AD3NxAH4AEAAAEj90ABdtaW5lY3JhZnQ6c3BsYXNoX3BvdGlvbnEAfgAUc3EAfgAVP0AAAAAAAAx3CAAAABAAAAABdAAZbWluZWNyYWZ0OnBvdGlvbl9jb250ZW50c3QAJXtwb3Rpb246Im1pbmVjcmFmdDpzdHJvbmdfc3dpZnRuZXNzIn14AHEAfgAUc3EAfgAAc3EAfgADdXEAfgAGAAAABnEAfgAIcQB+AAlxAH4ACnEAfgALcQB+AAxxAH4ADXVxAH4ABgAAAAZxAH4AD3NxAH4AEAAAEj90ABdtaW5lY3JhZnQ6c3BsYXNoX3BvdGlvbnEAfgAUc3EAfgAVP0AAAAAAAAx3CAAAABAAAAABdAAZbWluZWNyYWZ0OnBvdGlvbl9jb250ZW50c3QAJXtwb3Rpb246Im1pbmVjcmFmdDpzdHJvbmdfc3dpZnRuZXNzIn14AHEAfgAUc3EAfgAAc3EAfgADdXEAfgAGAAAABnEAfgAIcQB+AAlxAH4ACnEAfgALcQB+AAxxAH4ADXVxAH4ABgAAAAZxAH4AD3NxAH4AEAAAEj90ABdtaW5lY3JhZnQ6c3BsYXNoX3BvdGlvbnEAfgAUc3EAfgAVP0AAAAAAAAx3CAAAABAAAAABdAAZbWluZWNyYWZ0OnBvdGlvbl9jb250ZW50c3QAJXtwb3Rpb246Im1pbmVjcmFmdDpzdHJvbmdfc3dpZnRuZXNzIn14AHEAfgAUc3EAfgAAc3EAfgADdXEAfgAGAAAABnEAfgAIcQB+AAlxAH4ACnEAfgALcQB+AAxxAH4ADXVxAH4ABgAAAAZxAH4AD3NxAH4AEAAAEj90ABdtaW5lY3JhZnQ6c3BsYXNoX3BvdGlvbnEAfgAUc3EAfgAVP0AAAAAAAAx3CAAAABAAAAABdAAZbWluZWNyYWZ0OnBvdGlvbl9jb250ZW50c3QAJXtwb3Rpb246Im1pbmVjcmFmdDpzdHJvbmdfc3dpZnRuZXNzIn14AHEAfgAUc3EAfgAAc3EAfgADdXEAfgAGAAAABXEAfgAIcQB+AAlxAH4ACnEAfgALcQB+AA11cQB+AAYAAAAFcQB+AA9zcQB+ABAAABI/dAAWbWluZWNyYWZ0OmdvbGRlbl9hcHBsZXEAfgA0cQB+ABRzcQB+AABzcQB+AAN1cQB+AAYAAAAFcQB+AAhxAH4ACXEAfgAKcQB+AAtxAH4ADXVxAH4ABgAAAAVxAH4AD3NxAH4AEAAAEj90ABVtaW5lY3JhZnQ6ZW5kZXJfcGVhcmxxAH4ALXEAfgAUc3EAfgAAc3EAfgADdXEAfgAGAAAABnEAfgAIcQB+AAlxAH4ACnEAfgALcQB+AAxxAH4ADXVxAH4ABgAAAAZxAH4AD3NxAH4AEAAAEj90ABdtaW5lY3JhZnQ6c3BsYXNoX3BvdGlvbnEAfgAUc3EAfgAVP0AAAAAAAAx3CAAAABAAAAABdAAZbWluZWNyYWZ0OnBvdGlvbl9jb250ZW50c3QAJHtwb3Rpb246Im1pbmVjcmFmdDpzdHJvbmdfc3RyZW5ndGgifXgAcQB+ABRzcQB+AABzcQB+AAN1cQB+AAYAAAAGcQB+AAhxAH4ACXEAfgAKcQB+AAtxAH4ADHEAfgANdXEAfgAGAAAABnEAfgAPc3EAfgAQAAASP3QAF21pbmVjcmFmdDpzcGxhc2hfcG90aW9ucQB+ABRzcQB+ABU/QAAAAAAADHcIAAAAEAAAAAF0ABltaW5lY3JhZnQ6cG90aW9uX2NvbnRlbnRzdAAke3BvdGlvbjoibWluZWNyYWZ0OnN0cm9uZ19zdHJlbmd0aCJ9eABxAH4AFHNxAH4AAHNxAH4AA3VxAH4ABgAAAAZxAH4ACHEAfgAJcQB+AApxAH4AC3EAfgAMcQB+AA11cQB+AAYAAAAGcQB+AA9zcQB+ABAAABI/dAAXbWluZWNyYWZ0OnNwbGFzaF9wb3Rpb25xAH4AFHNxAH4AFT9AAAAAAAAMdwgAAAAQAAAAAXQAGW1pbmVjcmFmdDpwb3Rpb25fY29udGVudHN0ACR7cG90aW9uOiJtaW5lY3JhZnQ6c3Ryb25nX3N0cmVuZ3RoIn14AHEAfgAUc3EAfgAAc3EAfgADdXEAfgAGAAAABnEAfgAIcQB+AAlxAH4ACnEAfgALcQB+AAxxAH4ADXVxAH4ABgAAAAZxAH4AD3NxAH4AEAAAEj90ABdtaW5lY3JhZnQ6c3BsYXNoX3BvdGlvbnEAfgAUc3EAfgAVP0AAAAAAAAx3CAAAABAAAAABdAAZbWluZWNyYWZ0OnBvdGlvbl9jb250ZW50c3QAJXtwb3Rpb246Im1pbmVjcmFmdDpzdHJvbmdfc3dpZnRuZXNzIn14AHEAfgAUc3EAfgAAc3EAfgADdXEAfgAGAAAABnEAfgAIcQB+AAlxAH4ACnEAfgALcQB+AAxxAH4ADXVxAH4ABgAAAAZxAH4AD3NxAH4AEAAAEj90ABdtaW5lY3JhZnQ6c3BsYXNoX3BvdGlvbnEAfgAUc3EAfgAVP0AAAAAAAAx3CAAAABAAAAABdAAZbWluZWNyYWZ0OnBvdGlvbl9jb250ZW50c3QAJXtwb3Rpb246Im1pbmVjcmFmdDpzdHJvbmdfc3dpZnRuZXNzIn14AHEAfgAUc3EAfgAAc3EAfgADdXEAfgAGAAAABnEAfgAIcQB+AAlxAH4ACnEAfgALcQB+AAxxAH4ADXVxAH4ABgAAAAZxAH4AD3NxAH4AEAAAEj90ABdtaW5lY3JhZnQ6c3BsYXNoX3BvdGlvbnEAfgAUc3EAfgAVP0AAAAAAAAx3CAAAABAAAAABdAAZbWluZWNyYWZ0OnBvdGlvbl9jb250ZW50c3QAJXtwb3Rpb246Im1pbmVjcmFmdDpzdHJvbmdfc3dpZnRuZXNzIn14AHEAfgAUc3EAfgAAc3EAfgADdXEAfgAGAAAABXEAfgAIcQB+AAlxAH4ACnEAfgALcQB+AA11cQB+AAYAAAAFcQB+AA9zcQB+ABAAABI/dAAVbWluZWNyYWZ0OndpbmRfY2hhcmdlcQB+ADRxAH4AFHNxAH4AAHNxAH4AA3VxAH4ABgAAAAVxAH4ACHEAfgAJcQB+AApxAH4AC3EAfgANdXEAfgAGAAAABXEAfgAPc3EAfgAQAAASP3QAGm1pbmVjcmFmdDp0b3RlbV9vZl91bmR5aW5ncQB+ABRxAH4AFA== +armor: rO0ABXcEAAAABHNyABpvcmcuYnVra2l0LnV0aWwuaW8uV3JhcHBlcvJQR+zxEm8FAgABTAADbWFwdAAPTGphdmEvdXRpbC9NYXA7eHBzcgA1Y29tLmdvb2dsZS5jb21tb24uY29sbGVjdC5JbW11dGFibGVNYXAkU2VyaWFsaXplZEZvcm0AAAAAAAAAAAIAAkwABGtleXN0ABJMamF2YS9sYW5nL09iamVjdDtMAAZ2YWx1ZXNxAH4ABHhwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAABnQAAj09dAALRGF0YVZlcnNpb250AAJpZHQABWNvdW50dAAKY29tcG9uZW50c3QADnNjaGVtYV92ZXJzaW9udXEAfgAGAAAABnQAHm9yZy5idWtraXQuaW52ZW50b3J5Lkl0ZW1TdGFja3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAABI/dAAZbWluZWNyYWZ0Om5ldGhlcml0ZV9ib290c3NxAH4AEAAAAAFzcgAXamF2YS51dGlsLkxpbmtlZEhhc2hNYXA0wE5cEGzA+wIAAVoAC2FjY2Vzc09yZGVyeHIAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAADHcIAAAAEAAAAAJ0ABVtaW5lY3JhZnQ6cmVwYWlyX2Nvc3R0AAEzdAAWbWluZWNyYWZ0OmVuY2hhbnRtZW50c3QAOHsibWluZWNyYWZ0OmZlYXRoZXJfZmFsbGluZyI6NCwibWluZWNyYWZ0OnByb3RlY3Rpb24iOjR9eABxAH4AFHNxAH4AAHNxAH4AA3VxAH4ABgAAAAZxAH4ACHEAfgAJcQB+AApxAH4AC3EAfgAMcQB+AA11cQB+AAYAAAAGcQB+AA9zcQB+ABAAABI/dAAcbWluZWNyYWZ0Om5ldGhlcml0ZV9sZWdnaW5nc3EAfgAUc3EAfgAVP0AAAAAAAAx3CAAAABAAAAACdAAVbWluZWNyYWZ0OnJlcGFpcl9jb3N0dAABMXQAFm1pbmVjcmFmdDplbmNoYW50bWVudHN0ABp7Im1pbmVjcmFmdDpwcm90ZWN0aW9uIjo0fXgAcQB+ABRzcQB+AABzcQB+AAN1cQB+AAYAAAAGcQB+AAhxAH4ACXEAfgAKcQB+AAtxAH4ADHEAfgANdXEAfgAGAAAABnEAfgAPc3EAfgAQAAASP3QAHm1pbmVjcmFmdDpuZXRoZXJpdGVfY2hlc3RwbGF0ZXEAfgAUc3EAfgAVP0AAAAAAAAx3CAAAABAAAAACdAAVbWluZWNyYWZ0OnJlcGFpcl9jb3N0dAABMXQAFm1pbmVjcmFmdDplbmNoYW50bWVudHN0ABp7Im1pbmVjcmFmdDpwcm90ZWN0aW9uIjo0fXgAcQB+ABRzcQB+AABzcQB+AAN1cQB+AAYAAAAGcQB+AAhxAH4ACXEAfgAKcQB+AAtxAH4ADHEAfgANdXEAfgAGAAAABnEAfgAPc3EAfgAQAAASP3QAGm1pbmVjcmFmdDpuZXRoZXJpdGVfaGVsbWV0cQB+ABRzcQB+ABU/QAAAAAAADHcIAAAAEAAAAAJ0ABVtaW5lY3JhZnQ6cmVwYWlyX2Nvc3R0AAExdAAWbWluZWNyYWZ0OmVuY2hhbnRtZW50c3QAGnsibWluZWNyYWZ0OnByb3RlY3Rpb24iOjR9eABxAH4AFA== +extra: rO0ABXcEAAAAAXNyABpvcmcuYnVra2l0LnV0aWwuaW8uV3JhcHBlcvJQR+zxEm8FAgABTAADbWFwdAAPTGphdmEvdXRpbC9NYXA7eHBzcgA1Y29tLmdvb2dsZS5jb21tb24uY29sbGVjdC5JbW11dGFibGVNYXAkU2VyaWFsaXplZEZvcm0AAAAAAAAAAAIAAkwABGtleXN0ABJMamF2YS9sYW5nL09iamVjdDtMAAZ2YWx1ZXNxAH4ABHhwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAABXQAAj09dAALRGF0YVZlcnNpb250AAJpZHQABWNvdW50dAAOc2NoZW1hX3ZlcnNpb251cQB+AAYAAAAFdAAeb3JnLmJ1a2tpdC5pbnZlbnRvcnkuSXRlbVN0YWNrc3IAEWphdmEubGFuZy5JbnRlZ2VyEuKgpPeBhzgCAAFJAAV2YWx1ZXhyABBqYXZhLmxhbmcuTnVtYmVyhqyVHQuU4IsCAAB4cAAAEj90ABptaW5lY3JhZnQ6dG90ZW1fb2ZfdW5keWluZ3NxAH4ADwAAAAFxAH4AEw== +effects: + - ==: PotionEffect + effect: minecraft:speed + duration: 1800 + amplifier: 1 + ambient: false + has-particles: true + has-icon: true + - ==: PotionEffect + effect: minecraft:strength + duration: 1800 + amplifier: 1 + ambient: false + has-particles: true + has-icon: true + - ==: PotionEffect + effect: minecraft:absorption + duration: 2400 + amplifier: 1 + ambient: false + has-particles: true + has-icon: true diff --git a/core/src/main/resources/ladders/pearlfight.yml b/core/src/main/resources/ladders/pearlfight.yml index 8f71e73b..ac7c59ab 100644 --- a/core/src/main/resources/ladders/pearlfight.yml +++ b/core/src/main/resources/ladders/pearlfight.yml @@ -21,40 +21,13 @@ settings: tempbuild-delay: 6 icon: ==: org.bukkit.inventory.ItemStack - v: 3337 - type: ENDER_PEARL - meta: - ==: ItemMeta - meta-type: UNSPECIFIC - display-name: '{"extra":[{"bold":false,"italic":false,"underlined":false,"strikethrough":false,"obfuscated":false,"color":"dark_aqua","text":"Pearl - "},{"italic":false,"color":"dark_purple","text":"Fight"}],"text":""}' -armor: | - rO0ABXcEAAAABHNyABpvcmcuYnVra2l0LnV0aWwuaW8uV3JhcHBlcvJQR+zxEm8FAgABTAADbWFw - dAAPTGphdmEvdXRpbC9NYXA7eHBzcgA1Y29tLmdvb2dsZS5jb21tb24uY29sbGVjdC5JbW11dGFi - bGVNYXAkU2VyaWFsaXplZEZvcm0AAAAAAAAAAAIAAkwABGtleXN0ABJMamF2YS9sYW5nL09iamVj - dDtMAAZ2YWx1ZXNxAH4ABHhwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAA - A3QAAj09dAABdnQABHR5cGV1cQB+AAYAAAADdAAeb3JnLmJ1a2tpdC5pbnZlbnRvcnkuSXRlbVN0 - YWNrc3IAEWphdmEubGFuZy5JbnRlZ2VyEuKgpPeBhzgCAAFJAAV2YWx1ZXhyABBqYXZhLmxhbmcu - TnVtYmVyhqyVHQuU4IsCAAB4cAAADQl0AA1MRUFUSEVSX0JPT1RTc3EAfgAAc3EAfgADdXEAfgAG - AAAAA3EAfgAIcQB+AAlxAH4ACnVxAH4ABgAAAANxAH4ADHNxAH4ADQAADQl0ABBMRUFUSEVSX0xF - R0dJTkdTc3EAfgAAc3EAfgADdXEAfgAGAAAAA3EAfgAIcQB+AAlxAH4ACnVxAH4ABgAAAANxAH4A - DHNxAH4ADQAADQl0ABJMRUFUSEVSX0NIRVNUUExBVEVzcQB+AABzcQB+AAN1cQB+AAYAAAADcQB+ - AAhxAH4ACXEAfgAKdXEAfgAGAAAAA3EAfgAMc3EAfgANAAANCXQADkxFQVRIRVJfSEVMTUVU -inventory: | - rO0ABXcEAAAAJHNyABpvcmcuYnVra2l0LnV0aWwuaW8uV3JhcHBlcvJQR+zxEm8FAgABTAADbWFw - dAAPTGphdmEvdXRpbC9NYXA7eHBzcgA1Y29tLmdvb2dsZS5jb21tb24uY29sbGVjdC5JbW11dGFi - bGVNYXAkU2VyaWFsaXplZEZvcm0AAAAAAAAAAAIAAkwABGtleXN0ABJMamF2YS9sYW5nL09iamVj - dDtMAAZ2YWx1ZXNxAH4ABHhwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAA - BHQAAj09dAABdnQABHR5cGV0AARtZXRhdXEAfgAGAAAABHQAHm9yZy5idWtraXQuaW52ZW50b3J5 - Lkl0ZW1TdGFja3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2 - YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAA0JdAAJQkxBWkVfUk9Ec3EAfgAAc3EAfgADdXEA - fgAGAAAAA3EAfgAIdAAJbWV0YS10eXBldAAIZW5jaGFudHN1cQB+AAYAAAADdAAISXRlbU1ldGF0 - AApVTlNQRUNJRklDc3IAN2NvbS5nb29nbGUuY29tbW9uLmNvbGxlY3QuSW1tdXRhYmxlQmlNYXAk - U2VyaWFsaXplZEZvcm0AAAAAAAAAAAIAAHhxAH4AA3VxAH4ABgAAAAF0AAlLTk9DS0JBQ0t1cQB+ - AAYAAAABc3EAfgAOAAAAAXNxAH4AAHNxAH4AA3VxAH4ABgAAAARxAH4ACHEAfgAJcQB+AAp0AAZh - bW91bnR1cQB+AAYAAAAEcQB+AA1zcQB+AA4AAA0JdAALRU5ERVJfUEVBUkxzcQB+AA4AAAAIc3EA - fgAAc3EAfgADdXEAfgAGAAAABHEAfgAIcQB+AAlxAH4ACnEAfgAjdXEAfgAGAAAABHEAfgANc3EA - fgAOAAANCXQACldISVRFX1dPT0xzcQB+AA4AAAAMcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBw - cHBwcHBw -extra: | - rO0ABXcEAAAAAXA= + DataVersion: 4671 + id: minecraft:ender_pearl + count: 1 + components: + minecraft:custom_name: '{extra:[{bold:0b,color:"dark_aqua",italic:0b,obfuscated:0b,strikethrough:0b,text:"Pearl + ",underlined:0b},{color:"dark_purple",italic:0b,text:"Fight"}],text:""}' + schema_version: 1 +armor: rO0ABXcEAAAABHNyABpvcmcuYnVra2l0LnV0aWwuaW8uV3JhcHBlcvJQR+zxEm8FAgABTAADbWFwdAAPTGphdmEvdXRpbC9NYXA7eHBzcgA1Y29tLmdvb2dsZS5jb21tb24uY29sbGVjdC5JbW11dGFibGVNYXAkU2VyaWFsaXplZEZvcm0AAAAAAAAAAAIAAkwABGtleXN0ABJMamF2YS9sYW5nL09iamVjdDtMAAZ2YWx1ZXNxAH4ABHhwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAABnQAAj09dAALRGF0YVZlcnNpb250AAJpZHQABWNvdW50dAAKY29tcG9uZW50c3QADnNjaGVtYV92ZXJzaW9udXEAfgAGAAAABnQAHm9yZy5idWtraXQuaW52ZW50b3J5Lkl0ZW1TdGFja3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAABI/dAAXbWluZWNyYWZ0OmxlYXRoZXJfYm9vdHNzcQB+ABAAAAABc3IAF2phdmEudXRpbC5MaW5rZWRIYXNoTWFwNMBOXBBswPsCAAFaAAthY2Nlc3NPcmRlcnhyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABdAAUbWluZWNyYWZ0OmR5ZWRfY29sb3J0AAgxNjczMzUyNXgAcQB+ABRzcQB+AABzcQB+AAN1cQB+AAYAAAAGcQB+AAhxAH4ACXEAfgAKcQB+AAtxAH4ADHEAfgANdXEAfgAGAAAABnEAfgAPc3EAfgAQAAASP3QAGm1pbmVjcmFmdDpsZWF0aGVyX2xlZ2dpbmdzcQB+ABRzcQB+ABU/QAAAAAAADHcIAAAAEAAAAAF0ABRtaW5lY3JhZnQ6ZHllZF9jb2xvcnQACDE2NzMzNTI1eABxAH4AFHNxAH4AAHNxAH4AA3VxAH4ABgAAAAZxAH4ACHEAfgAJcQB+AApxAH4AC3EAfgAMcQB+AA11cQB+AAYAAAAGcQB+AA9zcQB+ABAAABI/dAAcbWluZWNyYWZ0OmxlYXRoZXJfY2hlc3RwbGF0ZXEAfgAUc3EAfgAVP0AAAAAAAAx3CAAAABAAAAABdAAUbWluZWNyYWZ0OmR5ZWRfY29sb3J0AAgxNjczMzUyNXgAcQB+ABRzcQB+AABzcQB+AAN1cQB+AAYAAAAGcQB+AAhxAH4ACXEAfgAKcQB+AAtxAH4ADHEAfgANdXEAfgAGAAAABnEAfgAPc3EAfgAQAAASP3QAGG1pbmVjcmFmdDpsZWF0aGVyX2hlbG1ldHEAfgAUc3EAfgAVP0AAAAAAAAx3CAAAABAAAAABdAAUbWluZWNyYWZ0OmR5ZWRfY29sb3J0AAgxNjczMzUyNXgAcQB+ABQ= +inventory: rO0ABXcEAAAAJHNyABpvcmcuYnVra2l0LnV0aWwuaW8uV3JhcHBlcvJQR+zxEm8FAgABTAADbWFwdAAPTGphdmEvdXRpbC9NYXA7eHBzcgA1Y29tLmdvb2dsZS5jb21tb24uY29sbGVjdC5JbW11dGFibGVNYXAkU2VyaWFsaXplZEZvcm0AAAAAAAAAAAIAAkwABGtleXN0ABJMamF2YS9sYW5nL09iamVjdDtMAAZ2YWx1ZXNxAH4ABHhwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAABnQAAj09dAALRGF0YVZlcnNpb250AAJpZHQABWNvdW50dAAKY29tcG9uZW50c3QADnNjaGVtYV92ZXJzaW9udXEAfgAGAAAABnQAHm9yZy5idWtraXQuaW52ZW50b3J5Lkl0ZW1TdGFja3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAABI/dAATbWluZWNyYWZ0OmJsYXplX3JvZHNxAH4AEAAAAAFzcgAXamF2YS51dGlsLkxpbmtlZEhhc2hNYXA0wE5cEGzA+wIAAVoAC2FjY2Vzc09yZGVyeHIAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAADHcIAAAAEAAAAAF0ABZtaW5lY3JhZnQ6ZW5jaGFudG1lbnRzdAAZeyJtaW5lY3JhZnQ6a25vY2tiYWNrIjoxfXgAcQB+ABRzcQB+AABzcQB+AAN1cQB+AAYAAAAFcQB+AAhxAH4ACXEAfgAKcQB+AAtxAH4ADXVxAH4ABgAAAAVxAH4AD3NxAH4AEAAAEj90ABVtaW5lY3JhZnQ6ZW5kZXJfcGVhcmxzcQB+ABAAAAAIcQB+ABRzcQB+AABzcQB+AAN1cQB+AAYAAAAFcQB+AAhxAH4ACXEAfgAKcQB+AAtxAH4ADXVxAH4ABgAAAAVxAH4AD3NxAH4AEAAAEj90ABBtaW5lY3JhZnQ6c2hlYXJzcQB+ABRxAH4AFHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcA== +extra: rO0ABXcEAAAAAXNyABpvcmcuYnVra2l0LnV0aWwuaW8uV3JhcHBlcvJQR+zxEm8FAgABTAADbWFwdAAPTGphdmEvdXRpbC9NYXA7eHBzcgA1Y29tLmdvb2dsZS5jb21tb24uY29sbGVjdC5JbW11dGFibGVNYXAkU2VyaWFsaXplZEZvcm0AAAAAAAAAAAIAAkwABGtleXN0ABJMamF2YS9sYW5nL09iamVjdDtMAAZ2YWx1ZXNxAH4ABHhwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAABXQAAj09dAALRGF0YVZlcnNpb250AAJpZHQABWNvdW50dAAOc2NoZW1hX3ZlcnNpb251cQB+AAYAAAAFdAAeb3JnLmJ1a2tpdC5pbnZlbnRvcnkuSXRlbVN0YWNrc3IAEWphdmEubGFuZy5JbnRlZ2VyEuKgpPeBhzgCAAFJAAV2YWx1ZXhyABBqYXZhLmxhbmcuTnVtYmVyhqyVHQuU4IsCAAB4cAAAEj90ABRtaW5lY3JhZnQ6d2hpdGVfd29vbHNxAH4ADwAAABBzcQB+AA8AAAAB From 1cae3c1229be6b49fbaf5a21ca88704cf1ccbd98 Mon Sep 17 00:00:00 2001 From: Nandor Dukat Date: Thu, 19 Mar 2026 18:08:50 +0100 Subject: [PATCH 22/26] fixed armor trim bug where it applied trim to wrong armor type --- .../manager/fight/match/util/KitUtil.java | 113 +++++++++--------- .../cosmetics/shield/ShieldCosmeticsUtil.java | 7 +- 2 files changed, 63 insertions(+), 57 deletions(-) diff --git a/core/src/main/java/dev/nandi0813/practice/manager/fight/match/util/KitUtil.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/match/util/KitUtil.java index df234241..24bd8b1a 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/fight/match/util/KitUtil.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/fight/match/util/KitUtil.java @@ -11,6 +11,7 @@ import dev.nandi0813.practice.manager.profile.cosmetics.armortrim.ArmorSlot; import dev.nandi0813.practice.manager.profile.cosmetics.armortrim.ArmorTrimTier; import dev.nandi0813.practice.util.KitData; +import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ArmorMeta; @@ -21,6 +22,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Objects; public enum KitUtil { ; @@ -33,11 +35,15 @@ public static void loadDefaultLadderKit(Player player, TeamEnum team, Ladder lad public static void loadKit(Player player, TeamEnum team, ItemStack[] armor, ItemStack[] inventory, ItemStack[] extra) { PlayerUtil.clearInventory(player); + ItemStack[] armorCopy = cloneItems(armor); + ItemStack[] inventoryCopy = cloneItems(inventory); + ItemStack[] extraCopy = cloneItems(extra); + if (team == null) { - LadderUtil.loadInventory(player, armor, inventory, extra); + LadderUtil.loadInventory(player, armorCopy, inventoryCopy, extraCopy); } else { List inventoryList = new ArrayList<>(); - for (ItemStack item : new ArrayList<>(Arrays.asList(inventory.clone()))) { + for (ItemStack item : new ArrayList<>(Arrays.asList(inventoryCopy))) { if (item != null) { item = LadderUtil.changeItemColor(item, team.getColor()); inventoryList.add(item); @@ -47,7 +53,7 @@ public static void loadKit(Player player, TeamEnum team, ItemStack[] armor, Item } List armorList = new ArrayList<>(); - for (ItemStack item : new ArrayList<>(Arrays.asList(armor.clone()))) { + for (ItemStack item : new ArrayList<>(Arrays.asList(armorCopy))) { if (item != null) { item = LadderUtil.changeItemColor(item, team.getColor()); armorList.add(item); @@ -57,8 +63,8 @@ public static void loadKit(Player player, TeamEnum team, ItemStack[] armor, Item } List extraList = new ArrayList<>(); - if (extra != null) { - for (ItemStack item : new ArrayList<>(Arrays.asList(extra.clone()))) { + if (extraCopy != null) { + for (ItemStack item : new ArrayList<>(Arrays.asList(extraCopy))) { if (item != null) { item = LadderUtil.changeItemColor(item, team.getColor()); extraList.add(item); @@ -71,7 +77,7 @@ public static void loadKit(Player player, TeamEnum team, ItemStack[] armor, Item LadderUtil.loadInventory(player, armorList.toArray(new ItemStack[0]), inventoryList.toArray(new ItemStack[0]), - extra != null ? extraList.toArray(new ItemStack[0]) : null); + extraCopy != null ? extraList.toArray(new ItemStack[0]) : null); } applyArmorTrimCosmetics(player); @@ -95,30 +101,19 @@ private static void applyArmorTrimCosmetics(Player player) { return; } - ArmorTrimTier activeTier = profile.getCosmeticsData().getActiveTier(); - if (!player.hasPermission(activeTier.getPermissionNode())) { - return; - } - // Helmet (index 3) - applyTrimToArmor(player, armorContents[3], - profile.getCosmeticsData().getPattern(activeTier, ArmorSlot.HELMET), - profile.getCosmeticsData().getMaterial(activeTier, ArmorSlot.HELMET), 3); + applyTrimToArmor(player, profile, armorContents, ArmorSlot.HELMET, 3); // Chestplate (index 2) - applyTrimToArmor(player, armorContents[2], - profile.getCosmeticsData().getPattern(activeTier, ArmorSlot.CHESTPLATE), - profile.getCosmeticsData().getMaterial(activeTier, ArmorSlot.CHESTPLATE), 2); + applyTrimToArmor(player, profile, armorContents, ArmorSlot.CHESTPLATE, 2); // Leggings (index 1) - applyTrimToArmor(player, armorContents[1], - profile.getCosmeticsData().getPattern(activeTier, ArmorSlot.LEGGINGS), - profile.getCosmeticsData().getMaterial(activeTier, ArmorSlot.LEGGINGS), 1); + applyTrimToArmor(player, profile, armorContents, ArmorSlot.LEGGINGS, 1); // Boots (index 0) - applyTrimToArmor(player, armorContents[0], - profile.getCosmeticsData().getPattern(activeTier, ArmorSlot.BOOTS), - profile.getCosmeticsData().getMaterial(activeTier, ArmorSlot.BOOTS), 0); + applyTrimToArmor(player, profile, armorContents, ArmorSlot.BOOTS, 0); + + player.getInventory().setArmorContents(armorContents); } catch (Exception e) { // Silently fail - if cosmetics cannot be applied, continue with kit distribution @@ -127,19 +122,6 @@ private static void applyArmorTrimCosmetics(Player player) { private static void applyShieldCosmetics(Player player) { try { - Profile profile = ProfileManager.getInstance().getProfile(player); - if (profile == null || profile.getCosmeticsData() == null) { - return; - } - - if (!CosmeticsPermissionManager.hasShieldPermission(player)) { - return; - } - - if (profile.getCosmeticsData().getActiveShieldLayout() == null) { - return; - } - ShieldCosmeticsUtil.applyShieldToPlayer(player); } catch (Exception e) { // Silently fail - if shield cosmetics cannot be applied, continue with kit distribution @@ -149,44 +131,65 @@ private static void applyShieldCosmetics(Player player) { /** * Apply a trim pattern and material to an armor piece if both are set. */ - private static void applyTrimToArmor(Player player, ItemStack item, - TrimPattern pattern, TrimMaterial material, int armorIndex) { + private static void applyTrimToArmor(Player player, Profile profile, ItemStack[] armorContents, + ArmorSlot slot, int armorIndex) { + ItemStack item = armorContents[armorIndex]; if (item == null || !item.hasItemMeta()) { return; } - // Both pattern and material must be set to apply trim - if (pattern == null || material == null) { + if (!(item.getItemMeta() instanceof ArmorMeta armorMeta)) { return; } - // Verify permission before applying - if (!player.hasPermission("zpp.cosmetics.armortrim.pattern." + getTrimId(pattern)) || - !player.hasPermission("zpp.cosmetics.armortrim.material." + getTrimId(material))) { - return; + ArmorTrim targetTrim = null; + ArmorTrimTier armorTier = getArmorTier(item.getType()); + if (armorTier != null && CosmeticsPermissionManager.hasBasePermission(player, armorTier)) { + TrimPattern pattern = profile.getCosmeticsData().getPattern(armorTier, slot); + TrimMaterial material = profile.getCosmeticsData().getMaterial(armorTier, slot); + if (pattern != null + && material != null + && CosmeticsPermissionManager.hasPatternPermission(player, pattern) + && CosmeticsPermissionManager.hasMaterialPermission(player, material)) { + targetTrim = new ArmorTrim(material, pattern); + } } try { - var meta = (ArmorMeta) item.getItemMeta(); - if (meta != null) { - meta.setTrim(new ArmorTrim(material, pattern)); - item.setItemMeta(meta); - - ItemStack[] armorContents = player.getInventory().getArmorContents(); + ArmorTrim currentTrim = armorMeta.getTrim(); + if (!Objects.equals(currentTrim, targetTrim)) { + armorMeta.setTrim(targetTrim); + item.setItemMeta(armorMeta); armorContents[armorIndex] = item; - player.getInventory().setArmorContents(armorContents); } } catch (Exception e) { // Silently fail - trim application may not be supported on this version or item type } } - private static String getTrimId(TrimPattern pattern) { - return CosmeticsPermissionManager.getTrimId(pattern); + private static ArmorTrimTier getArmorTier(Material material) { + return switch (material) { + case LEATHER_HELMET, LEATHER_CHESTPLATE, LEATHER_LEGGINGS, LEATHER_BOOTS -> ArmorTrimTier.LEATHER; + case GOLDEN_HELMET, GOLDEN_CHESTPLATE, GOLDEN_LEGGINGS, GOLDEN_BOOTS -> ArmorTrimTier.GOLD; + case IRON_HELMET, IRON_CHESTPLATE, IRON_LEGGINGS, IRON_BOOTS -> ArmorTrimTier.IRON; + case DIAMOND_HELMET, DIAMOND_CHESTPLATE, DIAMOND_LEGGINGS, DIAMOND_BOOTS -> ArmorTrimTier.DIAMOND; + case NETHERITE_HELMET, NETHERITE_CHESTPLATE, NETHERITE_LEGGINGS, NETHERITE_BOOTS -> ArmorTrimTier.NETHERITE; + default -> null; + }; } - private static String getTrimId(TrimMaterial material) { - return CosmeticsPermissionManager.getTrimId(material); + private static ItemStack[] cloneItems(ItemStack[] source) { + if (source == null) { + return null; + } + + ItemStack[] copy = source.clone(); + for (int i = 0; i < copy.length; i++) { + if (copy[i] != null) { + copy[i] = copy[i].clone(); + } + } + return copy; } } diff --git a/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/shield/ShieldCosmeticsUtil.java b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/shield/ShieldCosmeticsUtil.java index cb49b1fc..8b6ce850 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/shield/ShieldCosmeticsUtil.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/shield/ShieldCosmeticsUtil.java @@ -2,6 +2,7 @@ import dev.nandi0813.practice.manager.profile.Profile; import dev.nandi0813.practice.manager.profile.ProfileManager; +import dev.nandi0813.practice.manager.profile.cosmetics.CosmeticsPermissionManager; import dev.nandi0813.practice.manager.profile.cosmetics.shield.ShieldLayout; import org.bukkit.DyeColor; import org.bukkit.Material; @@ -30,14 +31,16 @@ public static void applyShieldToPlayer(Player player) { Profile profile = ProfileManager.getInstance().getProfile(player); if (profile == null || profile.getCosmeticsData() == null) return; - ShieldLayout active = profile.getCosmeticsData().getActiveShieldLayout(); + boolean hasPermission = CosmeticsPermissionManager.hasShieldPermission(player); + ShieldLayout active = hasPermission ? profile.getCosmeticsData().getActiveShieldLayout() : null; for (ItemStack item : player.getInventory().getContents()) { if (item == null || item.getType() != Material.SHIELD) continue; if (active != null) { applyLayoutToItem(item, active); + } else { + clearShield(item); } - // No active layout → leave the shield completely untouched (plain default look) } player.updateInventory(); } From 52901fa1bbdbc5bca1d7c403b0899aad91b1e9dc Mon Sep 17 00:00:00 2001 From: Nandor Dukat Date: Thu, 19 Mar 2026 19:01:38 +0100 Subject: [PATCH 23/26] Revert "fixed armor trim bug where it applied trim to wrong armor type" This reverts commit 1cae3c1229be6b49fbaf5a21ca88704cf1ccbd98. --- .../manager/fight/match/util/KitUtil.java | 113 +++++++++--------- .../cosmetics/shield/ShieldCosmeticsUtil.java | 7 +- 2 files changed, 57 insertions(+), 63 deletions(-) diff --git a/core/src/main/java/dev/nandi0813/practice/manager/fight/match/util/KitUtil.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/match/util/KitUtil.java index 24bd8b1a..df234241 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/fight/match/util/KitUtil.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/fight/match/util/KitUtil.java @@ -11,7 +11,6 @@ import dev.nandi0813.practice.manager.profile.cosmetics.armortrim.ArmorSlot; import dev.nandi0813.practice.manager.profile.cosmetics.armortrim.ArmorTrimTier; import dev.nandi0813.practice.util.KitData; -import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ArmorMeta; @@ -22,7 +21,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Objects; public enum KitUtil { ; @@ -35,15 +33,11 @@ public static void loadDefaultLadderKit(Player player, TeamEnum team, Ladder lad public static void loadKit(Player player, TeamEnum team, ItemStack[] armor, ItemStack[] inventory, ItemStack[] extra) { PlayerUtil.clearInventory(player); - ItemStack[] armorCopy = cloneItems(armor); - ItemStack[] inventoryCopy = cloneItems(inventory); - ItemStack[] extraCopy = cloneItems(extra); - if (team == null) { - LadderUtil.loadInventory(player, armorCopy, inventoryCopy, extraCopy); + LadderUtil.loadInventory(player, armor, inventory, extra); } else { List inventoryList = new ArrayList<>(); - for (ItemStack item : new ArrayList<>(Arrays.asList(inventoryCopy))) { + for (ItemStack item : new ArrayList<>(Arrays.asList(inventory.clone()))) { if (item != null) { item = LadderUtil.changeItemColor(item, team.getColor()); inventoryList.add(item); @@ -53,7 +47,7 @@ public static void loadKit(Player player, TeamEnum team, ItemStack[] armor, Item } List armorList = new ArrayList<>(); - for (ItemStack item : new ArrayList<>(Arrays.asList(armorCopy))) { + for (ItemStack item : new ArrayList<>(Arrays.asList(armor.clone()))) { if (item != null) { item = LadderUtil.changeItemColor(item, team.getColor()); armorList.add(item); @@ -63,8 +57,8 @@ public static void loadKit(Player player, TeamEnum team, ItemStack[] armor, Item } List extraList = new ArrayList<>(); - if (extraCopy != null) { - for (ItemStack item : new ArrayList<>(Arrays.asList(extraCopy))) { + if (extra != null) { + for (ItemStack item : new ArrayList<>(Arrays.asList(extra.clone()))) { if (item != null) { item = LadderUtil.changeItemColor(item, team.getColor()); extraList.add(item); @@ -77,7 +71,7 @@ public static void loadKit(Player player, TeamEnum team, ItemStack[] armor, Item LadderUtil.loadInventory(player, armorList.toArray(new ItemStack[0]), inventoryList.toArray(new ItemStack[0]), - extraCopy != null ? extraList.toArray(new ItemStack[0]) : null); + extra != null ? extraList.toArray(new ItemStack[0]) : null); } applyArmorTrimCosmetics(player); @@ -101,19 +95,30 @@ private static void applyArmorTrimCosmetics(Player player) { return; } + ArmorTrimTier activeTier = profile.getCosmeticsData().getActiveTier(); + if (!player.hasPermission(activeTier.getPermissionNode())) { + return; + } + // Helmet (index 3) - applyTrimToArmor(player, profile, armorContents, ArmorSlot.HELMET, 3); + applyTrimToArmor(player, armorContents[3], + profile.getCosmeticsData().getPattern(activeTier, ArmorSlot.HELMET), + profile.getCosmeticsData().getMaterial(activeTier, ArmorSlot.HELMET), 3); // Chestplate (index 2) - applyTrimToArmor(player, profile, armorContents, ArmorSlot.CHESTPLATE, 2); + applyTrimToArmor(player, armorContents[2], + profile.getCosmeticsData().getPattern(activeTier, ArmorSlot.CHESTPLATE), + profile.getCosmeticsData().getMaterial(activeTier, ArmorSlot.CHESTPLATE), 2); // Leggings (index 1) - applyTrimToArmor(player, profile, armorContents, ArmorSlot.LEGGINGS, 1); + applyTrimToArmor(player, armorContents[1], + profile.getCosmeticsData().getPattern(activeTier, ArmorSlot.LEGGINGS), + profile.getCosmeticsData().getMaterial(activeTier, ArmorSlot.LEGGINGS), 1); // Boots (index 0) - applyTrimToArmor(player, profile, armorContents, ArmorSlot.BOOTS, 0); - - player.getInventory().setArmorContents(armorContents); + applyTrimToArmor(player, armorContents[0], + profile.getCosmeticsData().getPattern(activeTier, ArmorSlot.BOOTS), + profile.getCosmeticsData().getMaterial(activeTier, ArmorSlot.BOOTS), 0); } catch (Exception e) { // Silently fail - if cosmetics cannot be applied, continue with kit distribution @@ -122,6 +127,19 @@ private static void applyArmorTrimCosmetics(Player player) { private static void applyShieldCosmetics(Player player) { try { + Profile profile = ProfileManager.getInstance().getProfile(player); + if (profile == null || profile.getCosmeticsData() == null) { + return; + } + + if (!CosmeticsPermissionManager.hasShieldPermission(player)) { + return; + } + + if (profile.getCosmeticsData().getActiveShieldLayout() == null) { + return; + } + ShieldCosmeticsUtil.applyShieldToPlayer(player); } catch (Exception e) { // Silently fail - if shield cosmetics cannot be applied, continue with kit distribution @@ -131,65 +149,44 @@ private static void applyShieldCosmetics(Player player) { /** * Apply a trim pattern and material to an armor piece if both are set. */ - private static void applyTrimToArmor(Player player, Profile profile, ItemStack[] armorContents, - ArmorSlot slot, int armorIndex) { - ItemStack item = armorContents[armorIndex]; + private static void applyTrimToArmor(Player player, ItemStack item, + TrimPattern pattern, TrimMaterial material, int armorIndex) { if (item == null || !item.hasItemMeta()) { return; } - if (!(item.getItemMeta() instanceof ArmorMeta armorMeta)) { + // Both pattern and material must be set to apply trim + if (pattern == null || material == null) { return; } - ArmorTrim targetTrim = null; - ArmorTrimTier armorTier = getArmorTier(item.getType()); - if (armorTier != null && CosmeticsPermissionManager.hasBasePermission(player, armorTier)) { - TrimPattern pattern = profile.getCosmeticsData().getPattern(armorTier, slot); - TrimMaterial material = profile.getCosmeticsData().getMaterial(armorTier, slot); - if (pattern != null - && material != null - && CosmeticsPermissionManager.hasPatternPermission(player, pattern) - && CosmeticsPermissionManager.hasMaterialPermission(player, material)) { - targetTrim = new ArmorTrim(material, pattern); - } + // Verify permission before applying + if (!player.hasPermission("zpp.cosmetics.armortrim.pattern." + getTrimId(pattern)) || + !player.hasPermission("zpp.cosmetics.armortrim.material." + getTrimId(material))) { + return; } try { - ArmorTrim currentTrim = armorMeta.getTrim(); - if (!Objects.equals(currentTrim, targetTrim)) { - armorMeta.setTrim(targetTrim); - item.setItemMeta(armorMeta); + var meta = (ArmorMeta) item.getItemMeta(); + if (meta != null) { + meta.setTrim(new ArmorTrim(material, pattern)); + item.setItemMeta(meta); + + ItemStack[] armorContents = player.getInventory().getArmorContents(); armorContents[armorIndex] = item; + player.getInventory().setArmorContents(armorContents); } } catch (Exception e) { // Silently fail - trim application may not be supported on this version or item type } } - private static ArmorTrimTier getArmorTier(Material material) { - return switch (material) { - case LEATHER_HELMET, LEATHER_CHESTPLATE, LEATHER_LEGGINGS, LEATHER_BOOTS -> ArmorTrimTier.LEATHER; - case GOLDEN_HELMET, GOLDEN_CHESTPLATE, GOLDEN_LEGGINGS, GOLDEN_BOOTS -> ArmorTrimTier.GOLD; - case IRON_HELMET, IRON_CHESTPLATE, IRON_LEGGINGS, IRON_BOOTS -> ArmorTrimTier.IRON; - case DIAMOND_HELMET, DIAMOND_CHESTPLATE, DIAMOND_LEGGINGS, DIAMOND_BOOTS -> ArmorTrimTier.DIAMOND; - case NETHERITE_HELMET, NETHERITE_CHESTPLATE, NETHERITE_LEGGINGS, NETHERITE_BOOTS -> ArmorTrimTier.NETHERITE; - default -> null; - }; + private static String getTrimId(TrimPattern pattern) { + return CosmeticsPermissionManager.getTrimId(pattern); } - private static ItemStack[] cloneItems(ItemStack[] source) { - if (source == null) { - return null; - } - - ItemStack[] copy = source.clone(); - for (int i = 0; i < copy.length; i++) { - if (copy[i] != null) { - copy[i] = copy[i].clone(); - } - } - return copy; + private static String getTrimId(TrimMaterial material) { + return CosmeticsPermissionManager.getTrimId(material); } } diff --git a/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/shield/ShieldCosmeticsUtil.java b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/shield/ShieldCosmeticsUtil.java index 8b6ce850..cb49b1fc 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/shield/ShieldCosmeticsUtil.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/shield/ShieldCosmeticsUtil.java @@ -2,7 +2,6 @@ import dev.nandi0813.practice.manager.profile.Profile; import dev.nandi0813.practice.manager.profile.ProfileManager; -import dev.nandi0813.practice.manager.profile.cosmetics.CosmeticsPermissionManager; import dev.nandi0813.practice.manager.profile.cosmetics.shield.ShieldLayout; import org.bukkit.DyeColor; import org.bukkit.Material; @@ -31,16 +30,14 @@ public static void applyShieldToPlayer(Player player) { Profile profile = ProfileManager.getInstance().getProfile(player); if (profile == null || profile.getCosmeticsData() == null) return; - boolean hasPermission = CosmeticsPermissionManager.hasShieldPermission(player); - ShieldLayout active = hasPermission ? profile.getCosmeticsData().getActiveShieldLayout() : null; + ShieldLayout active = profile.getCosmeticsData().getActiveShieldLayout(); for (ItemStack item : player.getInventory().getContents()) { if (item == null || item.getType() != Material.SHIELD) continue; if (active != null) { applyLayoutToItem(item, active); - } else { - clearShield(item); } + // No active layout → leave the shield completely untouched (plain default look) } player.updateInventory(); } From 160d4c1fe7a142f5dcb663f31512e3c773a2ce43 Mon Sep 17 00:00:00 2001 From: Nandor Dukat Date: Thu, 19 Mar 2026 19:02:44 +0100 Subject: [PATCH 24/26] updated for armor trim bug #2 --- .../manager/fight/match/util/KitUtil.java | 115 +++++++++--------- 1 file changed, 59 insertions(+), 56 deletions(-) diff --git a/core/src/main/java/dev/nandi0813/practice/manager/fight/match/util/KitUtil.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/match/util/KitUtil.java index df234241..5099f99a 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/fight/match/util/KitUtil.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/fight/match/util/KitUtil.java @@ -11,6 +11,7 @@ import dev.nandi0813.practice.manager.profile.cosmetics.armortrim.ArmorSlot; import dev.nandi0813.practice.manager.profile.cosmetics.armortrim.ArmorTrimTier; import dev.nandi0813.practice.util.KitData; +import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ArmorMeta; @@ -21,6 +22,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Objects; public enum KitUtil { ; @@ -33,11 +35,15 @@ public static void loadDefaultLadderKit(Player player, TeamEnum team, Ladder lad public static void loadKit(Player player, TeamEnum team, ItemStack[] armor, ItemStack[] inventory, ItemStack[] extra) { PlayerUtil.clearInventory(player); + ItemStack[] armorCopy = cloneItems(armor); + ItemStack[] inventoryCopy = cloneItems(inventory); + ItemStack[] extraCopy = cloneItems(extra); + if (team == null) { - LadderUtil.loadInventory(player, armor, inventory, extra); + LadderUtil.loadInventory(player, armorCopy, inventoryCopy, extraCopy); } else { List inventoryList = new ArrayList<>(); - for (ItemStack item : new ArrayList<>(Arrays.asList(inventory.clone()))) { + for (ItemStack item : new ArrayList<>(Arrays.asList(inventoryCopy))) { if (item != null) { item = LadderUtil.changeItemColor(item, team.getColor()); inventoryList.add(item); @@ -47,7 +53,7 @@ public static void loadKit(Player player, TeamEnum team, ItemStack[] armor, Item } List armorList = new ArrayList<>(); - for (ItemStack item : new ArrayList<>(Arrays.asList(armor.clone()))) { + for (ItemStack item : new ArrayList<>(Arrays.asList(armorCopy))) { if (item != null) { item = LadderUtil.changeItemColor(item, team.getColor()); armorList.add(item); @@ -57,8 +63,8 @@ public static void loadKit(Player player, TeamEnum team, ItemStack[] armor, Item } List extraList = new ArrayList<>(); - if (extra != null) { - for (ItemStack item : new ArrayList<>(Arrays.asList(extra.clone()))) { + if (extraCopy != null) { + for (ItemStack item : new ArrayList<>(Arrays.asList(extraCopy))) { if (item != null) { item = LadderUtil.changeItemColor(item, team.getColor()); extraList.add(item); @@ -71,7 +77,7 @@ public static void loadKit(Player player, TeamEnum team, ItemStack[] armor, Item LadderUtil.loadInventory(player, armorList.toArray(new ItemStack[0]), inventoryList.toArray(new ItemStack[0]), - extra != null ? extraList.toArray(new ItemStack[0]) : null); + extraCopy != null ? extraList.toArray(new ItemStack[0]) : null); } applyArmorTrimCosmetics(player); @@ -95,30 +101,19 @@ private static void applyArmorTrimCosmetics(Player player) { return; } - ArmorTrimTier activeTier = profile.getCosmeticsData().getActiveTier(); - if (!player.hasPermission(activeTier.getPermissionNode())) { - return; - } - // Helmet (index 3) - applyTrimToArmor(player, armorContents[3], - profile.getCosmeticsData().getPattern(activeTier, ArmorSlot.HELMET), - profile.getCosmeticsData().getMaterial(activeTier, ArmorSlot.HELMET), 3); + applyTrimToArmor(player, profile, armorContents, ArmorSlot.HELMET, 3); // Chestplate (index 2) - applyTrimToArmor(player, armorContents[2], - profile.getCosmeticsData().getPattern(activeTier, ArmorSlot.CHESTPLATE), - profile.getCosmeticsData().getMaterial(activeTier, ArmorSlot.CHESTPLATE), 2); + applyTrimToArmor(player, profile, armorContents, ArmorSlot.CHESTPLATE, 2); // Leggings (index 1) - applyTrimToArmor(player, armorContents[1], - profile.getCosmeticsData().getPattern(activeTier, ArmorSlot.LEGGINGS), - profile.getCosmeticsData().getMaterial(activeTier, ArmorSlot.LEGGINGS), 1); + applyTrimToArmor(player, profile, armorContents, ArmorSlot.LEGGINGS, 1); // Boots (index 0) - applyTrimToArmor(player, armorContents[0], - profile.getCosmeticsData().getPattern(activeTier, ArmorSlot.BOOTS), - profile.getCosmeticsData().getMaterial(activeTier, ArmorSlot.BOOTS), 0); + applyTrimToArmor(player, profile, armorContents, ArmorSlot.BOOTS, 0); + + player.getInventory().setArmorContents(armorContents); } catch (Exception e) { // Silently fail - if cosmetics cannot be applied, continue with kit distribution @@ -127,19 +122,6 @@ private static void applyArmorTrimCosmetics(Player player) { private static void applyShieldCosmetics(Player player) { try { - Profile profile = ProfileManager.getInstance().getProfile(player); - if (profile == null || profile.getCosmeticsData() == null) { - return; - } - - if (!CosmeticsPermissionManager.hasShieldPermission(player)) { - return; - } - - if (profile.getCosmeticsData().getActiveShieldLayout() == null) { - return; - } - ShieldCosmeticsUtil.applyShieldToPlayer(player); } catch (Exception e) { // Silently fail - if shield cosmetics cannot be applied, continue with kit distribution @@ -149,44 +131,65 @@ private static void applyShieldCosmetics(Player player) { /** * Apply a trim pattern and material to an armor piece if both are set. */ - private static void applyTrimToArmor(Player player, ItemStack item, - TrimPattern pattern, TrimMaterial material, int armorIndex) { + private static void applyTrimToArmor(Player player, Profile profile, ItemStack[] armorContents, + ArmorSlot slot, int armorIndex) { + ItemStack item = armorContents[armorIndex]; if (item == null || !item.hasItemMeta()) { return; } - // Both pattern and material must be set to apply trim - if (pattern == null || material == null) { + if (!(item.getItemMeta() instanceof ArmorMeta armorMeta)) { return; } - // Verify permission before applying - if (!player.hasPermission("zpp.cosmetics.armortrim.pattern." + getTrimId(pattern)) || - !player.hasPermission("zpp.cosmetics.armortrim.material." + getTrimId(material))) { - return; + ArmorTrim targetTrim = null; + ArmorTrimTier armorTier = getArmorTier(item.getType()); + if (armorTier != null && CosmeticsPermissionManager.hasBasePermission(player, armorTier)) { + TrimPattern pattern = profile.getCosmeticsData().getPattern(armorTier, slot); + TrimMaterial material = profile.getCosmeticsData().getMaterial(armorTier, slot); + if (pattern != null + && material != null + && CosmeticsPermissionManager.hasPatternPermission(player, pattern) + && CosmeticsPermissionManager.hasMaterialPermission(player, material)) { + targetTrim = new ArmorTrim(material, pattern); + } } try { - var meta = (ArmorMeta) item.getItemMeta(); - if (meta != null) { - meta.setTrim(new ArmorTrim(material, pattern)); - item.setItemMeta(meta); - - ItemStack[] armorContents = player.getInventory().getArmorContents(); + ArmorTrim currentTrim = armorMeta.getTrim(); + if (!Objects.equals(currentTrim, targetTrim)) { + armorMeta.setTrim(targetTrim); + item.setItemMeta(armorMeta); armorContents[armorIndex] = item; - player.getInventory().setArmorContents(armorContents); } } catch (Exception e) { // Silently fail - trim application may not be supported on this version or item type } } - private static String getTrimId(TrimPattern pattern) { - return CosmeticsPermissionManager.getTrimId(pattern); + private static ArmorTrimTier getArmorTier(Material material) { + return switch (material) { + case LEATHER_HELMET, LEATHER_CHESTPLATE, LEATHER_LEGGINGS, LEATHER_BOOTS -> ArmorTrimTier.LEATHER; + case GOLDEN_HELMET, GOLDEN_CHESTPLATE, GOLDEN_LEGGINGS, GOLDEN_BOOTS -> ArmorTrimTier.GOLD; + case IRON_HELMET, IRON_CHESTPLATE, IRON_LEGGINGS, IRON_BOOTS -> ArmorTrimTier.IRON; + case DIAMOND_HELMET, DIAMOND_CHESTPLATE, DIAMOND_LEGGINGS, DIAMOND_BOOTS -> ArmorTrimTier.DIAMOND; + case NETHERITE_HELMET, NETHERITE_CHESTPLATE, NETHERITE_LEGGINGS, NETHERITE_BOOTS -> ArmorTrimTier.NETHERITE; + default -> null; + }; } - private static String getTrimId(TrimMaterial material) { - return CosmeticsPermissionManager.getTrimId(material); + private static ItemStack[] cloneItems(ItemStack[] source) { + if (source == null) { + return null; + } + + ItemStack[] copy = source.clone(); + for (int i = 0; i < copy.length; i++) { + if (copy[i] != null) { + copy[i] = copy[i].clone(); + } + } + return copy; } -} +} \ No newline at end of file From 3ac80c2b0a884311443488ea83fd89edd9c78083 Mon Sep 17 00:00:00 2001 From: Nandor Dukat Date: Thu, 19 Mar 2026 19:15:20 +0100 Subject: [PATCH 25/26] fixed temporary build ladders to return to same hand --- .../abstraction/interfaces/TempBuild.java | 4 +- .../fightmapchange/FightChangeOptimized.java | 67 +++++++++++++++++-- 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/dev/nandi0813/practice/manager/ladder/abstraction/interfaces/TempBuild.java b/core/src/main/java/dev/nandi0813/practice/manager/ladder/abstraction/interfaces/TempBuild.java index 420dd49e..62f9d343 100644 --- a/core/src/main/java/dev/nandi0813/practice/manager/ladder/abstraction/interfaces/TempBuild.java +++ b/core/src/main/java/dev/nandi0813/practice/manager/ladder/abstraction/interfaces/TempBuild.java @@ -38,7 +38,7 @@ static void onBucketEmpty(final @NotNull PlayerBucketEmptyEvent e, final @NotNul Object mv = BlockUtil.getMetadata(relative, PLACED_IN_FIGHT, Object.class); if (ListenerUtil.checkMetaData(mv) || relative.getType().isSolid()) continue; - match.getFightChange().addBlockChange(new ChangedBlock(block), player, buildDelay); + match.getFightChange().addBlockChange(new ChangedBlock(block), player, buildDelay, e.getHand()); Block b2 = block.getLocation().subtract(0, 1, 0).getBlock(); if (ArenaUtil.turnsToDirt(b2)) @@ -55,7 +55,7 @@ static void onBlockPlace(final @NotNull BlockPlaceEvent e, final @NotNull Match BlockUtil.setMetadata(block, PLACED_IN_FIGHT, match); - match.getFightChange().addBlockChange(new ChangedBlock(e), player, buildDelay); + match.getFightChange().addBlockChange(new ChangedBlock(e), player, buildDelay, e.getHand()); Block block2 = e.getBlockPlaced().getLocation().subtract(0, 1, 0).getBlock(); if (ArenaUtil.turnsToDirt(block2)) 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 7b36ddc7..f7e7cc82 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 @@ -12,6 +12,9 @@ import org.bukkit.block.Block; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; import org.bukkit.scheduler.BukkitRunnable; import org.bukkit.scheduler.BukkitTask; @@ -135,11 +138,19 @@ public void addArenaBlockChange(ChangedBlock change) { * Adds a temporary block change that will auto-remove after delay. */ public void addBlockChange(ChangedBlock change, Player player, int destroyTime) { + addBlockChange(change, player, destroyTime, EquipmentSlot.HAND); + } + + /** + * Adds a temporary block change that will auto-remove after delay. + * Tracks the hand used for smarter item return placement. + */ + public void addBlockChange(ChangedBlock change, Player player, int destroyTime, @org.jetbrains.annotations.Nullable EquipmentSlot handUsed) { if (change == null) return; long pos = BlockPosition.encode(change.getLocation()); BlockChangeEntry entry = blocks.computeIfAbsent(pos, k -> new BlockChangeEntry(change)); - entry.setTempData(player, destroyTime * 20); // Convert seconds to ticks + entry.setTempData(player, destroyTime * 20, handUsed); // Convert seconds to ticks // Start ticker if not running ensureTempBlockTickerRunning(); @@ -214,12 +225,50 @@ private void tickTempBlocks() { private void removeTempBlock(BlockChangeEntry entry) { if (entry.tempData.returnItem && entry.tempData.player.isOnline()) { Block block = entry.changedBlock.getLocation().getBlock(); - entry.tempData.player.getInventory().addItem(block.getDrops().toArray(new org.bukkit.inventory.ItemStack[0])); + for (ItemStack drop : block.getDrops()) { + giveReturnedItem(entry.tempData.player, drop, entry.tempData.handUsed); + } } entry.changedBlock.reset(); } + private void giveReturnedItem(Player player, ItemStack drop, @org.jetbrains.annotations.Nullable EquipmentSlot handUsed) { + if (player == null || drop == null || drop.getType().isAir()) { + return; + } + + ItemStack remaining = drop.clone(); + PlayerInventory inventory = player.getInventory(); + + if (handUsed == EquipmentSlot.OFF_HAND) { + ItemStack offhand = inventory.getItemInOffHand(); + if (offhand == null || offhand.getType().isAir()) { + inventory.setItemInOffHand(remaining); + return; + } + + if (offhand.isSimilar(remaining)) { + int maxStack = offhand.getMaxStackSize(); + int space = maxStack - offhand.getAmount(); + if (space > 0) { + int moved = Math.min(space, remaining.getAmount()); + offhand.setAmount(offhand.getAmount() + moved); + inventory.setItemInOffHand(offhand); + remaining.setAmount(remaining.getAmount() - moved); + if (remaining.getAmount() <= 0) { + return; + } + } + } + } + + Map overflow = inventory.addItem(remaining); + if (!overflow.isEmpty()) { + overflow.values().forEach(item -> player.getWorld().dropItemNaturally(player.getLocation(), item)); + } + } + private static boolean isVineLike(org.bukkit.Material material) { String name = material.name(); return name.equals("VINE") || name.contains("_VINE") || name.contains("_VINES"); @@ -515,8 +564,8 @@ public static class BlockChangeEntry { this.changedBlock = changedBlock; } - void setTempData(Player player, int ticksRemaining) { - this.tempData = new TempBlockData(player, ticksRemaining); + void setTempData(Player player, int ticksRemaining, @org.jetbrains.annotations.Nullable EquipmentSlot handUsed) { + this.tempData = new TempBlockData(player, ticksRemaining, handUsed); } } @@ -527,13 +576,17 @@ void setTempData(Player player, int ticksRemaining) { public static class TempBlockData { @Getter final Player player; + @Getter + @org.jetbrains.annotations.Nullable + final EquipmentSlot handUsed; int ticksRemaining; @Setter boolean returnItem = true; - TempBlockData(Player player, int ticksRemaining) { + TempBlockData(Player player, int ticksRemaining, @org.jetbrains.annotations.Nullable EquipmentSlot handUsed) { this.player = player; this.ticksRemaining = ticksRemaining; + this.handUsed = handUsed; } /** @@ -542,7 +595,9 @@ public static class TempBlockData { public void reset(FightChangeOptimized fightChange, ChangedBlock changedBlock, long position) { if (returnItem && player.isOnline()) { Block block = changedBlock.getLocation().getBlock(); - player.getInventory().addItem(block.getDrops().toArray(new org.bukkit.inventory.ItemStack[0])); + for (ItemStack drop : block.getDrops()) { + fightChange.giveReturnedItem(player, drop, handUsed); + } } changedBlock.reset(); fightChange.getBlocks().remove(position); From 7e9cc6d3b7c86565b5961007de1b23ae0340edd2 Mon Sep 17 00:00:00 2001 From: Nandor Dukat Date: Thu, 19 Mar 2026 19:24:22 +0100 Subject: [PATCH 26/26] changed version to 7.1.0-SNAPSHOT --- core/pom.xml | 2 +- distribution/pom.xml | 4 ++-- pom.xml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/pom.xml b/core/pom.xml index cc09fb72..2262e5a6 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -7,7 +7,7 @@ dev.nandi0813 practice-parent - 7.0.0-SNAPSHOT + 7.1.0-SNAPSHOT practice-core diff --git a/distribution/pom.xml b/distribution/pom.xml index 9898f878..c3fe6a48 100644 --- a/distribution/pom.xml +++ b/distribution/pom.xml @@ -10,7 +10,7 @@ dev.nandi0813 practice-parent - 7.0.0-SNAPSHOT + 7.1.0-SNAPSHOT @@ -31,7 +31,7 @@ clean install - ZonePractice Pro v7.0.0-SNAPSHOT + ZonePractice Pro v7.1.0-SNAPSHOT diff --git a/pom.xml b/pom.xml index 3290be65..67fe9161 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ dev.nandi0813 practice-parent - 7.0.0-SNAPSHOT + 7.1.0-SNAPSHOT pom ZonePractice Pro