Add configurable particle slot model

Introduce parameters, particle types, designs and entries that build the
existing particle elements, store each of the 7 particle slots in its own
UserConfig key, and let LobbyPlayer select a slot next to the old enum
particles. Slots 4-5 need a team, 6-7 the server team.
This commit is contained in:
2026-10-26 09:47:49 +01:00
parent ef041272bb
commit 1980375e73
22 changed files with 1396 additions and 5 deletions
@@ -19,6 +19,9 @@
package de.steamwar.lobby.particle;
import de.steamwar.lobby.particle.slot.ParticleEntry;
import de.steamwar.lobby.particle.slot.ParticleSet;
import java.util.ArrayList;
import java.util.List;
@@ -35,6 +38,17 @@ public class CompiledSet {
return compiledSet;
}
// Entries whose design is not unlocked are skipped, not removed
public static CompiledSet of(ParticleSet particleSet, UnlockContext context) {
CompiledSet compiledSet = new CompiledSet();
for (ParticleEntry entry : particleSet.getEntries()) {
if (entry.getDesign().getRequirement().test(context)) {
compiledSet.add(entry.build());
}
}
return compiledSet;
}
public void add(ParticleElement particleElement) {
if (particleElement.tickType() == ParticleTickType.ALWAYS) {
always.add(particleElement);
@@ -34,6 +34,7 @@ import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerQuitEvent;
@@ -94,6 +95,12 @@ public class ParticleListener implements Listener {
}
}
// Ranks may have changed since the last session, which can lock or unlock the active slot
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
LobbyPlayer.getLobbyPlayer(event.getPlayer()).recompile();
}
@EventHandler(ignoreCancelled = true)
public void onPlayerQuit(PlayerQuitEvent event) {
movingPlayers.remove(event.getPlayer());
@@ -41,6 +41,10 @@ public interface ParticleRequirement {
boolean test(SteamwarUser user, Set<Integer> eventTeilname, String eggHuntConfig);
default boolean test(UnlockContext context) {
return test(context.getUser(), context.getEvents(), context.getEggHuntConfig());
}
ParticleRequirement SERVER_TEAM = new ParticleRequirement() {
@Override
public String getRequirementName(Player player) {
@@ -51,6 +55,11 @@ public interface ParticleRequirement {
public boolean test(SteamwarUser user, Set<Integer> eventTeilname, String eggHuntConfig) {
return user.hasPerm(UserPerm.TEAM);
}
@Override
public boolean test(UnlockContext context) {
return context.getUser().hasPerm(UserPerm.TEAM);
}
};
ParticleRequirement NO_REQUIRMENT = new ParticleRequirement() {
@@ -63,6 +72,29 @@ public interface ParticleRequirement {
public boolean test(SteamwarUser user, Set<Integer> eventTeilname, String eggHuntConfig) {
return true;
}
@Override
public boolean test(UnlockContext context) {
return true;
}
};
// Unlike HAS_TEAM this does not also pass for the server team
ParticleRequirement TEAM_MEMBER = new ParticleRequirement() {
@Override
public String getRequirementName(Player player) {
return LobbySystem.getMessage().parse("PARTICLE_UNLOCKED_BY_TEAM", player);
}
@Override
public boolean test(SteamwarUser user, Set<Integer> eventTeilname, String eggHuntConfig) {
return user.getTeam() != 0;
}
@Override
public boolean test(UnlockContext context) {
return context.getUser().getTeam() != 0;
}
};
ParticleRequirement HAS_TEAM = new ParticleRequirement() {
@Override
@@ -0,0 +1,59 @@
/*
* 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;
import de.steamwar.lobby.special.easter.EggHunt;
import de.steamwar.sql.Event;
import de.steamwar.sql.SteamwarUser;
import de.steamwar.sql.TeamTeilnahme;
import de.steamwar.sql.UserConfig;
import lombok.Getter;
import java.util.Set;
import java.util.stream.Collectors;
// Everything requirements check against, loaded only when a requirement needs it
public class UnlockContext {
@Getter
private final SteamwarUser user;
private Set<Integer> events;
private String eggHuntConfig;
private boolean eggHuntConfigLoaded;
public UnlockContext(SteamwarUser user) {
this.user = user;
}
public Set<Integer> getEvents() {
if (events == null) {
events = user.getTeam() == 0 ? Set.of() : TeamTeilnahme.getEvents(user.getTeam()).stream().map(Event::getEventID).collect(Collectors.toSet());
}
return events;
}
public String getEggHuntConfig() {
if (!eggHuntConfigLoaded) {
eggHuntConfig = UserConfig.getConfig(user.getId(), EggHunt.EGG_HUNT_CONFIG_KEY);
eggHuntConfigLoaded = true;
}
return eggHuntConfig;
}
}
@@ -0,0 +1,40 @@
/*
* 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.design;
import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
public class BooleanParameter extends Parameter<Boolean> {
public BooleanParameter(String key, String nameKey, boolean defaultValue) {
super(key, nameKey, defaultValue);
}
@Override
public JsonElement toJson(Boolean value) {
return new JsonPrimitive(value);
}
@Override
protected Boolean read(JsonElement json) {
return json.getAsBoolean();
}
}
@@ -0,0 +1,86 @@
/*
* 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.design;
import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
import lombok.Getter;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
@Getter
public class ChoiceParameter<E> extends Parameter<E> {
// Internal choices are usable by presets but never offered in the builder or accepted from storage
public record Choice<E>(String id, E value, boolean internal) {
}
private final List<Choice<E>> choices;
public ChoiceParameter(String key, String nameKey, E defaultValue, List<Choice<E>> choices) {
super(key, nameKey, defaultValue);
this.choices = choices;
}
public static <E extends Enum<E>> ChoiceParameter<E> ofEnum(String key, String nameKey, E defaultValue, Predicate<E> internal) {
List<Choice<E>> choices = new ArrayList<>();
for (E value : defaultValue.getDeclaringClass().getEnumConstants()) {
choices.add(new Choice<>(value.name(), value, internal.test(value)));
}
return new ChoiceParameter<>(key, nameKey, defaultValue, choices);
}
public static <E extends Enum<E>> ChoiceParameter<E> ofEnum(String key, String nameKey, E defaultValue) {
return ofEnum(key, nameKey, defaultValue, value -> false);
}
public List<E> getBuildableValues() {
return choices.stream().filter(choice -> !choice.internal()).map(Choice::value).toList();
}
@Override
public JsonElement toJson(E value) {
return new JsonPrimitive(choiceOf(value).id());
}
@Override
protected E read(JsonElement json) {
String id = json.getAsString();
return choices.stream()
.filter(choice -> !choice.internal() && choice.id().equals(id))
.map(Choice::value)
.findFirst()
.orElse(null);
}
@Override
public E validate(E value) {
return choices.stream().anyMatch(choice -> choice.value() == value) ? value : getDefaultValue();
}
private Choice<E> choiceOf(E value) {
return choices.stream()
.filter(choice -> choice.value() == value)
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Unknown choice for " + getKey()));
}
}
@@ -0,0 +1,29 @@
/*
* 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.design;
public enum DesignVisibility {
// Offered in the builder to everyone
BASIC,
// Offered in the builder, usable once its requirement is met
REWARD,
// Only used by presets, never offered in the builder or accepted from storage
INTERNAL,
}
@@ -0,0 +1,118 @@
/*
* 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.design;
import de.steamwar.lobby.particle.ParticleElement;
import de.steamwar.lobby.particle.WingDesign;
import de.steamwar.lobby.particle.elements.Circle;
import de.steamwar.lobby.particle.elements.Cloud;
import de.steamwar.lobby.particle.elements.Wing;
import de.steamwar.lobby.particle.elements.custom.YOffset;
import lombok.experimental.UtilityClass;
import org.bukkit.Material;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@UtilityClass
public class Designs {
private final Map<String, ParticleDesign> DESIGNS = new LinkedHashMap<>();
public final ParticleDesign SIMPLE = register(new ParticleDesign("simple", Material.BLAZE_POWDER, "PARTICLE_DESIGN_SIMPLE") {
@Override
public ParticleElement build(ParameterValues values, ParticleElement particle) {
return particle;
}
});
public final DoubleParameter CIRCLE_RADIUS = new DoubleParameter("radius", "PARTICLE_PARAM_RADIUS", 1, 0.2, 3, 0.1);
public final DoubleParameter CIRCLE_SPEED = new DoubleParameter("circleSpeed", "PARTICLE_PARAM_CIRCLE_SPEED", 1, -20, 20, 0.5);
public final DoubleParameter CIRCLE_PHASE = new DoubleParameter("phase", "PARTICLE_PARAM_PHASE", 0, 0, 345, 15);
public final DoubleParameter CIRCLE_RISE = new DoubleParameter("rise", "PARTICLE_PARAM_RISE", 0, 0, 3, 0.1);
public final DoubleParameter CIRCLE_RISE_SPEED = new DoubleParameter("riseSpeed", "PARTICLE_PARAM_RISE_SPEED", 20, 0, 100, 5);
public final BooleanParameter CIRCLE_BOUNCE = new BooleanParameter("bounce", "PARTICLE_PARAM_BOUNCE", true);
public final BooleanParameter CIRCLE_INVERTED = new BooleanParameter("inverted", "PARTICLE_PARAM_INVERTED", false);
public final ParticleDesign CIRCLE = register(new ParticleDesign("circle", Material.MAGMA_CREAM, "PARTICLE_DESIGN_CIRCLE",
CIRCLE_RADIUS, CIRCLE_SPEED, CIRCLE_PHASE, CIRCLE_RISE, CIRCLE_RISE_SPEED, CIRCLE_BOUNCE, CIRCLE_INVERTED) {
@Override
public ParticleElement build(ParameterValues values, ParticleElement particle) {
double rise = values.get(CIRCLE_RISE);
if (rise > 0) {
particle = new YOffset(particle, values.get(CIRCLE_RISE_SPEED), 0, rise, values.get(CIRCLE_INVERTED), values.get(CIRCLE_BOUNCE));
}
return new Circle(particle, values.get(CIRCLE_RADIUS), values.get(CIRCLE_SPEED), values.get(CIRCLE_PHASE));
}
});
public final ChoiceParameter<WingDesign> WING_IMAGE = new ChoiceParameter<>("image", "PARTICLE_PARAM_WING_IMAGE", WingDesign.SIMPLE, List.of(
new ChoiceParameter.Choice<>("simple", WingDesign.SIMPLE, false),
new ChoiceParameter.Choice<>("complex", WingDesign.COMPLEX, false),
new ChoiceParameter.Choice<>("sword", WingDesign.SWORD, false),
new ChoiceParameter.Choice<>("sw", WingDesign.SW, true),
new ChoiceParameter.Choice<>("wgs", WingDesign.WGS, true),
new ChoiceParameter.Choice<>("sword_crossed", WingDesign.SWORD_CROSSED, true),
new ChoiceParameter.Choice<>("mwgl", WingDesign.MWGL, true),
new ChoiceParameter.Choice<>("eclipse", WingDesign.ECLIPSE, true),
new ChoiceParameter.Choice<>("pl", WingDesign.PL, true),
new ChoiceParameter.Choice<>("plompa_easter", WingDesign.PlompaEasterWings, true),
new ChoiceParameter.Choice<>("ely_e", WingDesign.ELY_E, true),
new ChoiceParameter.Choice<>("ely_l", WingDesign.ELY_L, true),
new ChoiceParameter.Choice<>("ely_y", WingDesign.ELY_Y, true),
new ChoiceParameter.Choice<>("ev", WingDesign.EV, true)
));
public final DoubleParameter WING_SIZE = new DoubleParameter("size", "PARTICLE_PARAM_WING_SIZE", 0.15, 0.05, 0.3, 0.01);
public final ParticleDesign WING = register(new ParticleDesign("wing", Material.ELYTRA, "PARTICLE_DESIGN_WING", WING_IMAGE, WING_SIZE) {
@Override
public ParticleElement build(ParameterValues values, ParticleElement particle) {
return new Wing(particle, values.get(WING_SIZE), values.get(WING_IMAGE));
}
@Override
public int points(ParameterValues values) {
return values.get(WING_IMAGE).getVectors().length;
}
});
public final ParticleDesign CLOUD = register(new ParticleDesign("cloud", Material.WHITE_WOOL, "PARTICLE_DESIGN_CLOUD") {
@Override
public ParticleElement build(ParameterValues values, ParticleElement particle) {
return new Cloud(particle);
}
});
private ParticleDesign register(ParticleDesign design) {
if (DESIGNS.put(design.getId(), design) != null) {
throw new IllegalStateException("Duplicate particle design " + design.getId());
}
return design;
}
public ParticleDesign byId(String id) {
return DESIGNS.get(id);
}
public List<ParticleDesign> all() {
return List.copyOf(DESIGNS.values());
}
}
@@ -0,0 +1,57 @@
/*
* 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.design;
import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
import lombok.Getter;
@Getter
public class DoubleParameter extends Parameter<Double> {
private final double min;
private final double max;
private final double step;
public DoubleParameter(String key, String nameKey, double defaultValue, double min, double max, double step) {
super(key, nameKey, defaultValue);
this.min = min;
this.max = max;
this.step = step;
}
@Override
public JsonElement toJson(Double value) {
return new JsonPrimitive(value);
}
@Override
protected Double read(JsonElement json) {
return json.getAsDouble();
}
@Override
public Double validate(Double value) {
if (value.isNaN()) {
return getDefaultValue();
}
return Math.max(min, Math.min(max, value));
}
}
@@ -0,0 +1,151 @@
/*
* 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.design;
import de.steamwar.lobby.particle.ParticleElement;
import de.steamwar.lobby.particle.elements.*;
import de.steamwar.lobby.particle.elements.custom.NonMoving;
import lombok.experimental.UtilityClass;
import java.util.List;
import java.util.function.UnaryOperator;
// Parameters every entry has, independent of its design
@UtilityClass
public class EntryParameters {
public enum TickMode {
MOVE,
ALWAYS,
}
public enum Movement {
ANY(element -> element),
MOVING(element -> data -> {
if (data.isMoving()) element.tick(data);
}),
STANDING_STILL(NonMoving::new),
;
private final UnaryOperator<ParticleElement> wrapper;
Movement(UnaryOperator<ParticleElement> wrapper) {
this.wrapper = wrapper;
}
public ParticleElement wrap(ParticleElement element) {
return wrapper.apply(element);
}
}
public enum Gliding {
ANY(element -> element),
GLIDING(Flying::new),
NOT_GLIDING(NonFlying::new),
;
private final UnaryOperator<ParticleElement> wrapper;
Gliding(UnaryOperator<ParticleElement> wrapper) {
this.wrapper = wrapper;
}
public ParticleElement wrap(ParticleElement element) {
return wrapper.apply(element);
}
}
public enum Ground {
ANY(element -> element),
ON_GROUND(Floor::new),
IN_AIR(NonFloor::new),
;
private final UnaryOperator<ParticleElement> wrapper;
Ground(UnaryOperator<ParticleElement> wrapper) {
this.wrapper = wrapper;
}
public ParticleElement wrap(ParticleElement element) {
return wrapper.apply(element);
}
}
public enum Sneak {
ANY(element -> element),
SNEAKING(Sneaking::new),
NOT_SNEAKING(element -> data -> {
if (!data.getPlayer().isSneaking()) element.tick(data);
}),
;
private final UnaryOperator<ParticleElement> wrapper;
Sneak(UnaryOperator<ParticleElement> wrapper) {
this.wrapper = wrapper;
}
public ParticleElement wrap(ParticleElement element) {
return wrapper.apply(element);
}
}
public enum Visibility {
EVERYONE(element -> element),
ONLY_SELF(OnlySelf::new),
ONLY_OTHERS(OnlyOthers::new),
;
private final UnaryOperator<ParticleElement> wrapper;
Visibility(UnaryOperator<ParticleElement> wrapper) {
this.wrapper = wrapper;
}
public ParticleElement wrap(ParticleElement element) {
return wrapper.apply(element);
}
}
public final ChoiceParameter<ParticleType> PARTICLE = ChoiceParameter.ofEnum("particle", "PARTICLE_PARAM_PARTICLE", ParticleType.FLAME, type -> !type.isBuildable());
public final IntParameter COUNT = new IntParameter("count", "PARTICLE_PARAM_COUNT", 5, 1, 20);
public final DoubleParameter SPREAD_X = new DoubleParameter("spreadX", "PARTICLE_PARAM_SPREAD_X", 0, 0, 2, 0.05);
public final DoubleParameter SPREAD_Y = new DoubleParameter("spreadY", "PARTICLE_PARAM_SPREAD_Y", 0, 0, 2, 0.05);
public final DoubleParameter SPREAD_Z = new DoubleParameter("spreadZ", "PARTICLE_PARAM_SPREAD_Z", 0, 0, 2, 0.05);
public final DoubleParameter SPEED = new DoubleParameter("speed", "PARTICLE_PARAM_SPEED", 0.01, 0, 1, 0.01);
public final GradientParameter COLORS = new GradientParameter("colors", "PARTICLE_PARAM_COLORS", 8);
public final DoubleParameter OFFSET_X = new DoubleParameter("offsetX", "PARTICLE_PARAM_OFFSET_X", 0, -3, 3, 0.1);
public final DoubleParameter OFFSET_Y = new DoubleParameter("offsetY", "PARTICLE_PARAM_OFFSET_Y", 0, -3, 3, 0.1);
public final DoubleParameter OFFSET_Z = new DoubleParameter("offsetZ", "PARTICLE_PARAM_OFFSET_Z", 0, -3, 3, 0.1);
public final IntParameter INTERVAL = new IntParameter("interval", "PARTICLE_PARAM_INTERVAL", 1, 1, 100);
public final ChoiceParameter<TickMode> TICK_MODE = ChoiceParameter.ofEnum("tickMode", "PARTICLE_PARAM_TICK_MODE", TickMode.MOVE);
public final ChoiceParameter<Movement> MOVEMENT = ChoiceParameter.ofEnum("movement", "PARTICLE_PARAM_MOVEMENT", Movement.ANY);
public final ChoiceParameter<Gliding> GLIDING = ChoiceParameter.ofEnum("gliding", "PARTICLE_PARAM_GLIDING", Gliding.ANY);
public final ChoiceParameter<Ground> GROUND = ChoiceParameter.ofEnum("ground", "PARTICLE_PARAM_GROUND", Ground.ANY);
public final ChoiceParameter<Sneak> SNEAK = ChoiceParameter.ofEnum("sneak", "PARTICLE_PARAM_SNEAK", Sneak.ANY);
public final ChoiceParameter<Visibility> VISIBILITY = ChoiceParameter.ofEnum("visibility", "PARTICLE_PARAM_VISIBILITY", Visibility.EVERYONE);
public final List<Parameter<?>> ALL = List.of(
PARTICLE, COUNT, SPREAD_X, SPREAD_Y, SPREAD_Z, SPEED, COLORS,
OFFSET_X, OFFSET_Y, OFFSET_Z, INTERVAL, TICK_MODE,
MOVEMENT, GLIDING, GROUND, SNEAK, VISIBILITY
);
}
@@ -0,0 +1,61 @@
/*
* 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.design;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import lombok.Getter;
import java.awt.Color;
import java.util.ArrayList;
import java.util.List;
// An empty gradient means a random colour per spawn
@Getter
public class GradientParameter extends Parameter<List<Color>> {
private final int maxColors;
public GradientParameter(String key, String nameKey, int maxColors) {
super(key, nameKey, List.of());
this.maxColors = maxColors;
}
@Override
public JsonElement toJson(List<Color> value) {
JsonArray array = new JsonArray();
value.forEach(color -> array.add(color.getRGB() & 0xFFFFFF));
return array;
}
@Override
protected List<Color> read(JsonElement json) {
List<Color> colors = new ArrayList<>();
for (JsonElement element : json.getAsJsonArray()) {
colors.add(new Color(element.getAsInt() & 0xFFFFFF));
}
return colors;
}
@Override
public List<Color> validate(List<Color> value) {
return List.copyOf(value.size() > maxColors ? value.subList(0, maxColors) : value);
}
}
@@ -0,0 +1,52 @@
/*
* 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.design;
import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
import lombok.Getter;
@Getter
public class IntParameter extends Parameter<Integer> {
private final int min;
private final int max;
public IntParameter(String key, String nameKey, int defaultValue, int min, int max) {
super(key, nameKey, defaultValue);
this.min = min;
this.max = max;
}
@Override
public JsonElement toJson(Integer value) {
return new JsonPrimitive(value);
}
@Override
protected Integer read(JsonElement json) {
return json.getAsInt();
}
@Override
public Integer validate(Integer value) {
return Math.max(min, Math.min(max, value));
}
}
@@ -0,0 +1,58 @@
/*
* 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.design;
import com.google.gson.JsonElement;
import lombok.Getter;
@Getter
public abstract class Parameter<T> {
private final String key;
private final String nameKey;
private final T defaultValue;
protected Parameter(String key, String nameKey, T defaultValue) {
this.key = key;
this.nameKey = nameKey;
this.defaultValue = defaultValue;
}
public abstract JsonElement toJson(T value);
// Returns null when the stored value cannot be used
protected abstract T read(JsonElement json);
public T fromJson(JsonElement json) {
if (json == null || json.isJsonNull()) {
return defaultValue;
}
try {
T value = read(json);
return value == null ? defaultValue : validate(value);
} catch (RuntimeException e) {
return defaultValue;
}
}
public T validate(T value) {
return value;
}
}
@@ -0,0 +1,64 @@
/*
* 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.design;
import com.google.gson.JsonObject;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ParameterValues {
private final Map<String, Object> values = new HashMap<>();
@SuppressWarnings("unchecked")
public <T> T get(Parameter<T> parameter) {
Object value = values.get(parameter.getKey());
return value == null ? parameter.getDefaultValue() : (T) value;
}
public <T> ParameterValues set(Parameter<T> parameter, T value) {
values.put(parameter.getKey(), parameter.validate(value));
return this;
}
public ParameterValues copy() {
ParameterValues copy = new ParameterValues();
copy.values.putAll(values);
return copy;
}
public JsonObject toJson(List<Parameter<?>> parameters) {
JsonObject json = new JsonObject();
parameters.forEach(parameter -> write(json, parameter));
return json;
}
public static ParameterValues fromJson(JsonObject json, List<Parameter<?>> parameters) {
ParameterValues values = new ParameterValues();
parameters.forEach(parameter -> values.values.put(parameter.getKey(), parameter.fromJson(json.get(parameter.getKey()))));
return values;
}
private <T> void write(JsonObject json, Parameter<T> parameter) {
json.add(parameter.getKey(), parameter.toJson(get(parameter)));
}
}
@@ -0,0 +1,59 @@
/*
* 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.design;
import de.steamwar.lobby.particle.ParticleElement;
import de.steamwar.lobby.particle.ParticleRequirement;
import lombok.Getter;
import org.bukkit.Material;
import java.util.List;
@Getter
public abstract class ParticleDesign {
private final String id;
private final Material icon;
private final String nameKey;
private final DesignVisibility visibility;
private final ParticleRequirement requirement;
private final List<Parameter<?>> parameters;
protected ParticleDesign(String id, Material icon, String nameKey, DesignVisibility visibility, ParticleRequirement requirement, Parameter<?>... parameters) {
this.id = id;
this.icon = icon;
this.nameKey = nameKey;
this.visibility = visibility;
this.requirement = requirement;
this.parameters = List.of(parameters);
}
protected ParticleDesign(String id, Material icon, String nameKey, Parameter<?>... parameters) {
this(id, icon, nameKey, DesignVisibility.BASIC, ParticleRequirement.NO_REQUIRMENT, parameters);
}
// Wraps the entry's particle into this design's shape
public abstract ParticleElement build(ParameterValues values, ParticleElement particle);
// Number of particle positions this design spawns per tick
public int points(ParameterValues values) {
return 1;
}
}
@@ -0,0 +1,84 @@
/*
* 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.design;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.bukkit.Material;
import org.bukkit.Particle;
// Particles that need extra data (BLOCK, ITEM, VIBRATION, DUST_COLOR_TRANSITION) or disturb other players are left out
@AllArgsConstructor
@Getter
public enum ParticleType {
DUST(Material.REDSTONE, Particle.DUST, true),
FLAME(Material.FLINT_AND_STEEL, Particle.FLAME, true),
SOUL(Material.ZOMBIE_HEAD, Particle.SOUL, true),
SMOKE(Material.COBWEB, Particle.SMOKE, true),
LARGE_SMOKE(Material.GRAY_WOOL, Particle.LARGE_SMOKE, true),
CAMPFIRE_COSY_SMOKE(Material.CAMPFIRE, Particle.CAMPFIRE_COSY_SMOKE, true),
CLOUD(Material.WHITE_WOOL, Particle.CLOUD, true),
SNEEZE(Material.SLIME_BLOCK, Particle.SNEEZE, true),
DRIPPING_LAVA(Material.LAVA_BUCKET, Particle.DRIPPING_LAVA, true),
DRIPPING_WATER(Material.WATER_BUCKET, Particle.DRIPPING_WATER, true),
DRIPPING_HONEY(Material.HONEY_BOTTLE, Particle.DRIPPING_HONEY, true),
FALLING_HONEY(Material.HONEY_BLOCK, Particle.FALLING_HONEY, true),
FALLING_NECTAR(Material.HONEYCOMB, Particle.FALLING_NECTAR, true),
FALLING_SPORE_BLOSSOM(Material.SPORE_BLOSSOM, Particle.FALLING_SPORE_BLOSSOM, true),
FALLING_OBSIDIAN_TEAR(Material.CRYING_OBSIDIAN, Particle.FALLING_OBSIDIAN_TEAR, true),
RAIN(Material.CYAN_DYE, Particle.RAIN, true),
UNDERWATER(Material.PRISMARINE_SHARD, Particle.UNDERWATER, true),
BUBBLE_POP(Material.TUBE_CORAL, Particle.BUBBLE_POP, true),
DOLPHIN(Material.BLUE_DYE, Particle.DOLPHIN, true),
NAUTILUS(Material.NAUTILUS_SHELL, Particle.NAUTILUS, true),
SQUID_INK(Material.INK_SAC, Particle.SQUID_INK, true),
GLOW_SQUID_INK(Material.GLOW_INK_SAC, Particle.GLOW_SQUID_INK, true),
SNOWFLAKE(Material.SNOWBALL, Particle.SNOWFLAKE, true),
ITEM_SNOWBALL(Material.SNOW_BLOCK, Particle.ITEM_SNOWBALL, true),
ITEM_SLIME(Material.SLIME_BALL, Particle.ITEM_SLIME, true),
HEART(Material.RED_DYE, Particle.HEART, true),
NOTE(Material.NOTE_BLOCK, Particle.NOTE, true),
ANGRY_VILLAGER(Material.REDSTONE_BLOCK, Particle.ANGRY_VILLAGER, true),
DAMAGE_INDICATOR(Material.SPIDER_EYE, Particle.DAMAGE_INDICATOR, true),
CRIT(Material.CAULDRON, Particle.CRIT, true),
ENCHANTED_HIT(Material.GLOWSTONE_DUST, Particle.ENCHANTED_HIT, true),
ENCHANT(Material.ENCHANTING_TABLE, Particle.ENCHANT, true),
WITCH(Material.EXPERIENCE_BOTTLE, Particle.WITCH, true),
END_ROD(Material.END_ROD, Particle.END_ROD, true),
DRAGON_BREATH(Material.DRAGON_BREATH, Particle.DRAGON_BREATH, true),
FIREWORK(Material.FIRE_CHARGE, Particle.FIREWORK, true),
TOTEM_OF_UNDYING(Material.TOTEM_OF_UNDYING, Particle.TOTEM_OF_UNDYING, true),
RAID_OMEN(Material.FIREWORK_STAR, Particle.RAID_OMEN, true),
EXPLOSION(Material.TNT, Particle.EXPLOSION, false),
;
private final Material icon;
private final Particle particle;
private final boolean buildable;
public String getNameKey() {
return "PARTICLE_TYPE_" + name();
}
public boolean isDust() {
return particle == Particle.DUST;
}
}
@@ -28,6 +28,7 @@ public class Circle extends DelegatingParticleElement {
private double distance;
private double speed;
private double phase;
public Circle(ParticleElement particleElement) {
super(particleElement);
@@ -41,6 +42,11 @@ public class Circle extends DelegatingParticleElement {
this.speed = speed;
}
public Circle(ParticleElement particleElement, double distance, double speed, double phaseDegrees) {
this(particleElement, distance, speed);
this.phase = Math.toRadians(phaseDegrees);
}
@Override
public String attribute() {
return "PARTICLE_ATTRIBUTE_CIRCLE";
@@ -51,7 +57,7 @@ public class Circle extends DelegatingParticleElement {
Location location = particleTickData.getLocation();
Vector vector = new Vector(distance, 0, 0);
vector.rotateAroundY((particleTickData.getDeg() * speed) % 360);
vector.rotateAroundY((particleTickData.getDeg() * speed) % 360 + phase);
ParticleTickData nParticleTickData = particleTickData.withLocation(location.clone().add(vector));
particleElement.tick(nParticleTickData);
}
@@ -0,0 +1,128 @@
/*
* 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.slot;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import de.steamwar.lobby.particle.Gradient;
import de.steamwar.lobby.particle.ParticleElement;
import de.steamwar.lobby.particle.design.*;
import de.steamwar.lobby.particle.elements.*;
import lombok.Getter;
import java.awt.Color;
import java.util.ArrayList;
import java.util.List;
import static de.steamwar.lobby.particle.design.EntryParameters.*;
@Getter
public class ParticleEntry {
private final ParticleDesign design;
private final ParameterValues values;
public ParticleEntry(ParticleDesign design, ParameterValues values) {
this.design = design;
this.values = values;
}
public ParticleEntry(ParticleDesign design) {
this(design, new ParameterValues());
}
public List<Parameter<?>> getParameters() {
return parametersOf(design);
}
private static List<Parameter<?>> parametersOf(ParticleDesign design) {
List<Parameter<?>> parameters = new ArrayList<>(EntryParameters.ALL);
parameters.addAll(design.getParameters());
return parameters;
}
public ParticleElement build() {
ParticleType type = values.get(PARTICLE);
float spreadX = values.get(SPREAD_X).floatValue();
float spreadY = values.get(SPREAD_Y).floatValue();
float spreadZ = values.get(SPREAD_Z).floatValue();
double speed = values.get(SPEED);
int count = values.get(COUNT);
ParticleElement element;
if (type.isDust()) {
List<Color> colors = values.get(COLORS);
Gradient gradient = colors.isEmpty() ? null : new Gradient(colors.toArray(new Color[0]));
element = new DustParticle(type.getParticle(), spreadX, spreadY, spreadZ, speed, count, gradient);
} else {
element = new SimpleParticle(type.getParticle(), spreadX, spreadY, spreadZ, speed, count);
}
double offsetX = values.get(OFFSET_X);
double offsetY = values.get(OFFSET_Y);
double offsetZ = values.get(OFFSET_Z);
if (offsetX != 0 || offsetY != 0 || offsetZ != 0) {
element = new LocationMutator(element, offsetX, offsetY, offsetZ);
}
element = design.build(values, element);
element = values.get(MOVEMENT).wrap(element);
element = values.get(GLIDING).wrap(element);
element = values.get(GROUND).wrap(element);
element = values.get(SNEAK).wrap(element);
element = values.get(VISIBILITY).wrap(element);
int interval = values.get(INTERVAL);
if (interval > 1) {
element = new Delayed(element, interval);
}
if (values.get(TICK_MODE) == TickMode.ALWAYS) {
element = new Always(element);
}
return element;
}
// Particles spawned per tick, used for the slot budget
public double cost() {
return (double) design.points(values) * values.get(COUNT) / values.get(INTERVAL);
}
public JsonObject toJson() {
JsonObject json = new JsonObject();
json.addProperty("design", design.getId());
json.add("values", values.toJson(getParameters()));
return json;
}
// Returns null for entries that cannot be loaded from player storage
public static ParticleEntry fromJson(JsonObject json) {
JsonElement designId = json.get("design");
if (designId == null || !designId.isJsonPrimitive()) {
return null;
}
ParticleDesign design = Designs.byId(designId.getAsString());
if (design == null || design.getVisibility() == DesignVisibility.INTERNAL) {
return null;
}
JsonElement values = json.get("values");
JsonObject valuesJson = values != null && values.isJsonObject() ? values.getAsJsonObject() : new JsonObject();
return new ParticleEntry(design, ParameterValues.fromJson(valuesJson, parametersOf(design)));
}
}
@@ -0,0 +1,27 @@
/*
* 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.slot;
import java.util.List;
// A list of entries that are shown together: a player's slot or a preset
public interface ParticleSet {
List<ParticleEntry> getEntries();
}
@@ -0,0 +1,103 @@
/*
* 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.slot;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import de.steamwar.lobby.particle.design.EntryParameters;
import lombok.Getter;
import lombok.Setter;
import org.bukkit.Material;
import java.util.ArrayList;
import java.util.List;
@Getter
@Setter
public class ParticleSlot implements ParticleSet {
public static final int VERSION = 1;
private static final int MAX_NAME_LENGTH = 32;
private String name;
// Null means the icon of the first entry's particle type
private Material icon;
private final List<ParticleEntry> entries = new ArrayList<>();
public ParticleSlot(String name) {
this.name = name;
}
public Material getDisplayIcon() {
if (icon != null) {
return icon;
}
if (entries.isEmpty()) {
return Material.FIREWORK_STAR;
}
return entries.get(0).getValues().get(EntryParameters.PARTICLE).getIcon();
}
public void setName(String name) {
this.name = name.length() > MAX_NAME_LENGTH ? name.substring(0, MAX_NAME_LENGTH) : name;
}
public JsonObject toJson() {
JsonObject json = new JsonObject();
json.addProperty("version", VERSION);
json.addProperty("name", name);
if (icon != null) {
json.addProperty("icon", icon.name());
}
JsonArray array = new JsonArray();
entries.forEach(entry -> array.add(entry.toJson()));
json.add("entries", array);
return json;
}
public static ParticleSlot fromJson(JsonObject json, String defaultName) {
JsonElement name = json.get("name");
ParticleSlot slot = new ParticleSlot(defaultName);
if (name != null && name.isJsonPrimitive()) {
slot.setName(name.getAsString());
}
JsonElement icon = json.get("icon");
if (icon != null && icon.isJsonPrimitive()) {
Material material = Material.matchMaterial(icon.getAsString());
if (material != null && material.isItem()) {
slot.icon = material;
}
}
JsonElement entries = json.get("entries");
if (entries != null && entries.isJsonArray()) {
for (JsonElement element : entries.getAsJsonArray()) {
if (!element.isJsonObject()) continue;
ParticleEntry entry = ParticleEntry.fromJson(element.getAsJsonObject());
if (entry != null) {
slot.entries.add(entry);
}
}
}
return slot;
}
}
@@ -0,0 +1,74 @@
/*
* 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.slot;
import com.google.gson.JsonParser;
import de.steamwar.lobby.LobbySystem;
import de.steamwar.lobby.particle.ParticleRequirement;
import de.steamwar.sql.UserConfig;
import lombok.experimental.UtilityClass;
import java.util.logging.Level;
@UtilityClass
public class ParticleSlots {
public final int COUNT = 7;
private final String KEY_PREFIX = "lobby-particle-slot-";
// 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;
if (index < 5) return ParticleRequirement.TEAM_MEMBER;
return ParticleRequirement.SERVER_TEAM;
}
public String defaultName(int index) {
return "Slot " + (index + 1);
}
public ParticleSlot load(int userId, int index) {
String saved = UserConfig.getConfig(userId, key(index));
if (saved == null) {
return null;
}
try {
return ParticleSlot.fromJson(JsonParser.parseString(saved).getAsJsonObject(), defaultName(index));
} catch (RuntimeException e) {
LobbySystem.getInstance().getLogger().log(Level.WARNING, "Could not load particle slot " + (index + 1) + " of user " + userId, e);
return null;
}
}
public void save(int userId, int index, ParticleSlot slot) {
if (slot == null) {
UserConfig.removePlayerConfig(userId, key(index));
} else {
UserConfig.updatePlayerConfig(userId, key(index), slot.toJson().toString());
}
}
private String key(int index) {
if (index < 0 || index >= COUNT) {
throw new IllegalArgumentException("Invalid particle slot " + index);
}
return KEY_PREFIX + (index + 1);
}
}
@@ -22,6 +22,9 @@ package de.steamwar.lobby.util;
import de.steamwar.lobby.particle.CompiledSet;
import de.steamwar.lobby.particle.ParticleEnum;
import de.steamwar.lobby.particle.ParticleInventory;
import de.steamwar.lobby.particle.UnlockContext;
import de.steamwar.lobby.particle.slot.ParticleSlot;
import de.steamwar.lobby.particle.slot.ParticleSlots;
import de.steamwar.sql.SteamwarUser;
import de.steamwar.sql.UserConfig;
import org.bukkit.entity.Player;
@@ -32,15 +35,23 @@ import java.util.UUID;
public class LobbyPlayer {
private static final String ACTIVE_KEY = "lobby-particle-active";
private static final String ACTIVE_SLOT_PREFIX = "slot:";
private static Map<UUID, LobbyPlayer> cache = new HashMap<>();
private final UUID uuid;
private ParticleEnum particle;
private final ParticleSlot[] slots = new ParticleSlot[ParticleSlots.COUNT];
private final boolean[] loadedSlots = new boolean[ParticleSlots.COUNT];
private int activeSlot = -1;
private CompiledSet compiledSet;
private int userId;
private boolean fly;
private LobbyPlayer(UUID uuid) {
this.uuid = uuid;
userId = SteamwarUser.get(uuid).getId();
cache.put(uuid, this);
particle = null;
@@ -48,7 +59,18 @@ public class LobbyPlayer {
if (saved != null) {
particle = ParticleInventory.convertFromString(saved);
}
compile();
String active = UserConfig.getConfig(userId, ACTIVE_KEY);
if (active != null && active.startsWith(ACTIVE_SLOT_PREFIX)) {
try {
int index = Integer.parseInt(active.substring(ACTIVE_SLOT_PREFIX.length())) - 1;
if (index >= 0 && index < ParticleSlots.COUNT) {
activeSlot = index;
}
} catch (NumberFormatException e) {
// Ignore unreadable selection, the player simply has no slot selected
}
}
recompile();
}
public boolean isFlying() {
@@ -70,16 +92,76 @@ public class LobbyPlayer {
String saved = ParticleInventory.convertToString(particle);
UserConfig.updatePlayerConfig(userId, "lobby-particle", saved);
}
setActiveSlot(-1);
this.particle = particle;
compile();
recompile();
}
public ParticleSlot getSlot(int index) {
if (!loadedSlots[index]) {
slots[index] = ParticleSlots.load(userId, index);
loadedSlots[index] = true;
}
return slots[index];
}
// Null deletes the slot
public void setSlot(int index, ParticleSlot slot) {
ParticleSlots.save(userId, index, slot);
slots[index] = slot;
loadedSlots[index] = true;
if (index == activeSlot) {
recompile();
}
}
public int getActiveSlot() {
return activeSlot;
}
// Returns false when the slot is empty or locked for this player
public boolean selectSlot(int index) {
if (getSlot(index) == null || !ParticleSlots.requirement(index).test(unlockContext())) {
return false;
}
if (particle != null) {
UserConfig.removePlayerConfig(userId, "lobby-particle");
particle = null;
}
setActiveSlot(index);
recompile();
return true;
}
public CompiledSet getCompiledSet() {
return compiledSet;
}
private void compile() {
compiledSet = particle == null ? null : CompiledSet.of(particle.getParticle().getParticleElement());
// A locked active slot shows nothing but stays selected, so it comes back once the rank does
public void recompile() {
if (activeSlot >= 0) {
ParticleSlot slot = getSlot(activeSlot);
UnlockContext context = unlockContext();
compiledSet = slot != null && ParticleSlots.requirement(activeSlot).test(context) ? CompiledSet.of(slot, context) : null;
} else {
compiledSet = particle == null ? null : CompiledSet.of(particle.getParticle().getParticleElement());
}
}
private void setActiveSlot(int index) {
if (index == activeSlot) {
return;
}
activeSlot = index;
if (index < 0) {
UserConfig.removePlayerConfig(userId, ACTIVE_KEY);
} else {
UserConfig.updatePlayerConfig(userId, ACTIVE_KEY, ACTIVE_SLOT_PREFIX + (index + 1));
}
}
private UnlockContext unlockContext() {
return new UnlockContext(SteamwarUser.get(uuid));
}
public static LobbyPlayer getLobbyPlayer(UUID uuid) {