Simplify lobby particle menus

Every menu item now acts on a plain click: slots open their editor with a
use button, entries open their editor, and duplicating is gone. The entry
editor groups settings into labelled rows, numbers and colours get their
own screens instead of relying on right click or Q, and slot names show
the particles per tick used out of the maximum.
This commit is contained in:
2026-10-26 09:48:20 +01:00
parent 98f04451a4
commit b60cdfc88d
8 changed files with 347 additions and 221 deletions
@@ -66,15 +66,33 @@ PARTICLE_LOCKED = §c§lLocked
PARTICLE_DEACTIVATED = §cThere are too many players online, particles are deactivated PARTICLE_DEACTIVATED = §cThere are too many players online, particles are deactivated
PARTICLE_BACK = §7Back PARTICLE_BACK = §7Back
PARTICLE_SLOT_OPEN = §eClick to open
PARTICLE_SLOT_NAME = §f{0} §8(§e{1}§8/§7{2}§8)
PARTICLE_SLOT_TITLE = §0{0} §8({1}/{2})
PARTICLE_SLOT_USE = §aUse this slot
PARTICLE_SLOT_STOP_USING = §cStop using this slot
PARTICLE_SHIFT_TO_DELETE = §7Shift click to delete
PARTICLE_SECTION_DESIGN = §fDesign
PARTICLE_SECTION_PARTICLE = §fParticle
PARTICLE_SECTION_SHAPE = §fShape
PARTICLE_SECTION_TIMING = §fTiming and position
PARTICLE_SECTION_CONDITIONS = §fConditions
PARTICLE_FIXED_LOOK = §7This design always looks the same
PARTICLE_VALUE_CLICK = §eClick to change
PARTICLE_VALUE_TOGGLE = §eClick to toggle
PARTICLE_VALUE_SELECTED = §aSelected
PARTICLE_VALUE_RESET = §eReset to {0}
PARTICLE_NUMBER_RANGE = §7Range§8: §e{0} §8- §e{1}
PARTICLE_NUMBER_TYPE = §eClick to type a value
PARTICLE_COLOR_EDITOR = §0Colours
PARTICLE_COLOR_REMOVE = §eClick to remove
PARTICLE_COLOR_ADD = §aAdd colour
PARTICLE_COLOR_RANDOM = §eUse random colours
PARTICLE_SLOT_EMPTY = §7Empty slot PARTICLE_SLOT_EMPTY = §7Empty slot
PARTICLE_SLOT_CREATE = §eClick to create PARTICLE_SLOT_CREATE = §eClick to create
PARTICLE_SLOT_ENTRIES = §7{0} entries PARTICLE_SLOT_ENTRIES = §7{0} entries
PARTICLE_SLOT_ACTIVE = §aActive PARTICLE_SLOT_ACTIVE = §aActive
PARTICLE_SLOT_SELECT = §eLeft click§8: §7select PARTICLE_SLOT_VIEW = §eClick to view
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_ADD_ENTRY = §aAdd entry
PARTICLE_SLOT_MAX_ENTRIES = §cThis slot is full PARTICLE_SLOT_MAX_ENTRIES = §cThis slot is full
PARTICLE_SLOT_OVER_BUDGET = §cThat would spawn too many particles for this slot PARTICLE_SLOT_OVER_BUDGET = §cThat would spawn too many particles for this slot
@@ -83,30 +101,20 @@ PARTICLE_SLOT_RENAME_TITLE = Slot name
PARTICLE_SLOT_ICON = §eChange icon PARTICLE_SLOT_ICON = §eChange icon
PARTICLE_SLOT_ICON_TITLE = §0Slot icon PARTICLE_SLOT_ICON_TITLE = §0Slot icon
PARTICLE_SLOT_DELETE = §cDelete slot 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_SLOT_COST = §7Particles per tick§8: §e{0}§8/§7{1}
PARTICLE_ENTRY_EDITOR = §0Edit entry PARTICLE_ENTRY_EDITOR = §0{0}
PARTICLE_ENTRY_DESIGN = §7Design§8: {0} PARTICLE_ENTRY_DESIGN = §7Design§8: {0}
PARTICLE_ENTRY_PARTICLE = §7Particle§8: {0} PARTICLE_ENTRY_PARTICLE = §7Particle§8: {0}
PARTICLE_ENTRY_LOCKED = §cDesign locked, not shown PARTICLE_ENTRY_LOCKED = §cDesign locked, not shown
PARTICLE_ENTRY_EDIT = §eLeft click§8: §7edit PARTICLE_ENTRY_EDIT = §eClick to edit
PARTICLE_ENTRY_DUPLICATE = §eRight click§8: §7duplicate PARTICLE_ENTRY_DELETE = §cDelete entry
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_DESIGN_PICKER = §0Choose design
PARTICLE_CHOICE_PICKER = §0{0} PARTICLE_CHOICE_PICKER = §0{0}
PARTICLE_COLOR_PICKER = §0Choose colour PARTICLE_COLOR_PICKER = §0Choose colour
PARTICLE_VALUE = §7Value§8: {0} 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_RANDOM = §7Random colours
PARTICLE_VALUE_YES = §aYes PARTICLE_VALUE_YES = §aYes
PARTICLE_VALUE_NO = §cNo PARTICLE_VALUE_NO = §cNo
@@ -66,15 +66,33 @@ PARTICLE_LOCKED = §c§lGesperrt
PARTICLE_DEACTIVATED = §cEs sind zu viele Spieler online! Partikel sind deaktiviert. PARTICLE_DEACTIVATED = §cEs sind zu viele Spieler online! Partikel sind deaktiviert.
PARTICLE_BACK = §7Zurück PARTICLE_BACK = §7Zurück
PARTICLE_SLOT_OPEN = §eZum Öffnen klicken
PARTICLE_SLOT_NAME = §f{0} §8(§e{1}§8/§7{2}§8)
PARTICLE_SLOT_TITLE = §0{0} §8({1}/{2})
PARTICLE_SLOT_USE = §aDiesen Slot verwenden
PARTICLE_SLOT_STOP_USING = §cDiesen Slot nicht mehr verwenden
PARTICLE_SHIFT_TO_DELETE = §7Zum Löschen Shift-Klick
PARTICLE_SECTION_DESIGN = §fDesign
PARTICLE_SECTION_PARTICLE = §fPartikel
PARTICLE_SECTION_SHAPE = §fForm
PARTICLE_SECTION_TIMING = §fZeit und Position
PARTICLE_SECTION_CONDITIONS = §fBedingungen
PARTICLE_FIXED_LOOK = §7Dieses Design sieht immer gleich aus
PARTICLE_VALUE_CLICK = §eZum Ändern klicken
PARTICLE_VALUE_TOGGLE = §eZum Umschalten klicken
PARTICLE_VALUE_SELECTED = §aAusgewählt
PARTICLE_VALUE_RESET = §eAuf {0} zurücksetzen
PARTICLE_NUMBER_RANGE = §7Bereich§8: §e{0} §8- §e{1}
PARTICLE_NUMBER_TYPE = §eZum Eingeben klicken
PARTICLE_COLOR_EDITOR = §0Farben
PARTICLE_COLOR_REMOVE = §eZum Entfernen klicken
PARTICLE_COLOR_ADD = §aFarbe hinzufügen
PARTICLE_COLOR_RANDOM = §eZufällige Farben verwenden
PARTICLE_SLOT_EMPTY = §7Leerer Slot PARTICLE_SLOT_EMPTY = §7Leerer Slot
PARTICLE_SLOT_CREATE = §eZum Erstellen klicken PARTICLE_SLOT_CREATE = §eZum Erstellen klicken
PARTICLE_SLOT_ENTRIES = §7{0} Einträge PARTICLE_SLOT_ENTRIES = §7{0} Einträge
PARTICLE_SLOT_ACTIVE = §aAktiv PARTICLE_SLOT_ACTIVE = §aAktiv
PARTICLE_SLOT_SELECT = §eLinksklick§8: §7auswählen PARTICLE_SLOT_VIEW = §eZum Ansehen klicken
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_ADD_ENTRY = §aEintrag hinzufügen
PARTICLE_SLOT_MAX_ENTRIES = §cDieser Slot ist voll PARTICLE_SLOT_MAX_ENTRIES = §cDieser Slot ist voll
PARTICLE_SLOT_OVER_BUDGET = §cDamit würde dieser Slot zu viele Partikel erzeugen PARTICLE_SLOT_OVER_BUDGET = §cDamit würde dieser Slot zu viele Partikel erzeugen
@@ -83,30 +101,20 @@ PARTICLE_SLOT_RENAME_TITLE = Slotname
PARTICLE_SLOT_ICON = §eSymbol ändern PARTICLE_SLOT_ICON = §eSymbol ändern
PARTICLE_SLOT_ICON_TITLE = §0Slotsymbol PARTICLE_SLOT_ICON_TITLE = §0Slotsymbol
PARTICLE_SLOT_DELETE = §cSlot löschen 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_SLOT_COST = §7Partikel pro Tick§8: §e{0}§8/§7{1}
PARTICLE_ENTRY_EDITOR = §0Eintrag bearbeiten PARTICLE_ENTRY_EDITOR = §0{0}
PARTICLE_ENTRY_DESIGN = §7Design§8: {0} PARTICLE_ENTRY_DESIGN = §7Design§8: {0}
PARTICLE_ENTRY_PARTICLE = §7Partikel§8: {0} PARTICLE_ENTRY_PARTICLE = §7Partikel§8: {0}
PARTICLE_ENTRY_LOCKED = §cDesign gesperrt, wird nicht angezeigt PARTICLE_ENTRY_LOCKED = §cDesign gesperrt, wird nicht angezeigt
PARTICLE_ENTRY_EDIT = §eLinksklick§8: §7bearbeiten PARTICLE_ENTRY_EDIT = §eZum Bearbeiten klicken
PARTICLE_ENTRY_DUPLICATE = §eRechtsklick§8: §7duplizieren PARTICLE_ENTRY_DELETE = §cEintrag löschen
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_DESIGN_PICKER = §0Design wählen
PARTICLE_CHOICE_PICKER = §0{0} PARTICLE_CHOICE_PICKER = §0{0}
PARTICLE_COLOR_PICKER = §0Farbe wählen PARTICLE_COLOR_PICKER = §0Farbe wählen
PARTICLE_VALUE = §7Wert§8: {0} 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_RANDOM = §7Zufällige Farben
PARTICLE_VALUE_YES = §aJa PARTICLE_VALUE_YES = §aJa
PARTICLE_VALUE_NO = §cNein PARTICLE_VALUE_NO = §cNein
@@ -19,7 +19,6 @@
package de.steamwar.lobby.particle.gui; package de.steamwar.lobby.particle.gui;
import de.steamwar.inventory.SWAnvilInv;
import de.steamwar.inventory.SWInventory; import de.steamwar.inventory.SWInventory;
import de.steamwar.inventory.SWItem; import de.steamwar.inventory.SWItem;
import de.steamwar.lobby.LobbySystem; import de.steamwar.lobby.LobbySystem;
@@ -32,32 +31,53 @@ import de.steamwar.lobby.particle.slot.ParticleSlots;
import de.steamwar.lobby.util.LobbyPlayer; import de.steamwar.lobby.util.LobbyPlayer;
import de.steamwar.sql.SteamwarUser; import de.steamwar.sql.SteamwarUser;
import lombok.experimental.UtilityClass; import lombok.experimental.UtilityClass;
import org.bukkit.Bukkit;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
import java.awt.Color;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Map;
import static de.steamwar.lobby.particle.design.EntryParameters.*; import static de.steamwar.lobby.particle.design.EntryParameters.*;
import static de.steamwar.lobby.particle.gui.ParticleGuiText.*; import static de.steamwar.lobby.particle.gui.ParticleGuiText.*;
// One item per parameter of an entry; every change is saved and shown right away // Settings of one entry in labelled rows; a click on a setting toggles it or opens its own screen
@UtilityClass @UtilityClass
class EntryEditor { class EntryEditor {
private final int DESIGN = 4; private final int ROW_DESIGN = 0;
private final int FIRST_PARAMETER = 9; private final int ROW_PARTICLE = 1;
private final int ROW_SHAPE = 2;
private final int ROW_TIMING = 3;
private final int ROW_CONDITIONS = 4;
private final int BACK = 45; private final int BACK = 45;
private final int COST = 49; private final int COST = 49;
private final int DELETE = 53; 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 final Map<Parameter<?>, Material> ICONS = Map.ofEntries(
Map.entry(COUNT, Material.GLOWSTONE_DUST),
Map.entry(SPEED, Material.FEATHER),
Map.entry(SPREAD_X, Material.GUNPOWDER),
Map.entry(SPREAD_Y, Material.GUNPOWDER),
Map.entry(SPREAD_Z, Material.GUNPOWDER),
Map.entry(OFFSET_X, Material.SPECTRAL_ARROW),
Map.entry(OFFSET_Y, Material.SPECTRAL_ARROW),
Map.entry(OFFSET_Z, Material.SPECTRAL_ARROW),
Map.entry(INTERVAL, Material.CLOCK),
Map.entry(TICK_MODE, Material.REPEATER),
Map.entry(MOVEMENT, Material.LEATHER_BOOTS),
Map.entry(GLIDING, Material.ELYTRA),
Map.entry(GROUND, Material.GRASS_BLOCK),
Map.entry(SNEAK, Material.LEATHER_LEGGINGS),
Map.entry(VISIBILITY, Material.SPYGLASS),
Map.entry(Designs.CIRCLE_RADIUS, Material.COMPASS),
Map.entry(Designs.CIRCLE_SPEED, Material.RECOVERY_COMPASS),
Map.entry(Designs.CIRCLE_PHASE, Material.TARGET),
Map.entry(Designs.CIRCLE_RISE, Material.LADDER),
Map.entry(Designs.CIRCLE_RISE_SPEED, Material.RABBIT_FOOT),
Map.entry(Designs.WING_IMAGE, Material.PHANTOM_MEMBRANE),
Map.entry(Designs.WING_SIZE, Material.SCAFFOLDING)
);
private record Context(Player player, SteamwarUser user, int index, int entryIndex, ParticleSlot slot, ParticleEntry entry) { private record Context(Player player, SteamwarUser user, int index, int entryIndex, ParticleSlot slot, ParticleEntry entry) {
void reopen() { void reopen() {
@@ -75,159 +95,113 @@ class EntryEditor {
} }
Context context = new Context(player, user, index, entryIndex, slot, slot.getEntries().get(entryIndex)); Context context = new Context(player, user, index, entryIndex, slot, slot.getEntries().get(entryIndex));
ParticleEntry entry = context.entry(); ParticleEntry entry = context.entry();
ParticleDesign design = entry.getDesign();
SWInventory inventory = new SWInventory(player, 54, msg("PARTICLE_ENTRY_EDITOR", player)); SWInventory inventory = new SWInventory(player, 54, msg("PARTICLE_ENTRY_EDITOR", player, designName(design, player)));
inventory.setItem(DESIGN, new SWItem(entry.getDesign().getIcon(), designName(entry.getDesign(), player), List.of(msg("PARTICLE_ENTRY_CHANGE_DESIGN", player)), false, header(inventory, ROW_DESIGN, "PARTICLE_SECTION_DESIGN", player);
clickType -> Pickers.design(player, design -> changeDesign(context, design), context::reopen))); header(inventory, ROW_PARTICLE, "PARTICLE_SECTION_PARTICLE", player);
header(inventory, ROW_SHAPE, "PARTICLE_SECTION_SHAPE", player);
header(inventory, ROW_TIMING, "PARTICLE_SECTION_TIMING", player);
header(inventory, ROW_CONDITIONS, "PARTICLE_SECTION_CONDITIONS", player);
int position = FIRST_PARAMETER; inventory.setItem(ROW_DESIGN * 9 + 1, new SWItem(design.getIcon(), designName(design, player), List.of(msg("PARTICLE_VALUE_CLICK", player)), false,
for (Parameter<?> parameter : visibleParameters(entry)) { clickType -> Pickers.design(player, design, picked -> changeDesign(context, picked), context::reopen)));
if (position >= BACK) break; if (design.usesEntryParticle()) {
inventory.setItem(position++, parameterItem(context, parameter)); inventory.setItem(ROW_DESIGN * 9 + 2, parameterItem(context, PARTICLE));
if (entry.getValues().get(PARTICLE).isDust()) {
inventory.setItem(ROW_DESIGN * 9 + 3, parameterItem(context, COLORS));
}
row(inventory, context, ROW_PARTICLE, List.of(COUNT, SPEED, SPREAD_X, SPREAD_Y, SPREAD_Z));
} else {
inventory.setItem(ROW_PARTICLE * 9 + 1, new SWItem(Material.STRUCTURE_VOID, msg("PARTICLE_FIXED_LOOK", player), clickType -> {
}));
} }
row(inventory, context, ROW_SHAPE, design.getParameters());
row(inventory, context, ROW_TIMING, List.of(TICK_MODE, INTERVAL, OFFSET_X, OFFSET_Y, OFFSET_Z));
row(inventory, context, ROW_CONDITIONS, List.of(MOVEMENT, GLIDING, GROUND, SNEAK, VISIBILITY));
inventory.setItem(BACK, new SWItem(Material.ARROW, msg("PARTICLE_BACK", player), clickType -> SlotEditor.open(player, index))); 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(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 -> { inventory.setItem(DELETE, new SWItem(Material.LAVA_BUCKET, msg("PARTICLE_ENTRY_DELETE", player), List.of(msg("PARTICLE_SHIFT_TO_DELETE", player)), false, clickType -> {
if (!clickType.isShiftClick()) return; if (!clickType.isShiftClick()) return;
slot.getEntries().remove(entryIndex); slot.getEntries().remove(entryIndex);
SlotEditor.save(player, index, slot); SlotEditor.save(player, index, slot);
SlotEditor.open(player, index); SlotEditor.open(player, index);
})); }));
SlotEditor.fill(inventory, 0, 54);
inventory.open(); inventory.open();
} }
private List<Parameter<?>> visibleParameters(ParticleEntry entry) { private void header(SWInventory inventory, int row, String key, Player player) {
List<Parameter<?>> parameters = new ArrayList<>(); inventory.setItem(row * 9, new SWItem(Material.LIGHT_GRAY_STAINED_GLASS_PANE, msg(key, player), clickType -> {
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; private void row(SWInventory inventory, Context context, int row, List<Parameter<?>> parameters) {
parameters.add(parameter); for (int i = 0; i < parameters.size() && i < 8; i++) {
inventory.setItem(row * 9 + 1 + i, parameterItem(context, parameters.get(i)));
} }
return parameters;
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private SWItem parameterItem(Context context, Parameter<?> parameter) { private SWItem parameterItem(Context context, Parameter<?> parameter) {
if (parameter instanceof DoubleParameter number) return doubleItem(context, number); Player player = context.player();
if (parameter instanceof IntParameter number) return intItem(context, number); Object value = context.entry().getValues().get(parameter);
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<>(); List<String> lore = new ArrayList<>();
lore.add(msg("PARTICLE_VALUE", context.player(), valueText(parameter, context.entry().getValues().get(parameter), context.player()))); lore.add(msg("PARTICLE_VALUE", player, valueText(parameter, value, player)));
lore.add(""); lore.add("");
for (String line : help) lore.add(line); lore.add(msg(parameter instanceof BooleanParameter ? "PARTICLE_VALUE_TOGGLE" : "PARTICLE_VALUE_CLICK", player));
return lore;
}
private SWItem doubleItem(Context context, DoubleParameter parameter) { Material icon = ICONS.getOrDefault(parameter, Material.PAPER);
Player player = context.player(); if (parameter instanceof BooleanParameter) icon = (Boolean) value ? Material.LIME_DYE : Material.GRAY_DYE;
double value = context.entry().getValues().get(parameter); if (parameter == PARTICLE) icon = ((ParticleType) value).getIcon();
List<String> lore = lore(context, parameter, msg("PARTICLE_VALUE_NUMBER_HELP", player, number(parameter.getStep())), msg("PARTICLE_VALUE_EXACT_HELP", player)); if (parameter == COLORS) icon = Material.RED_DYE;
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) { return new SWItem(icon, name(parameter.getNameKey(), player), lore, false, clickType -> {
Player player = context.player(); if (parameter instanceof BooleanParameter toggle) {
int value = context.entry().getValues().get(parameter); apply(context, toggle, !(Boolean) value);
List<String> lore = lore(context, parameter, msg("PARTICLE_VALUE_NUMBER_HELP", player, 1), msg("PARTICLE_VALUE_EXACT_HELP", player)); context.reopen();
return new SWItem(Material.COMPARATOR, name(parameter.getNameKey(), player), lore, false, clickType -> { } else if (parameter instanceof DoubleParameter number) {
if (clickType == ClickType.DROP || clickType == ClickType.CONTROL_DROP) { ValueEditors.editNumber(player, name(number.getNameKey(), player), () -> context.entry().getValues().get(number),
askNumber(context, parameter, String.valueOf(value), input -> change(context, parameter, (int) Math.round(input))); number.getStep(), number.getMin(), number.getMax(), number.getDefaultValue(), false,
return; next -> apply(context, number, next), context::reopen);
} } else if (parameter instanceof IntParameter number) {
int step = (clickType.isShiftClick() ? 10 : 1) * (clickType.isRightClick() ? -1 : 1); ValueEditors.editNumber(player, name(number.getNameKey(), player), () -> (double) context.entry().getValues().get(number),
change(context, parameter, value + step); 1, number.getMin(), number.getMax(), number.getDefaultValue(), true,
}); next -> apply(context, number, (int) Math.round(next)), context::reopen);
} } else if (parameter instanceof GradientParameter gradient) {
ValueEditors.editColors(player, () -> context.entry().getValues().get(gradient), gradient.getMaxColors(),
private SWItem booleanItem(Context context, BooleanParameter parameter) { colors -> apply(context, gradient, colors), context::reopen);
boolean value = context.entry().getValues().get(parameter); } else if (parameter instanceof ChoiceParameter<?> choice) {
List<String> lore = lore(context, parameter, msg("PARTICLE_VALUE_TOGGLE_HELP", context.player())); pick(context, (ChoiceParameter<Object>) choice, value);
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) { private <E> void pick(Context context, ChoiceParameter<E> parameter, E current) {
Player player = context.player(); Pickers.choice(context.player(), parameter, current, EntryEditor::choiceIcon, picked -> {
E value = context.entry().getValues().get(parameter); apply(context, parameter, picked);
List<E> values = parameter.getBuildableValues(); context.reopen();
boolean list = values.size() > MAX_CYCLE_CHOICES; }, context::reopen);
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) { private Material choiceIcon(Object value) {
if (value instanceof ParticleType type) return type.getIcon(); if (value instanceof ParticleType type) return type.getIcon();
if (value instanceof WingDesign) return Material.ELYTRA; if (value instanceof WingDesign) return Material.PHANTOM_MEMBRANE;
return Material.LEVER; return Material.LEVER;
} }
private void askNumber(Context context, Parameter<?> parameter, String current, java.util.function.DoubleConsumer onInput) { // Saves the change, or reverts it and tells the player when the slot would go over its budget
Player player = context.player(); private <T> boolean apply(Context context, Parameter<T> parameter, T value) {
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); T previous = context.entry().getValues().get(parameter);
context.entry().getValues().set(parameter, value); context.entry().getValues().set(parameter, value);
if (!SlotEditor.fits(context.user(), context.slot())) { if (!SlotEditor.fits(context.user(), context.slot())) {
context.entry().getValues().set(parameter, previous); context.entry().getValues().set(parameter, previous);
LobbySystem.getMessage().send("PARTICLE_SLOT_OVER_BUDGET", context.player()); LobbySystem.getMessage().send("PARTICLE_SLOT_OVER_BUDGET", context.player());
} else { return false;
SlotEditor.save(context.player(), context.index(), context.slot());
} }
context.reopen(); SlotEditor.save(context.player(), context.index(), context.slot());
return true;
} }
// Keeps the particle, offset and conditions; a fixed design brings its own conditions and timing // Keeps the particle, offset and conditions; a fixed design brings its own conditions and timing
@@ -25,6 +25,8 @@ import de.steamwar.lobby.particle.UnlockContext;
import de.steamwar.lobby.particle.design.*; import de.steamwar.lobby.particle.design.*;
import de.steamwar.lobby.particle.slot.ParticleEntry; import de.steamwar.lobby.particle.slot.ParticleEntry;
import de.steamwar.lobby.particle.slot.ParticleSlot; import de.steamwar.lobby.particle.slot.ParticleSlot;
import de.steamwar.lobby.particle.slot.ParticleSlots;
import de.steamwar.sql.SteamwarUser;
import lombok.experimental.UtilityClass; import lombok.experimental.UtilityClass;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@@ -136,6 +138,15 @@ class ParticleGuiText {
} }
} }
// The slot's name followed by how many of its allowed particles per tick it uses
String slotName(ParticleSlot slot, SteamwarUser user, Player player) {
return msg("PARTICLE_SLOT_NAME", player, slot.getName(), number(slot.cost()), number(ParticleSlots.maxCost(user)));
}
String slotTitle(ParticleSlot slot, SteamwarUser user, Player player) {
return msg("PARTICLE_SLOT_TITLE", player, slot.getName(), number(slot.cost()), number(ParticleSlots.maxCost(user)));
}
List<String> slotSummary(ParticleSlot slot, Player player) { List<String> slotSummary(ParticleSlot slot, Player player) {
List<String> lore = new ArrayList<>(); List<String> lore = new ArrayList<>();
lore.add(msg("PARTICLE_SLOT_ENTRIES", player, slot.getEntries().size())); lore.add(msg("PARTICLE_SLOT_ENTRIES", player, slot.getEntries().size()));
@@ -34,7 +34,6 @@ import de.steamwar.sql.SteamwarUser;
import lombok.experimental.UtilityClass; import lombok.experimental.UtilityClass;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -96,28 +95,22 @@ public class ParticleMenu {
List<String> lore = new ArrayList<>(ParticleGuiText.slotSummary(slot, player)); List<String> lore = new ArrayList<>(ParticleGuiText.slotSummary(slot, player));
lore.add(""); lore.add("");
boolean active = lobbyPlayer.getActiveSlot() == index; String name = slotName(slot, context.getUser(), player);
if (!unlocked) { if (!unlocked) {
lore.add(msg("PARTICLE_LOCKED", player)); lore.add(msg("PARTICLE_LOCKED", player));
lore.addAll(ParticleGuiText.requirement(requirement, player)); lore.addAll(ParticleGuiText.requirement(requirement, player));
lore.add(msg("PARTICLE_SLOT_VIEW", player)); lore.add(msg("PARTICLE_SLOT_VIEW", player));
return new SWItem(slot.getDisplayIcon(), "§f" + slot.getName(), lore, false, clickType -> SlotEditor.view(player, index)); return new SWItem(slot.getDisplayIcon(), name, lore, false, clickType -> SlotEditor.open(player, index));
} }
boolean active = lobbyPlayer.getActiveSlot() == index;
if (active) { if (active) {
lore.add(msg("PARTICLE_SLOT_ACTIVE", player)); lore.add(msg("PARTICLE_SLOT_ACTIVE", player));
} }
lore.add(msg("PARTICLE_SLOT_SELECT", player)); lore.add(msg("PARTICLE_SLOT_OPEN", player));
lore.add(msg("PARTICLE_SLOT_EDIT", player)); return new SWItem(slot.getDisplayIcon(), name, lore, active, clickType -> SlotEditor.open(player, index));
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) { void selected(Player player) {
player.closeInventory(); player.closeInventory();
if (ParticleListener.disabled(player)) { if (ParticleListener.disabled(player)) {
LobbySystem.getMessage().send("PARTICLE_DEACTIVATED", player); LobbySystem.getMessage().send("PARTICLE_DEACTIVATED", player);
@@ -41,8 +41,9 @@ class Pickers {
private final int BACK = 49; private final int BACK = 49;
// Basic designs first, then rewards; locked rewards are shown with their requirement but cannot be picked // 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) { // The current design, if any, glows.
void design(Player player, ParticleDesign current, Consumer<ParticleDesign> onPick, Runnable onBack) {
UnlockContext context = new UnlockContext(SteamwarUser.get(player.getUniqueId())); UnlockContext context = new UnlockContext(SteamwarUser.get(player.getUniqueId()));
List<SWListInv.SWListEntry<ParticleDesign>> list = new ArrayList<>(); List<SWListInv.SWListEntry<ParticleDesign>> list = new ArrayList<>();
for (DesignVisibility visibility : List.of(DesignVisibility.BASIC, DesignVisibility.REWARD)) { for (DesignVisibility visibility : List.of(DesignVisibility.BASIC, DesignVisibility.REWARD)) {
@@ -51,18 +52,20 @@ class Pickers {
List<String> lore = new ArrayList<>(requirement(design.getRequirement(), player)); List<String> lore = new ArrayList<>(requirement(design.getRequirement(), player));
boolean unlocked = design.getRequirement().test(context); boolean unlocked = design.getRequirement().test(context);
if (!lore.isEmpty()) lore.add(""); if (!lore.isEmpty()) lore.add("");
lore.add(msg(unlocked ? "PARTICLE_SELECT" : "PARTICLE_LOCKED", player)); lore.add(msg(design == current ? "PARTICLE_VALUE_SELECTED" : unlocked ? "PARTICLE_SELECT" : "PARTICLE_LOCKED", player));
list.add(new SWListInv.SWListEntry<>(new SWItem(design.getIcon(), designName(design, player), lore, false, clickType -> { list.add(new SWListInv.SWListEntry<>(new SWItem(design.getIcon(), designName(design, player), lore, design == current, clickType -> {
}), unlocked ? design : null)); }), unlocked ? design : null));
} }
} }
open(player, msg("PARTICLE_DESIGN_PICKER", player), list, onPick, onBack); 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) { // Lists every option; the current one glows
<E> void choice(Player player, ChoiceParameter<E> parameter, E current, Function<E, Material> icon, Consumer<E> onPick, Runnable onBack) {
List<SWListInv.SWListEntry<E>> list = new ArrayList<>(); List<SWListInv.SWListEntry<E>> list = new ArrayList<>();
for (E value : parameter.getBuildableValues()) { for (E value : parameter.getBuildableValues()) {
list.add(new SWListInv.SWListEntry<>(new SWItem(icon.apply(value), choiceName(parameter, value, player), clickType -> { List<String> lore = List.of(msg(value == current ? "PARTICLE_VALUE_SELECTED" : "PARTICLE_SELECT", player));
list.add(new SWListInv.SWListEntry<>(new SWItem(icon.apply(value), choiceName(parameter, value, player), lore, value == current, clickType -> {
}), value)); }), value));
} }
open(player, msg("PARTICLE_CHOICE_PICKER", player, name(parameter.getNameKey(), player)), list, onPick, onBack); open(player, msg("PARTICLE_CHOICE_PICKER", player, name(parameter.getNameKey(), player)), list, onPick, onBack);
@@ -33,18 +33,18 @@ import lombok.experimental.UtilityClass;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import static de.steamwar.lobby.particle.gui.ParticleGuiText.*; import static de.steamwar.lobby.particle.gui.ParticleGuiText.*;
// Lists a slot's entries; opened read-only for slots the player has lost access to // Lists a slot's entries; slots the player has lost access to are shown read-only
@UtilityClass @UtilityClass
public class SlotEditor { public class SlotEditor {
private final int BACK = 45; private final int BACK = 45;
private final int USE = 46;
private final int ADD_ENTRY = 47; private final int ADD_ENTRY = 47;
private final int RENAME = 48; private final int RENAME = 48;
private final int ICON = 49; private final int ICON = 49;
@@ -52,14 +52,6 @@ public class SlotEditor {
private final int DELETE = 53; private final int DELETE = 53;
public void open(Player player, int index) { 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); LobbyPlayer lobbyPlayer = LobbyPlayer.getLobbyPlayer(player);
ParticleSlot slot = lobbyPlayer.getSlot(index); ParticleSlot slot = lobbyPlayer.getSlot(index);
if (slot == null) { if (slot == null) {
@@ -68,11 +60,9 @@ public class SlotEditor {
} }
SteamwarUser user = SteamwarUser.get(player.getUniqueId()); SteamwarUser user = SteamwarUser.get(player.getUniqueId());
UnlockContext context = new UnlockContext(user); UnlockContext context = new UnlockContext(user);
// Locked slots can only ever be viewed, whatever the caller asked for boolean editable = ParticleSlots.requirement(index).test(context);
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())); SWInventory inventory = new SWInventory(player, 54, slotTitle(slot, user, player));
List<ParticleEntry> entries = slot.getEntries(); List<ParticleEntry> entries = slot.getEntries();
for (int i = 0; i < entries.size(); i++) { for (int i = 0; i < entries.size(); i++) {
ParticleEntry entry = entries.get(i); ParticleEntry entry = entries.get(i);
@@ -80,32 +70,10 @@ public class SlotEditor {
if (editable) { if (editable) {
lore.add(""); lore.add("");
lore.add(msg("PARTICLE_ENTRY_EDIT", player)); lore.add(msg("PARTICLE_ENTRY_EDIT", player));
lore.add(msg("PARTICLE_ENTRY_DUPLICATE", player));
lore.add(msg("PARTICLE_ENTRY_DELETE", player));
} }
int entryIndex = i; int entryIndex = i;
inventory.setItem(i, new SWItem(icon(entry), designName(entry.getDesign(), player), lore, false, clickType -> { inventory.setItem(i, new SWItem(entry.getDesign().getIcon(), designName(entry.getDesign(), player), lore, false, clickType -> {
if (!editable) return; if (editable) EntryEditor.open(player, index, entryIndex);
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);
})); }));
} }
@@ -114,16 +82,26 @@ public class SlotEditor {
List<String> lore = new ArrayList<>(requirement(ParticleSlots.requirement(index), player)); 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.setItem(ICON, new SWItem(Material.BARRIER, msg("PARTICLE_LOCKED", player), lore, false, clickType -> {
})); }));
fill(inventory, 45, 54);
inventory.open(); inventory.open();
return; return;
} }
boolean active = lobbyPlayer.getActiveSlot() == index;
inventory.setItem(USE, new SWItem(active ? Material.LIME_DYE : Material.GRAY_DYE, msg(active ? "PARTICLE_SLOT_STOP_USING" : "PARTICLE_SLOT_USE", player), List.of(), active, clickType -> {
if (active) {
lobbyPlayer.deselect();
open(player, index);
} else if (lobbyPlayer.selectSlot(index)) {
ParticleMenu.selected(player);
}
}));
inventory.setItem(ADD_ENTRY, new SWItem(Material.NETHER_STAR, msg("PARTICLE_SLOT_ADD_ENTRY", player), clickType -> { inventory.setItem(ADD_ENTRY, new SWItem(Material.NETHER_STAR, msg("PARTICLE_SLOT_ADD_ENTRY", player), clickType -> {
if (entries.size() >= ParticleSlots.MAX_ENTRIES) { if (entries.size() >= ParticleSlots.MAX_ENTRIES) {
LobbySystem.getMessage().send("PARTICLE_SLOT_MAX_ENTRIES", player); LobbySystem.getMessage().send("PARTICLE_SLOT_MAX_ENTRIES", player);
return; return;
} }
Pickers.design(player, design -> { Pickers.design(player, null, design -> {
entries.add(new ParticleEntry(design)); entries.add(new ParticleEntry(design));
if (!fits(user, slot)) { if (!fits(user, slot)) {
entries.remove(entries.size() - 1); entries.remove(entries.size() - 1);
@@ -152,7 +130,7 @@ public class SlotEditor {
open(player, index); open(player, index);
}, () -> open(player, index)))); }, () -> open(player, index))));
inventory.setItem(COST, costItem(player, user, slot)); 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 -> { inventory.setItem(DELETE, new SWItem(Material.LAVA_BUCKET, msg("PARTICLE_SLOT_DELETE", player), List.of(msg("PARTICLE_SHIFT_TO_DELETE", player)), false, clickType -> {
if (!clickType.isShiftClick()) return; if (!clickType.isShiftClick()) return;
if (lobbyPlayer.getActiveSlot() == index) { if (lobbyPlayer.getActiveSlot() == index) {
lobbyPlayer.deselect(); lobbyPlayer.deselect();
@@ -160,6 +138,7 @@ public class SlotEditor {
lobbyPlayer.setSlot(index, null); lobbyPlayer.setSlot(index, null);
ParticleMenu.open(player); ParticleMenu.open(player);
})); }));
fill(inventory, 45, 54);
inventory.open(); inventory.open();
} }
@@ -176,7 +155,13 @@ public class SlotEditor {
LobbyPlayer.getLobbyPlayer(player).setSlot(index, slot); LobbyPlayer.getLobbyPlayer(player).setSlot(index, slot);
} }
Material icon(ParticleEntry entry) { // Puts filler panes on the empty positions in the given range
return entry.getDesign().getIcon(); void fill(SWInventory inventory, int from, int to) {
for (int position = from; position < to; position++) {
if (!inventory.hasItem(position)) {
inventory.setItem(position, new SWItem(Material.BLACK_STAINED_GLASS_PANE, " ", clickType -> {
}));
}
}
} }
} }
@@ -0,0 +1,144 @@
/*
* 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 lombok.experimental.UtilityClass;
import org.bukkit.Bukkit;
import org.bukkit.DyeColor;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import java.awt.Color;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import java.util.function.Supplier;
import static de.steamwar.lobby.particle.gui.ParticleGuiText.*;
// Screens for settings that need more than a click: numbers and colour lists
@UtilityClass
class ValueEditors {
private final int BACK = 18;
// Buttons step the value down or up; clicking the value itself lets the player type one
void editNumber(Player player, String title, Supplier<Double> current, double step, double min, double max, double defaultValue, boolean integer,
Predicate<Double> apply, Runnable back) {
double value = current.get();
Runnable reopen = () -> editNumber(player, title, current, step, min, max, defaultValue, integer, apply, back);
SWInventory inventory = new SWInventory(player, 27, "§0" + strip(title));
inventory.setItem(10, stepItem(player, Material.RED_CONCRETE, -10 * step, step, value, min, max, integer, apply, reopen));
inventory.setItem(11, stepItem(player, Material.RED_STAINED_GLASS_PANE, -step, step, value, min, max, integer, apply, reopen));
inventory.setItem(15, stepItem(player, Material.LIME_STAINED_GLASS_PANE, step, step, value, min, max, integer, apply, reopen));
inventory.setItem(16, stepItem(player, Material.LIME_CONCRETE, 10 * step, step, value, min, max, integer, apply, reopen));
List<String> lore = List.of(msg("PARTICLE_NUMBER_RANGE", player, number(min), number(max)), "", msg("PARTICLE_NUMBER_TYPE", player));
inventory.setItem(13, new SWItem(Material.PAPER, "§e" + number(value), lore, false, clickType -> {
SWAnvilInv anvil = new SWAnvilInv(player, strip(title), number(value));
anvil.setItem(Material.PAPER);
anvil.setCallback(input -> Bukkit.getScheduler().runTask(LobbySystem.getInstance(), () -> {
try {
double typed = Double.parseDouble(input.trim().replace(',', '.'));
apply.test(integer ? Math.round(typed) : typed);
} catch (NumberFormatException e) {
// Keep the old value
}
reopen.run();
}));
anvil.open();
}));
inventory.setItem(BACK, new SWItem(Material.ARROW, msg("PARTICLE_BACK", player), clickType -> back.run()));
inventory.setItem(22, new SWItem(Material.STRUCTURE_VOID, msg("PARTICLE_VALUE_RESET", player, number(defaultValue)), clickType -> {
apply.test(defaultValue);
reopen.run();
}));
SlotEditor.fill(inventory, 0, 27);
inventory.open();
}
private SWItem stepItem(Player player, Material material, double delta, double step, double value, double min, double max, boolean integer, Predicate<Double> apply, Runnable reopen) {
String label = (delta < 0 ? "§c-" : "§a+") + number(Math.abs(delta));
return new SWItem(material, label, clickType -> {
// Snap to the step grid and drop floating point noise, so repeated steps do not drift
double next = integer ? Math.round(value + delta) : Math.round(Math.round((value + delta) / step) * step * 1e6) / 1e6;
apply.test(Math.max(min, Math.min(max, next)));
reopen.run();
});
}
// Shows the colours in order; a click removes one, the buttons below add one or switch to random colours
void editColors(Player player, Supplier<List<Color>> current, int maxColors, Predicate<List<Color>> apply, Runnable back) {
List<Color> colors = current.get();
Runnable reopen = () -> editColors(player, current, maxColors, apply, back);
SWInventory inventory = new SWInventory(player, 27, msg("PARTICLE_COLOR_EDITOR", player));
for (int i = 0; i < colors.size() && i < 9; i++) {
int position = i;
Color color = colors.get(i);
inventory.setItem(i, new SWItem(dye(color), net.md_5.bungee.api.ChatColor.of(color) + "█ #" + String.format("%06X", color.getRGB() & 0xFFFFFF),
List.of(msg("PARTICLE_COLOR_REMOVE", player)), false, clickType -> {
List<Color> next = new ArrayList<>(colors);
next.remove(position);
apply.test(next);
reopen.run();
}));
}
if (colors.isEmpty()) {
inventory.setItem(4, new SWItem(Material.WHITE_DYE, msg("PARTICLE_VALUE_RANDOM", player), clickType -> {
}));
}
inventory.setItem(BACK, new SWItem(Material.ARROW, msg("PARTICLE_BACK", player), clickType -> back.run()));
if (colors.size() < maxColors) {
inventory.setItem(21, new SWItem(Material.NETHER_STAR, msg("PARTICLE_COLOR_ADD", player), clickType -> Pickers.color(player, color -> {
List<Color> next = new ArrayList<>(colors);
next.add(color);
apply.test(next);
reopen.run();
}, reopen)));
}
inventory.setItem(23, new SWItem(Material.WHITE_DYE, msg("PARTICLE_COLOR_RANDOM", player), clickType -> {
apply.test(List.of());
reopen.run();
}));
SlotEditor.fill(inventory, 0, 27);
inventory.open();
}
private Material dye(Color color) {
for (DyeColor dye : DyeColor.values()) {
if ((dye.getColor().asRGB() & 0xFFFFFF) == (color.getRGB() & 0xFFFFFF)) {
return Material.valueOf(dye.name() + "_DYE");
}
}
return Material.WHITE_DYE;
}
private String strip(String text) {
return text.replaceAll("§.", "");
}
}