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:
@@ -85,6 +85,105 @@ PARTICLE_SELECT = §eClick to select
|
||||
PARTICLE_LOCKED = §c§lLocked
|
||||
PARTICLE_DEACTIVATED = §cThere are too many players online, particles are deactivated
|
||||
|
||||
PARTICLE_BACK = §7Back
|
||||
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_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
|
||||
PARTICLE_SLOT_RENAME = §eRename
|
||||
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_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_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
|
||||
|
||||
PARTICLE_DESIGN_SIMPLE = §fSimple
|
||||
PARTICLE_DESIGN_CIRCLE = §fCircle
|
||||
PARTICLE_DESIGN_WING = §fWings
|
||||
PARTICLE_DESIGN_CLOUD = §fCloud
|
||||
|
||||
PARTICLE_PARAM_PARTICLE = §fParticle
|
||||
PARTICLE_PARAM_COUNT = §fAmount
|
||||
PARTICLE_PARAM_SPREAD_X = §fSpread X
|
||||
PARTICLE_PARAM_SPREAD_Y = §fSpread Y
|
||||
PARTICLE_PARAM_SPREAD_Z = §fSpread Z
|
||||
PARTICLE_PARAM_SPEED = §fSpeed
|
||||
PARTICLE_PARAM_COLORS = §fColours
|
||||
PARTICLE_PARAM_OFFSET_X = §fOffset X
|
||||
PARTICLE_PARAM_OFFSET_Y = §fOffset Y
|
||||
PARTICLE_PARAM_OFFSET_Z = §fOffset Z
|
||||
PARTICLE_PARAM_INTERVAL = §fEvery n ticks
|
||||
PARTICLE_PARAM_TICK_MODE = §fShown
|
||||
PARTICLE_PARAM_MOVEMENT = §fMovement
|
||||
PARTICLE_PARAM_GLIDING = §fElytra
|
||||
PARTICLE_PARAM_GROUND = §fGround
|
||||
PARTICLE_PARAM_SNEAK = §fSneaking
|
||||
PARTICLE_PARAM_VISIBILITY = §fVisible to
|
||||
PARTICLE_PARAM_RADIUS = §fRadius
|
||||
PARTICLE_PARAM_CIRCLE_SPEED = §fRotation speed
|
||||
PARTICLE_PARAM_PHASE = §fStart angle
|
||||
PARTICLE_PARAM_RISE = §fUp and down height
|
||||
PARTICLE_PARAM_RISE_SPEED = §fUp and down speed
|
||||
PARTICLE_PARAM_BOUNCE = §fBounce back
|
||||
PARTICLE_PARAM_INVERTED = §fStart at the top
|
||||
PARTICLE_PARAM_WING_IMAGE = §fWing shape
|
||||
PARTICLE_PARAM_WING_SIZE = §fWing size
|
||||
|
||||
PARTICLE_CHOICE_TICKMODE_MOVE = §fWhile moving
|
||||
PARTICLE_CHOICE_TICKMODE_ALWAYS = §fAlways
|
||||
PARTICLE_CHOICE_MOVEMENT_ANY = §fAny movement
|
||||
PARTICLE_CHOICE_MOVEMENT_MOVING = §fOnly while moving
|
||||
PARTICLE_CHOICE_MOVEMENT_STANDING_STILL = §fOnly while standing still
|
||||
PARTICLE_CHOICE_GLIDING_ANY = §fWith or without elytra
|
||||
PARTICLE_CHOICE_GLIDING_GLIDING = §fOnly while gliding
|
||||
PARTICLE_CHOICE_GLIDING_NOT_GLIDING = §fNot while gliding
|
||||
PARTICLE_CHOICE_GROUND_ANY = §fOn ground and in air
|
||||
PARTICLE_CHOICE_GROUND_ON_GROUND = §fOnly on ground
|
||||
PARTICLE_CHOICE_GROUND_IN_AIR = §fOnly in air
|
||||
PARTICLE_CHOICE_SNEAK_ANY = §fSneaking or not
|
||||
PARTICLE_CHOICE_SNEAK_SNEAKING = §fOnly while sneaking
|
||||
PARTICLE_CHOICE_SNEAK_NOT_SNEAKING = §fNot while sneaking
|
||||
PARTICLE_CHOICE_VISIBILITY_EVERYONE = §fEveryone
|
||||
PARTICLE_CHOICE_VISIBILITY_ONLY_SELF = §fOnly me
|
||||
PARTICLE_CHOICE_VISIBILITY_ONLY_OTHERS = §fOnly others
|
||||
PARTICLE_CHOICE_IMAGE_SIMPLE = §fSimple
|
||||
PARTICLE_CHOICE_IMAGE_COMPLEX = §fComplex
|
||||
PARTICLE_CHOICE_IMAGE_SWORD = §fSword
|
||||
|
||||
PARTICLE_SNEEZE = §aSneeze
|
||||
PARTICLE_SMOKE = §7Smoke
|
||||
PARTICLE_FIRE = §cFire
|
||||
|
||||
@@ -84,6 +84,105 @@ PARTICLE_SELECT = §eZum Auswählen klicken
|
||||
PARTICLE_LOCKED = §c§lGesperrt
|
||||
PARTICLE_DEACTIVATED = §cEs sind zu viele Spieler online! Partikel sind deaktiviert.
|
||||
|
||||
PARTICLE_BACK = §7Zurück
|
||||
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_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
|
||||
PARTICLE_SLOT_RENAME = §eUmbenennen
|
||||
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_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_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
|
||||
|
||||
PARTICLE_DESIGN_SIMPLE = §fEinfach
|
||||
PARTICLE_DESIGN_CIRCLE = §fKreis
|
||||
PARTICLE_DESIGN_WING = §fFlügel
|
||||
PARTICLE_DESIGN_CLOUD = §fWolke
|
||||
|
||||
PARTICLE_PARAM_PARTICLE = §fPartikel
|
||||
PARTICLE_PARAM_COUNT = §fAnzahl
|
||||
PARTICLE_PARAM_SPREAD_X = §fStreuung X
|
||||
PARTICLE_PARAM_SPREAD_Y = §fStreuung Y
|
||||
PARTICLE_PARAM_SPREAD_Z = §fStreuung Z
|
||||
PARTICLE_PARAM_SPEED = §fGeschwindigkeit
|
||||
PARTICLE_PARAM_COLORS = §fFarben
|
||||
PARTICLE_PARAM_OFFSET_X = §fVersatz X
|
||||
PARTICLE_PARAM_OFFSET_Y = §fVersatz Y
|
||||
PARTICLE_PARAM_OFFSET_Z = §fVersatz Z
|
||||
PARTICLE_PARAM_INTERVAL = §fAlle n Ticks
|
||||
PARTICLE_PARAM_TICK_MODE = §fAnzeige
|
||||
PARTICLE_PARAM_MOVEMENT = §fBewegung
|
||||
PARTICLE_PARAM_GLIDING = §fElytra
|
||||
PARTICLE_PARAM_GROUND = §fBoden
|
||||
PARTICLE_PARAM_SNEAK = §fDucken
|
||||
PARTICLE_PARAM_VISIBILITY = §fSichtbar für
|
||||
PARTICLE_PARAM_RADIUS = §fRadius
|
||||
PARTICLE_PARAM_CIRCLE_SPEED = §fDrehgeschwindigkeit
|
||||
PARTICLE_PARAM_PHASE = §fStartwinkel
|
||||
PARTICLE_PARAM_RISE = §fAuf und ab Höhe
|
||||
PARTICLE_PARAM_RISE_SPEED = §fAuf und ab Geschwindigkeit
|
||||
PARTICLE_PARAM_BOUNCE = §fZurückspringen
|
||||
PARTICLE_PARAM_INVERTED = §fOben beginnen
|
||||
PARTICLE_PARAM_WING_IMAGE = §fFlügelform
|
||||
PARTICLE_PARAM_WING_SIZE = §fFlügelgröße
|
||||
|
||||
PARTICLE_CHOICE_TICKMODE_MOVE = §fBeim Bewegen
|
||||
PARTICLE_CHOICE_TICKMODE_ALWAYS = §fImmer
|
||||
PARTICLE_CHOICE_MOVEMENT_ANY = §fJede Bewegung
|
||||
PARTICLE_CHOICE_MOVEMENT_MOVING = §fNur beim Bewegen
|
||||
PARTICLE_CHOICE_MOVEMENT_STANDING_STILL = §fNur beim Stehen
|
||||
PARTICLE_CHOICE_GLIDING_ANY = §fMit oder ohne Elytra
|
||||
PARTICLE_CHOICE_GLIDING_GLIDING = §fNur beim Gleiten
|
||||
PARTICLE_CHOICE_GLIDING_NOT_GLIDING = §fNicht beim Gleiten
|
||||
PARTICLE_CHOICE_GROUND_ANY = §fAm Boden und in der Luft
|
||||
PARTICLE_CHOICE_GROUND_ON_GROUND = §fNur am Boden
|
||||
PARTICLE_CHOICE_GROUND_IN_AIR = §fNur in der Luft
|
||||
PARTICLE_CHOICE_SNEAK_ANY = §fDucken egal
|
||||
PARTICLE_CHOICE_SNEAK_SNEAKING = §fNur beim Ducken
|
||||
PARTICLE_CHOICE_SNEAK_NOT_SNEAKING = §fNicht beim Ducken
|
||||
PARTICLE_CHOICE_VISIBILITY_EVERYONE = §fAlle
|
||||
PARTICLE_CHOICE_VISIBILITY_ONLY_SELF = §fNur ich
|
||||
PARTICLE_CHOICE_VISIBILITY_ONLY_OTHERS = §fNur andere
|
||||
PARTICLE_CHOICE_IMAGE_SIMPLE = §fEinfach
|
||||
PARTICLE_CHOICE_IMAGE_COMPLEX = §fKomplex
|
||||
PARTICLE_CHOICE_IMAGE_SWORD = §fSchwert
|
||||
|
||||
PARTICLE_SNEEZE = §aSneeze
|
||||
PARTICLE_SMOKE = §7Rauch
|
||||
PARTICLE_FIRE = §cFeuer
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2025 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;
|
||||
|
||||
import de.steamwar.inventory.SWItem;
|
||||
import de.steamwar.inventory.SWListInv;
|
||||
import de.steamwar.lobby.LobbySystem;
|
||||
import de.steamwar.lobby.particle.slot.ParticlePreset;
|
||||
import de.steamwar.lobby.particle.slot.Presets;
|
||||
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;
|
||||
|
||||
// Lists the presets a player has unlocked; the slot menus replace this
|
||||
@UtilityClass
|
||||
public class ParticleInventory {
|
||||
|
||||
public void openInventory(Player player) {
|
||||
LobbyPlayer lobbyPlayer = LobbyPlayer.getLobbyPlayer(player);
|
||||
UnlockContext context = new UnlockContext(SteamwarUser.get(player.getUniqueId()));
|
||||
|
||||
List<SWListInv.SWListEntry<ParticlePreset>> presetList = new ArrayList<>();
|
||||
for (ParticlePreset preset : Presets.all()) {
|
||||
if (!preset.getRequirement().test(context)) continue;
|
||||
List<String> lore = new ArrayList<>();
|
||||
lore.add("");
|
||||
lore.add(LobbySystem.getMessage().parse("PARTICLE_SELECT", player));
|
||||
presetList.add(new SWListInv.SWListEntry<>(new SWItem(preset.getIcon(), translate(preset.getNameKey(), player), lore, preset == lobbyPlayer.getActivePreset(), clickType -> {
|
||||
}), preset));
|
||||
}
|
||||
|
||||
SWListInv<ParticlePreset> inventory = new SWListInv<>(player, LobbySystem.getMessage().parse("PARTICLE_INVENTORY", player), false, presetList, (clickType, preset) -> {
|
||||
if (preset == null) return;
|
||||
lobbyPlayer.selectPreset(preset);
|
||||
player.closeInventory();
|
||||
if (ParticleListener.disabled(player)) {
|
||||
LobbySystem.getMessage().send("PARTICLE_DEACTIVATED", player);
|
||||
}
|
||||
});
|
||||
inventory.setItem(49, Material.BARRIER, LobbySystem.getMessage().parse("PARTICLE_DESELECT", player), new ArrayList<>(), false, clickType -> {
|
||||
lobbyPlayer.deselect();
|
||||
player.closeInventory();
|
||||
});
|
||||
inventory.open();
|
||||
}
|
||||
|
||||
private String translate(String key, Player player) {
|
||||
try {
|
||||
return LobbySystem.getMessage().parse(key, player);
|
||||
} catch (Exception e) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import de.steamwar.linkage.Linked;
|
||||
import de.steamwar.lobby.LobbySystem;
|
||||
import de.steamwar.lobby.jumpandrun.JumpAndRun;
|
||||
import de.steamwar.lobby.listener.PlayerSpawn;
|
||||
import de.steamwar.lobby.particle.gui.ParticleMenu;
|
||||
import de.steamwar.lobby.util.LobbyPlayer;
|
||||
import de.steamwar.sql.SteamwarUser;
|
||||
import de.steamwar.sql.UserPerm;
|
||||
@@ -76,7 +77,7 @@ public class ParticleListener implements Listener {
|
||||
if (!PlayerSpawn.PARTICLE.equals(event.getItem())) return;
|
||||
event.setCancelled(true);
|
||||
|
||||
ParticleInventory.openInventory(player);
|
||||
ParticleMenu.open(player);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
|
||||
@@ -93,6 +93,16 @@ public class Designs {
|
||||
public int points(ParameterValues values) {
|
||||
return values.get(WING_IMAGE).getVectors().length;
|
||||
}
|
||||
|
||||
// Wings spawn a particle on every pixel of the image, so they start sparse
|
||||
@Override
|
||||
public void applyDefaults(ParameterValues values) {
|
||||
values.set(EntryParameters.COUNT, 1)
|
||||
.set(EntryParameters.SPEED, 0.0)
|
||||
.set(EntryParameters.TICK_MODE, EntryParameters.TickMode.ALWAYS)
|
||||
.set(EntryParameters.INTERVAL, 20)
|
||||
.set(EntryParameters.GLIDING, EntryParameters.Gliding.NOT_GLIDING);
|
||||
}
|
||||
};
|
||||
|
||||
public final ParticleDesign CLOUD = new ParticleDesign("cloud", Material.WHITE_WOOL, "PARTICLE_DESIGN_CLOUD") {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* 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.lobby.LobbySystem;
|
||||
import de.steamwar.lobby.particle.ParticleRequirement;
|
||||
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 lombok.experimental.UtilityClass;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.MissingResourceException;
|
||||
|
||||
import static de.steamwar.lobby.particle.design.EntryParameters.*;
|
||||
|
||||
@UtilityClass
|
||||
class ParticleGuiText {
|
||||
|
||||
String msg(String key, Player player, Object... params) {
|
||||
return LobbySystem.getMessage().parse(key, player, params);
|
||||
}
|
||||
|
||||
// Falls back to a readable form of the key for names without a translation
|
||||
String name(String key, Player player) {
|
||||
try {
|
||||
return LobbySystem.getMessage().parse(key, player);
|
||||
} catch (MissingResourceException e) {
|
||||
return "§f" + prettify(key.replaceFirst("^PARTICLE_(TYPE_|DESIGN_|PARAM_)?", ""));
|
||||
}
|
||||
}
|
||||
|
||||
String prettify(String id) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (String word : id.toLowerCase(Locale.ROOT).split("_")) {
|
||||
if (word.isEmpty()) continue;
|
||||
if (!builder.isEmpty()) builder.append(' ');
|
||||
builder.append(Character.toUpperCase(word.charAt(0))).append(word.substring(1));
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
String designName(ParticleDesign design, Player player) {
|
||||
return name(design.getNameKey(), player);
|
||||
}
|
||||
|
||||
String particleName(ParticleType type, Player player) {
|
||||
return name(type.getNameKey(), player);
|
||||
}
|
||||
|
||||
<E> String choiceName(ChoiceParameter<E> parameter, E value, Player player) {
|
||||
for (ChoiceParameter.Choice<E> choice : parameter.getChoices()) {
|
||||
if (choice.value() != value) continue;
|
||||
if (value instanceof ParticleType type) return particleName(type, player);
|
||||
return name("PARTICLE_CHOICE_" + parameter.getKey().toUpperCase(Locale.ROOT) + "_" + choice.id().toUpperCase(Locale.ROOT), player);
|
||||
}
|
||||
return String.valueOf(value);
|
||||
}
|
||||
|
||||
String number(double value) {
|
||||
return value == Math.rint(value) ? String.valueOf((long) value) : String.format(Locale.ROOT, "%.2f", value).replaceAll("0+$", "");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
String valueText(Parameter<?> parameter, Object value, Player player) {
|
||||
if (parameter instanceof DoubleParameter) return "§e" + number((Double) value);
|
||||
if (parameter instanceof IntParameter) return "§e" + value;
|
||||
if (parameter instanceof BooleanParameter) return msg((Boolean) value ? "PARTICLE_VALUE_YES" : "PARTICLE_VALUE_NO", player);
|
||||
if (parameter instanceof ChoiceParameter<?> choice) return choiceName((ChoiceParameter<Object>) choice, value, player);
|
||||
if (parameter instanceof GradientParameter) {
|
||||
List<Color> colors = (List<Color>) value;
|
||||
if (colors.isEmpty()) return msg("PARTICLE_VALUE_RANDOM", player);
|
||||
StringBuilder builder = new StringBuilder();
|
||||
colors.forEach(color -> builder.append(net.md_5.bungee.api.ChatColor.of(color)).append('█'));
|
||||
return builder.toString();
|
||||
}
|
||||
return String.valueOf(value);
|
||||
}
|
||||
|
||||
// Requirement lines for an item's lore, empty when there is no requirement
|
||||
List<String> requirement(ParticleRequirement requirement, Player player) {
|
||||
List<String> lore = new ArrayList<>();
|
||||
String name = requirement.getRequirementName(player);
|
||||
if (name != null) {
|
||||
lore.add(msg("PARTICLE_UNLOCKED_BY", player));
|
||||
lore.add(name);
|
||||
}
|
||||
return lore;
|
||||
}
|
||||
|
||||
// Short description of an entry: design, particle and the conditions that are set
|
||||
List<String> entrySummary(ParticleEntry entry, UnlockContext context, Player player) {
|
||||
List<String> lore = new ArrayList<>();
|
||||
ParameterValues values = entry.getValues();
|
||||
lore.add(msg("PARTICLE_ENTRY_DESIGN", player, designName(entry.getDesign(), player)));
|
||||
if (entry.getDesign().usesEntryParticle()) {
|
||||
lore.add(msg("PARTICLE_ENTRY_PARTICLE", player, particleName(values.get(PARTICLE), player)));
|
||||
}
|
||||
lore.add("§8- " + choiceName(TICK_MODE, values.get(TICK_MODE), player));
|
||||
addCondition(lore, MOVEMENT, values.get(MOVEMENT), Movement.ANY, player);
|
||||
addCondition(lore, GLIDING, values.get(GLIDING), Gliding.ANY, player);
|
||||
addCondition(lore, GROUND, values.get(GROUND), Ground.ANY, player);
|
||||
addCondition(lore, SNEAK, values.get(SNEAK), Sneak.ANY, player);
|
||||
addCondition(lore, VISIBILITY, values.get(VISIBILITY), Visibility.EVERYONE, player);
|
||||
if (!entry.getDesign().getRequirement().test(context)) {
|
||||
lore.add(msg("PARTICLE_ENTRY_LOCKED", player));
|
||||
}
|
||||
return lore;
|
||||
}
|
||||
|
||||
private <E> void addCondition(List<String> lore, ChoiceParameter<E> parameter, E value, E none, Player player) {
|
||||
if (value != none) {
|
||||
lore.add("§8- " + choiceName(parameter, value, player));
|
||||
}
|
||||
}
|
||||
|
||||
List<String> slotSummary(ParticleSlot slot, Player player) {
|
||||
List<String> lore = new ArrayList<>();
|
||||
lore.add(msg("PARTICLE_SLOT_ENTRIES", player, slot.getEntries().size()));
|
||||
for (ParticleEntry entry : slot.getEntries()) {
|
||||
String particle = entry.getDesign().usesEntryParticle() ? " §8(" + particleName(entry.getValues().get(PARTICLE), player) + "§8)" : "";
|
||||
lore.add("§8- " + designName(entry.getDesign(), player) + particle);
|
||||
}
|
||||
return lore;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* 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.ParticleListener;
|
||||
import de.steamwar.lobby.particle.ParticleRequirement;
|
||||
import de.steamwar.lobby.particle.UnlockContext;
|
||||
import de.steamwar.lobby.particle.slot.ParticlePreset;
|
||||
import de.steamwar.lobby.particle.slot.ParticleSlot;
|
||||
import de.steamwar.lobby.particle.slot.ParticleSlots;
|
||||
import de.steamwar.lobby.particle.slot.Presets;
|
||||
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 org.bukkit.event.inventory.ClickType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static de.steamwar.lobby.particle.gui.ParticleGuiText.*;
|
||||
|
||||
// The particle item's menu: the player's slots in the top row, their unlocked presets below
|
||||
@UtilityClass
|
||||
public class ParticleMenu {
|
||||
|
||||
private final int NO_PARTICLE_POSITION = 8;
|
||||
private final int PRESET_START = 18;
|
||||
|
||||
public void open(Player player) {
|
||||
LobbyPlayer lobbyPlayer = LobbyPlayer.getLobbyPlayer(player);
|
||||
UnlockContext context = new UnlockContext(SteamwarUser.get(player.getUniqueId()));
|
||||
List<ParticlePreset> presets = Presets.all().stream().filter(preset -> preset.getRequirement().test(context)).toList();
|
||||
int rows = presets.isEmpty() ? 1 : Math.min(6, 2 + (presets.size() + 8) / 9);
|
||||
|
||||
SWInventory inventory = new SWInventory(player, rows * 9, msg("PARTICLE_INVENTORY", player));
|
||||
for (int index = 0; index < ParticleSlots.COUNT; index++) {
|
||||
inventory.setItem(index, slotItem(player, lobbyPlayer, context, index));
|
||||
}
|
||||
inventory.setItem(NO_PARTICLE_POSITION, new SWItem(Material.BARRIER, msg("PARTICLE_DESELECT", player), clickType -> {
|
||||
lobbyPlayer.deselect();
|
||||
player.closeInventory();
|
||||
}));
|
||||
|
||||
for (int i = 0; i < presets.size() && PRESET_START + i < rows * 9; i++) {
|
||||
ParticlePreset preset = presets.get(i);
|
||||
List<String> lore = new ArrayList<>();
|
||||
lore.add("");
|
||||
lore.add(msg("PARTICLE_SELECT", player));
|
||||
inventory.setItem(PRESET_START + i, new SWItem(preset.getIcon(), ParticleGuiText.name(preset.getNameKey(), player), lore, preset == lobbyPlayer.getActivePreset(), clickType -> {
|
||||
lobbyPlayer.selectPreset(preset);
|
||||
selected(player);
|
||||
}));
|
||||
}
|
||||
inventory.open();
|
||||
}
|
||||
|
||||
private SWItem slotItem(Player player, LobbyPlayer lobbyPlayer, UnlockContext context, int index) {
|
||||
ParticleSlot slot = lobbyPlayer.getSlot(index);
|
||||
ParticleRequirement requirement = ParticleSlots.requirement(index);
|
||||
boolean unlocked = requirement.test(context);
|
||||
|
||||
if (slot == null) {
|
||||
if (!unlocked) {
|
||||
List<String> lore = new ArrayList<>(ParticleGuiText.requirement(requirement, player));
|
||||
return new SWItem(Material.BARRIER, msg("PARTICLE_LOCKED", player), lore, false, clickType -> {
|
||||
});
|
||||
}
|
||||
List<String> lore = List.of(msg("PARTICLE_SLOT_CREATE", player));
|
||||
return new SWItem(Material.GRAY_STAINED_GLASS_PANE, msg("PARTICLE_SLOT_EMPTY", player), lore, false, clickType -> {
|
||||
lobbyPlayer.setSlot(index, new ParticleSlot(ParticleSlots.defaultName(index)));
|
||||
SlotEditor.open(player, index);
|
||||
});
|
||||
}
|
||||
|
||||
List<String> lore = new ArrayList<>(ParticleGuiText.slotSummary(slot, player));
|
||||
lore.add("");
|
||||
boolean active = lobbyPlayer.getActiveSlot() == index;
|
||||
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));
|
||||
}
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void selected(Player player) {
|
||||
player.closeInventory();
|
||||
if (ParticleListener.disabled(player)) {
|
||||
LobbySystem.getMessage().send("PARTICLE_DEACTIVATED", player);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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.SWItem;
|
||||
import de.steamwar.inventory.SWListInv;
|
||||
import de.steamwar.lobby.particle.UnlockContext;
|
||||
import de.steamwar.lobby.particle.design.*;
|
||||
import de.steamwar.sql.SteamwarUser;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.bukkit.DyeColor;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static de.steamwar.lobby.particle.gui.ParticleGuiText.*;
|
||||
|
||||
@UtilityClass
|
||||
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<ParticleDesign> onPick, Runnable onBack) {
|
||||
UnlockContext context = new UnlockContext(SteamwarUser.get(player.getUniqueId()));
|
||||
List<SWListInv.SWListEntry<ParticleDesign>> list = new ArrayList<>();
|
||||
for (DesignVisibility visibility : List.of(DesignVisibility.BASIC, DesignVisibility.REWARD)) {
|
||||
for (ParticleDesign design : Designs.all()) {
|
||||
if (design.getVisibility() != visibility) continue;
|
||||
List<String> 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 -> {
|
||||
}), unlocked ? design : null));
|
||||
}
|
||||
}
|
||||
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) {
|
||||
List<SWListInv.SWListEntry<E>> list = new ArrayList<>();
|
||||
for (E value : parameter.getBuildableValues()) {
|
||||
list.add(new SWListInv.SWListEntry<>(new SWItem(icon.apply(value), choiceName(parameter, value, player), clickType -> {
|
||||
}), value));
|
||||
}
|
||||
open(player, msg("PARTICLE_CHOICE_PICKER", player, name(parameter.getNameKey(), player)), list, onPick, onBack);
|
||||
}
|
||||
|
||||
void color(Player player, Consumer<Color> onPick, Runnable onBack) {
|
||||
List<SWListInv.SWListEntry<Color>> list = new ArrayList<>();
|
||||
for (DyeColor dye : DyeColor.values()) {
|
||||
Color color = new Color(dye.getColor().asRGB());
|
||||
list.add(new SWListInv.SWListEntry<>(new SWItem(Material.valueOf(dye.name() + "_DYE"), net.md_5.bungee.api.ChatColor.of(color) + prettify(dye.name()), clickType -> {
|
||||
}), color));
|
||||
}
|
||||
open(player, msg("PARTICLE_COLOR_PICKER", player), list, onPick, onBack);
|
||||
}
|
||||
|
||||
void icon(Player player, Consumer<Material> onPick, Runnable onBack) {
|
||||
Set<Material> icons = new LinkedHashSet<>();
|
||||
for (ParticleType type : ParticleType.values()) {
|
||||
if (type.isBuildable()) icons.add(type.getIcon());
|
||||
}
|
||||
for (ParticleDesign design : Designs.all()) {
|
||||
if (design.getVisibility() == DesignVisibility.BASIC) icons.add(design.getIcon());
|
||||
}
|
||||
icons.addAll(List.of(Material.DIAMOND, Material.EMERALD, Material.GOLD_INGOT, Material.AMETHYST_SHARD, Material.HEART_OF_THE_SEA, Material.NETHER_STAR, Material.FEATHER, Material.TNT));
|
||||
|
||||
List<SWListInv.SWListEntry<Material>> list = new ArrayList<>();
|
||||
for (Material material : icons) {
|
||||
list.add(new SWListInv.SWListEntry<>(new SWItem(material, "§f" + prettify(material.name()), clickType -> {
|
||||
}), material));
|
||||
}
|
||||
open(player, msg("PARTICLE_SLOT_ICON_TITLE", player), list, onPick, onBack);
|
||||
}
|
||||
|
||||
private <T> void open(Player player, String title, List<SWListInv.SWListEntry<T>> list, Consumer<T> onPick, Runnable onBack) {
|
||||
SWListInv<T> inventory = new SWListInv<>(player, title, false, list, (clickType, value) -> {
|
||||
if (value != null) onPick.accept(value);
|
||||
});
|
||||
inventory.setItem(BACK, new SWItem(Material.ARROW, msg("PARTICLE_BACK", player), clickType -> onBack.run()));
|
||||
inventory.open();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* 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.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.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
|
||||
@UtilityClass
|
||||
public class SlotEditor {
|
||||
|
||||
private final int BACK = 45;
|
||||
private final int ADD_ENTRY = 47;
|
||||
private final int RENAME = 48;
|
||||
private final int ICON = 49;
|
||||
private final int COST = 50;
|
||||
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) {
|
||||
ParticleMenu.open(player);
|
||||
return;
|
||||
}
|
||||
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;
|
||||
|
||||
SWInventory inventory = new SWInventory(player, 54, msg(editable ? "PARTICLE_SLOT_EDITOR" : "PARTICLE_SLOT_VIEWER", player, slot.getName()));
|
||||
List<ParticleEntry> entries = slot.getEntries();
|
||||
for (int i = 0; i < entries.size(); i++) {
|
||||
ParticleEntry entry = entries.get(i);
|
||||
List<String> lore = new ArrayList<>(entrySummary(entry, context, player));
|
||||
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(BACK, new SWItem(Material.ARROW, msg("PARTICLE_BACK", player), clickType -> ParticleMenu.open(player)));
|
||||
if (!editable) {
|
||||
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.open();
|
||||
return;
|
||||
}
|
||||
|
||||
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 -> {
|
||||
entries.add(new ParticleEntry(design));
|
||||
if (!fits(user, slot)) {
|
||||
entries.remove(entries.size() - 1);
|
||||
LobbySystem.getMessage().send("PARTICLE_SLOT_OVER_BUDGET", player);
|
||||
open(player, index);
|
||||
return;
|
||||
}
|
||||
save(player, index, slot);
|
||||
EntryEditor.open(player, index, entries.size() - 1);
|
||||
}, () -> open(player, index));
|
||||
}));
|
||||
inventory.setItem(RENAME, new SWItem(Material.NAME_TAG, msg("PARTICLE_SLOT_RENAME", player), clickType -> {
|
||||
SWAnvilInv anvil = new SWAnvilInv(player, msg("PARTICLE_SLOT_RENAME_TITLE", player), slot.getName());
|
||||
anvil.setItem(Material.NAME_TAG);
|
||||
anvil.setCallback(name -> {
|
||||
String cleaned = name.replace("§", "").trim();
|
||||
slot.setName(cleaned.isEmpty() ? ParticleSlots.defaultName(index) : cleaned);
|
||||
save(player, index, slot);
|
||||
Bukkit.getScheduler().runTask(LobbySystem.getInstance(), () -> open(player, index));
|
||||
});
|
||||
anvil.open();
|
||||
}));
|
||||
inventory.setItem(ICON, new SWItem(slot.getDisplayIcon(), msg("PARTICLE_SLOT_ICON", player), clickType -> Pickers.icon(player, icon -> {
|
||||
slot.setIcon(icon);
|
||||
save(player, index, slot);
|
||||
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 -> {
|
||||
if (!clickType.isShiftClick()) return;
|
||||
if (lobbyPlayer.getActiveSlot() == index) {
|
||||
lobbyPlayer.deselect();
|
||||
}
|
||||
lobbyPlayer.setSlot(index, null);
|
||||
ParticleMenu.open(player);
|
||||
}));
|
||||
inventory.open();
|
||||
}
|
||||
|
||||
SWItem costItem(Player player, SteamwarUser user, ParticleSlot slot) {
|
||||
return new SWItem(Material.PAPER, msg("PARTICLE_SLOT_COST", player, number(slot.cost()), number(ParticleSlots.maxCost(user))), clickType -> {
|
||||
});
|
||||
}
|
||||
|
||||
boolean fits(SteamwarUser user, ParticleSlot slot) {
|
||||
return slot.cost() <= ParticleSlots.maxCost(user);
|
||||
}
|
||||
|
||||
void save(Player player, int index, ParticleSlot slot) {
|
||||
LobbyPlayer.getLobbyPlayer(player).setSlot(index, slot);
|
||||
}
|
||||
|
||||
Material icon(ParticleEntry entry) {
|
||||
return entry.getDesign().getIcon();
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,10 @@ public class ParticleSlot implements ParticleSet {
|
||||
return entries.get(0).getValues().get(EntryParameters.PARTICLE).getIcon();
|
||||
}
|
||||
|
||||
public double cost() {
|
||||
return entries.stream().mapToDouble(ParticleEntry::cost).sum();
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name.length() > MAX_NAME_LENGTH ? name.substring(0, MAX_NAME_LENGTH) : name;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,9 @@ package de.steamwar.lobby.particle.slot;
|
||||
import com.google.gson.JsonParser;
|
||||
import de.steamwar.lobby.LobbySystem;
|
||||
import de.steamwar.lobby.particle.ParticleRequirement;
|
||||
import de.steamwar.sql.SteamwarUser;
|
||||
import de.steamwar.sql.UserConfig;
|
||||
import de.steamwar.sql.UserPerm;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
import java.util.logging.Level;
|
||||
@@ -31,8 +33,14 @@ import java.util.logging.Level;
|
||||
public class ParticleSlots {
|
||||
|
||||
public final int COUNT = 7;
|
||||
public final int MAX_ENTRIES = 10;
|
||||
private final String KEY_PREFIX = "lobby-particle-slot-";
|
||||
|
||||
// Particle positions per tick a slot may spawn
|
||||
public double maxCost(SteamwarUser user) {
|
||||
return user.hasPerm(UserPerm.TEAM) ? 80 : 40;
|
||||
}
|
||||
|
||||
// Slots 1-3 for everyone, 4-5 for team members, 6-7 for the server team
|
||||
public ParticleRequirement requirement(int index) {
|
||||
if (index < 3) return ParticleRequirement.NO_REQUIRMENT;
|
||||
|
||||
Reference in New Issue
Block a user