forked from SteamWar/SteamWar
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:
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user