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.
This commit is contained in:
2026-10-26 09:48:00 +01:00
parent 211e2693cf
commit c95901bc42
12 changed files with 1041 additions and 77 deletions
@@ -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
@@ -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
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<SWListInv.SWListEntry<ParticlePreset>> presetList = new ArrayList<>();
for (ParticlePreset preset : Presets.all()) {
if (!preset.getRequirement().test(context)) continue;
List<String> 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<ParticlePreset> 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;
}
}
}
@@ -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
@@ -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") {
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Parameter<?>> 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<Parameter<?>> visibleParameters(ParticleEntry entry) {
List<Parameter<?>> 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<Object>) choice);
throw new IllegalArgumentException("No editor for parameter " + parameter.getKey());
}
private List<String> lore(Context context, Parameter<?> parameter, String... help) {
List<String> 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<String> 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<String> 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<String> 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<Color> colors = context.entry().getValues().get(parameter);
List<String> 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<Color> next = new ArrayList<>(colors);
next.add(color);
change(context, parameter, next);
}, context::reopen);
}
});
}
private <E> SWItem choiceItem(Context context, ChoiceParameter<E> parameter) {
Player player = context.player();
E value = context.entry().getValues().get(parameter);
List<E> values = parameter.getBuildableValues();
boolean list = values.size() > MAX_CYCLE_CHOICES;
List<String> 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 <T> void change(Context context, Parameter<T> 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<ParticleEntry> 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 <T> void copy(Parameter<T> parameter, ParameterValues from, ParameterValues to) {
to.set(parameter, from.get(parameter));
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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);
}
<E> String choiceName(ChoiceParameter<E> parameter, E value, Player player) {
for (ChoiceParameter.Choice<E> 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<Object>) choice, value, player);
if (parameter instanceof GradientParameter) {
List<Color> colors = (List<Color>) 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<String> requirement(ParticleRequirement requirement, Player player) {
List<String> 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<String> entrySummary(ParticleEntry entry, UnlockContext context, Player player) {
List<String> 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 <E> void addCondition(List<String> lore, ChoiceParameter<E> parameter, E value, E none, Player player) {
if (value != none) {
lore.add("§8- " + choiceName(parameter, value, player));
}
}
List<String> slotSummary(ParticleSlot slot, Player player) {
List<String> 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;
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<ParticlePreset> 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<String> 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<String> lore = new ArrayList<>(ParticleGuiText.requirement(requirement, player));
return new SWItem(Material.BARRIER, msg("PARTICLE_LOCKED", player), lore, false, clickType -> {
});
}
List<String> 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<String> 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);
}
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<ParticleDesign> onPick, Runnable onBack) {
UnlockContext context = new UnlockContext(SteamwarUser.get(player.getUniqueId()));
List<SWListInv.SWListEntry<ParticleDesign>> list = new ArrayList<>();
for (DesignVisibility visibility : List.of(DesignVisibility.BASIC, DesignVisibility.REWARD)) {
for (ParticleDesign design : Designs.all()) {
if (design.getVisibility() != visibility) continue;
List<String> 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);
}
<E> void choice(Player player, ChoiceParameter<E> parameter, Function<E, Material> icon, Consumer<E> onPick, Runnable onBack) {
List<SWListInv.SWListEntry<E>> 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<Color> onPick, Runnable onBack) {
List<SWListInv.SWListEntry<Color>> 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<Material> onPick, Runnable onBack) {
Set<Material> 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<SWListInv.SWListEntry<Material>> 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 <T> void open(Player player, String title, List<SWListInv.SWListEntry<T>> list, Consumer<T> onPick, Runnable onBack) {
SWListInv<T> 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();
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<ParticleEntry> entries = slot.getEntries();
for (int i = 0; i < entries.size(); i++) {
ParticleEntry entry = entries.get(i);
List<String> 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<String> 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();
}
}
@@ -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;
}
@@ -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;