From 1980375e73706f8071c0952f24d896e843021bea Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Fri, 23 Oct 2026 14:36:06 +0200 Subject: [PATCH] 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. --- .../steamwar/lobby/particle/CompiledSet.java | 14 ++ .../lobby/particle/ParticleListener.java | 7 + .../lobby/particle/ParticleRequirement.java | 32 ++++ .../lobby/particle/UnlockContext.java | 59 +++++++ .../particle/design/BooleanParameter.java | 40 +++++ .../particle/design/ChoiceParameter.java | 86 ++++++++++ .../particle/design/DesignVisibility.java | 29 ++++ .../lobby/particle/design/Designs.java | 118 ++++++++++++++ .../particle/design/DoubleParameter.java | 57 +++++++ .../particle/design/EntryParameters.java | 151 ++++++++++++++++++ .../particle/design/GradientParameter.java | 61 +++++++ .../lobby/particle/design/IntParameter.java | 52 ++++++ .../lobby/particle/design/Parameter.java | 58 +++++++ .../particle/design/ParameterValues.java | 64 ++++++++ .../lobby/particle/design/ParticleDesign.java | 59 +++++++ .../lobby/particle/design/ParticleType.java | 84 ++++++++++ .../lobby/particle/elements/Circle.java | 8 +- .../lobby/particle/slot/ParticleEntry.java | 128 +++++++++++++++ .../lobby/particle/slot/ParticleSet.java | 27 ++++ .../lobby/particle/slot/ParticleSlot.java | 103 ++++++++++++ .../lobby/particle/slot/ParticleSlots.java | 74 +++++++++ .../de/steamwar/lobby/util/LobbyPlayer.java | 90 ++++++++++- 22 files changed, 1396 insertions(+), 5 deletions(-) create mode 100644 LobbySystem/src/de/steamwar/lobby/particle/UnlockContext.java create mode 100644 LobbySystem/src/de/steamwar/lobby/particle/design/BooleanParameter.java create mode 100644 LobbySystem/src/de/steamwar/lobby/particle/design/ChoiceParameter.java create mode 100644 LobbySystem/src/de/steamwar/lobby/particle/design/DesignVisibility.java create mode 100644 LobbySystem/src/de/steamwar/lobby/particle/design/Designs.java create mode 100644 LobbySystem/src/de/steamwar/lobby/particle/design/DoubleParameter.java create mode 100644 LobbySystem/src/de/steamwar/lobby/particle/design/EntryParameters.java create mode 100644 LobbySystem/src/de/steamwar/lobby/particle/design/GradientParameter.java create mode 100644 LobbySystem/src/de/steamwar/lobby/particle/design/IntParameter.java create mode 100644 LobbySystem/src/de/steamwar/lobby/particle/design/Parameter.java create mode 100644 LobbySystem/src/de/steamwar/lobby/particle/design/ParameterValues.java create mode 100644 LobbySystem/src/de/steamwar/lobby/particle/design/ParticleDesign.java create mode 100644 LobbySystem/src/de/steamwar/lobby/particle/design/ParticleType.java create mode 100644 LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleEntry.java create mode 100644 LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleSet.java create mode 100644 LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleSlot.java create mode 100644 LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleSlots.java diff --git a/LobbySystem/src/de/steamwar/lobby/particle/CompiledSet.java b/LobbySystem/src/de/steamwar/lobby/particle/CompiledSet.java index bfbecb7c..5a668e2b 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/CompiledSet.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/CompiledSet.java @@ -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); diff --git a/LobbySystem/src/de/steamwar/lobby/particle/ParticleListener.java b/LobbySystem/src/de/steamwar/lobby/particle/ParticleListener.java index 90a0dc9d..ea758a44 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/ParticleListener.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/ParticleListener.java @@ -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()); diff --git a/LobbySystem/src/de/steamwar/lobby/particle/ParticleRequirement.java b/LobbySystem/src/de/steamwar/lobby/particle/ParticleRequirement.java index c2ccc14b..f5e29349 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/ParticleRequirement.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/ParticleRequirement.java @@ -41,6 +41,10 @@ public interface ParticleRequirement { boolean test(SteamwarUser user, Set 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 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 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 eventTeilname, String eggHuntConfig) { + return user.getTeam() != 0; + } + + @Override + public boolean test(UnlockContext context) { + return context.getUser().getTeam() != 0; + } }; ParticleRequirement HAS_TEAM = new ParticleRequirement() { @Override diff --git a/LobbySystem/src/de/steamwar/lobby/particle/UnlockContext.java b/LobbySystem/src/de/steamwar/lobby/particle/UnlockContext.java new file mode 100644 index 00000000..27f54c71 --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/UnlockContext.java @@ -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 . + */ + +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 events; + private String eggHuntConfig; + private boolean eggHuntConfigLoaded; + + public UnlockContext(SteamwarUser user) { + this.user = user; + } + + public Set 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; + } +} diff --git a/LobbySystem/src/de/steamwar/lobby/particle/design/BooleanParameter.java b/LobbySystem/src/de/steamwar/lobby/particle/design/BooleanParameter.java new file mode 100644 index 00000000..e7631c80 --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/design/BooleanParameter.java @@ -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 . + */ + +package de.steamwar.lobby.particle.design; + +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; + +public class BooleanParameter extends Parameter { + + 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(); + } +} diff --git a/LobbySystem/src/de/steamwar/lobby/particle/design/ChoiceParameter.java b/LobbySystem/src/de/steamwar/lobby/particle/design/ChoiceParameter.java new file mode 100644 index 00000000..9df4eed0 --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/design/ChoiceParameter.java @@ -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 . + */ + +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 extends Parameter { + + // Internal choices are usable by presets but never offered in the builder or accepted from storage + public record Choice(String id, E value, boolean internal) { + } + + private final List> choices; + + public ChoiceParameter(String key, String nameKey, E defaultValue, List> choices) { + super(key, nameKey, defaultValue); + this.choices = choices; + } + + public static > ChoiceParameter ofEnum(String key, String nameKey, E defaultValue, Predicate internal) { + List> 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 > ChoiceParameter ofEnum(String key, String nameKey, E defaultValue) { + return ofEnum(key, nameKey, defaultValue, value -> false); + } + + public List 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 choiceOf(E value) { + return choices.stream() + .filter(choice -> choice.value() == value) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Unknown choice for " + getKey())); + } +} diff --git a/LobbySystem/src/de/steamwar/lobby/particle/design/DesignVisibility.java b/LobbySystem/src/de/steamwar/lobby/particle/design/DesignVisibility.java new file mode 100644 index 00000000..97b9dfef --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/design/DesignVisibility.java @@ -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 . + */ + +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, +} diff --git a/LobbySystem/src/de/steamwar/lobby/particle/design/Designs.java b/LobbySystem/src/de/steamwar/lobby/particle/design/Designs.java new file mode 100644 index 00000000..64d5024c --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/design/Designs.java @@ -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 . + */ + +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 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 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 all() { + return List.copyOf(DESIGNS.values()); + } +} diff --git a/LobbySystem/src/de/steamwar/lobby/particle/design/DoubleParameter.java b/LobbySystem/src/de/steamwar/lobby/particle/design/DoubleParameter.java new file mode 100644 index 00000000..de278946 --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/design/DoubleParameter.java @@ -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 . + */ + +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 { + + 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)); + } +} diff --git a/LobbySystem/src/de/steamwar/lobby/particle/design/EntryParameters.java b/LobbySystem/src/de/steamwar/lobby/particle/design/EntryParameters.java new file mode 100644 index 00000000..b4415e9c --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/design/EntryParameters.java @@ -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 . + */ + +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 wrapper; + + Movement(UnaryOperator 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 wrapper; + + Gliding(UnaryOperator 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 wrapper; + + Ground(UnaryOperator 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 wrapper; + + Sneak(UnaryOperator 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 wrapper; + + Visibility(UnaryOperator wrapper) { + this.wrapper = wrapper; + } + + public ParticleElement wrap(ParticleElement element) { + return wrapper.apply(element); + } + } + + public final ChoiceParameter 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 TICK_MODE = ChoiceParameter.ofEnum("tickMode", "PARTICLE_PARAM_TICK_MODE", TickMode.MOVE); + public final ChoiceParameter MOVEMENT = ChoiceParameter.ofEnum("movement", "PARTICLE_PARAM_MOVEMENT", Movement.ANY); + public final ChoiceParameter GLIDING = ChoiceParameter.ofEnum("gliding", "PARTICLE_PARAM_GLIDING", Gliding.ANY); + public final ChoiceParameter GROUND = ChoiceParameter.ofEnum("ground", "PARTICLE_PARAM_GROUND", Ground.ANY); + public final ChoiceParameter SNEAK = ChoiceParameter.ofEnum("sneak", "PARTICLE_PARAM_SNEAK", Sneak.ANY); + public final ChoiceParameter VISIBILITY = ChoiceParameter.ofEnum("visibility", "PARTICLE_PARAM_VISIBILITY", Visibility.EVERYONE); + + public final List> 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 + ); +} diff --git a/LobbySystem/src/de/steamwar/lobby/particle/design/GradientParameter.java b/LobbySystem/src/de/steamwar/lobby/particle/design/GradientParameter.java new file mode 100644 index 00000000..75fd42d3 --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/design/GradientParameter.java @@ -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 . + */ + +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> { + + 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 value) { + JsonArray array = new JsonArray(); + value.forEach(color -> array.add(color.getRGB() & 0xFFFFFF)); + return array; + } + + @Override + protected List read(JsonElement json) { + List colors = new ArrayList<>(); + for (JsonElement element : json.getAsJsonArray()) { + colors.add(new Color(element.getAsInt() & 0xFFFFFF)); + } + return colors; + } + + @Override + public List validate(List value) { + return List.copyOf(value.size() > maxColors ? value.subList(0, maxColors) : value); + } +} diff --git a/LobbySystem/src/de/steamwar/lobby/particle/design/IntParameter.java b/LobbySystem/src/de/steamwar/lobby/particle/design/IntParameter.java new file mode 100644 index 00000000..042a8634 --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/design/IntParameter.java @@ -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 . + */ + +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 { + + 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)); + } +} diff --git a/LobbySystem/src/de/steamwar/lobby/particle/design/Parameter.java b/LobbySystem/src/de/steamwar/lobby/particle/design/Parameter.java new file mode 100644 index 00000000..117b1fdf --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/design/Parameter.java @@ -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 . + */ + +package de.steamwar.lobby.particle.design; + +import com.google.gson.JsonElement; +import lombok.Getter; + +@Getter +public abstract class Parameter { + + 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; + } +} diff --git a/LobbySystem/src/de/steamwar/lobby/particle/design/ParameterValues.java b/LobbySystem/src/de/steamwar/lobby/particle/design/ParameterValues.java new file mode 100644 index 00000000..4f1512d4 --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/design/ParameterValues.java @@ -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 . + */ + +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 values = new HashMap<>(); + + @SuppressWarnings("unchecked") + public T get(Parameter parameter) { + Object value = values.get(parameter.getKey()); + return value == null ? parameter.getDefaultValue() : (T) value; + } + + public ParameterValues set(Parameter 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> parameters) { + JsonObject json = new JsonObject(); + parameters.forEach(parameter -> write(json, parameter)); + return json; + } + + public static ParameterValues fromJson(JsonObject json, List> parameters) { + ParameterValues values = new ParameterValues(); + parameters.forEach(parameter -> values.values.put(parameter.getKey(), parameter.fromJson(json.get(parameter.getKey())))); + return values; + } + + private void write(JsonObject json, Parameter parameter) { + json.add(parameter.getKey(), parameter.toJson(get(parameter))); + } +} diff --git a/LobbySystem/src/de/steamwar/lobby/particle/design/ParticleDesign.java b/LobbySystem/src/de/steamwar/lobby/particle/design/ParticleDesign.java new file mode 100644 index 00000000..c463fd91 --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/design/ParticleDesign.java @@ -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 . + */ + +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> 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; + } +} diff --git a/LobbySystem/src/de/steamwar/lobby/particle/design/ParticleType.java b/LobbySystem/src/de/steamwar/lobby/particle/design/ParticleType.java new file mode 100644 index 00000000..93d585fa --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/design/ParticleType.java @@ -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 . + */ + +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; + } +} diff --git a/LobbySystem/src/de/steamwar/lobby/particle/elements/Circle.java b/LobbySystem/src/de/steamwar/lobby/particle/elements/Circle.java index 969bd247..9b9d344c 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/elements/Circle.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/elements/Circle.java @@ -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); } diff --git a/LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleEntry.java b/LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleEntry.java new file mode 100644 index 00000000..9178951c --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleEntry.java @@ -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 . + */ + +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> getParameters() { + return parametersOf(design); + } + + private static List> parametersOf(ParticleDesign design) { + List> 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 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))); + } +} diff --git a/LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleSet.java b/LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleSet.java new file mode 100644 index 00000000..c53fa303 --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleSet.java @@ -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 . + */ + +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 getEntries(); +} diff --git a/LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleSlot.java b/LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleSlot.java new file mode 100644 index 00000000..094c848d --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleSlot.java @@ -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 . + */ + +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 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; + } +} diff --git a/LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleSlots.java b/LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleSlots.java new file mode 100644 index 00000000..c8de7589 --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleSlots.java @@ -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 . + */ + +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); + } +} diff --git a/LobbySystem/src/de/steamwar/lobby/util/LobbyPlayer.java b/LobbySystem/src/de/steamwar/lobby/util/LobbyPlayer.java index 1eaab799..8bc49ef7 100644 --- a/LobbySystem/src/de/steamwar/lobby/util/LobbyPlayer.java +++ b/LobbySystem/src/de/steamwar/lobby/util/LobbyPlayer.java @@ -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 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) {