Files
SteamWar/LobbySystem/src/de/steamwar/lobby/particle/gui/EntryEditor.java
T
YoyoNow c3eda01e20 Cycle particle settings with up to five options
Choices with up to five options cycle forward on left click and backward
on right click, listing all options in the lore. Two-option choices still
read as a toggle; longer choices keep their list.
2026-10-26 09:49:07 +01:00

276 lines
14 KiB
Java

/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.lobby.particle.gui;
import de.steamwar.inventory.SWInventory;
import de.steamwar.inventory.SWItem;
import de.steamwar.lobby.LobbySystem;
import de.steamwar.lobby.particle.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.Material;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static de.steamwar.lobby.particle.design.EntryParameters.*;
import static de.steamwar.lobby.particle.gui.ParticleGuiText.*;
// 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 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 SIZE = 54;
private final int BACK = BottomBar.position(SIZE, BottomBar.BACK);
private final int COST = BottomBar.position(SIZE, BottomBar.COST);
private final int DELETE = BottomBar.position(SIZE, BottomBar.DELETE);
private final int MAX_CYCLED_OPTIONS = 5;
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) {
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();
ParticleDesign design = entry.getDesign();
SWInventory inventory = new SWInventory(player, SIZE, msg("PARTICLE_ENTRY_EDITOR", player, designName(design, player)));
header(inventory, ROW_DESIGN, Material.PAINTING, "PARTICLE_SECTION_DESIGN", player);
header(inventory, ROW_PARTICLE, Material.FIREWORK_STAR, "PARTICLE_SECTION_PARTICLE", player);
header(inventory, ROW_SHAPE, Material.ARMOR_STAND, "PARTICLE_SECTION_SHAPE", player);
header(inventory, ROW_TIMING, Material.DAYLIGHT_DETECTOR, "PARTICLE_SECTION_TIMING", player);
header(inventory, ROW_CONDITIONS, Material.OBSERVER, "PARTICLE_SECTION_CONDITIONS", player);
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));
ParticleType type = entry.getValues().get(PARTICLE);
if (type.usesColors()) {
inventory.setItem(ROW_DESIGN * 9 + 3, parameterItem(context, COLORS));
} else if (type.usesBlock()) {
inventory.setItem(ROW_DESIGN * 9 + 3, parameterItem(context, BLOCK));
}
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", 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);
}));
BottomBar.fillUnused(inventory, SIZE, false);
SlotEditor.fill(inventory, 0, SIZE);
inventory.open();
}
// Glowing so the row labels stand apart from the settings next to them
private void header(SWInventory inventory, int row, Material icon, String key, Player player) {
inventory.setItem(row * 9, new SWItem(icon, msg(key, player), List.of(), true, clickType -> {
}));
}
private void row(SWInventory inventory, Context context, int row, List<Parameter<?>> parameters) {
for (int i = 0; i < parameters.size() && i < 8; i++) {
inventory.setItem(row * 9 + 1 + i, parameterItem(context, parameters.get(i)));
}
}
@SuppressWarnings("unchecked")
private SWItem parameterItem(Context context, Parameter<?> parameter) {
Player player = context.player();
Object value = context.entry().getValues().get(parameter);
List<String> lore = new ArrayList<>();
if (parameter instanceof ChoiceParameter<?> choice && isCycled(choice)) {
lore.addAll(optionLines((ChoiceParameter<Object>) choice, value, player));
} else {
lore.add(msg("PARTICLE_VALUE", player, valueText(parameter, value, player)));
}
lore.add("");
lore.add(msg(isToggle(parameter) ? "PARTICLE_VALUE_TOGGLE" : isCycled(parameter) ? "PARTICLE_VALUE_CYCLE" : "PARTICLE_VALUE_CLICK", player));
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;
if (parameter == BLOCK) icon = (Material) value;
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) {
if (isCycled(choice)) {
cycle(context, (ChoiceParameter<Object>) choice, value, clickType.isRightClick() ? -1 : 1);
} else {
pick(context, (ChoiceParameter<Object>) choice, value);
}
}
});
}
// Choices with a few options cycle through them on click instead of opening a list
private boolean isCycled(Parameter<?> parameter) {
return parameter instanceof ChoiceParameter<?> choice && choice.getBuildableValues().size() <= MAX_CYCLED_OPTIONS;
}
// With two options both click directions lead to the same value, so it reads as a plain toggle
private boolean isToggle(Parameter<?> parameter) {
return parameter instanceof BooleanParameter || (isCycled(parameter) && ((ChoiceParameter<?>) parameter).getBuildableValues().size() == 2);
}
// All options of a cycled choice, the current one highlighted
private <E> List<String> optionLines(ChoiceParameter<E> parameter, E current, Player player) {
List<String> lines = new ArrayList<>();
for (E option : parameter.getBuildableValues()) {
String name = choiceName(parameter, option, player).replaceAll("§.", "");
lines.add(option == current ? "§a» " + name : "§8» §7" + name);
}
return lines;
}
// Left click moves forward, right click backward
private <E> void cycle(Context context, ChoiceParameter<E> parameter, E current, int direction) {
List<E> values = parameter.getBuildableValues();
apply(context, parameter, values.get(Math.floorMod(values.indexOf(current) + direction, values.size())));
context.reopen();
}
private <E> void pick(Context context, ChoiceParameter<E> 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.PHANTOM_MEMBRANE;
if (value instanceof Material material) return material;
return Material.LEVER;
}
// Saves the change, or reverts it and tells the player when the slot would go over its budget
private <T> boolean apply(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());
return false;
}
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
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));
}
}