/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
package de.steamwar.lobby.particle.gui;
import de.steamwar.inventory.SWAnvilInv;
import de.steamwar.inventory.SWInventory;
import de.steamwar.inventory.SWItem;
import de.steamwar.lobby.LobbySystem;
import lombok.experimental.UtilityClass;
import org.bukkit.Bukkit;
import org.bukkit.DyeColor;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import java.awt.Color;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import java.util.function.Supplier;
import static de.steamwar.lobby.particle.gui.ParticleGuiText.*;
// Screens for settings that need more than a click: numbers and colour lists
@UtilityClass
class ValueEditors {
private final int SIZE = 27;
private final int BACK = BottomBar.position(SIZE, BottomBar.BACK);
private final int ADD = BottomBar.position(SIZE, BottomBar.ADD);
private final int RESET = BottomBar.position(SIZE, BottomBar.RESET);
// Buttons step the value down or up; clicking the value itself lets the player type one
void editNumber(Player player, String title, Supplier current, double step, double min, double max, double defaultValue, boolean integer,
Predicate apply, Runnable back) {
double value = current.get();
Runnable reopen = () -> editNumber(player, title, current, step, min, max, defaultValue, integer, apply, back);
SWInventory inventory = new SWInventory(player, SIZE, "§0" + strip(title));
inventory.setItem(10, stepItem(player, Material.RED_CONCRETE, -10 * step, step, value, min, max, integer, apply, reopen));
inventory.setItem(11, stepItem(player, Material.RED_STAINED_GLASS_PANE, -step, step, value, min, max, integer, apply, reopen));
inventory.setItem(15, stepItem(player, Material.LIME_STAINED_GLASS_PANE, step, step, value, min, max, integer, apply, reopen));
inventory.setItem(16, stepItem(player, Material.LIME_CONCRETE, 10 * step, step, value, min, max, integer, apply, reopen));
List lore = List.of(msg("PARTICLE_NUMBER_RANGE", player, number(min), number(max)), "", msg("PARTICLE_NUMBER_TYPE", player));
inventory.setItem(13, new SWItem(Material.PAPER, "§e" + number(value), lore, false, clickType -> {
SWAnvilInv anvil = new SWAnvilInv(player, strip(title), number(value));
anvil.setItem(Material.PAPER);
anvil.setCallback(input -> Bukkit.getScheduler().runTask(LobbySystem.getInstance(), () -> {
try {
double typed = Double.parseDouble(input.trim().replace(',', '.'));
apply.test(integer ? Math.round(typed) : typed);
} catch (NumberFormatException e) {
// Keep the old value
}
reopen.run();
}));
anvil.open();
}));
inventory.setItem(BACK, new SWItem(Material.ARROW, msg("PARTICLE_BACK", player), clickType -> back.run()));
inventory.setItem(RESET, new SWItem(Material.STRUCTURE_VOID, msg("PARTICLE_VALUE_RESET", player, number(defaultValue)), clickType -> {
apply.test(defaultValue);
reopen.run();
}));
BottomBar.fillUnused(inventory, SIZE, false);
SlotEditor.fill(inventory, 0, SIZE);
inventory.open();
}
private SWItem stepItem(Player player, Material material, double delta, double step, double value, double min, double max, boolean integer, Predicate apply, Runnable reopen) {
String label = (delta < 0 ? "§c-" : "§a+") + number(Math.abs(delta));
// Snap to the step grid and drop floating point noise, so repeated steps do not drift
double unclamped = integer ? Math.round(value + delta) : Math.round(Math.round((value + delta) / step) * step * 1e6) / 1e6;
double next = Math.max(min, Math.min(max, unclamped));
String limit = next == max && unclamped >= max ? "PARTICLE_NUMBER_AT_MAX" : next == min && unclamped <= min ? "PARTICLE_NUMBER_AT_MIN" : "PARTICLE_NUMBER_RESULT";
return new SWItem(material, label, List.of(msg(limit, player, number(next))), false, clickType -> {
apply.test(next);
reopen.run();
});
}
// Shows the colours in order; a click removes one, the buttons below add one or switch to random colours
void editColors(Player player, Supplier> current, int maxColors, Predicate> apply, Runnable back) {
List colors = current.get();
Runnable reopen = () -> editColors(player, current, maxColors, apply, back);
SWInventory inventory = new SWInventory(player, SIZE, msg("PARTICLE_COLOR_EDITOR", player));
for (int i = 0; i < colors.size() && i < 9; i++) {
int position = i;
Color color = colors.get(i);
inventory.setItem(i, new SWItem(dye(color), net.md_5.bungee.api.ChatColor.of(color) + "█ #" + String.format("%06X", color.getRGB() & 0xFFFFFF),
List.of(msg("PARTICLE_COLOR_REMOVE", player)), false, clickType -> {
List next = new ArrayList<>(colors);
next.remove(position);
apply.test(next);
reopen.run();
}));
}
if (colors.isEmpty()) {
inventory.setItem(4, new SWItem(Material.WHITE_DYE, msg("PARTICLE_VALUE_RANDOM", player), clickType -> {
}));
}
inventory.setItem(BACK, new SWItem(Material.ARROW, msg("PARTICLE_BACK", player), clickType -> back.run()));
if (colors.size() < maxColors) {
inventory.setItem(ADD, new SWItem(Material.NETHER_STAR, msg("PARTICLE_COLOR_ADD", player), clickType -> Pickers.color(player, color -> {
List next = new ArrayList<>(colors);
next.add(color);
apply.test(next);
reopen.run();
}, reopen)));
// Same position as typing a value on the number screen
inventory.setItem(13, new SWItem(Material.NAME_TAG, msg("PARTICLE_COLOR_HEX", player), List.of(msg("PARTICLE_COLOR_HEX_EXAMPLE", player)), false, clickType -> {
SWAnvilInv anvil = new SWAnvilInv(player, msg("PARTICLE_COLOR_HEX_TITLE", player), "#");
anvil.setItem(Material.NAME_TAG);
anvil.setCallback(input -> Bukkit.getScheduler().runTask(LobbySystem.getInstance(), () -> {
Color color = parseHex(input);
if (color == null) {
LobbySystem.getMessage().send("PARTICLE_COLOR_HEX_INVALID", player);
} else {
List next = new ArrayList<>(colors);
next.add(color);
apply.test(next);
}
reopen.run();
}));
anvil.open();
}));
}
inventory.setItem(RESET, new SWItem(Material.WHITE_DYE, msg("PARTICLE_COLOR_RANDOM", player), clickType -> {
apply.test(List.of());
reopen.run();
}));
BottomBar.fillUnused(inventory, SIZE, false);
SlotEditor.fill(inventory, 0, SIZE);
inventory.open();
}
// Accepts #RRGGBB and #RGB, with or without the #; null for anything else
Color parseHex(String input) {
String hex = input.trim();
if (hex.startsWith("#")) hex = hex.substring(1);
if (hex.length() == 3) {
hex = "" + hex.charAt(0) + hex.charAt(0) + hex.charAt(1) + hex.charAt(1) + hex.charAt(2) + hex.charAt(2);
}
if (!hex.matches("[0-9a-fA-F]{6}")) {
return null;
}
return new Color(Integer.parseInt(hex, 16));
}
private Material dye(Color color) {
for (DyeColor dye : DyeColor.values()) {
if ((dye.getColor().asRGB() & 0xFFFFFF) == (color.getRGB() & 0xFFFFFF)) {
return Material.valueOf(dye.name() + "_DYE");
}
}
return Material.WHITE_DYE;
}
private String strip(String text) {
return text.replaceAll("§.", "");
}
}