forked from SteamWar/SteamWar
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.
276 lines
14 KiB
Java
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));
|
|
}
|
|
}
|