From b60cdfc88d3ec1001c817c459449a2275ba23c39 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Fri, 23 Oct 2026 17:42:42 +0200 Subject: [PATCH] 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. --- .../de/steamwar/lobby/LobbySystem.properties | 44 ++-- .../steamwar/lobby/LobbySystem_de.properties | 44 ++-- .../lobby/particle/gui/EntryEditor.java | 222 ++++++++---------- .../lobby/particle/gui/ParticleGuiText.java | 11 + .../lobby/particle/gui/ParticleMenu.java | 19 +- .../steamwar/lobby/particle/gui/Pickers.java | 15 +- .../lobby/particle/gui/SlotEditor.java | 69 +++--- .../lobby/particle/gui/ValueEditors.java | 144 ++++++++++++ 8 files changed, 347 insertions(+), 221 deletions(-) create mode 100644 LobbySystem/src/de/steamwar/lobby/particle/gui/ValueEditors.java diff --git a/LobbySystem/src/de/steamwar/lobby/LobbySystem.properties b/LobbySystem/src/de/steamwar/lobby/LobbySystem.properties index 702e340e..0581eb0f 100644 --- a/LobbySystem/src/de/steamwar/lobby/LobbySystem.properties +++ b/LobbySystem/src/de/steamwar/lobby/LobbySystem.properties @@ -66,15 +66,33 @@ PARTICLE_LOCKED = §c§lLocked PARTICLE_DEACTIVATED = §cThere are too many players online, particles are deactivated 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_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_VIEW = §eClick to view 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 @@ -83,30 +101,20 @@ 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_EDITOR = §0{0} 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_ENTRY_EDIT = §eClick to edit +PARTICLE_ENTRY_DELETE = §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 diff --git a/LobbySystem/src/de/steamwar/lobby/LobbySystem_de.properties b/LobbySystem/src/de/steamwar/lobby/LobbySystem_de.properties index 59647e67..947ac020 100644 --- a/LobbySystem/src/de/steamwar/lobby/LobbySystem_de.properties +++ b/LobbySystem/src/de/steamwar/lobby/LobbySystem_de.properties @@ -66,15 +66,33 @@ PARTICLE_LOCKED = §c§lGesperrt PARTICLE_DEACTIVATED = §cEs sind zu viele Spieler online! Partikel sind deaktiviert. 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_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_VIEW = §eZum Ansehen klicken 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 @@ -83,30 +101,20 @@ 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_EDITOR = §0{0} 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_ENTRY_EDIT = §eZum Bearbeiten klicken +PARTICLE_ENTRY_DELETE = §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 diff --git a/LobbySystem/src/de/steamwar/lobby/particle/gui/EntryEditor.java b/LobbySystem/src/de/steamwar/lobby/particle/gui/EntryEditor.java index 1f9229a0..1b06daea 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/gui/EntryEditor.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/gui/EntryEditor.java @@ -19,7 +19,6 @@ 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; @@ -32,32 +31,53 @@ 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 java.util.Map; 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 +// Settings of one entry in labelled rows; a click on a setting toggles it or opens its own screen @UtilityClass class EntryEditor { - private final int DESIGN = 4; - private final int FIRST_PARAMETER = 9; + private final int ROW_DESIGN = 0; + 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 COST = 49; private final int DELETE = 53; - // Choices with more options than this open a list instead of cycling on click - private final int MAX_CYCLE_CHOICES = 6; - private final Set> PARTICLE_PARAMETERS = Set.of(PARTICLE, COUNT, SPREAD_X, SPREAD_Y, SPREAD_Z, SPEED, COLORS); + private final Map, 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) { void reopen() { @@ -75,159 +95,113 @@ class EntryEditor { } Context context = new Context(player, user, index, entryIndex, slot, slot.getEntries().get(entryIndex)); ParticleEntry entry = context.entry(); + ParticleDesign design = entry.getDesign(); - 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))); + SWInventory inventory = new SWInventory(player, 54, msg("PARTICLE_ENTRY_EDITOR", player, designName(design, player))); + header(inventory, ROW_DESIGN, "PARTICLE_SECTION_DESIGN", player); + 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; - for (Parameter parameter : visibleParameters(entry)) { - if (position >= BACK) break; - inventory.setItem(position++, parameterItem(context, parameter)); + inventory.setItem(ROW_DESIGN * 9 + 1, new SWItem(design.getIcon(), designName(design, player), List.of(msg("PARTICLE_VALUE_CLICK", player)), false, + clickType -> Pickers.design(player, design, picked -> changeDesign(context, picked), context::reopen))); + if (design.usesEntryParticle()) { + 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(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; slot.getEntries().remove(entryIndex); SlotEditor.save(player, index, slot); SlotEditor.open(player, index); })); + SlotEditor.fill(inventory, 0, 54); inventory.open(); } - private List> visibleParameters(ParticleEntry entry) { - List> parameters = new ArrayList<>(); - boolean dust = entry.getValues().get(PARTICLE).isDust(); - for (Parameter parameter : entry.getParameters()) { - if (!entry.getDesign().usesEntryParticle() && PARTICLE_PARAMETERS.contains(parameter)) continue; - if (parameter == COLORS && !dust) continue; - parameters.add(parameter); + private void header(SWInventory inventory, int row, String key, Player player) { + inventory.setItem(row * 9, new SWItem(Material.LIGHT_GRAY_STAINED_GLASS_PANE, msg(key, player), clickType -> { + })); + } + + private void row(SWInventory inventory, Context context, int row, List> parameters) { + 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") private SWItem parameterItem(Context context, Parameter parameter) { - if (parameter instanceof DoubleParameter number) return doubleItem(context, number); - if (parameter instanceof IntParameter number) return intItem(context, number); - if (parameter instanceof BooleanParameter toggle) return booleanItem(context, toggle); - if (parameter instanceof GradientParameter gradient) return gradientItem(context, gradient); - if (parameter instanceof ChoiceParameter choice) return choiceItem(context, (ChoiceParameter) choice); - throw new IllegalArgumentException("No editor for parameter " + parameter.getKey()); - } - - private List lore(Context context, Parameter parameter, String... help) { + Player player = context.player(); + Object value = context.entry().getValues().get(parameter); List 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(""); - for (String line : help) lore.add(line); - return lore; - } + lore.add(msg(parameter instanceof BooleanParameter ? "PARTICLE_VALUE_TOGGLE" : "PARTICLE_VALUE_CLICK", player)); - private SWItem doubleItem(Context context, DoubleParameter parameter) { - Player player = context.player(); - double value = context.entry().getValues().get(parameter); - List lore = lore(context, parameter, msg("PARTICLE_VALUE_NUMBER_HELP", player, number(parameter.getStep())), msg("PARTICLE_VALUE_EXACT_HELP", player)); - return new SWItem(Material.REPEATER, name(parameter.getNameKey(), player), lore, false, clickType -> { - if (clickType == ClickType.DROP || clickType == ClickType.CONTROL_DROP) { - askNumber(context, parameter, ParticleGuiText.number(value), input -> change(context, parameter, input)); - return; - } - double step = parameter.getStep() * (clickType.isShiftClick() ? 10 : 1) * (clickType.isRightClick() ? -1 : 1); - double next = Math.round((value + step) / parameter.getStep()) * parameter.getStep(); - change(context, parameter, next); - }); - } + Material icon = ICONS.getOrDefault(parameter, Material.PAPER); + if (parameter instanceof BooleanParameter) icon = (Boolean) value ? Material.LIME_DYE : Material.GRAY_DYE; + if (parameter == PARTICLE) icon = ((ParticleType) value).getIcon(); + if (parameter == COLORS) icon = Material.RED_DYE; - private SWItem intItem(Context context, IntParameter parameter) { - Player player = context.player(); - int value = context.entry().getValues().get(parameter); - List lore = lore(context, parameter, msg("PARTICLE_VALUE_NUMBER_HELP", player, 1), msg("PARTICLE_VALUE_EXACT_HELP", player)); - return new SWItem(Material.COMPARATOR, name(parameter.getNameKey(), player), lore, false, clickType -> { - if (clickType == ClickType.DROP || clickType == ClickType.CONTROL_DROP) { - askNumber(context, parameter, String.valueOf(value), input -> change(context, parameter, (int) Math.round(input))); - return; - } - int step = (clickType.isShiftClick() ? 10 : 1) * (clickType.isRightClick() ? -1 : 1); - change(context, parameter, value + step); - }); - } - - private SWItem booleanItem(Context context, BooleanParameter parameter) { - boolean value = context.entry().getValues().get(parameter); - List lore = lore(context, parameter, msg("PARTICLE_VALUE_TOGGLE_HELP", context.player())); - return new SWItem(value ? Material.LIME_DYE : Material.GRAY_DYE, name(parameter.getNameKey(), context.player()), lore, false, - clickType -> change(context, parameter, !value)); - } - - private SWItem gradientItem(Context context, GradientParameter parameter) { - Player player = context.player(); - List colors = context.entry().getValues().get(parameter); - List lore = lore(context, parameter, msg("PARTICLE_VALUE_COLORS_HELP", player)); - return new SWItem(Material.WHITE_DYE, name(parameter.getNameKey(), player), lore, false, clickType -> { - if (clickType.isRightClick()) { - if (colors.isEmpty()) return; - change(context, parameter, colors.subList(0, colors.size() - 1)); - } else if (colors.size() < parameter.getMaxColors()) { - Pickers.color(player, color -> { - List next = new ArrayList<>(colors); - next.add(color); - change(context, parameter, next); - }, context::reopen); + return new SWItem(icon, name(parameter.getNameKey(), player), lore, false, clickType -> { + if (parameter instanceof BooleanParameter toggle) { + apply(context, toggle, !(Boolean) value); + context.reopen(); + } else if (parameter instanceof DoubleParameter number) { + ValueEditors.editNumber(player, name(number.getNameKey(), player), () -> context.entry().getValues().get(number), + number.getStep(), number.getMin(), number.getMax(), number.getDefaultValue(), false, + next -> apply(context, number, next), context::reopen); + } else if (parameter instanceof IntParameter number) { + ValueEditors.editNumber(player, name(number.getNameKey(), player), () -> (double) context.entry().getValues().get(number), + 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(), + colors -> apply(context, gradient, colors), context::reopen); + } else if (parameter instanceof ChoiceParameter choice) { + pick(context, (ChoiceParameter) choice, value); } }); } - private SWItem choiceItem(Context context, ChoiceParameter parameter) { - Player player = context.player(); - E value = context.entry().getValues().get(parameter); - List values = parameter.getBuildableValues(); - boolean list = values.size() > MAX_CYCLE_CHOICES; - List lore = lore(context, parameter, msg(list ? "PARTICLE_VALUE_LIST_HELP" : "PARTICLE_VALUE_CHOICE_HELP", player)); - return new SWItem(choiceIcon(value), name(parameter.getNameKey(), player), lore, false, clickType -> { - if (list) { - Pickers.choice(player, parameter, EntryEditor::choiceIcon, picked -> change(context, parameter, picked), context::reopen); - return; - } - int current = values.indexOf(value); - int next = Math.floorMod(current + (clickType.isRightClick() ? -1 : 1), values.size()); - change(context, parameter, values.get(next)); - }); + private void pick(Context context, ChoiceParameter parameter, E current) { + Pickers.choice(context.player(), parameter, current, EntryEditor::choiceIcon, picked -> { + apply(context, parameter, picked); + context.reopen(); + }, context::reopen); } private Material choiceIcon(Object value) { 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; } - private void askNumber(Context context, Parameter parameter, String current, java.util.function.DoubleConsumer onInput) { - Player player = context.player(); - SWAnvilInv anvil = new SWAnvilInv(player, name(parameter.getNameKey(), player), current); - anvil.setItem(Material.PAPER); - anvil.setCallback(input -> Bukkit.getScheduler().runTask(LobbySystem.getInstance(), () -> { - try { - onInput.accept(Double.parseDouble(input.trim().replace(',', '.'))); - } catch (NumberFormatException e) { - context.reopen(); - } - })); - anvil.open(); - } - - // Reverts the change when it would push the slot over its budget - private void change(Context context, Parameter parameter, T value) { + // Saves the change, or reverts it and tells the player when the slot would go over its budget + private boolean apply(Context context, Parameter parameter, T value) { T previous = context.entry().getValues().get(parameter); context.entry().getValues().set(parameter, value); if (!SlotEditor.fits(context.user(), context.slot())) { context.entry().getValues().set(parameter, previous); LobbySystem.getMessage().send("PARTICLE_SLOT_OVER_BUDGET", context.player()); - } else { - SlotEditor.save(context.player(), context.index(), context.slot()); + return false; } - 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 diff --git a/LobbySystem/src/de/steamwar/lobby/particle/gui/ParticleGuiText.java b/LobbySystem/src/de/steamwar/lobby/particle/gui/ParticleGuiText.java index 208aafdb..3ddd4133 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/gui/ParticleGuiText.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/gui/ParticleGuiText.java @@ -25,6 +25,8 @@ 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 de.steamwar.lobby.particle.slot.ParticleSlots; +import de.steamwar.sql.SteamwarUser; import lombok.experimental.UtilityClass; 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 slotSummary(ParticleSlot slot, Player player) { List lore = new ArrayList<>(); lore.add(msg("PARTICLE_SLOT_ENTRIES", player, slot.getEntries().size())); diff --git a/LobbySystem/src/de/steamwar/lobby/particle/gui/ParticleMenu.java b/LobbySystem/src/de/steamwar/lobby/particle/gui/ParticleMenu.java index 1127c7b6..7611e036 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/gui/ParticleMenu.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/gui/ParticleMenu.java @@ -34,7 +34,6 @@ 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; @@ -96,28 +95,22 @@ public class ParticleMenu { List lore = new ArrayList<>(ParticleGuiText.slotSummary(slot, player)); lore.add(""); - boolean active = lobbyPlayer.getActiveSlot() == index; + String name = slotName(slot, context.getUser(), player); 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)); + return new SWItem(slot.getDisplayIcon(), name, lore, false, clickType -> SlotEditor.open(player, index)); } + boolean active = lobbyPlayer.getActiveSlot() == 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); - } - }); + lore.add(msg("PARTICLE_SLOT_OPEN", player)); + return new SWItem(slot.getDisplayIcon(), name, lore, active, clickType -> SlotEditor.open(player, index)); } - private void selected(Player player) { + void selected(Player player) { player.closeInventory(); if (ParticleListener.disabled(player)) { LobbySystem.getMessage().send("PARTICLE_DEACTIVATED", player); diff --git a/LobbySystem/src/de/steamwar/lobby/particle/gui/Pickers.java b/LobbySystem/src/de/steamwar/lobby/particle/gui/Pickers.java index 9f26e11f..90a70a01 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/gui/Pickers.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/gui/Pickers.java @@ -41,8 +41,9 @@ class Pickers { private final int BACK = 49; - // Basic designs first, then rewards; locked rewards are shown with their requirement but cannot be picked - void design(Player player, Consumer onPick, Runnable onBack) { + // Basic designs first, then rewards; locked rewards are shown with their requirement but cannot be picked. + // The current design, if any, glows. + void design(Player player, ParticleDesign current, Consumer onPick, Runnable onBack) { UnlockContext context = new UnlockContext(SteamwarUser.get(player.getUniqueId())); List> list = new ArrayList<>(); for (DesignVisibility visibility : List.of(DesignVisibility.BASIC, DesignVisibility.REWARD)) { @@ -51,18 +52,20 @@ class Pickers { List lore = new ArrayList<>(requirement(design.getRequirement(), player)); boolean unlocked = design.getRequirement().test(context); if (!lore.isEmpty()) lore.add(""); - lore.add(msg(unlocked ? "PARTICLE_SELECT" : "PARTICLE_LOCKED", player)); - list.add(new SWListInv.SWListEntry<>(new SWItem(design.getIcon(), designName(design, player), lore, false, clickType -> { + 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, design == current, clickType -> { }), unlocked ? design : null)); } } open(player, msg("PARTICLE_DESIGN_PICKER", player), list, onPick, onBack); } - void choice(Player player, ChoiceParameter parameter, Function icon, Consumer onPick, Runnable onBack) { + // Lists every option; the current one glows + void choice(Player player, ChoiceParameter parameter, E current, Function icon, Consumer onPick, Runnable onBack) { List> list = new ArrayList<>(); for (E value : parameter.getBuildableValues()) { - list.add(new SWListInv.SWListEntry<>(new SWItem(icon.apply(value), choiceName(parameter, value, player), clickType -> { + List 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)); } open(player, msg("PARTICLE_CHOICE_PICKER", player, name(parameter.getNameKey(), player)), list, onPick, onBack); diff --git a/LobbySystem/src/de/steamwar/lobby/particle/gui/SlotEditor.java b/LobbySystem/src/de/steamwar/lobby/particle/gui/SlotEditor.java index 9afa3e01..4b672a88 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/gui/SlotEditor.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/gui/SlotEditor.java @@ -33,18 +33,18 @@ 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 +// Lists a slot's entries; slots the player has lost access to are shown read-only @UtilityClass public class SlotEditor { private final int BACK = 45; + private final int USE = 46; private final int ADD_ENTRY = 47; private final int RENAME = 48; private final int ICON = 49; @@ -52,14 +52,6 @@ public class SlotEditor { 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) { @@ -68,11 +60,9 @@ public class SlotEditor { } 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; + boolean editable = ParticleSlots.requirement(index).test(context); - 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 entries = slot.getEntries(); for (int i = 0; i < entries.size(); i++) { ParticleEntry entry = entries.get(i); @@ -80,32 +70,10 @@ public class SlotEditor { 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(i, new SWItem(entry.getDesign().getIcon(), designName(entry.getDesign(), player), lore, false, clickType -> { + if (editable) EntryEditor.open(player, index, entryIndex); })); } @@ -114,16 +82,26 @@ public class SlotEditor { List lore = new ArrayList<>(requirement(ParticleSlots.requirement(index), player)); inventory.setItem(ICON, new SWItem(Material.BARRIER, msg("PARTICLE_LOCKED", player), lore, false, clickType -> { })); + fill(inventory, 45, 54); inventory.open(); 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 -> { if (entries.size() >= ParticleSlots.MAX_ENTRIES) { LobbySystem.getMessage().send("PARTICLE_SLOT_MAX_ENTRIES", player); return; } - Pickers.design(player, design -> { + Pickers.design(player, null, design -> { entries.add(new ParticleEntry(design)); if (!fits(user, slot)) { entries.remove(entries.size() - 1); @@ -152,7 +130,7 @@ public class SlotEditor { 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 -> { + 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 (lobbyPlayer.getActiveSlot() == index) { lobbyPlayer.deselect(); @@ -160,6 +138,7 @@ public class SlotEditor { lobbyPlayer.setSlot(index, null); ParticleMenu.open(player); })); + fill(inventory, 45, 54); inventory.open(); } @@ -176,7 +155,13 @@ public class SlotEditor { LobbyPlayer.getLobbyPlayer(player).setSlot(index, slot); } - Material icon(ParticleEntry entry) { - return entry.getDesign().getIcon(); + // Puts filler panes on the empty positions in the given range + 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 -> { + })); + } + } } } diff --git a/LobbySystem/src/de/steamwar/lobby/particle/gui/ValueEditors.java b/LobbySystem/src/de/steamwar/lobby/particle/gui/ValueEditors.java new file mode 100644 index 00000000..b248e501 --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/gui/ValueEditors.java @@ -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 . + */ + +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 current, double step, double min, double max, double defaultValue, boolean integer, + Predicate 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 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 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> current, int maxColors, Predicate> apply, Runnable back) { + List 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 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 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("§.", ""); + } +}