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' 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/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/core/src/main/java/dev/nandi0813/practice/ZonePractice.java b/core/src/main/java/dev/nandi0813/practice/ZonePractice.java index 461684fa..ab446651 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; @@ -47,6 +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.CosmeticsPermissionManager; import dev.nandi0813.practice.manager.server.ServerManager; import dev.nandi0813.practice.manager.sidebar.SidebarManager; import dev.nandi0813.practice.util.*; @@ -118,6 +109,7 @@ public void onEnable() { DivisionManager.getInstance().getData(); ArenaWorldUtil.createArenaWorld(); BackendManager.createFile(this); + CosmeticsPermissionManager.registerAllPermissions(); ZonePracticeApiImpl.setup(); StartUpUtil.loadStartUpProgressMap(); @@ -342,6 +334,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/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/singlecommands/CosmeticsCommand.java b/core/src/main/java/dev/nandi0813/practice/command/singlecommands/CosmeticsCommand.java new file mode 100644 index 00000000..ce947281 --- /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.CosmeticsHubGui; +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) + CosmeticsHubGui cosmeticsHubGui = new CosmeticsHubGui(profile); + cosmeticsHubGui.open(player); + + return true; + } +} + 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 90% 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..50ca212a 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; @@ -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/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; 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..682c1d2b 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; @@ -13,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; @@ -47,41 +47,54 @@ 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) { + consumeUsedSoup(player, e.getHand()); + player.setHealth(health + regen); + } else if ((health + regen) >= maxHealth) { + consumeUsedSoup(player, e.getHand()); + player.setHealth(maxHealth); } + player.updateInventory(); break; } } + 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(); 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..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,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.armortrim.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/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/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); } 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--; 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..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; @@ -373,10 +374,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 +385,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)) { @@ -408,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/ffa/game/BuildRollback.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/ffa/game/BuildRollback.java index 53c5ffc6..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 @@ -13,10 +13,12 @@ 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) { + public BuildRollback(FightChangeOptimized fightChange, java.lang.Runnable onRollbackComplete) { super(20L, 20L, false); this.fightChange = fightChange; + this.onRollbackComplete = onRollbackComplete; this.seconds = ROLLBACK_SECONDS; } @@ -43,9 +45,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..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 @@ -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; @@ -78,7 +80,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(); } @@ -201,9 +203,15 @@ 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)); } + + if (arena.isHealthResetOnKill()) { + applyHealthResetOnKill(killer); + } } if (arena.isLobbyAfterDeath()) { @@ -217,6 +225,43 @@ 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; + 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. @@ -258,6 +303,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/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/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/match/Match.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/match/Match.java index df73321e..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 @@ -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) { @@ -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); @@ -504,6 +531,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 +549,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/match/listener/LadderTypeListener.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/match/listener/LadderTypeListener.java index 3570058b..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; @@ -452,8 +453,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; @@ -569,7 +571,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); + } } } @@ -584,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); } } } @@ -594,18 +598,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/fight/match/util/KitUtil.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/match/util/KitUtil.java index 081b1997..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 @@ -2,15 +2,27 @@ 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.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.Material; 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; import java.util.List; +import java.util.Objects; public enum KitUtil { ; @@ -23,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); @@ -37,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); @@ -47,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); @@ -61,10 +77,119 @@ 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); + applyShieldCosmetics(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; + } + + // Helmet (index 3) + applyTrimToArmor(player, profile, armorContents, ArmorSlot.HELMET, 3); + + // Chestplate (index 2) + applyTrimToArmor(player, profile, armorContents, ArmorSlot.CHESTPLATE, 2); + + // Leggings (index 1) + applyTrimToArmor(player, profile, armorContents, ArmorSlot.LEGGINGS, 1); + + // Boots (index 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 + } + } + + private static void applyShieldCosmetics(Player player) { + try { + 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. + */ + private static void applyTrimToArmor(Player player, Profile profile, ItemStack[] armorContents, + ArmorSlot slot, int armorIndex) { + ItemStack item = armorContents[armorIndex]; + if (item == null || !item.hasItemMeta()) { + return; + } + + if (!(item.getItemMeta() instanceof ArmorMeta armorMeta)) { + 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 { + ArmorTrim currentTrim = armorMeta.getTrim(); + if (!Objects.equals(currentTrim, targetTrim)) { + armorMeta.setTrim(targetTrim); + item.setItemMeta(armorMeta); + armorContents[armorIndex] = item; + } + } 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 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 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/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/fight/util/PlayerUtil.java b/core/src/main/java/dev/nandi0813/practice/manager/fight/util/PlayerUtil.java index 0a933eba..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 @@ -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; @@ -70,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); @@ -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; + } + } 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..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 @@ -96,4 +96,22 @@ public enum GUIType { FFA_Arena_Selector, FFA_Ladder_Selector, + // Cosmetics GUIs + 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/armortrim/ArmorPieceHubGui.java b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/armortrim/ArmorPieceHubGui.java new file mode 100644 index 00000000..69699503 --- /dev/null +++ b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/armortrim/ArmorPieceHubGui.java @@ -0,0 +1,195 @@ +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.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; +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 = CosmeticsPermissionManager.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(CosmeticsPermissionManager.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/armortrim/ArmorTrimMainGui.java b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/armortrim/ArmorTrimMainGui.java new file mode 100644 index 00000000..58298a4e --- /dev/null +++ b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/armortrim/ArmorTrimMainGui.java @@ -0,0 +1,293 @@ +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.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; +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 ArmorTrimMainGui extends GUI { + + private static final int MAIN_ROWS = 5; + 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; + 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 ArmorTrimMainGui(Profile profile, GUI backToGui) { + super(GUIType.ArmorTrimMainGui); + 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; + } + + 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; + 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 = CosmeticsPermissionManager.getRegisteredPatterns().size(); + int totalMaterialPermissions = CosmeticsPermissionManager.getRegisteredMaterials().size(); + int playerPatternPermissions = 0; + int playerMaterialPermissions = 0; + + if (player != null) { + for (TrimPattern pattern : CosmeticsPermissionManager.getRegisteredPatterns()) { + if (player.hasPermission("zpp.cosmetics.armortrim.pattern." + getPermissionId(pattern))) { + playerPatternPermissions++; + } + } + + for (TrimMaterial material : CosmeticsPermissionManager.getRegisteredMaterials()) { + if (player.hasPermission("zpp.cosmetics.armortrim.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 CosmeticsPermissionManager.getTrimId(trimPattern); + } + + if (trimValue instanceof TrimMaterial trimMaterial) { + return CosmeticsPermissionManager.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/armortrim/MaterialSelectionGui.java b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/armortrim/MaterialSelectionGui.java new file mode 100644 index 00000000..dd303cc6 --- /dev/null +++ b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/armortrim/MaterialSelectionGui.java @@ -0,0 +1,195 @@ +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.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; +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 : CosmeticsPermissionManager.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.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); + return; + } + + profile.getCosmeticsData().setMaterial(tier, armorSlot, material); + profile.saveData(); + update(true); + } + + private ItemStack buildMaterialItem(Player player, ArmorTrimTier tier, TrimMaterial material) { + 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 + && CosmeticsPermissionManager.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/armortrim/PatternSelectionGui.java b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/armortrim/PatternSelectionGui.java new file mode 100644 index 00000000..a3a1ec22 --- /dev/null +++ b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/cosmetics/armortrim/PatternSelectionGui.java @@ -0,0 +1,165 @@ +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.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; +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 : CosmeticsPermissionManager.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.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); + return; + } + + profile.getCosmeticsData().setPattern(tier, armorSlot, pattern); + profile.saveData(); + update(true); + } + + private ItemStack buildPatternItem(Player player, ArmorTrimTier tier, TrimPattern pattern) { + 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 + && CosmeticsPermissionManager.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/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/gui/guis/customladder/premadecustom/CustomLadderEditorGui.java b/core/src/main/java/dev/nandi0813/practice/manager/gui/guis/customladder/premadecustom/CustomLadderEditorGui.java index 56ad4423..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,13 +239,10 @@ public void open(Player player) { player.getInventory().setContents(ladder.getKitData().getStorage()); } - 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(); - case RANKED: - return GUIFile.getGuiItem("GUIS.KIT-EDITOR.KIT-EDITOR.ICONS.ONLY-RANKED").replace("%weightClass%", WeightClass.RANKED.getName()).get(); + case UNRANKED, RANKED: + 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(); 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/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/java/dev/nandi0813/practice/manager/inventory/InventoryListener.java b/core/src/main/java/dev/nandi0813/practice/manager/inventory/InventoryListener.java index 65ec561a..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 @@ -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(); @@ -144,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 (ServerManager.getInstance().getInWorld().containsKey(player) && ServerManager.getInstance().getInWorld().get(player).equals(WorldEnum.LOBBY)) { - e.setCancelled(!player.hasPermission("zpp.admin")); + if (isLobbyStatus(profileStatus)) { + if (!isLobbyProtectionAllowed("allow-block-break") && !player.hasPermission("zpp.admin")) { + e.setCancelled(true); + } + return; + } + + 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; } } @@ -165,15 +195,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 +218,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 +244,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 +268,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 +295,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 +332,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/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/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/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; } 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..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 @@ -6,6 +6,9 @@ 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.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; @@ -74,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<>(); @@ -117,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) { @@ -195,12 +201,20 @@ 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)); } 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..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,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.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; 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.*; public class ProfileFile extends ConfigFile { @@ -61,6 +70,38 @@ 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()); + 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()) { + String basePath = "cosmetics.tiers." + tier.getId() + "." + slot.getId(); + + TrimPattern pattern = profile.getCosmeticsData().getPattern(tier, slot); + if (pattern != null) { + 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:" + CosmeticsPermissionManager.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 +178,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 +195,71 @@ 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); + 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()) { + 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 +305,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/CosmeticsData.java b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/CosmeticsData.java new file mode 100644 index 00000000..7f9832a0 --- /dev/null +++ b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/CosmeticsData.java @@ -0,0 +1,110 @@ +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 +@Setter +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); + + 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()); + } + + 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/CosmeticsPermissionManager.java b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/CosmeticsPermissionManager.java new file mode 100644 index 00000000..bab21522 --- /dev/null +++ b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/CosmeticsPermissionManager.java @@ -0,0 +1,266 @@ +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; +import org.bukkit.entity.Player; +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 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_./-]+)"); + + 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."); + } + + 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.armortrim.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.armortrim.material." + id, + "Use armor trim material " + id + "."); + }); + + 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() { + return Collections.unmodifiableList(REGISTERED_PATTERNS); + } + + 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.armortrim.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.armortrim.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.armortrim.pattern.*") + || player.hasPermission(node); + } + + public static boolean hasMaterialPermission(Player player, TrimMaterial material) { + if (player == null || material == null) { + return false; + } + + return hasMaterialPermission(player, "zpp.cosmetics.armortrim.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.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"; + } + + 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) { + if (value == null || value.isBlank()) { + return "unknown"; + } + 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/armortrim/ArmorSlot.java b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/armortrim/ArmorSlot.java new file mode 100644 index 00000000..57f927f8 --- /dev/null +++ b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/armortrim/ArmorSlot.java @@ -0,0 +1,22 @@ +package dev.nandi0813.practice.manager.profile.cosmetics.armortrim; + +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/armortrim/ArmorTrimTier.java b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/armortrim/ArmorTrimTier.java new file mode 100644 index 00000000..f9b3055c --- /dev/null +++ b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/armortrim/ArmorTrimTier.java @@ -0,0 +1,101 @@ +package dev.nandi0813.practice.manager.profile.cosmetics.armortrim; + +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.armortrim.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/armortrim/CosmeticsPermissionSanitizer.java b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/armortrim/CosmeticsPermissionSanitizer.java new file mode 100644 index 00000000..1b306906 --- /dev/null +++ b/core/src/main/java/dev/nandi0813/practice/manager/profile/cosmetics/armortrim/CosmeticsPermissionSanitizer.java @@ -0,0 +1,112 @@ +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; + +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; + + EnumSet supportedSlots = EnumSet.of( + ArmorSlot.HELMET, + ArmorSlot.CHESTPLATE, + ArmorSlot.LEGGINGS, + ArmorSlot.BOOTS + ); + + for (ArmorTrimTier tier : ArmorTrimTier.values()) { + boolean hasTierPermission = CosmeticsPermissionManager.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 && !CosmeticsPermissionManager.hasPatternPermission(player, pattern)) { + profile.getCosmeticsData().setPattern(tier, slot, null); + changed = true; + } + + if (material != null && !CosmeticsPermissionManager.hasMaterialPermission(player, material)) { + profile.getCosmeticsData().setMaterial(tier, slot, null); + changed = true; + } + } + } + + ArmorTrimTier activeTier = profile.getCosmeticsData().getActiveTier(); + if (!CosmeticsPermissionManager.hasBasePermission(player, activeTier)) { + ArmorTrimTier replacement = null; + for (ArmorTrimTier tier : ArmorTrimTier.values()) { + if (CosmeticsPermissionManager.hasBasePermission(player, tier)) { + replacement = tier; + break; + } + } + + if (replacement == null) { + replacement = ArmorTrimTier.LEATHER; + } + + if (replacement != activeTier) { + profile.getCosmeticsData().setActiveTier(replacement); + changed = true; + } + } + + 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(); + } + + return changed; + } +} + 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/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/java/dev/nandi0813/practice/util/fightmapchange/FightChangeOptimized.java b/core/src/main/java/dev/nandi0813/practice/util/fightmapchange/FightChangeOptimized.java index 111b3d35..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,80 @@ 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"); + } + + 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 +409,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 +465,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; @@ -487,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); } } @@ -499,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; } /** @@ -514,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); diff --git a/core/src/main/resources/config.yml b/core/src/main/resources/config.yml index 69a1e9f8..2460e83c 100644 --- a/core/src/main/resources/config.yml +++ b/core/src/main/resources/config.yml @@ -1,4 +1,4 @@ -VERSION: 19 +VERSION: 22 # 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. @@ -263,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 @@ -504,6 +504,15 @@ 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 + allow-block-break: false + allow-block-place: false LOBBY-NAMETAG: ENABLED: true NAMETAG-MANAGEMENT: 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: diff --git a/core/src/main/resources/guis.yml b/core/src/main/resources/guis.yml index ee7f6149..0fab0c2a 100644 --- a/core/src/main/resources/guis.yml +++ b/core/src/main/resources/guis.yml @@ -1,4 +1,4 @@ -VERSION: 13 +VERSION: 17 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" @@ -1569,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: @@ -2396,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: @@ -2819,4 +2843,324 @@ 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%" + - "" + - "&eLeft-Click to customize" + - "&cRight-Click to reset" + TIER-TOGGLE-LORE: + - "" + - "&7Left-click: &cPrevious tier" + - "&7Right-click: &aNext tier" + 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%" + # 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%" + MATERIAL-ICONS: + 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/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 diff --git a/core/src/main/resources/plugin.yml b/core/src/main/resources/plugin.yml index 02d7bc90..20582211 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.armortrim.base.*: + description: Allow access to all armor base tiers. + children: + 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.armortrim.base.gold: + description: Allow access to Gold armor tier. + zpp.cosmetics.armortrim.base.iron: + description: Allow access to Iron armor tier. + zpp.cosmetics.armortrim.base.diamond: + description: Allow access to Diamond armor tier. + zpp.cosmetics.armortrim.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 in the server's Minecraft version. + # 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. 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