From c95901bc4254e1d9ec068a4a79e3adfef7fed3a9 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Fri, 23 Oct 2026 14:57:39 +0200 Subject: [PATCH] Add lobby particle slot menus Replace the particle inventory with a main menu showing the 7 slots and unlocked presets, a slot editor with a read-only view for locked slots, and an entry editor with pickers for designs, choices, colours and icons. Slots are limited to 10 entries and a particles-per-tick budget. --- .../de/steamwar/lobby/LobbySystem.properties | 99 +++++++ .../steamwar/lobby/LobbySystem_de.properties | 99 +++++++ .../lobby/particle/ParticleInventory.java | 76 ------ .../lobby/particle/ParticleListener.java | 3 +- .../lobby/particle/design/Designs.java | 10 + .../lobby/particle/gui/EntryEditor.java | 257 ++++++++++++++++++ .../lobby/particle/gui/ParticleGuiText.java | 148 ++++++++++ .../lobby/particle/gui/ParticleMenu.java | 126 +++++++++ .../steamwar/lobby/particle/gui/Pickers.java | 106 ++++++++ .../lobby/particle/gui/SlotEditor.java | 182 +++++++++++++ .../lobby/particle/slot/ParticleSlot.java | 4 + .../lobby/particle/slot/ParticleSlots.java | 8 + 12 files changed, 1041 insertions(+), 77 deletions(-) delete mode 100644 LobbySystem/src/de/steamwar/lobby/particle/ParticleInventory.java create mode 100644 LobbySystem/src/de/steamwar/lobby/particle/gui/EntryEditor.java create mode 100644 LobbySystem/src/de/steamwar/lobby/particle/gui/ParticleGuiText.java create mode 100644 LobbySystem/src/de/steamwar/lobby/particle/gui/ParticleMenu.java create mode 100644 LobbySystem/src/de/steamwar/lobby/particle/gui/Pickers.java create mode 100644 LobbySystem/src/de/steamwar/lobby/particle/gui/SlotEditor.java diff --git a/LobbySystem/src/de/steamwar/lobby/LobbySystem.properties b/LobbySystem/src/de/steamwar/lobby/LobbySystem.properties index a699b80f..2d80d7f9 100644 --- a/LobbySystem/src/de/steamwar/lobby/LobbySystem.properties +++ b/LobbySystem/src/de/steamwar/lobby/LobbySystem.properties @@ -85,6 +85,105 @@ PARTICLE_SELECT = §eClick to select PARTICLE_LOCKED = §c§lLocked PARTICLE_DEACTIVATED = §cThere are too many players online, particles are deactivated +PARTICLE_BACK = §7Back +PARTICLE_SLOT_EMPTY = §7Empty slot +PARTICLE_SLOT_CREATE = §eClick to create +PARTICLE_SLOT_ENTRIES = §7{0} entries +PARTICLE_SLOT_ACTIVE = §aActive +PARTICLE_SLOT_SELECT = §eLeft click§8: §7select +PARTICLE_SLOT_EDIT = §eRight click§8: §7edit +PARTICLE_SLOT_VIEW = §eClick§8: §7view +PARTICLE_SLOT_EDITOR = §0Edit {0} +PARTICLE_SLOT_VIEWER = §0{0} +PARTICLE_SLOT_ADD_ENTRY = §aAdd entry +PARTICLE_SLOT_MAX_ENTRIES = §cThis slot is full +PARTICLE_SLOT_OVER_BUDGET = §cThat would spawn too many particles for this slot +PARTICLE_SLOT_RENAME = §eRename +PARTICLE_SLOT_RENAME_TITLE = Slot name +PARTICLE_SLOT_ICON = §eChange icon +PARTICLE_SLOT_ICON_TITLE = §0Slot icon +PARTICLE_SLOT_DELETE = §cDelete slot +PARTICLE_SLOT_DELETE_CONFIRM = §7Shift click to delete +PARTICLE_SLOT_COST = §7Particles per tick§8: §e{0}§8/§7{1} + +PARTICLE_ENTRY_EDITOR = §0Edit entry +PARTICLE_ENTRY_DESIGN = §7Design§8: {0} +PARTICLE_ENTRY_PARTICLE = §7Particle§8: {0} +PARTICLE_ENTRY_LOCKED = §cDesign locked, not shown +PARTICLE_ENTRY_EDIT = §eLeft click§8: §7edit +PARTICLE_ENTRY_DUPLICATE = §eRight click§8: §7duplicate +PARTICLE_ENTRY_DELETE = §eShift right click§8: §7delete +PARTICLE_ENTRY_CHANGE_DESIGN = §eClick to change the design +PARTICLE_ENTRY_DELETE_ITEM = §cDelete entry + +PARTICLE_DESIGN_PICKER = §0Choose design +PARTICLE_CHOICE_PICKER = §0{0} +PARTICLE_COLOR_PICKER = §0Choose colour + +PARTICLE_VALUE = §7Value§8: {0} +PARTICLE_VALUE_NUMBER_HELP = §eLeft§8/§eright click§8: §7+/- {0}, §eshift§8: §7x10 +PARTICLE_VALUE_EXACT_HELP = §eQ§8: §7enter value +PARTICLE_VALUE_TOGGLE_HELP = §eClick§8: §7toggle +PARTICLE_VALUE_CHOICE_HELP = §eLeft§8/§eright click§8: §7next/previous +PARTICLE_VALUE_LIST_HELP = §eClick§8: §7choose +PARTICLE_VALUE_COLORS_HELP = §eLeft click§8: §7add colour, §eright click§8: §7remove last +PARTICLE_VALUE_RANDOM = §7Random colours +PARTICLE_VALUE_YES = §aYes +PARTICLE_VALUE_NO = §cNo + +PARTICLE_DESIGN_SIMPLE = §fSimple +PARTICLE_DESIGN_CIRCLE = §fCircle +PARTICLE_DESIGN_WING = §fWings +PARTICLE_DESIGN_CLOUD = §fCloud + +PARTICLE_PARAM_PARTICLE = §fParticle +PARTICLE_PARAM_COUNT = §fAmount +PARTICLE_PARAM_SPREAD_X = §fSpread X +PARTICLE_PARAM_SPREAD_Y = §fSpread Y +PARTICLE_PARAM_SPREAD_Z = §fSpread Z +PARTICLE_PARAM_SPEED = §fSpeed +PARTICLE_PARAM_COLORS = §fColours +PARTICLE_PARAM_OFFSET_X = §fOffset X +PARTICLE_PARAM_OFFSET_Y = §fOffset Y +PARTICLE_PARAM_OFFSET_Z = §fOffset Z +PARTICLE_PARAM_INTERVAL = §fEvery n ticks +PARTICLE_PARAM_TICK_MODE = §fShown +PARTICLE_PARAM_MOVEMENT = §fMovement +PARTICLE_PARAM_GLIDING = §fElytra +PARTICLE_PARAM_GROUND = §fGround +PARTICLE_PARAM_SNEAK = §fSneaking +PARTICLE_PARAM_VISIBILITY = §fVisible to +PARTICLE_PARAM_RADIUS = §fRadius +PARTICLE_PARAM_CIRCLE_SPEED = §fRotation speed +PARTICLE_PARAM_PHASE = §fStart angle +PARTICLE_PARAM_RISE = §fUp and down height +PARTICLE_PARAM_RISE_SPEED = §fUp and down speed +PARTICLE_PARAM_BOUNCE = §fBounce back +PARTICLE_PARAM_INVERTED = §fStart at the top +PARTICLE_PARAM_WING_IMAGE = §fWing shape +PARTICLE_PARAM_WING_SIZE = §fWing size + +PARTICLE_CHOICE_TICKMODE_MOVE = §fWhile moving +PARTICLE_CHOICE_TICKMODE_ALWAYS = §fAlways +PARTICLE_CHOICE_MOVEMENT_ANY = §fAny movement +PARTICLE_CHOICE_MOVEMENT_MOVING = §fOnly while moving +PARTICLE_CHOICE_MOVEMENT_STANDING_STILL = §fOnly while standing still +PARTICLE_CHOICE_GLIDING_ANY = §fWith or without elytra +PARTICLE_CHOICE_GLIDING_GLIDING = §fOnly while gliding +PARTICLE_CHOICE_GLIDING_NOT_GLIDING = §fNot while gliding +PARTICLE_CHOICE_GROUND_ANY = §fOn ground and in air +PARTICLE_CHOICE_GROUND_ON_GROUND = §fOnly on ground +PARTICLE_CHOICE_GROUND_IN_AIR = §fOnly in air +PARTICLE_CHOICE_SNEAK_ANY = §fSneaking or not +PARTICLE_CHOICE_SNEAK_SNEAKING = §fOnly while sneaking +PARTICLE_CHOICE_SNEAK_NOT_SNEAKING = §fNot while sneaking +PARTICLE_CHOICE_VISIBILITY_EVERYONE = §fEveryone +PARTICLE_CHOICE_VISIBILITY_ONLY_SELF = §fOnly me +PARTICLE_CHOICE_VISIBILITY_ONLY_OTHERS = §fOnly others +PARTICLE_CHOICE_IMAGE_SIMPLE = §fSimple +PARTICLE_CHOICE_IMAGE_COMPLEX = §fComplex +PARTICLE_CHOICE_IMAGE_SWORD = §fSword + PARTICLE_SNEEZE = §aSneeze PARTICLE_SMOKE = §7Smoke PARTICLE_FIRE = §cFire diff --git a/LobbySystem/src/de/steamwar/lobby/LobbySystem_de.properties b/LobbySystem/src/de/steamwar/lobby/LobbySystem_de.properties index 3931fc26..6c01a373 100644 --- a/LobbySystem/src/de/steamwar/lobby/LobbySystem_de.properties +++ b/LobbySystem/src/de/steamwar/lobby/LobbySystem_de.properties @@ -84,6 +84,105 @@ PARTICLE_SELECT = §eZum Auswählen klicken PARTICLE_LOCKED = §c§lGesperrt PARTICLE_DEACTIVATED = §cEs sind zu viele Spieler online! Partikel sind deaktiviert. +PARTICLE_BACK = §7Zurück +PARTICLE_SLOT_EMPTY = §7Leerer Slot +PARTICLE_SLOT_CREATE = §eZum Erstellen klicken +PARTICLE_SLOT_ENTRIES = §7{0} Einträge +PARTICLE_SLOT_ACTIVE = §aAktiv +PARTICLE_SLOT_SELECT = §eLinksklick§8: §7auswählen +PARTICLE_SLOT_EDIT = §eRechtsklick§8: §7bearbeiten +PARTICLE_SLOT_VIEW = §eKlick§8: §7ansehen +PARTICLE_SLOT_EDITOR = §0{0} bearbeiten +PARTICLE_SLOT_VIEWER = §0{0} +PARTICLE_SLOT_ADD_ENTRY = §aEintrag hinzufügen +PARTICLE_SLOT_MAX_ENTRIES = §cDieser Slot ist voll +PARTICLE_SLOT_OVER_BUDGET = §cDamit würde dieser Slot zu viele Partikel erzeugen +PARTICLE_SLOT_RENAME = §eUmbenennen +PARTICLE_SLOT_RENAME_TITLE = Slotname +PARTICLE_SLOT_ICON = §eSymbol ändern +PARTICLE_SLOT_ICON_TITLE = §0Slotsymbol +PARTICLE_SLOT_DELETE = §cSlot löschen +PARTICLE_SLOT_DELETE_CONFIRM = §7Zum Löschen Shift-Klick +PARTICLE_SLOT_COST = §7Partikel pro Tick§8: §e{0}§8/§7{1} + +PARTICLE_ENTRY_EDITOR = §0Eintrag bearbeiten +PARTICLE_ENTRY_DESIGN = §7Design§8: {0} +PARTICLE_ENTRY_PARTICLE = §7Partikel§8: {0} +PARTICLE_ENTRY_LOCKED = §cDesign gesperrt, wird nicht angezeigt +PARTICLE_ENTRY_EDIT = §eLinksklick§8: §7bearbeiten +PARTICLE_ENTRY_DUPLICATE = §eRechtsklick§8: §7duplizieren +PARTICLE_ENTRY_DELETE = §eShift-Rechtsklick§8: §7löschen +PARTICLE_ENTRY_CHANGE_DESIGN = §eZum Ändern des Designs klicken +PARTICLE_ENTRY_DELETE_ITEM = §cEintrag löschen + +PARTICLE_DESIGN_PICKER = §0Design wählen +PARTICLE_CHOICE_PICKER = §0{0} +PARTICLE_COLOR_PICKER = §0Farbe wählen + +PARTICLE_VALUE = §7Wert§8: {0} +PARTICLE_VALUE_NUMBER_HELP = §eLinks§8/§erechtsklick§8: §7+/- {0}, §eShift§8: §7x10 +PARTICLE_VALUE_EXACT_HELP = §eQ§8: §7Wert eingeben +PARTICLE_VALUE_TOGGLE_HELP = §eKlick§8: §7umschalten +PARTICLE_VALUE_CHOICE_HELP = §eLinks§8/§erechtsklick§8: §7weiter/zurück +PARTICLE_VALUE_LIST_HELP = §eKlick§8: §7auswählen +PARTICLE_VALUE_COLORS_HELP = §eLinksklick§8: §7Farbe hinzufügen, §eRechtsklick§8: §7letzte entfernen +PARTICLE_VALUE_RANDOM = §7Zufällige Farben +PARTICLE_VALUE_YES = §aJa +PARTICLE_VALUE_NO = §cNein + +PARTICLE_DESIGN_SIMPLE = §fEinfach +PARTICLE_DESIGN_CIRCLE = §fKreis +PARTICLE_DESIGN_WING = §fFlügel +PARTICLE_DESIGN_CLOUD = §fWolke + +PARTICLE_PARAM_PARTICLE = §fPartikel +PARTICLE_PARAM_COUNT = §fAnzahl +PARTICLE_PARAM_SPREAD_X = §fStreuung X +PARTICLE_PARAM_SPREAD_Y = §fStreuung Y +PARTICLE_PARAM_SPREAD_Z = §fStreuung Z +PARTICLE_PARAM_SPEED = §fGeschwindigkeit +PARTICLE_PARAM_COLORS = §fFarben +PARTICLE_PARAM_OFFSET_X = §fVersatz X +PARTICLE_PARAM_OFFSET_Y = §fVersatz Y +PARTICLE_PARAM_OFFSET_Z = §fVersatz Z +PARTICLE_PARAM_INTERVAL = §fAlle n Ticks +PARTICLE_PARAM_TICK_MODE = §fAnzeige +PARTICLE_PARAM_MOVEMENT = §fBewegung +PARTICLE_PARAM_GLIDING = §fElytra +PARTICLE_PARAM_GROUND = §fBoden +PARTICLE_PARAM_SNEAK = §fDucken +PARTICLE_PARAM_VISIBILITY = §fSichtbar für +PARTICLE_PARAM_RADIUS = §fRadius +PARTICLE_PARAM_CIRCLE_SPEED = §fDrehgeschwindigkeit +PARTICLE_PARAM_PHASE = §fStartwinkel +PARTICLE_PARAM_RISE = §fAuf und ab Höhe +PARTICLE_PARAM_RISE_SPEED = §fAuf und ab Geschwindigkeit +PARTICLE_PARAM_BOUNCE = §fZurückspringen +PARTICLE_PARAM_INVERTED = §fOben beginnen +PARTICLE_PARAM_WING_IMAGE = §fFlügelform +PARTICLE_PARAM_WING_SIZE = §fFlügelgröße + +PARTICLE_CHOICE_TICKMODE_MOVE = §fBeim Bewegen +PARTICLE_CHOICE_TICKMODE_ALWAYS = §fImmer +PARTICLE_CHOICE_MOVEMENT_ANY = §fJede Bewegung +PARTICLE_CHOICE_MOVEMENT_MOVING = §fNur beim Bewegen +PARTICLE_CHOICE_MOVEMENT_STANDING_STILL = §fNur beim Stehen +PARTICLE_CHOICE_GLIDING_ANY = §fMit oder ohne Elytra +PARTICLE_CHOICE_GLIDING_GLIDING = §fNur beim Gleiten +PARTICLE_CHOICE_GLIDING_NOT_GLIDING = §fNicht beim Gleiten +PARTICLE_CHOICE_GROUND_ANY = §fAm Boden und in der Luft +PARTICLE_CHOICE_GROUND_ON_GROUND = §fNur am Boden +PARTICLE_CHOICE_GROUND_IN_AIR = §fNur in der Luft +PARTICLE_CHOICE_SNEAK_ANY = §fDucken egal +PARTICLE_CHOICE_SNEAK_SNEAKING = §fNur beim Ducken +PARTICLE_CHOICE_SNEAK_NOT_SNEAKING = §fNicht beim Ducken +PARTICLE_CHOICE_VISIBILITY_EVERYONE = §fAlle +PARTICLE_CHOICE_VISIBILITY_ONLY_SELF = §fNur ich +PARTICLE_CHOICE_VISIBILITY_ONLY_OTHERS = §fNur andere +PARTICLE_CHOICE_IMAGE_SIMPLE = §fEinfach +PARTICLE_CHOICE_IMAGE_COMPLEX = §fKomplex +PARTICLE_CHOICE_IMAGE_SWORD = §fSchwert + PARTICLE_SNEEZE = §aSneeze PARTICLE_SMOKE = §7Rauch PARTICLE_FIRE = §cFeuer diff --git a/LobbySystem/src/de/steamwar/lobby/particle/ParticleInventory.java b/LobbySystem/src/de/steamwar/lobby/particle/ParticleInventory.java deleted file mode 100644 index 0ebe3b41..00000000 --- a/LobbySystem/src/de/steamwar/lobby/particle/ParticleInventory.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * This file is a part of the SteamWar software. - * - * Copyright (C) 2025 SteamWar.de-Serverteam - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package de.steamwar.lobby.particle; - -import de.steamwar.inventory.SWItem; -import de.steamwar.inventory.SWListInv; -import de.steamwar.lobby.LobbySystem; -import de.steamwar.lobby.particle.slot.ParticlePreset; -import de.steamwar.lobby.particle.slot.Presets; -import de.steamwar.lobby.util.LobbyPlayer; -import de.steamwar.sql.SteamwarUser; -import lombok.experimental.UtilityClass; -import org.bukkit.Material; -import org.bukkit.entity.Player; - -import java.util.ArrayList; -import java.util.List; - -// Lists the presets a player has unlocked; the slot menus replace this -@UtilityClass -public class ParticleInventory { - - public void openInventory(Player player) { - LobbyPlayer lobbyPlayer = LobbyPlayer.getLobbyPlayer(player); - UnlockContext context = new UnlockContext(SteamwarUser.get(player.getUniqueId())); - - List> presetList = new ArrayList<>(); - for (ParticlePreset preset : Presets.all()) { - if (!preset.getRequirement().test(context)) continue; - List lore = new ArrayList<>(); - lore.add(""); - lore.add(LobbySystem.getMessage().parse("PARTICLE_SELECT", player)); - presetList.add(new SWListInv.SWListEntry<>(new SWItem(preset.getIcon(), translate(preset.getNameKey(), player), lore, preset == lobbyPlayer.getActivePreset(), clickType -> { - }), preset)); - } - - SWListInv inventory = new SWListInv<>(player, LobbySystem.getMessage().parse("PARTICLE_INVENTORY", player), false, presetList, (clickType, preset) -> { - if (preset == null) return; - lobbyPlayer.selectPreset(preset); - player.closeInventory(); - if (ParticleListener.disabled(player)) { - LobbySystem.getMessage().send("PARTICLE_DEACTIVATED", player); - } - }); - inventory.setItem(49, Material.BARRIER, LobbySystem.getMessage().parse("PARTICLE_DESELECT", player), new ArrayList<>(), false, clickType -> { - lobbyPlayer.deselect(); - player.closeInventory(); - }); - inventory.open(); - } - - private String translate(String key, Player player) { - try { - return LobbySystem.getMessage().parse(key, player); - } catch (Exception e) { - return key; - } - } -} diff --git a/LobbySystem/src/de/steamwar/lobby/particle/ParticleListener.java b/LobbySystem/src/de/steamwar/lobby/particle/ParticleListener.java index 5b19c9e0..9d569ebb 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/ParticleListener.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/ParticleListener.java @@ -23,6 +23,7 @@ import de.steamwar.linkage.Linked; import de.steamwar.lobby.LobbySystem; import de.steamwar.lobby.jumpandrun.JumpAndRun; import de.steamwar.lobby.listener.PlayerSpawn; +import de.steamwar.lobby.particle.gui.ParticleMenu; import de.steamwar.lobby.util.LobbyPlayer; import de.steamwar.sql.SteamwarUser; import de.steamwar.sql.UserPerm; @@ -76,7 +77,7 @@ public class ParticleListener implements Listener { if (!PlayerSpawn.PARTICLE.equals(event.getItem())) return; event.setCancelled(true); - ParticleInventory.openInventory(player); + ParticleMenu.open(player); } @EventHandler diff --git a/LobbySystem/src/de/steamwar/lobby/particle/design/Designs.java b/LobbySystem/src/de/steamwar/lobby/particle/design/Designs.java index 7b6d3590..cfa4a419 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/design/Designs.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/design/Designs.java @@ -93,6 +93,16 @@ public class Designs { public int points(ParameterValues values) { return values.get(WING_IMAGE).getVectors().length; } + + // Wings spawn a particle on every pixel of the image, so they start sparse + @Override + public void applyDefaults(ParameterValues values) { + values.set(EntryParameters.COUNT, 1) + .set(EntryParameters.SPEED, 0.0) + .set(EntryParameters.TICK_MODE, EntryParameters.TickMode.ALWAYS) + .set(EntryParameters.INTERVAL, 20) + .set(EntryParameters.GLIDING, EntryParameters.Gliding.NOT_GLIDING); + } }; public final ParticleDesign CLOUD = new ParticleDesign("cloud", Material.WHITE_WOOL, "PARTICLE_DESIGN_CLOUD") { diff --git a/LobbySystem/src/de/steamwar/lobby/particle/gui/EntryEditor.java b/LobbySystem/src/de/steamwar/lobby/particle/gui/EntryEditor.java new file mode 100644 index 00000000..1f9229a0 --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/gui/EntryEditor.java @@ -0,0 +1,257 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.lobby.particle.gui; + +import de.steamwar.inventory.SWAnvilInv; +import de.steamwar.inventory.SWInventory; +import de.steamwar.inventory.SWItem; +import de.steamwar.lobby.LobbySystem; +import de.steamwar.lobby.particle.UnlockContext; +import de.steamwar.lobby.particle.WingDesign; +import de.steamwar.lobby.particle.design.*; +import de.steamwar.lobby.particle.slot.ParticleEntry; +import de.steamwar.lobby.particle.slot.ParticleSlot; +import de.steamwar.lobby.particle.slot.ParticleSlots; +import de.steamwar.lobby.util.LobbyPlayer; +import de.steamwar.sql.SteamwarUser; +import lombok.experimental.UtilityClass; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import static de.steamwar.lobby.particle.design.EntryParameters.*; +import static de.steamwar.lobby.particle.gui.ParticleGuiText.*; + +// One item per parameter of an entry; every change is saved and shown right away +@UtilityClass +class EntryEditor { + + private final int DESIGN = 4; + private final int FIRST_PARAMETER = 9; + private final int BACK = 45; + private final int COST = 49; + private final int DELETE = 53; + // Choices with more options than this open a list instead of cycling on click + private final int MAX_CYCLE_CHOICES = 6; + + private final Set> PARTICLE_PARAMETERS = Set.of(PARTICLE, COUNT, SPREAD_X, SPREAD_Y, SPREAD_Z, SPEED, COLORS); + + private record Context(Player player, SteamwarUser user, int index, int entryIndex, ParticleSlot slot, ParticleEntry entry) { + void reopen() { + open(player, index, entryIndex); + } + } + + void open(Player player, int index, int entryIndex) { + LobbyPlayer lobbyPlayer = LobbyPlayer.getLobbyPlayer(player); + ParticleSlot slot = lobbyPlayer.getSlot(index); + SteamwarUser user = SteamwarUser.get(player.getUniqueId()); + if (slot == null || entryIndex >= slot.getEntries().size() || !ParticleSlots.requirement(index).test(new UnlockContext(user))) { + SlotEditor.open(player, index); + return; + } + Context context = new Context(player, user, index, entryIndex, slot, slot.getEntries().get(entryIndex)); + ParticleEntry entry = context.entry(); + + SWInventory inventory = new SWInventory(player, 54, msg("PARTICLE_ENTRY_EDITOR", player)); + inventory.setItem(DESIGN, new SWItem(entry.getDesign().getIcon(), designName(entry.getDesign(), player), List.of(msg("PARTICLE_ENTRY_CHANGE_DESIGN", player)), false, + clickType -> Pickers.design(player, design -> changeDesign(context, design), context::reopen))); + + int position = FIRST_PARAMETER; + for (Parameter parameter : visibleParameters(entry)) { + if (position >= BACK) break; + inventory.setItem(position++, parameterItem(context, parameter)); + } + + inventory.setItem(BACK, new SWItem(Material.ARROW, msg("PARTICLE_BACK", player), clickType -> SlotEditor.open(player, index))); + inventory.setItem(COST, SlotEditor.costItem(player, user, slot)); + inventory.setItem(DELETE, new SWItem(Material.LAVA_BUCKET, msg("PARTICLE_ENTRY_DELETE_ITEM", player), List.of(msg("PARTICLE_SLOT_DELETE_CONFIRM", player)), false, clickType -> { + if (!clickType.isShiftClick()) return; + slot.getEntries().remove(entryIndex); + SlotEditor.save(player, index, slot); + SlotEditor.open(player, index); + })); + inventory.open(); + } + + private List> visibleParameters(ParticleEntry entry) { + List> parameters = new ArrayList<>(); + boolean dust = entry.getValues().get(PARTICLE).isDust(); + for (Parameter parameter : entry.getParameters()) { + if (!entry.getDesign().usesEntryParticle() && PARTICLE_PARAMETERS.contains(parameter)) continue; + if (parameter == COLORS && !dust) continue; + parameters.add(parameter); + } + return parameters; + } + + @SuppressWarnings("unchecked") + private SWItem parameterItem(Context context, Parameter parameter) { + if (parameter instanceof DoubleParameter number) return doubleItem(context, number); + if (parameter instanceof IntParameter number) return intItem(context, number); + if (parameter instanceof BooleanParameter toggle) return booleanItem(context, toggle); + if (parameter instanceof GradientParameter gradient) return gradientItem(context, gradient); + if (parameter instanceof ChoiceParameter choice) return choiceItem(context, (ChoiceParameter) choice); + throw new IllegalArgumentException("No editor for parameter " + parameter.getKey()); + } + + private List lore(Context context, Parameter parameter, String... help) { + List lore = new ArrayList<>(); + lore.add(msg("PARTICLE_VALUE", context.player(), valueText(parameter, context.entry().getValues().get(parameter), context.player()))); + lore.add(""); + for (String line : help) lore.add(line); + return lore; + } + + private SWItem doubleItem(Context context, DoubleParameter parameter) { + Player player = context.player(); + double value = context.entry().getValues().get(parameter); + List lore = lore(context, parameter, msg("PARTICLE_VALUE_NUMBER_HELP", player, number(parameter.getStep())), msg("PARTICLE_VALUE_EXACT_HELP", player)); + return new SWItem(Material.REPEATER, name(parameter.getNameKey(), player), lore, false, clickType -> { + if (clickType == ClickType.DROP || clickType == ClickType.CONTROL_DROP) { + askNumber(context, parameter, ParticleGuiText.number(value), input -> change(context, parameter, input)); + return; + } + double step = parameter.getStep() * (clickType.isShiftClick() ? 10 : 1) * (clickType.isRightClick() ? -1 : 1); + double next = Math.round((value + step) / parameter.getStep()) * parameter.getStep(); + change(context, parameter, next); + }); + } + + private SWItem intItem(Context context, IntParameter parameter) { + Player player = context.player(); + int value = context.entry().getValues().get(parameter); + List lore = lore(context, parameter, msg("PARTICLE_VALUE_NUMBER_HELP", player, 1), msg("PARTICLE_VALUE_EXACT_HELP", player)); + return new SWItem(Material.COMPARATOR, name(parameter.getNameKey(), player), lore, false, clickType -> { + if (clickType == ClickType.DROP || clickType == ClickType.CONTROL_DROP) { + askNumber(context, parameter, String.valueOf(value), input -> change(context, parameter, (int) Math.round(input))); + return; + } + int step = (clickType.isShiftClick() ? 10 : 1) * (clickType.isRightClick() ? -1 : 1); + change(context, parameter, value + step); + }); + } + + private SWItem booleanItem(Context context, BooleanParameter parameter) { + boolean value = context.entry().getValues().get(parameter); + List lore = lore(context, parameter, msg("PARTICLE_VALUE_TOGGLE_HELP", context.player())); + return new SWItem(value ? Material.LIME_DYE : Material.GRAY_DYE, name(parameter.getNameKey(), context.player()), lore, false, + clickType -> change(context, parameter, !value)); + } + + private SWItem gradientItem(Context context, GradientParameter parameter) { + Player player = context.player(); + List colors = context.entry().getValues().get(parameter); + List lore = lore(context, parameter, msg("PARTICLE_VALUE_COLORS_HELP", player)); + return new SWItem(Material.WHITE_DYE, name(parameter.getNameKey(), player), lore, false, clickType -> { + if (clickType.isRightClick()) { + if (colors.isEmpty()) return; + change(context, parameter, colors.subList(0, colors.size() - 1)); + } else if (colors.size() < parameter.getMaxColors()) { + Pickers.color(player, color -> { + List next = new ArrayList<>(colors); + next.add(color); + change(context, parameter, next); + }, context::reopen); + } + }); + } + + private SWItem choiceItem(Context context, ChoiceParameter parameter) { + Player player = context.player(); + E value = context.entry().getValues().get(parameter); + List values = parameter.getBuildableValues(); + boolean list = values.size() > MAX_CYCLE_CHOICES; + List lore = lore(context, parameter, msg(list ? "PARTICLE_VALUE_LIST_HELP" : "PARTICLE_VALUE_CHOICE_HELP", player)); + return new SWItem(choiceIcon(value), name(parameter.getNameKey(), player), lore, false, clickType -> { + if (list) { + Pickers.choice(player, parameter, EntryEditor::choiceIcon, picked -> change(context, parameter, picked), context::reopen); + return; + } + int current = values.indexOf(value); + int next = Math.floorMod(current + (clickType.isRightClick() ? -1 : 1), values.size()); + change(context, parameter, values.get(next)); + }); + } + + private Material choiceIcon(Object value) { + if (value instanceof ParticleType type) return type.getIcon(); + if (value instanceof WingDesign) return Material.ELYTRA; + return Material.LEVER; + } + + private void askNumber(Context context, Parameter parameter, String current, java.util.function.DoubleConsumer onInput) { + Player player = context.player(); + SWAnvilInv anvil = new SWAnvilInv(player, name(parameter.getNameKey(), player), current); + anvil.setItem(Material.PAPER); + anvil.setCallback(input -> Bukkit.getScheduler().runTask(LobbySystem.getInstance(), () -> { + try { + onInput.accept(Double.parseDouble(input.trim().replace(',', '.'))); + } catch (NumberFormatException e) { + context.reopen(); + } + })); + anvil.open(); + } + + // Reverts the change when it would push the slot over its budget + private void change(Context context, Parameter parameter, T value) { + T previous = context.entry().getValues().get(parameter); + context.entry().getValues().set(parameter, value); + if (!SlotEditor.fits(context.user(), context.slot())) { + context.entry().getValues().set(parameter, previous); + LobbySystem.getMessage().send("PARTICLE_SLOT_OVER_BUDGET", context.player()); + } else { + SlotEditor.save(context.player(), context.index(), context.slot()); + } + context.reopen(); + } + + // Keeps the particle, offset and conditions; a fixed design brings its own conditions and timing + private void changeDesign(Context context, ParticleDesign design) { + ParticleEntry previous = context.entry(); + ParticleEntry next = new ParticleEntry(design); + for (Parameter parameter : EntryParameters.ALL) { + copy(parameter, previous.getValues(), next.getValues()); + } + if (!design.usesEntryParticle()) { + design.applyDefaults(next.getValues()); + } + List entries = context.slot().getEntries(); + entries.set(context.entryIndex(), next); + if (!SlotEditor.fits(context.user(), context.slot())) { + entries.set(context.entryIndex(), previous); + LobbySystem.getMessage().send("PARTICLE_SLOT_OVER_BUDGET", context.player()); + } else { + SlotEditor.save(context.player(), context.index(), context.slot()); + } + context.reopen(); + } + + private void copy(Parameter parameter, ParameterValues from, ParameterValues to) { + to.set(parameter, from.get(parameter)); + } +} diff --git a/LobbySystem/src/de/steamwar/lobby/particle/gui/ParticleGuiText.java b/LobbySystem/src/de/steamwar/lobby/particle/gui/ParticleGuiText.java new file mode 100644 index 00000000..208aafdb --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/gui/ParticleGuiText.java @@ -0,0 +1,148 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.lobby.particle.gui; + +import de.steamwar.lobby.LobbySystem; +import de.steamwar.lobby.particle.ParticleRequirement; +import de.steamwar.lobby.particle.UnlockContext; +import de.steamwar.lobby.particle.design.*; +import de.steamwar.lobby.particle.slot.ParticleEntry; +import de.steamwar.lobby.particle.slot.ParticleSlot; +import lombok.experimental.UtilityClass; +import org.bukkit.entity.Player; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.MissingResourceException; + +import static de.steamwar.lobby.particle.design.EntryParameters.*; + +@UtilityClass +class ParticleGuiText { + + String msg(String key, Player player, Object... params) { + return LobbySystem.getMessage().parse(key, player, params); + } + + // Falls back to a readable form of the key for names without a translation + String name(String key, Player player) { + try { + return LobbySystem.getMessage().parse(key, player); + } catch (MissingResourceException e) { + return "§f" + prettify(key.replaceFirst("^PARTICLE_(TYPE_|DESIGN_|PARAM_)?", "")); + } + } + + String prettify(String id) { + StringBuilder builder = new StringBuilder(); + for (String word : id.toLowerCase(Locale.ROOT).split("_")) { + if (word.isEmpty()) continue; + if (!builder.isEmpty()) builder.append(' '); + builder.append(Character.toUpperCase(word.charAt(0))).append(word.substring(1)); + } + return builder.toString(); + } + + String designName(ParticleDesign design, Player player) { + return name(design.getNameKey(), player); + } + + String particleName(ParticleType type, Player player) { + return name(type.getNameKey(), player); + } + + String choiceName(ChoiceParameter parameter, E value, Player player) { + for (ChoiceParameter.Choice choice : parameter.getChoices()) { + if (choice.value() != value) continue; + if (value instanceof ParticleType type) return particleName(type, player); + return name("PARTICLE_CHOICE_" + parameter.getKey().toUpperCase(Locale.ROOT) + "_" + choice.id().toUpperCase(Locale.ROOT), player); + } + return String.valueOf(value); + } + + String number(double value) { + return value == Math.rint(value) ? String.valueOf((long) value) : String.format(Locale.ROOT, "%.2f", value).replaceAll("0+$", ""); + } + + @SuppressWarnings("unchecked") + String valueText(Parameter parameter, Object value, Player player) { + if (parameter instanceof DoubleParameter) return "§e" + number((Double) value); + if (parameter instanceof IntParameter) return "§e" + value; + if (parameter instanceof BooleanParameter) return msg((Boolean) value ? "PARTICLE_VALUE_YES" : "PARTICLE_VALUE_NO", player); + if (parameter instanceof ChoiceParameter choice) return choiceName((ChoiceParameter) choice, value, player); + if (parameter instanceof GradientParameter) { + List colors = (List) value; + if (colors.isEmpty()) return msg("PARTICLE_VALUE_RANDOM", player); + StringBuilder builder = new StringBuilder(); + colors.forEach(color -> builder.append(net.md_5.bungee.api.ChatColor.of(color)).append('█')); + return builder.toString(); + } + return String.valueOf(value); + } + + // Requirement lines for an item's lore, empty when there is no requirement + List requirement(ParticleRequirement requirement, Player player) { + List lore = new ArrayList<>(); + String name = requirement.getRequirementName(player); + if (name != null) { + lore.add(msg("PARTICLE_UNLOCKED_BY", player)); + lore.add(name); + } + return lore; + } + + // Short description of an entry: design, particle and the conditions that are set + List entrySummary(ParticleEntry entry, UnlockContext context, Player player) { + List lore = new ArrayList<>(); + ParameterValues values = entry.getValues(); + lore.add(msg("PARTICLE_ENTRY_DESIGN", player, designName(entry.getDesign(), player))); + if (entry.getDesign().usesEntryParticle()) { + lore.add(msg("PARTICLE_ENTRY_PARTICLE", player, particleName(values.get(PARTICLE), player))); + } + lore.add("§8- " + choiceName(TICK_MODE, values.get(TICK_MODE), player)); + addCondition(lore, MOVEMENT, values.get(MOVEMENT), Movement.ANY, player); + addCondition(lore, GLIDING, values.get(GLIDING), Gliding.ANY, player); + addCondition(lore, GROUND, values.get(GROUND), Ground.ANY, player); + addCondition(lore, SNEAK, values.get(SNEAK), Sneak.ANY, player); + addCondition(lore, VISIBILITY, values.get(VISIBILITY), Visibility.EVERYONE, player); + if (!entry.getDesign().getRequirement().test(context)) { + lore.add(msg("PARTICLE_ENTRY_LOCKED", player)); + } + return lore; + } + + private void addCondition(List lore, ChoiceParameter parameter, E value, E none, Player player) { + if (value != none) { + lore.add("§8- " + choiceName(parameter, value, player)); + } + } + + List slotSummary(ParticleSlot slot, Player player) { + List lore = new ArrayList<>(); + lore.add(msg("PARTICLE_SLOT_ENTRIES", player, slot.getEntries().size())); + for (ParticleEntry entry : slot.getEntries()) { + String particle = entry.getDesign().usesEntryParticle() ? " §8(" + particleName(entry.getValues().get(PARTICLE), player) + "§8)" : ""; + lore.add("§8- " + designName(entry.getDesign(), player) + particle); + } + return lore; + } +} diff --git a/LobbySystem/src/de/steamwar/lobby/particle/gui/ParticleMenu.java b/LobbySystem/src/de/steamwar/lobby/particle/gui/ParticleMenu.java new file mode 100644 index 00000000..1127c7b6 --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/gui/ParticleMenu.java @@ -0,0 +1,126 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.lobby.particle.gui; + +import de.steamwar.inventory.SWInventory; +import de.steamwar.inventory.SWItem; +import de.steamwar.lobby.LobbySystem; +import de.steamwar.lobby.particle.ParticleListener; +import de.steamwar.lobby.particle.ParticleRequirement; +import de.steamwar.lobby.particle.UnlockContext; +import de.steamwar.lobby.particle.slot.ParticlePreset; +import de.steamwar.lobby.particle.slot.ParticleSlot; +import de.steamwar.lobby.particle.slot.ParticleSlots; +import de.steamwar.lobby.particle.slot.Presets; +import de.steamwar.lobby.util.LobbyPlayer; +import de.steamwar.sql.SteamwarUser; +import lombok.experimental.UtilityClass; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; + +import java.util.ArrayList; +import java.util.List; + +import static de.steamwar.lobby.particle.gui.ParticleGuiText.*; + +// The particle item's menu: the player's slots in the top row, their unlocked presets below +@UtilityClass +public class ParticleMenu { + + private final int NO_PARTICLE_POSITION = 8; + private final int PRESET_START = 18; + + public void open(Player player) { + LobbyPlayer lobbyPlayer = LobbyPlayer.getLobbyPlayer(player); + UnlockContext context = new UnlockContext(SteamwarUser.get(player.getUniqueId())); + List presets = Presets.all().stream().filter(preset -> preset.getRequirement().test(context)).toList(); + int rows = presets.isEmpty() ? 1 : Math.min(6, 2 + (presets.size() + 8) / 9); + + SWInventory inventory = new SWInventory(player, rows * 9, msg("PARTICLE_INVENTORY", player)); + for (int index = 0; index < ParticleSlots.COUNT; index++) { + inventory.setItem(index, slotItem(player, lobbyPlayer, context, index)); + } + inventory.setItem(NO_PARTICLE_POSITION, new SWItem(Material.BARRIER, msg("PARTICLE_DESELECT", player), clickType -> { + lobbyPlayer.deselect(); + player.closeInventory(); + })); + + for (int i = 0; i < presets.size() && PRESET_START + i < rows * 9; i++) { + ParticlePreset preset = presets.get(i); + List lore = new ArrayList<>(); + lore.add(""); + lore.add(msg("PARTICLE_SELECT", player)); + inventory.setItem(PRESET_START + i, new SWItem(preset.getIcon(), ParticleGuiText.name(preset.getNameKey(), player), lore, preset == lobbyPlayer.getActivePreset(), clickType -> { + lobbyPlayer.selectPreset(preset); + selected(player); + })); + } + inventory.open(); + } + + private SWItem slotItem(Player player, LobbyPlayer lobbyPlayer, UnlockContext context, int index) { + ParticleSlot slot = lobbyPlayer.getSlot(index); + ParticleRequirement requirement = ParticleSlots.requirement(index); + boolean unlocked = requirement.test(context); + + if (slot == null) { + if (!unlocked) { + List lore = new ArrayList<>(ParticleGuiText.requirement(requirement, player)); + return new SWItem(Material.BARRIER, msg("PARTICLE_LOCKED", player), lore, false, clickType -> { + }); + } + List lore = List.of(msg("PARTICLE_SLOT_CREATE", player)); + return new SWItem(Material.GRAY_STAINED_GLASS_PANE, msg("PARTICLE_SLOT_EMPTY", player), lore, false, clickType -> { + lobbyPlayer.setSlot(index, new ParticleSlot(ParticleSlots.defaultName(index))); + SlotEditor.open(player, index); + }); + } + + List lore = new ArrayList<>(ParticleGuiText.slotSummary(slot, player)); + lore.add(""); + boolean active = lobbyPlayer.getActiveSlot() == index; + if (!unlocked) { + lore.add(msg("PARTICLE_LOCKED", player)); + lore.addAll(ParticleGuiText.requirement(requirement, player)); + lore.add(msg("PARTICLE_SLOT_VIEW", player)); + return new SWItem(slot.getDisplayIcon(), "§f" + slot.getName(), lore, false, clickType -> SlotEditor.view(player, index)); + } + if (active) { + lore.add(msg("PARTICLE_SLOT_ACTIVE", player)); + } + lore.add(msg("PARTICLE_SLOT_SELECT", player)); + lore.add(msg("PARTICLE_SLOT_EDIT", player)); + return new SWItem(slot.getDisplayIcon(), "§f" + slot.getName(), lore, active, clickType -> { + if (clickType == ClickType.RIGHT || clickType == ClickType.SHIFT_RIGHT) { + SlotEditor.open(player, index); + } else if (lobbyPlayer.selectSlot(index)) { + selected(player); + } + }); + } + + private void selected(Player player) { + player.closeInventory(); + if (ParticleListener.disabled(player)) { + LobbySystem.getMessage().send("PARTICLE_DEACTIVATED", player); + } + } +} diff --git a/LobbySystem/src/de/steamwar/lobby/particle/gui/Pickers.java b/LobbySystem/src/de/steamwar/lobby/particle/gui/Pickers.java new file mode 100644 index 00000000..9f26e11f --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/gui/Pickers.java @@ -0,0 +1,106 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.lobby.particle.gui; + +import de.steamwar.inventory.SWItem; +import de.steamwar.inventory.SWListInv; +import de.steamwar.lobby.particle.UnlockContext; +import de.steamwar.lobby.particle.design.*; +import de.steamwar.sql.SteamwarUser; +import lombok.experimental.UtilityClass; +import org.bukkit.DyeColor; +import org.bukkit.Material; +import org.bukkit.entity.Player; + +import java.awt.Color; +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Function; + +import static de.steamwar.lobby.particle.gui.ParticleGuiText.*; + +@UtilityClass +class Pickers { + + private final int BACK = 49; + + // Basic designs first, then rewards; locked rewards are shown with their requirement but cannot be picked + void design(Player player, Consumer onPick, Runnable onBack) { + UnlockContext context = new UnlockContext(SteamwarUser.get(player.getUniqueId())); + List> list = new ArrayList<>(); + for (DesignVisibility visibility : List.of(DesignVisibility.BASIC, DesignVisibility.REWARD)) { + for (ParticleDesign design : Designs.all()) { + if (design.getVisibility() != visibility) continue; + List lore = new ArrayList<>(requirement(design.getRequirement(), player)); + boolean unlocked = design.getRequirement().test(context); + if (!lore.isEmpty()) lore.add(""); + lore.add(msg(unlocked ? "PARTICLE_SELECT" : "PARTICLE_LOCKED", player)); + list.add(new SWListInv.SWListEntry<>(new SWItem(design.getIcon(), designName(design, player), lore, false, clickType -> { + }), unlocked ? design : null)); + } + } + open(player, msg("PARTICLE_DESIGN_PICKER", player), list, onPick, onBack); + } + + void choice(Player player, ChoiceParameter parameter, Function icon, Consumer onPick, Runnable onBack) { + List> list = new ArrayList<>(); + for (E value : parameter.getBuildableValues()) { + list.add(new SWListInv.SWListEntry<>(new SWItem(icon.apply(value), choiceName(parameter, value, player), clickType -> { + }), value)); + } + open(player, msg("PARTICLE_CHOICE_PICKER", player, name(parameter.getNameKey(), player)), list, onPick, onBack); + } + + void color(Player player, Consumer onPick, Runnable onBack) { + List> list = new ArrayList<>(); + for (DyeColor dye : DyeColor.values()) { + Color color = new Color(dye.getColor().asRGB()); + list.add(new SWListInv.SWListEntry<>(new SWItem(Material.valueOf(dye.name() + "_DYE"), net.md_5.bungee.api.ChatColor.of(color) + prettify(dye.name()), clickType -> { + }), color)); + } + open(player, msg("PARTICLE_COLOR_PICKER", player), list, onPick, onBack); + } + + void icon(Player player, Consumer onPick, Runnable onBack) { + Set icons = new LinkedHashSet<>(); + for (ParticleType type : ParticleType.values()) { + if (type.isBuildable()) icons.add(type.getIcon()); + } + for (ParticleDesign design : Designs.all()) { + if (design.getVisibility() == DesignVisibility.BASIC) icons.add(design.getIcon()); + } + icons.addAll(List.of(Material.DIAMOND, Material.EMERALD, Material.GOLD_INGOT, Material.AMETHYST_SHARD, Material.HEART_OF_THE_SEA, Material.NETHER_STAR, Material.FEATHER, Material.TNT)); + + List> list = new ArrayList<>(); + for (Material material : icons) { + list.add(new SWListInv.SWListEntry<>(new SWItem(material, "§f" + prettify(material.name()), clickType -> { + }), material)); + } + open(player, msg("PARTICLE_SLOT_ICON_TITLE", player), list, onPick, onBack); + } + + private void open(Player player, String title, List> list, Consumer onPick, Runnable onBack) { + SWListInv inventory = new SWListInv<>(player, title, false, list, (clickType, value) -> { + if (value != null) onPick.accept(value); + }); + inventory.setItem(BACK, new SWItem(Material.ARROW, msg("PARTICLE_BACK", player), clickType -> onBack.run())); + inventory.open(); + } +} diff --git a/LobbySystem/src/de/steamwar/lobby/particle/gui/SlotEditor.java b/LobbySystem/src/de/steamwar/lobby/particle/gui/SlotEditor.java new file mode 100644 index 00000000..9afa3e01 --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/gui/SlotEditor.java @@ -0,0 +1,182 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.lobby.particle.gui; + +import de.steamwar.inventory.SWAnvilInv; +import de.steamwar.inventory.SWInventory; +import de.steamwar.inventory.SWItem; +import de.steamwar.lobby.LobbySystem; +import de.steamwar.lobby.particle.UnlockContext; +import de.steamwar.lobby.particle.slot.ParticleEntry; +import de.steamwar.lobby.particle.slot.ParticleSlot; +import de.steamwar.lobby.particle.slot.ParticleSlots; +import de.steamwar.lobby.util.LobbyPlayer; +import de.steamwar.sql.SteamwarUser; +import lombok.experimental.UtilityClass; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; + +import java.util.ArrayList; +import java.util.List; + +import static de.steamwar.lobby.particle.gui.ParticleGuiText.*; + +// Lists a slot's entries; opened read-only for slots the player has lost access to +@UtilityClass +public class SlotEditor { + + private final int BACK = 45; + private final int ADD_ENTRY = 47; + private final int RENAME = 48; + private final int ICON = 49; + private final int COST = 50; + private final int DELETE = 53; + + public void open(Player player, int index) { + open(player, index, false); + } + + public void view(Player player, int index) { + open(player, index, true); + } + + private void open(Player player, int index, boolean readOnly) { + LobbyPlayer lobbyPlayer = LobbyPlayer.getLobbyPlayer(player); + ParticleSlot slot = lobbyPlayer.getSlot(index); + if (slot == null) { + ParticleMenu.open(player); + return; + } + SteamwarUser user = SteamwarUser.get(player.getUniqueId()); + UnlockContext context = new UnlockContext(user); + // Locked slots can only ever be viewed, whatever the caller asked for + boolean locked = !ParticleSlots.requirement(index).test(context); + boolean editable = !readOnly && !locked; + + SWInventory inventory = new SWInventory(player, 54, msg(editable ? "PARTICLE_SLOT_EDITOR" : "PARTICLE_SLOT_VIEWER", player, slot.getName())); + List entries = slot.getEntries(); + for (int i = 0; i < entries.size(); i++) { + ParticleEntry entry = entries.get(i); + List lore = new ArrayList<>(entrySummary(entry, context, player)); + if (editable) { + lore.add(""); + lore.add(msg("PARTICLE_ENTRY_EDIT", player)); + lore.add(msg("PARTICLE_ENTRY_DUPLICATE", player)); + lore.add(msg("PARTICLE_ENTRY_DELETE", player)); + } + int entryIndex = i; + inventory.setItem(i, new SWItem(icon(entry), designName(entry.getDesign(), player), lore, false, clickType -> { + if (!editable) return; + if (clickType == ClickType.SHIFT_RIGHT) { + entries.remove(entryIndex); + save(player, index, slot); + } else if (clickType == ClickType.RIGHT) { + if (entries.size() >= ParticleSlots.MAX_ENTRIES) { + LobbySystem.getMessage().send("PARTICLE_SLOT_MAX_ENTRIES", player); + return; + } + entries.add(entryIndex + 1, new ParticleEntry(entry.getDesign(), entry.getValues().copy())); + if (!fits(user, slot)) { + entries.remove(entryIndex + 1); + LobbySystem.getMessage().send("PARTICLE_SLOT_OVER_BUDGET", player); + return; + } + save(player, index, slot); + } else { + EntryEditor.open(player, index, entryIndex); + return; + } + open(player, index); + })); + } + + inventory.setItem(BACK, new SWItem(Material.ARROW, msg("PARTICLE_BACK", player), clickType -> ParticleMenu.open(player))); + if (!editable) { + List lore = new ArrayList<>(requirement(ParticleSlots.requirement(index), player)); + inventory.setItem(ICON, new SWItem(Material.BARRIER, msg("PARTICLE_LOCKED", player), lore, false, clickType -> { + })); + inventory.open(); + return; + } + + inventory.setItem(ADD_ENTRY, new SWItem(Material.NETHER_STAR, msg("PARTICLE_SLOT_ADD_ENTRY", player), clickType -> { + if (entries.size() >= ParticleSlots.MAX_ENTRIES) { + LobbySystem.getMessage().send("PARTICLE_SLOT_MAX_ENTRIES", player); + return; + } + Pickers.design(player, design -> { + entries.add(new ParticleEntry(design)); + if (!fits(user, slot)) { + entries.remove(entries.size() - 1); + LobbySystem.getMessage().send("PARTICLE_SLOT_OVER_BUDGET", player); + open(player, index); + return; + } + save(player, index, slot); + EntryEditor.open(player, index, entries.size() - 1); + }, () -> open(player, index)); + })); + inventory.setItem(RENAME, new SWItem(Material.NAME_TAG, msg("PARTICLE_SLOT_RENAME", player), clickType -> { + SWAnvilInv anvil = new SWAnvilInv(player, msg("PARTICLE_SLOT_RENAME_TITLE", player), slot.getName()); + anvil.setItem(Material.NAME_TAG); + anvil.setCallback(name -> { + String cleaned = name.replace("§", "").trim(); + slot.setName(cleaned.isEmpty() ? ParticleSlots.defaultName(index) : cleaned); + save(player, index, slot); + Bukkit.getScheduler().runTask(LobbySystem.getInstance(), () -> open(player, index)); + }); + anvil.open(); + })); + inventory.setItem(ICON, new SWItem(slot.getDisplayIcon(), msg("PARTICLE_SLOT_ICON", player), clickType -> Pickers.icon(player, icon -> { + slot.setIcon(icon); + save(player, index, slot); + open(player, index); + }, () -> open(player, index)))); + inventory.setItem(COST, costItem(player, user, slot)); + inventory.setItem(DELETE, new SWItem(Material.LAVA_BUCKET, msg("PARTICLE_SLOT_DELETE", player), List.of(msg("PARTICLE_SLOT_DELETE_CONFIRM", player)), false, clickType -> { + if (!clickType.isShiftClick()) return; + if (lobbyPlayer.getActiveSlot() == index) { + lobbyPlayer.deselect(); + } + lobbyPlayer.setSlot(index, null); + ParticleMenu.open(player); + })); + inventory.open(); + } + + SWItem costItem(Player player, SteamwarUser user, ParticleSlot slot) { + return new SWItem(Material.PAPER, msg("PARTICLE_SLOT_COST", player, number(slot.cost()), number(ParticleSlots.maxCost(user))), clickType -> { + }); + } + + boolean fits(SteamwarUser user, ParticleSlot slot) { + return slot.cost() <= ParticleSlots.maxCost(user); + } + + void save(Player player, int index, ParticleSlot slot) { + LobbyPlayer.getLobbyPlayer(player).setSlot(index, slot); + } + + Material icon(ParticleEntry entry) { + return entry.getDesign().getIcon(); + } +} diff --git a/LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleSlot.java b/LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleSlot.java index 094c848d..107caad7 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleSlot.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleSlot.java @@ -56,6 +56,10 @@ public class ParticleSlot implements ParticleSet { return entries.get(0).getValues().get(EntryParameters.PARTICLE).getIcon(); } + public double cost() { + return entries.stream().mapToDouble(ParticleEntry::cost).sum(); + } + public void setName(String name) { this.name = name.length() > MAX_NAME_LENGTH ? name.substring(0, MAX_NAME_LENGTH) : name; } diff --git a/LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleSlots.java b/LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleSlots.java index c8de7589..47e254bd 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleSlots.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleSlots.java @@ -22,7 +22,9 @@ package de.steamwar.lobby.particle.slot; import com.google.gson.JsonParser; import de.steamwar.lobby.LobbySystem; import de.steamwar.lobby.particle.ParticleRequirement; +import de.steamwar.sql.SteamwarUser; import de.steamwar.sql.UserConfig; +import de.steamwar.sql.UserPerm; import lombok.experimental.UtilityClass; import java.util.logging.Level; @@ -31,8 +33,14 @@ import java.util.logging.Level; public class ParticleSlots { public final int COUNT = 7; + public final int MAX_ENTRIES = 10; private final String KEY_PREFIX = "lobby-particle-slot-"; + // Particle positions per tick a slot may spawn + public double maxCost(SteamwarUser user) { + return user.hasPerm(UserPerm.TEAM) ? 80 : 40; + } + // Slots 1-3 for everyone, 4-5 for team members, 6-7 for the server team public ParticleRequirement requirement(int index) { if (index < 3) return ParticleRequirement.NO_REQUIRMENT;