Compare commits

..
Author SHA1 Message Date
YoyoNow f63390915b Add glyph flight for enchant particles and blend team links
Entries with the enchant particle get a Glyph flight setting: float down as
usual, fly into the player or fly out of them, the latter two unlocked by the
WarGear Season placement and participation. Team link lines now start in the
drawing player's particle and blend into the partner's along the way.
2026-10-26 15:13:39 +01:00
YoyoNow db939e27d9 Turn the touches back into reward designs styled like any other
The Touch setting is gone. Its unique mechanics become reward designs that use
the entry's particle: Burst (on jumping, landing or sneaking, as a puff or a
ring), Footprints, Team link, Shimmer and Heartbeat. Each is unlocked by any
of the rewards that had it, and Burst's triggers are locked separately. The
wing beat becomes the Beat on jump wing motion, unlocked by WarGear Season
2022. December snow and the glyphs are dropped: the first is a plain particle
with a condition, the second only works with enchanting glyphs.
2026-10-26 15:08:08 +01:00
YoyoNow 36e6329f2b Redraw beating wings every tick only after a jump
The wing beat touch no longer lowers the entry's interval. Its wings are drawn
every tick during the beat and at their usual interval otherwise; the extra
redraws do not count towards the slot budget.
2026-10-26 15:02:56 +01:00
YoyoNow b2300c128e Let the wing beat touch beat the entry's own wings
On a Wings entry the touch adds a beat after each jump to the wings' motion
and draws nothing of its own; picking it redraws the wings every 2 ticks like
a motion does. Other designs keep the dragon breath wings.
2026-10-26 14:59:32 +01:00
YoyoNow 9cb4e0b7db Unlock everything for administrators
Every particle requirement passes for users with the administration
permission. Lore keeps naming the regular requirement.
2026-10-26 14:49:53 +01:00
YoyoNow 8e4730bd12 Turn reward touches into an unlockable Touch setting
Every entry gets a Touch setting next to its design, offering the 16 touches
the rewards carried, each unlocked by its reward. The reward designs that the
builder can now make are removed; only the underwater rain cloud and the
medium, hard and advanced egg hunt designs stay. Reward requirements move to
Unlocks, shared by touches, wing images and cosy smoke. The trail duration
moves to the particle row to make room in the design row.
2026-10-26 14:48:29 +01:00
YoyoNow baa278a1f5 Cap particle speed at 0.05 and give each wing shape its own icon
Speed steps by 0.005 up to 0.05; presets keep their faster particles. Number
values now show up to three decimals.
2026-10-26 14:43:44 +01:00
YoyoNow 8fd6fdf81e Unlock tournament wing images and cosy smoke for building
The MiniWarGear league, Underwater MWG, WarGear season 2022 and WarGear clash
wing images become Wings shapes, locked behind the placement that earns their
reward. Cosy campfire smoke becomes a buildable particle for Hells Bells
participants. Choice options can now carry a requirement: locked options show
it in the picker, are skipped when cycling and hide entries that use them.
2026-10-26 14:37:40 +01:00
YoyoNow 7d0f5aceae Give the rebuildable reward designs unique touches
Rewards whose look the basic designs can build now carry a touch the builder
cannot make: bursts on jumping, landing or sneaking, footprints, a line to
nearby players with the same reward, a December extra, aimed enchanting
glyphs, a heartbeat, a jump-beat wing or an orbiting shape. BodyMotion tracks
the jump, land and sneak events the touches react to.
2026-10-26 14:34:50 +01:00
YoyoNow ad9087c038 Remove the breathe wing motion
Wings keep the flap motions only; saved breathing wings load as none.
2026-10-26 13:57:59 +01:00
YoyoNow 872da5f62a Open wings with speed and let them flap or breathe
All wings stand 5 degrees open when idle and open up to 15 degrees the
faster the player moves. The wings design gets a motion setting to flap
slowly, flap quickly or breathe; picking a motion redraws the wing every
2 ticks so it looks smooth, and going back to none restores every 20.
Body rotation tracking now also tracks speed, as BodyMotion.
2026-10-26 13:55:58 +01:00
YoyoNow e830d087c9 Merge wing designs, turn wings with the body, limit leaves
Wing and Drawn wings become one Wings design whose shapes include the
images and the drawn bat, butterfly and fairy; for drawn shapes the points
per wing replace the amount. Body rotation is tracked per player the way
the client turns the body, and wings, the heart beat and body placement
follow it instead of the head. Leaves particles are for the server team
only, and typed hex colours show the closest dye.
2026-10-26 13:39:28 +01:00
YoyoNow b4661ea345 Add colour along the wing and sparkle to lobby wings
Both wing designs can colour their points from the spine to the tips
instead of changing colour over time, and a sparkle setting shows only a
share of the points each tick, which also lowers their cost. The angel
and phoenix drawn shapes are removed.
2026-10-26 13:34:23 +01:00
YoyoNow 4e5945d2bc Lower drawn wings to the shoulder blades
The drawn shapes had their shoulder joint at head height. They now sit
about 1.3 blocks above the feet at the default size.
2026-10-26 10:50:57 +01:00
YoyoNow a5b8f00c58 Add drawn wing shapes to the slot builder
A new Drawn wings design draws angel, bat, butterfly, fairy and phoenix
wings from lines and curves instead of images. The points per wing can be
set from 6 to 60, so sparse wings stay cheap enough to show often.
2026-10-26 10:48:32 +01:00
YoyoNow 8e23490b04 Settle open particle slot questions
Slots hold up to 9 entries, so the slot editor is one entry row plus the
bottom bar, and the server team may spawn 90 particles per tick. The
water and lava rings for taking part in any event return as reward
designs, and locked slots can be deleted from their read-only view.
2026-10-26 10:33:15 +01:00
YoyoNow 4c6b5e654a Quiet no-op particle setting clicks and show entry particle counts
Clicking a step past the minimum or maximum, or setting an unchanged
value, now does nothing instead of showing the budget message, and
changes that lower a slot's particles are allowed even over budget.
Entries show their particles per tick in the slot editor list, and the
entry editor's paper shows the edited entry's share.
2026-10-26 10:23:24 +01:00
YoyoNow 38a33b7298 Show the resulting value on number step buttons
Each step button on the number screen shows the value it leads to, and
marks when that value is the minimum or maximum.
2026-10-26 10:20:50 +01:00
YoyoNow 32797bd2a8 Limit lobby particle speed to 0.2
Speed went up to 1, which makes most particles shoot several blocks away.
It now goes up to 0.2; presets use at most 0.1.
2026-10-26 10:12:14 +01:00
YoyoNow 28b62bf27b Limit lobby particle spread to half a block
Spread went up to 2 blocks, which scatters a design's particles far
around the player. It now goes up to 0.5 in steps of 0.01.
2026-10-26 10:12:10 +01:00
YoyoNow db915f7be7 Keep large and lingering particles out of the slot builder
Explosion, sonic boom, sweep attack, gusts, campfire and large smoke,
dust pillar, block marker and shriek are a block or more in size or rise
for seconds and wall players in once a design spawns many of them. They
stay available to presets only.
2026-10-26 10:10:21 +01:00
YoyoNow aef26b5e9f Add the trail particle with duration and target
Trail particles fly from where they spawn to a target offset over the
chosen number of ticks, in the entry's colour. The target follows the
body like other offsets when the entry is placed relative to it.
2026-10-26 10:10:17 +01:00
YoyoNow 8009098965 Space out entry editor rows and show empty slots as firework stars
Settings in the entry editor start one column after the row headers, and
empty particle slots show a firework star instead of a gray pane.
2026-10-26 09:49:20 +01:00
YoyoNow 47f0d9858b Place lobby particle entries relative to the body
Entries get a World/Body setting. In body mode offsets, heights and wings
follow the body's facing and lie along the body while gliding or swimming,
so wings stay on the back. Rings circle the body but keep their world
angle, so they do not turn with the player. New wings default to body;
presets and rewards stay in world mode and look exactly as before.
2026-10-26 09:49:11 +01:00
YoyoNow c3eda01e20 Cycle particle settings with up to five options
Choices with up to five options cycle forward on left click and backward
on right click, listing all options in the lore. Two-option choices still
read as a toggle; longer choices keep their list.
2026-10-26 09:49:07 +01:00
YoyoNow 5e81081620 Toggle two-option particle settings on click
Choices with exactly two options switch on click like on/off settings,
and their lore lists both options with the current one highlighted.
2026-10-26 09:49:02 +01:00
YoyoNow ed70d65bc4 Size particle choice menus by their options
Lists that fit on one page get one row per nine options plus the bottom
bar, so a condition with three options opens a two row menu. Longer lists
keep the paged 6 row menu.
2026-10-26 09:48:57 +01:00
YoyoNow 2ce8799a19 Use black panes for the particle menu bottom bar
Unused bottom bar columns are black glass panes, empty places above it
are gray, including the free entry places of the slot editor.
2026-10-26 09:48:51 +01:00
YoyoNow 15036a7ba0 Allow hex colour codes for lobby particles
The colour screen gets a button to type a colour as #RRGGBB or #RGB,
next to picking one of the dye colours.
2026-10-26 09:48:46 +01:00
YoyoNow d77c014409 Translate lobby particle types and blocks
Add English and German names for all 104 selectable particle types and
the 43 blocks of the block setting.
2026-10-26 09:48:41 +01:00
YoyoNow 6b5441bb04 Offer nearly every vanilla particle in the slot builder
Add the remaining particles, including colour, colour transition, block
and item particles, which take their colours or a new block setting from
the entry. Only particles that cover the screen, show only under water or
need a target stay out.
2026-10-26 09:48:36 +01:00
YoyoNow 54d30f4a42 Fill unused particle menu columns with gray panes
Unused bottom bar columns show a gray glass pane instead of a barrier,
and the slot editor drops its use button since the particle menu
toggles slots with a click.
2026-10-26 09:48:32 +01:00
YoyoNow e41f76937e Give particle menus a shared bottom bar
Back, use, add, rename, icon, particle count and delete or reset now sit
in the same column of the last row in every particle menu, and columns a
menu does not use show a barrier.
2026-10-26 09:48:28 +01:00
YoyoNow 0c7ee052e8 Toggle particle slots with a click and label entry editor rows
A slot with entries is switched on or off with a click in the particle
menu and edited with a shift click; empty slots still open the editor.
The entry editor rows get distinct glowing header icons.
2026-10-26 09:48:25 +01:00
YoyoNow b60cdfc88d Simplify lobby particle menus
Every menu item now acts on a plain click: slots open their editor with a
use button, entries open their editor, and duplicating is gone. The entry
editor groups settings into labelled rows, numbers and colours get their
own screens instead of relying on right click or Q, and slot names show
the particles per tick used out of the maximum.
2026-10-26 09:48:20 +01:00
YoyoNow 98f04451a4 Polish lobby particle slots
Count the real particle amount of reward designs, set slot budgets to
30 particles per tick (60 for the server team), let requirements test
only against an UnlockContext, and drop requirements and messages the
old particle menu used.
2026-10-26 09:48:05 +01:00
YoyoNow c95901bc42 Add lobby particle slot menus
Replace the particle inventory with a main menu showing the 7 slots and
unlocked presets, a slot editor with a read-only view for locked slots,
and an entry editor with pickers for designs, choices, colours and icons.
Slots are limited to 10 entries and a particles-per-tick budget.
2026-10-26 09:48:00 +01:00
YoyoNow 211e2693cf Move lobby presets and rewards onto the particle slot model
Team- and user-specific particles become presets built from entries,
tournament and egg hunt particles become fixed reward designs, and the
old particle enums, ParticleEnum and ParticleData are removed. The
particle menu temporarily lists only unlocked presets.
2026-10-26 09:47:55 +01:00
YoyoNow 1980375e73 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.
2026-10-26 09:47:49 +01:00
YoyoNow ef041272bb Refactor lobby particle runtime for multi-entry particles
Tick particles from per-player cached always/move lists, drop the
per-instance listeners in Delayed and RandomParticle, and compute
particle viewers once per tick instead of once per spawned point.
2026-10-26 09:47:43 +01:00
YoyoNow bfbf98c1d6 Fix GUI.schemDialog for overriden Public list 2026-10-23 11:05:28 +02:00
YoyoNow c4b46e4431 Add override for Publics in GameModeConfig.Schematic 2026-10-21 08:22:05 +02:00
YoyoNow c238b9bdf2 Improve CheckCommand 2026-10-21 08:02:05 +02:00
YoyoNow fec4007120 Merge pull request 'Fixed Replay not starting for Normal Users' (#510) from 12312/SteamWar:main into main
Reviewed-on: SteamWar/SteamWar#510
2026-10-20 19:37:39 +02:00
94 changed files with 5632 additions and 1570 deletions
@@ -617,6 +617,15 @@ public final class GameModeConfig<M, W> {
*/ */
public final List<SchematicType> SubTypes; public final List<SchematicType> SubTypes;
/**
* List of publics that should be used. Mainly useful for an Event Mode
* where a subset of the regular publics should be used without duplicating
* those in the Database.
*
* @implSpec {@code []} by default, thus the GameModes publics are used.
*/
public final List<Integer> Publics;
/** /**
* Shortcut of the schematic type * Shortcut of the schematic type
* *
@@ -725,6 +734,7 @@ public final class GameModeConfig<M, W> {
Inset = new InsetConfig(loader.with("Inset")); Inset = new InsetConfig(loader.with("Inset"));
Type = null; Type = null;
SubTypesStrings = loader.getStringList("SubTypes"); SubTypesStrings = loader.getStringList("SubTypes");
Publics = loader.getIntList("Publics");
SubTypes = new ArrayList<>(); SubTypes = new ArrayList<>();
Shortcut = loader.getString("Shortcut", ""); Shortcut = loader.getString("Shortcut", "");
Material = loader.getMaterial("Material", "STONE_BUTTON"); Material = loader.getMaterial("Material", "STONE_BUTTON");
@@ -20,19 +20,14 @@
package de.steamwar.fightsystem.ai; package de.steamwar.fightsystem.ai;
import com.sk89q.worldedit.extent.clipboard.Clipboard; import com.sk89q.worldedit.extent.clipboard.Clipboard;
import de.steamwar.fightsystem.Config; import de.steamwar.fightsystem.fight.FightSchematic;
import de.steamwar.fightsystem.fight.FightTeam; import de.steamwar.fightsystem.fight.FightTeam;
import de.steamwar.fightsystem.states.FightState; import de.steamwar.fightsystem.states.FightState;
import de.steamwar.sql.SchematicNode; import de.steamwar.sql.SchematicNode;
import de.steamwar.sql.SteamwarUser; import de.steamwar.sql.SteamwarUser;
import java.util.List;
import java.util.Random;
public class DummyAI extends AI { public class DummyAI extends AI {
private static final Random random = new Random();
public DummyAI(FightTeam team) { public DummyAI(FightTeam team) {
super(team, SteamwarUser.get("public")); super(team, SteamwarUser.get("public"));
@@ -41,8 +36,7 @@ public class DummyAI extends AI {
@Override @Override
public SchematicNode chooseSchematic() { public SchematicNode chooseSchematic() {
List<SchematicNode> publics = SchematicNode.getAllSchematicsOfType(0, Config.GameModeConfig.Schematic.Type.toDB()); return FightSchematic.getPublicSchematic().orElseThrow();
return publics.get(random.nextInt(publics.size()));
} }
@Override @Override
@@ -255,7 +255,15 @@ public class GUI {
private static void schemDialog(Player p, SchematicType type, boolean publicSchems, boolean unchecked) { private static void schemDialog(Player p, SchematicType type, boolean publicSchems, boolean unchecked) {
SchematicSelector selector = new SchematicSelector(p, Config.test() ? SchematicSelector.selectSchematic() : SchematicSelector.selectSchematicType(unchecked ? type.checkType() : type), node -> { SchematicSelector selector = new SchematicSelector(p, Config.test() ? SchematicSelector.selectSchematic() : SchematicSelector.selectSchematicType(unchecked ? type.checkType() : type), node -> {
schemSelect(p, node); schemSelect(p, node);
}); }) {
@Override
protected List<SchematicNode> getSchematicList(SchematicNode parent) {
if (!publicSchems || parent != null) return super.getSchematicList(parent);
List<Integer> publics = Config.GameModeConfig.Schematic.Publics;
if (publics.isEmpty()) return super.getSchematicList(null);
return publics.stream().map(SchematicNode::byId).collect(Collectors.toList());
}
};
selector.setPublicMode(publicSchems ? SchematicSelector.PublicMode.PUBLIC_ONLY : SchematicSelector.PublicMode.PRIVATE_ONLY); selector.setPublicMode(publicSchems ? SchematicSelector.PublicMode.PUBLIC_ONLY : SchematicSelector.PublicMode.PRIVATE_ONLY);
selector.open(); selector.open();
} }
@@ -46,6 +46,7 @@ import org.bukkit.util.Vector;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.Random; import java.util.Random;
import java.util.logging.Level; import java.util.logging.Level;
@@ -109,11 +110,18 @@ public class FightSchematic extends StateDependent {
clipboard = null; clipboard = null;
} }
@Override private static final Random random = new Random();
public void enable() {
if (FightState.getFightState() == FightState.SPECTATE) return; public static Optional<SchematicNode> getPublicSchematic() {
List<Integer> publicsOverridden = Config.GameModeConfig.Schematic.Publics;
if (!publicsOverridden.isEmpty()) {
int publicId = publicsOverridden.get(random.nextInt(publicsOverridden.size()));
SchematicNode schematicNode = SchematicNode.getSchematicNode(publicId);
if (schematicNode != null) {
return Optional.of(schematicNode);
}
}
if (clipboard == null) {
List<SchematicNode> publics = SchematicNode.getAllSchematicsOfType(0, Config.GameModeConfig.Schematic.Type.toDB()); List<SchematicNode> publics = SchematicNode.getAllSchematicsOfType(0, Config.GameModeConfig.Schematic.Type.toDB());
if (publics.isEmpty()) { if (publics.isEmpty()) {
for (SchematicType type : Config.GameModeConfig.Schematic.SubTypes) { for (SchematicType type : Config.GameModeConfig.Schematic.SubTypes) {
@@ -123,17 +131,26 @@ public class FightSchematic extends StateDependent {
} }
} }
if (publics.isEmpty()) { if (publics.isEmpty()) {
return; return Optional.empty();
} }
} }
return Optional.of(publics.get(random.nextInt(publics.size())));
}
setSchematic(publics.get(new Random().nextInt(publics.size()))); @Override
public void enable() {
if (FightState.getFightState() == FightState.SPECTATE) return;
if (clipboard == null) {
Optional<SchematicNode> schematicNode = getPublicSchematic();
if (schematicNode.isEmpty()) return;
setSchematic(schematicNode.get());
} }
if (ArenaMode.AntiReplay.contains(Config.mode)) { if (ArenaMode.AntiReplay.contains(Config.mode)) {
boolean changeRotation = false; boolean changeRotation = false;
if (Config.GameModeConfig.WinConditions.contains(Winconditions.RANDOM_ROTATE)) { if (Config.GameModeConfig.WinConditions.contains(Winconditions.RANDOM_ROTATE)) {
changeRotation = new Random().nextBoolean(); changeRotation = random.nextBoolean();
usedRotate = rotate ^ changeRotation; usedRotate = rotate ^ changeRotation;
} }
if (team.isBlue()) { if (team.isBlue()) {
@@ -49,76 +49,341 @@ PORTAL_NO_WORLDEDIT_SELECTION = §cNo WorldEdit selection
PARTICLE_INVENTORY = §0Particle PARTICLE_INVENTORY = §0Particle
PARTICLE_DESELECT = §8No particle PARTICLE_DESELECT = §8No particle
PARTICLE_SHOW_HAS_ALL = §8You have all particles
PARTICLE_SHOW_ALL = §aShow all
PARTICLE_SHOW_UNLOCKED = §cShow unlocked
PARTICLE_UNLOCKED_BY = §eUnlocked by PARTICLE_UNLOCKED_BY = §eUnlocked by
PARTICLE_UNLOCKED_BY_TEAM = §fJoin a team PARTICLE_UNLOCKED_BY_TEAM = §fJoin a team
PARTICLE_UNLOCKED_BY_SPECIFIC_TEAM = §fTeam {0} PARTICLE_UNLOCKED_BY_SPECIFIC_TEAM = §fTeam {0}
PARTICLE_UNLOCKED_BY_SPECIFIC_TEAM_EASTER = §fTeam {0} (Easter Egg Hunt) PARTICLE_UNLOCKED_BY_SPECIFIC_TEAM_EASTER = §fTeam {0} (Easter Egg Hunt)
PARTICLE_UNLOCKED_BY_SPECIFIC_USER = §fUser {0} PARTICLE_UNLOCKED_BY_SPECIFIC_USER = §fUser {0}
PARTICLE_UNLOCKED_BY_SPECIFIC_USER_EASTER = §fUser {0} (Easter Egg Hunt) PARTICLE_UNLOCKED_BY_SPECIFIC_USER_EASTER = §fUser {0} (Easter Egg Hunt)
PARTICLE_UNLOCKED_BY_EVENT = §fEvent participation
PARTICLE_UNLOCKED_BY_SERVER_TEAM = §fServer Team PARTICLE_UNLOCKED_BY_SERVER_TEAM = §fServer Team
PARTICLE_UNLOCKED_BY_EVENT = §fEvent participation
PARTICLE_UNLOCKED_BY_EVENT_PLACEMENT = §f{0} 1., 2. or 3. Place PARTICLE_UNLOCKED_BY_EVENT_PLACEMENT = §f{0} 1., 2. or 3. Place
PARTICLE_UNLOCKED_BY_EVENT_PARTICIPATION = §f{0} PARTICLE_UNLOCKED_BY_EVENT_PARTICIPATION = §f{0}
PARTICLE_UNLOCKED_BY_EASTER_EGG_HUNT_HALF = §fFind 45 Easter Eggs PARTICLE_UNLOCKED_BY_EASTER_EGG_HUNT_HALF = §fFind 45 Easter Eggs
PARTICLE_UNLOCKED_BY_EASTER_EGG_HUNT_DIFFICULTY = §fFind all Easter Eggs with difficulty {0} PARTICLE_UNLOCKED_BY_EASTER_EGG_HUNT_DIFFICULTY = §fFind all Easter Eggs with difficulty {0}
PARTICLE_ATTRIBUTE = §eAttributes§7:
PARTICLE_ATTRIBUTE_CIRCLE = §8-§f Ring
PARTICLE_ATTRIBUTE_BI_CIRCLE = §8-§f Double ring
PARTICLE_ATTRIBUTE_TRI_CIRCLE = §8-§f Tripple ring
PARTICLE_ATTRIBUTE_CLOUD = §8-§f Cloud
PARTICLE_ATTRIBUTE_TICK = §8-§f Always active
PARTICLE_ATTRIBUTE_NON_FLOOR = §8-§f In air
PARTICLE_ATTRIBUTE_FLOOR = §8-§f On ground
PARTICLE_ATTRIBUTE_NON_FLYING = §8-§f Not flying
PARTICLE_ATTRIBUTE_FLYING = §8-§f Flying
PARTICLE_ATTRIBUTE_WING = §8-§f Wings
PARTICLE_ATTRIBUTE_SNEAKING = §8-§f Sneaking
PARTICLE_ATTRIBUTE_NON_MOVING = §8-§f Not moving
PARTICLE_ATTRIBUTE_SEPARATOR = §f
PARTICLE_SELECT = §eClick to select PARTICLE_SELECT = §eClick to select
PARTICLE_LOCKED = §c§lLocked PARTICLE_LOCKED = §c§lLocked
PARTICLE_DEACTIVATED = §cThere are too many players online, particles are deactivated PARTICLE_DEACTIVATED = §cThere are too many players online, particles are deactivated
PARTICLE_SNEEZE = §aSneeze PARTICLE_BACK = §7Back
PARTICLE_SMOKE = §7Smoke PARTICLE_SLOT_OPEN = §eClick to open
PARTICLE_FIRE = §cFire PARTICLE_SLOT_CLICK_USE = §eClick to use
PARTICLE_WATER = §bWater PARTICLE_SLOT_CLICK_STOP = §eClick to stop using
PARTICLE_HEART = §cHearts PARTICLE_SLOT_SHIFT_EDIT = §eShift click to edit
PARTICLE_NOTES = §eNotes PARTICLE_SLOT_NAME = §f{0} §8(§e{1}§8/§7{2}§8)
PARTICLE_NAUTILUS = §aNautilus PARTICLE_SLOT_TITLE = §0{0} §8({1}/{2})
PARTICLE_SNOWBALL = §fSnowball PARTICLE_SHIFT_TO_DELETE = §7Shift click to delete
PARTICLE_EFFECT = §5Effect PARTICLE_SECTION_DESIGN = §6§lDesign
PARTICLE_CAMPFIRE = §7Smoke PARTICLE_SECTION_PARTICLE = §6§lParticle
PARTICLE_MAGIC = §5Magic PARTICLE_SECTION_SHAPE = §6§lShape
PARTICLE_ANGRY = §4Angry PARTICLE_SECTION_TIMING = §6§lTiming and position
PARTICLE_SLIME = §aSlime PARTICLE_SECTION_CONDITIONS = §6§lConditions
PARTICLE_MOB = §7Mob PARTICLE_FIXED_LOOK = §7This design always looks the same
PARTICLE_SQUID = §8Squid PARTICLE_VALUE_CLICK = §eClick to change
PARTICLE_BUBBLE = §aBubbles PARTICLE_VALUE_TOGGLE = §eClick to toggle
PARTICLE_HONEY = §6Honey PARTICLE_VALUE_CYCLE = §eLeft click§8: §7next, §eright click§8: §7previous
PARTICLE_NECTAR = §6Nectar PARTICLE_VALUE_SELECTED = §aSelected
PARTICLE_FIREWORK = §7Firework PARTICLE_VALUE_RESET = §eReset to {0}
PARTICLE_DRAGON_BREATH = §5Dragon breath PARTICLE_NUMBER_RANGE = §7Range§8: §e{0} §8- §e{1}
PARTICLE_DAMAGE = §5Damage PARTICLE_NUMBER_TYPE = §eClick to type a value
PARTICLE_DOLPHIN = §dDolphin PARTICLE_NUMBER_RESULT = §7→ §e{0}
PARTICLE_WITCH = §5Witch PARTICLE_NUMBER_AT_MAX = §7→ §e{0} §8(max)
PARTICLE_ENCHANTING = §eEnchantment PARTICLE_NUMBER_AT_MIN = §7→ §e{0} §8(min)
PARTICLE_HAPPY = §2Joy PARTICLE_COLOR_EDITOR = §0Colours
PARTICLE_FLAME = §7Flames PARTICLE_COLOR_REMOVE = §eClick to remove
PARTICLE_END_ROD = §fEnd Rod PARTICLE_COLOR_ADD = §aAdd colour
PARTICLE_CLOUD = §7Cloud PARTICLE_COLOR_HEX = §eEnter hex code
PARTICLE_TOTEM = §aTotem PARTICLE_COLOR_HEX_EXAMPLE = §7For example #FF8800
PARTICLE_TOWN = §5Town PARTICLE_COLOR_HEX_TITLE = Hex colour
PARTICLE_ENCHANTING_CIRCLE = §fEnchanted PARTICLE_COLOR_HEX_INVALID = §cThat is not a hex colour like #FF8800
PARTICLE_WATER_FIRE = §bWater§7/§cFire PARTICLE_COLOR_RANDOM = §eUse random colours
PARTICLE_MAGIC_ENCHANTING = §5Magic/Enchantment PARTICLE_SLOT_EMPTY = §7Empty slot
PARTICLE_WINGS_EVIL = §5Purple wings PARTICLE_SLOT_CREATE = §eClick to create
PARTICLE_SLOT_ENTRIES = §7{0} entries
PARTICLE_SLOT_ACTIVE = §aActive
PARTICLE_SLOT_VIEW = §eClick to view
PARTICLE_SLOT_ADD_ENTRY = §aAdd entry
PARTICLE_SLOT_MAX_ENTRIES = §cThis slot is full
PARTICLE_SLOT_OVER_BUDGET = §cThat would spawn too many particles for this slot
PARTICLE_SLOT_RENAME = §eRename
PARTICLE_SLOT_RENAME_TITLE = Slot name
PARTICLE_SLOT_ICON = §eChange icon
PARTICLE_SLOT_ICON_TITLE = §0Slot icon
PARTICLE_SLOT_DELETE = §cDelete slot
PARTICLE_SLOT_COST = §7Particles per tick§8: §e{0}§8/§7{1}
PARTICLE_ENTRY_EDITOR = §0{0}
PARTICLE_ENTRY_DESIGN = §7Design§8: {0}
PARTICLE_ENTRY_PARTICLE = §7Particle§8: {0}
PARTICLE_ENTRY_LOCKED = §cDesign locked, not shown
PARTICLE_ENTRY_COST = §7Particles per tick§8: §e{0}
PARTICLE_ENTRY_COST_SHARE = §7This entry§8: §e{0}
PARTICLE_ENTRY_EDIT = §eClick to edit
PARTICLE_ENTRY_DELETE = §cDelete entry
PARTICLE_DESIGN_PICKER = §0Choose design
PARTICLE_CHOICE_PICKER = §0{0}
PARTICLE_COLOR_PICKER = §0Choose colour
PARTICLE_VALUE = §7Value§8: {0}
PARTICLE_VALUE_RANDOM = §7Random colours
PARTICLE_VALUE_YES = §aYes
PARTICLE_VALUE_NO = §cNo
PARTICLE_DESIGN_SIMPLE = §fSimple
PARTICLE_DESIGN_CIRCLE = §fCircle
PARTICLE_DESIGN_WING = §fWings
PARTICLE_DESIGN_CLOUD = §fCloud
PARTICLE_DESIGN_BURST = §fBurst
PARTICLE_DESIGN_FOOTPRINTS = §fFootprints
PARTICLE_DESIGN_TEAM_LINK = §fTeam link
PARTICLE_DESIGN_PULSE_SHIMMER = §fShimmer
PARTICLE_DESIGN_HEART_BEAT = §fHeartbeat
PARTICLE_PARAM_PARTICLE = §fParticle
PARTICLE_PARAM_GLYPH_FLIGHT = §fGlyph flight
PARTICLE_PARAM_BURST_TRIGGER = §fWhen
PARTICLE_PARAM_BURST_SHAPE = §fShape
PARTICLE_PARAM_BURST_RADIUS = §fRing radius
PARTICLE_PARAM_BURST_FALL = §fMinimum fall height
PARTICLE_PARAM_FOOTPRINT_LIFETIME = §fPrint lifetime (ticks)
PARTICLE_PARAM_FOOTPRINT_STEP = §fDistance between prints
PARTICLE_PARAM_LINK_SPACING = §fDistance between particles
PARTICLE_PARAM_PULSE_SPEED = §fPulse duration (ticks)
PARTICLE_PARAM_PULSE_ROTATION = §fRotation
PARTICLE_PARAM_HEART_BEAT_SPEED = §fBeat duration (ticks)
PARTICLE_PARAM_COUNT = §fAmount
PARTICLE_PARAM_SPREAD_X = §fSpread X
PARTICLE_PARAM_SPREAD_Y = §fSpread Y
PARTICLE_PARAM_SPREAD_Z = §fSpread Z
PARTICLE_PARAM_SPEED = §fSpeed
PARTICLE_PARAM_COLORS = §fColours
PARTICLE_PARAM_BLOCK = §fBlock
PARTICLE_PARAM_TRAIL_DURATION = §fTrail duration (ticks)
PARTICLE_PARAM_TRAIL_TARGET_X = §fTrail target X
PARTICLE_PARAM_TRAIL_TARGET_Y = §fTrail target Y
PARTICLE_PARAM_TRAIL_TARGET_Z = §fTrail target Z
PARTICLE_PARAM_OFFSET_X = §fOffset X
PARTICLE_PARAM_OFFSET_Y = §fOffset Y
PARTICLE_PARAM_OFFSET_Z = §fOffset Z
PARTICLE_PARAM_INTERVAL = §fEvery n ticks
PARTICLE_PARAM_PLACEMENT = §fRelative to
PARTICLE_PARAM_TICK_MODE = §fShown
PARTICLE_PARAM_MOVEMENT = §fMovement
PARTICLE_PARAM_GLIDING = §fElytra
PARTICLE_PARAM_GROUND = §fGround
PARTICLE_PARAM_SNEAK = §fSneaking
PARTICLE_PARAM_VISIBILITY = §fVisible to
PARTICLE_PARAM_RADIUS = §fRadius
PARTICLE_PARAM_CIRCLE_SPEED = §fRotation speed
PARTICLE_PARAM_PHASE = §fStart angle
PARTICLE_PARAM_RISE = §fUp and down height
PARTICLE_PARAM_RISE_SPEED = §fUp and down speed
PARTICLE_PARAM_BOUNCE = §fBounce back
PARTICLE_PARAM_INVERTED = §fStart at the top
PARTICLE_PARAM_WING_IMAGE = §fWing shape
PARTICLE_PARAM_WING_SIZE = §fWing size
PARTICLE_PARAM_WING_DETAIL = §fPoints per wing
PARTICLE_PARAM_WING_COLORING = §fColour mode
PARTICLE_PARAM_WING_SPARKLE = §fSparkle (points shown, %)
PARTICLE_PARAM_WING_MOTION = §fMotion
PARTICLE_CHOICE_TICKMODE_MOVE = §fWhile moving
PARTICLE_CHOICE_TICKMODE_ALWAYS = §fAlways
PARTICLE_CHOICE_PLACEMENT_WORLD = §fWorld (X east, Y up, Z south)
PARTICLE_CHOICE_PLACEMENT_BODY = §fBody (X right, Y up, Z forward)
PARTICLE_CHOICE_MOVEMENT_ANY = §fAny movement
PARTICLE_CHOICE_MOVEMENT_MOVING = §fOnly while moving
PARTICLE_CHOICE_MOVEMENT_STANDING_STILL = §fOnly while standing still
PARTICLE_CHOICE_GLIDING_ANY = §fWith or without elytra
PARTICLE_CHOICE_GLIDING_GLIDING = §fOnly while gliding
PARTICLE_CHOICE_GLIDING_NOT_GLIDING = §fNot while gliding
PARTICLE_CHOICE_GROUND_ANY = §fOn ground and in air
PARTICLE_CHOICE_GROUND_ON_GROUND = §fOnly on ground
PARTICLE_CHOICE_GROUND_IN_AIR = §fOnly in air
PARTICLE_CHOICE_SNEAK_ANY = §fSneaking or not
PARTICLE_CHOICE_SNEAK_SNEAKING = §fOnly while sneaking
PARTICLE_CHOICE_SNEAK_NOT_SNEAKING = §fNot while sneaking
PARTICLE_CHOICE_VISIBILITY_EVERYONE = §fEveryone
PARTICLE_CHOICE_VISIBILITY_ONLY_SELF = §fOnly me
PARTICLE_CHOICE_VISIBILITY_ONLY_OTHERS = §fOnly others
PARTICLE_CHOICE_IMAGE_SIMPLE = §fSimple
PARTICLE_CHOICE_IMAGE_COMPLEX = §fComplex
PARTICLE_CHOICE_IMAGE_SWORD = §fSword
PARTICLE_CHOICE_IMAGE_BAT = §fBat
PARTICLE_CHOICE_IMAGE_BUTTERFLY = §fButterfly
PARTICLE_CHOICE_IMAGE_FAIRY = §fFairy
PARTICLE_CHOICE_IMAGE_MWGL = §fMiniWarGear League
PARTICLE_CHOICE_IMAGE_SW = §fSteamWar
PARTICLE_CHOICE_IMAGE_WGS = §fWarGear Season
PARTICLE_CHOICE_IMAGE_SWORD_CROSSED = §fCrossed Swords
PARTICLE_CHOICE_COLORING_OVER_TIME = §fChange over time
PARTICLE_CHOICE_COLORING_ALONG_WING = §fFrom the back to the tips
PARTICLE_CHOICE_MOTION_NONE = §fNone
PARTICLE_CHOICE_MOTION_FLAP_SLOW = §fFlap slowly
PARTICLE_CHOICE_MOTION_FLAP_FAST = §fFlap quickly
PARTICLE_CHOICE_MOTION_BEAT_ON_JUMP = §fBeat on jumping
PARTICLE_CHOICE_TRIGGER_JUMP = §fOn jumping
PARTICLE_CHOICE_TRIGGER_LAND = §fOn landing
PARTICLE_CHOICE_TRIGGER_SNEAK = §fOn sneaking
PARTICLE_CHOICE_BURSTSHAPE_PUFF = §fPuff
PARTICLE_CHOICE_BURSTSHAPE_RING = §fRing
PARTICLE_CHOICE_GLYPHFLIGHT_NORMAL = §fFloat down
PARTICLE_CHOICE_GLYPHFLIGHT_INTO_PLAYER = §fInto the player
PARTICLE_CHOICE_GLYPHFLIGHT_OUT_OF_PLAYER = §fOut of the player
# Particle types
PARTICLE_TYPE_DUST = §fDust
PARTICLE_TYPE_DUST_COLOR_TRANSITION = §fColour fading dust
PARTICLE_TYPE_ENTITY_EFFECT = §fPotion swirl
PARTICLE_TYPE_TINTED_LEAVES = §fTinted leaves
PARTICLE_TYPE_BLOCK = §fBlock breaking
PARTICLE_TYPE_BLOCK_CRUMBLE = §fBlock crumbling
PARTICLE_TYPE_BLOCK_MARKER = §fBlock marker
PARTICLE_TYPE_DUST_PILLAR = §fDust pillar
PARTICLE_TYPE_FALLING_DUST = §fFalling dust
PARTICLE_TYPE_ITEM = §fItem breaking
PARTICLE_TYPE_TRAIL = §fTrail
PARTICLE_TYPE_FLAME = §fFlame
PARTICLE_TYPE_SMALL_FLAME = §fSmall flame
PARTICLE_TYPE_SOUL_FIRE_FLAME = §fSoul fire flame
PARTICLE_TYPE_SOUL = §fSoul
PARTICLE_TYPE_LAVA = §fLava pop
PARTICLE_TYPE_SMOKE = §fSmoke
PARTICLE_TYPE_LARGE_SMOKE = §fLarge smoke
PARTICLE_TYPE_WHITE_SMOKE = §fWhite smoke
PARTICLE_TYPE_CAMPFIRE_COSY_SMOKE = §fCampfire smoke
PARTICLE_TYPE_CAMPFIRE_SIGNAL_SMOKE = §fSignal smoke
PARTICLE_TYPE_ASH = §fAsh
PARTICLE_TYPE_WHITE_ASH = §fWhite ash
PARTICLE_TYPE_POOF = §fPoof
PARTICLE_TYPE_CLOUD = §fCloud
PARTICLE_TYPE_SNEEZE = §fSneeze
PARTICLE_TYPE_SPIT = §fSpit
PARTICLE_TYPE_EXPLOSION = §fExplosion
PARTICLE_TYPE_SONIC_BOOM = §fSonic boom
PARTICLE_TYPE_SWEEP_ATTACK = §fSweep attack
PARTICLE_TYPE_GUST = §fGust
PARTICLE_TYPE_SMALL_GUST = §fSmall gust
PARTICLE_TYPE_DRIPPING_WATER = §fDripping water
PARTICLE_TYPE_FALLING_WATER = §fFalling water
PARTICLE_TYPE_DRIPPING_LAVA = §fDripping lava
PARTICLE_TYPE_FALLING_LAVA = §fFalling lava
PARTICLE_TYPE_LANDING_LAVA = §fLanding lava
PARTICLE_TYPE_DRIPPING_HONEY = §fDripping honey
PARTICLE_TYPE_FALLING_HONEY = §fFalling honey
PARTICLE_TYPE_LANDING_HONEY = §fLanding honey
PARTICLE_TYPE_FALLING_NECTAR = §fFalling nectar
PARTICLE_TYPE_DRIPPING_OBSIDIAN_TEAR = §fDripping obsidian tear
PARTICLE_TYPE_FALLING_OBSIDIAN_TEAR = §fFalling obsidian tear
PARTICLE_TYPE_LANDING_OBSIDIAN_TEAR = §fLanding obsidian tear
PARTICLE_TYPE_DRIPPING_DRIPSTONE_WATER = §fDripstone water
PARTICLE_TYPE_FALLING_DRIPSTONE_WATER = §fFalling dripstone water
PARTICLE_TYPE_DRIPPING_DRIPSTONE_LAVA = §fDripstone lava
PARTICLE_TYPE_FALLING_DRIPSTONE_LAVA = §fFalling dripstone lava
PARTICLE_TYPE_RAIN = §fRain
PARTICLE_TYPE_SPLASH = §fSplash
PARTICLE_TYPE_FISHING = §fFishing wake
PARTICLE_TYPE_UNDERWATER = §fUnderwater
PARTICLE_TYPE_BUBBLE_POP = §fBubble pop
PARTICLE_TYPE_DOLPHIN = §fDolphin trail
PARTICLE_TYPE_NAUTILUS = §fNautilus
PARTICLE_TYPE_SQUID_INK = §fSquid ink
PARTICLE_TYPE_GLOW_SQUID_INK = §fGlow squid ink
PARTICLE_TYPE_GLOW = §fGlow
PARTICLE_TYPE_FALLING_SPORE_BLOSSOM = §fFalling spore blossom
PARTICLE_TYPE_SPORE_BLOSSOM_AIR = §fFloating spores
PARTICLE_TYPE_CHERRY_LEAVES = §fCherry leaves
PARTICLE_TYPE_PALE_OAK_LEAVES = §fPale oak leaves
PARTICLE_TYPE_CRIMSON_SPORE = §fCrimson spores
PARTICLE_TYPE_WARPED_SPORE = §fWarped spores
PARTICLE_TYPE_MYCELIUM = §fMycelium
PARTICLE_TYPE_COMPOSTER = §fComposter
PARTICLE_TYPE_FIREFLY = §fFirefly
PARTICLE_TYPE_SNOWFLAKE = §fSnowflake
PARTICLE_TYPE_ITEM_SNOWBALL = §fSnowball
PARTICLE_TYPE_ITEM_SLIME = §fSlime
PARTICLE_TYPE_ITEM_COBWEB = §fCobweb
PARTICLE_TYPE_EGG_CRACK = §fEgg crack
PARTICLE_TYPE_DUST_PLUME = §fDust plume
PARTICLE_TYPE_INFESTED = §fInfested
PARTICLE_TYPE_HEART = §fHeart
PARTICLE_TYPE_NOTE = §fNote
PARTICLE_TYPE_HAPPY_VILLAGER = §fHappy villager
PARTICLE_TYPE_ANGRY_VILLAGER = §fAngry villager
PARTICLE_TYPE_DAMAGE_INDICATOR = §fDamage
PARTICLE_TYPE_CRIT = §fCritical hit
PARTICLE_TYPE_ENCHANTED_HIT = §fEnchanted hit
PARTICLE_TYPE_ENCHANT = §fEnchanting glyphs
PARTICLE_TYPE_EFFECT = §fPotion effect
PARTICLE_TYPE_INSTANT_EFFECT = §fInstant effect
PARTICLE_TYPE_WITCH = §fWitch magic
PARTICLE_TYPE_END_ROD = §fEnd rod
PARTICLE_TYPE_PORTAL = §fPortal
PARTICLE_TYPE_REVERSE_PORTAL = §fReverse portal
PARTICLE_TYPE_DRAGON_BREATH = §fDragon breath
PARTICLE_TYPE_FIREWORK = §fFirework spark
PARTICLE_TYPE_TOTEM_OF_UNDYING = §fTotem
PARTICLE_TYPE_ELECTRIC_SPARK = §fElectric spark
PARTICLE_TYPE_SCRAPE = §fScrape
PARTICLE_TYPE_WAX_ON = §fWax on
PARTICLE_TYPE_WAX_OFF = §fWax off
PARTICLE_TYPE_SCULK_SOUL = §fSculk soul
PARTICLE_TYPE_SCULK_CHARGE = §fSculk charge
PARTICLE_TYPE_SCULK_CHARGE_POP = §fSculk charge pop
PARTICLE_TYPE_SHRIEK = §fShriek
PARTICLE_TYPE_RAID_OMEN = §fRaid omen
PARTICLE_TYPE_TRIAL_OMEN = §fTrial omen
PARTICLE_TYPE_OMINOUS_SPAWNING = §fOminous spawning
PARTICLE_TYPE_TRIAL_SPAWNER_DETECTION = §fTrial spawner flame
PARTICLE_TYPE_TRIAL_SPAWNER_DETECTION_OMINOUS = §fOminous trial spawner flame
PARTICLE_TYPE_VAULT_CONNECTION = §fVault connection
# Particle blocks
PARTICLE_CHOICE_BLOCK_STONE = §fStone
PARTICLE_CHOICE_BLOCK_COBBLESTONE = §fCobblestone
PARTICLE_CHOICE_BLOCK_DIRT = §fDirt
PARTICLE_CHOICE_BLOCK_GRASS_BLOCK = §fGrass block
PARTICLE_CHOICE_BLOCK_SAND = §fSand
PARTICLE_CHOICE_BLOCK_RED_SAND = §fRed sand
PARTICLE_CHOICE_BLOCK_GRAVEL = §fGravel
PARTICLE_CHOICE_BLOCK_OAK_PLANKS = §fOak planks
PARTICLE_CHOICE_BLOCK_OAK_LEAVES = §fOak leaves
PARTICLE_CHOICE_BLOCK_CHERRY_LEAVES = §fCherry leaves
PARTICLE_CHOICE_BLOCK_SNOW_BLOCK = §fSnow block
PARTICLE_CHOICE_BLOCK_ICE = §fIce
PARTICLE_CHOICE_BLOCK_GLASS = §fGlass
PARTICLE_CHOICE_BLOCK_TNT = §fTNT
PARTICLE_CHOICE_BLOCK_PUMPKIN = §fPumpkin
PARTICLE_CHOICE_BLOCK_MELON = §fMelon
PARTICLE_CHOICE_BLOCK_HONEY_BLOCK = §fHoney block
PARTICLE_CHOICE_BLOCK_SLIME_BLOCK = §fSlime block
PARTICLE_CHOICE_BLOCK_NETHERRACK = §fNetherrack
PARTICLE_CHOICE_BLOCK_SOUL_SAND = §fSoul sand
PARTICLE_CHOICE_BLOCK_MAGMA_BLOCK = §fMagma block
PARTICLE_CHOICE_BLOCK_END_STONE = §fEnd stone
PARTICLE_CHOICE_BLOCK_OBSIDIAN = §fObsidian
PARTICLE_CHOICE_BLOCK_PRISMARINE = §fPrismarine
PARTICLE_CHOICE_BLOCK_SCULK = §fSculk
PARTICLE_CHOICE_BLOCK_AMETHYST_BLOCK = §fAmethyst block
PARTICLE_CHOICE_BLOCK_COAL_BLOCK = §fBlock of coal
PARTICLE_CHOICE_BLOCK_IRON_BLOCK = §fBlock of iron
PARTICLE_CHOICE_BLOCK_GOLD_BLOCK = §fBlock of gold
PARTICLE_CHOICE_BLOCK_REDSTONE_BLOCK = §fBlock of redstone
PARTICLE_CHOICE_BLOCK_LAPIS_BLOCK = §fBlock of lapis lazuli
PARTICLE_CHOICE_BLOCK_EMERALD_BLOCK = §fBlock of emerald
PARTICLE_CHOICE_BLOCK_DIAMOND_BLOCK = §fBlock of diamond
PARTICLE_CHOICE_BLOCK_WHITE_WOOL = §fWhite wool
PARTICLE_CHOICE_BLOCK_RED_WOOL = §fRed wool
PARTICLE_CHOICE_BLOCK_ORANGE_WOOL = §fOrange wool
PARTICLE_CHOICE_BLOCK_YELLOW_WOOL = §fYellow wool
PARTICLE_CHOICE_BLOCK_LIME_WOOL = §fLime wool
PARTICLE_CHOICE_BLOCK_LIGHT_BLUE_WOOL = §fLight blue wool
PARTICLE_CHOICE_BLOCK_BLUE_WOOL = §fBlue wool
PARTICLE_CHOICE_BLOCK_PURPLE_WOOL = §fPurple wool
PARTICLE_CHOICE_BLOCK_PINK_WOOL = §fPink wool
PARTICLE_CHOICE_BLOCK_BLACK_WOOL = §fBlack wool
PARTICLE_PLAYER_HAYLIM_AURA = §fHaylim\'s Aura PARTICLE_PLAYER_HAYLIM_AURA = §fHaylim\'s Aura
PARTICLE_PLAYER_1063 = §7Circling Circle PARTICLE_PLAYER_1063 = §7Circling Circle
@@ -136,34 +401,12 @@ PARTICLE_TEAM_PULSE_AURA_3 = §fPulse Aura §fEnchanted
PARTICLE_TEAM_PULSE_LOGO = §fPulse Logo PARTICLE_TEAM_PULSE_LOGO = §fPulse Logo
PARTICLE_TEAM_PULSE_HEART_BEAT = §cHeart Beat PARTICLE_TEAM_PULSE_HEART_BEAT = §cHeart Beat
PARTICLE_RAINCLOUD_NORMAL = §fRaincloud
PARTICLE_RAINCLOUD_RED = §fLava cloud
PARTICLE_RAINCLOUD_YELLOW = §fNectar cloud
PARTICLE_RAINCLOUD_ORANGE = §fHoney cloud
PARTICLE_RAINCLOUD_GREEN = §fSpore Blossom cloud
PARTICLE_RAINCLOUD_PURPLE = §fObsidian Tears cloud
PARTICLE_RAINCLOUD_NAUTILUS = §fNautilus cloud
PARTICLE_RAINCLOUD_ENCHANTMENT = §fEnchantment cloud
PARTICLE_RAINCLOUD_SLIME = §fSlime cloud
PARTICLE_RAINCLOUD_HAIL = §fHail cloud
PARTICLE_RAINCLOUD_SNOW = §fSnow cloud
PARTICLE_RAINCLOUD_TOTEM = §fTotem cloud
PARTICLE_EVENT_ENCHANTING = §cEnchantment PARTICLE_EVENT_ENCHANTING = §cEnchantment
PARTICLE_EVENT_CLOUD = §fClouds
PARTICLE_EVENT_SMOKE = §7Smoke
PARTICLE_EVENT_WATER = §bWater
PARTICLE_EVENT_WINGS = §fWings
PARTICLE_EVENT_RAIN_CLOUD = §fRandom Raincloud PARTICLE_EVENT_RAIN_CLOUD = §fRandom Raincloud
PARTICLE_EVENT_WGS = §fWGS
PARTICLE_EVENT_WARGEARCLASH = §fClash
PARTICLE_EGG_HUNT_EASY = §aGreenery
PARTICLE_EGG_HUNT_MEDIUM = §eHide and Seek PARTICLE_EGG_HUNT_MEDIUM = §eHide and Seek
PARTICLE_EGG_HUNT_HARD = §cOrbit PARTICLE_EGG_HUNT_HARD = §cOrbit
PARTICLE_EGG_HUNT_EXTREME = §5Purple rain
PARTICLE_EGG_HUNT_ADVANCED = §5Lift off PARTICLE_EGG_HUNT_ADVANCED = §5Lift off
PARTICLE_EGG_HUNT_HALF = §fThe hunt is on
JUMP_AND_RUN_PROGRESS = §e{0}§8/§f{1} §c{2} §7{3} JUMP_AND_RUN_PROGRESS = §e{0}§8/§f{1} §c{2} §7{3}
JUMP_AND_RUN_CANCEL = {0} JUMP_AND_RUN_CANCEL = {0}
@@ -49,109 +49,353 @@ PORTAL_NO_WORLDEDIT_SELECTION = §cKeine WorldEdit Selection
PARTICLE_INVENTORY = §0Partikel PARTICLE_INVENTORY = §0Partikel
PARTICLE_DESELECT = §8Keine Partikel PARTICLE_DESELECT = §8Keine Partikel
PARTICLE_SHOW_HAS_ALL = §8Du hast alle Partikel
PARTICLE_SHOW_ALL = §aZeige alle
PARTICLE_SHOW_UNLOCKED = §cZeige freigeschaltete
PARTICLE_UNLOCKED_BY = §eFreigeschaltet durch PARTICLE_UNLOCKED_BY = §eFreigeschaltet durch
PARTICLE_UNLOCKED_BY_TEAM = §fTeambeitritt PARTICLE_UNLOCKED_BY_TEAM = §fTeambeitritt
PARTICLE_UNLOCKED_BY_SPECIFIC_TEAM = §fTeam {0} PARTICLE_UNLOCKED_BY_SPECIFIC_TEAM = §fTeam {0}
PARTICLE_UNLOCKED_BY_SPECIFIC_TEAM_EASTER = §fTeam {0} (Oster-Eierer-Suche) PARTICLE_UNLOCKED_BY_SPECIFIC_TEAM_EASTER = §fTeam {0} (Oster-Eierer-Suche)
PARTICLE_UNLOCKED_BY_SPECIFIC_USER = §fUser {0} PARTICLE_UNLOCKED_BY_SPECIFIC_USER = §fUser {0}
PARTICLE_UNLOCKED_BY_SPECIFIC_USER_EASTER = §fUser {0} (Oster-Eierer-Suche) PARTICLE_UNLOCKED_BY_SPECIFIC_USER_EASTER = §fUser {0} (Oster-Eierer-Suche)
PARTICLE_UNLOCKED_BY_EVENT = §fEventteilnahme
PARTICLE_UNLOCKED_BY_SERVER_TEAM = §fServerteam PARTICLE_UNLOCKED_BY_SERVER_TEAM = §fServerteam
PARTICLE_UNLOCKED_BY_EVENT = §fEventteilnahme
PARTICLE_UNLOCKED_BY_EVENT_PLACEMENT = §f{0} 1., 2. oder 3. Platz PARTICLE_UNLOCKED_BY_EVENT_PLACEMENT = §f{0} 1., 2. oder 3. Platz
PARTICLE_UNLOCKED_BY_EVENT_PARTICIPATION = §f{0} PARTICLE_UNLOCKED_BY_EVENT_PARTICIPATION = §f{0}
PARTICLE_UNLOCKED_BY_EASTER_EGG_HUNT_HALF = §fFinde 45 Oster-Eierer PARTICLE_UNLOCKED_BY_EASTER_EGG_HUNT_HALF = §fFinde 45 Oster-Eierer
PARTICLE_UNLOCKED_BY_EASTER_EGG_HUNT_DIFFICULTY = §fAlle Eierer mit Schwierigkeit {0} finden PARTICLE_UNLOCKED_BY_EASTER_EGG_HUNT_DIFFICULTY = §fAlle Eierer mit Schwierigkeit {0} finden
PARTICLE_ATTRIBUTE_CIRCLE = §8-§f Ring
PARTICLE_ATTRIBUTE_BI_CIRCLE = §8-§f Doppelring
PARTICLE_ATTRIBUTE_TRI_CIRCLE = §8-§f Dreifachring
PARTICLE_ATTRIBUTE_CLOUD = §8-§f Wolke
PARTICLE_ATTRIBUTE_TICK = §8-§f Immer aktiv
PARTICLE_ATTRIBUTE_NON_FLOOR = §8-§f In der Luft
PARTICLE_ATTRIBUTE_FLOOR = §8-§f Auf dem Boden
PARTICLE_ATTRIBUTE_NON_FLYING = §8-§f Nicht am Fliegen
PARTICLE_ATTRIBUTE_FLYING = §8-§f Am Fliegen
PARTICLE_ATTRIBUTE_WING = §8-§f Flügel
PARTICLE_ATTRIBUTE_SNEAKING = §8-§f Ducken
PARTICLE_ATTRIBUTE_NON_MOVING = §8-§f Beim Stehen
PARTICLE_ATTRIBUTE = §eAttribute§7:
PARTICLE_SELECT = §eZum Auswählen klicken PARTICLE_SELECT = §eZum Auswählen klicken
PARTICLE_LOCKED = §c§lGesperrt PARTICLE_LOCKED = §c§lGesperrt
PARTICLE_DEACTIVATED = §cEs sind zu viele Spieler online! Partikel sind deaktiviert. PARTICLE_DEACTIVATED = §cEs sind zu viele Spieler online! Partikel sind deaktiviert.
PARTICLE_SNEEZE = §aSneeze PARTICLE_BACK = §7Zurück
PARTICLE_SMOKE = §7Rauch PARTICLE_SLOT_OPEN = §eZum Öffnen klicken
PARTICLE_FIRE = §cFeuer PARTICLE_SLOT_CLICK_USE = §eZum Verwenden klicken
PARTICLE_WATER = §bWasser PARTICLE_SLOT_CLICK_STOP = §eZum Beenden klicken
PARTICLE_HEART = §cHerzen PARTICLE_SLOT_SHIFT_EDIT = §eZum Bearbeiten Shift-Klick
PARTICLE_NOTES = §eNoten PARTICLE_SLOT_NAME = §f{0} §8(§e{1}§8/§7{2}§8)
PARTICLE_NAUTILUS = §aNautilus PARTICLE_SLOT_TITLE = §0{0} §8({1}/{2})
PARTICLE_SNOWBALL = §fSchneeball PARTICLE_SHIFT_TO_DELETE = §7Zum Löschen Shift-Klick
PARTICLE_EFFECT = §5Effekt PARTICLE_SECTION_DESIGN = §6§lDesign
PARTICLE_CAMPFIRE = §7Rauch PARTICLE_SECTION_PARTICLE = §6§lPartikel
PARTICLE_MAGIC = §5Magie PARTICLE_SECTION_SHAPE = §6§lForm
PARTICLE_ANGRY = §4Wut PARTICLE_SECTION_TIMING = §6§lZeit und Position
PARTICLE_SLIME = §aSchleim PARTICLE_SECTION_CONDITIONS = §6§lBedingungen
PARTICLE_MOB = §7Mob PARTICLE_FIXED_LOOK = §7Dieses Design sieht immer gleich aus
PARTICLE_SQUID = §8Tintenfisch PARTICLE_VALUE_CLICK = §eZum Ändern klicken
PARTICLE_BUBBLE = §aBlasen PARTICLE_VALUE_TOGGLE = §eZum Umschalten klicken
PARTICLE_HONEY = §6Honig PARTICLE_VALUE_CYCLE = §eLinksklick§8: §7weiter, §eRechtsklick§8: §7zurück
PARTICLE_NECTAR = §6Nektar PARTICLE_VALUE_SELECTED = §aAusgewählt
PARTICLE_FIREWORK = §7Feuerwerk PARTICLE_VALUE_RESET = §eAuf {0} zurücksetzen
PARTICLE_DRAGON_BREATH = §5Drachenatem PARTICLE_NUMBER_RANGE = §7Bereich§8: §e{0} §8- §e{1}
PARTICLE_DAMAGE = §5Schaden PARTICLE_NUMBER_TYPE = §eZum Eingeben klicken
PARTICLE_DOLPHIN = §dDelphin PARTICLE_NUMBER_RESULT = §7→ §e{0}
PARTICLE_WITCH = §5Hexe PARTICLE_NUMBER_AT_MAX = §7→ §e{0} §8(Max)
PARTICLE_ENCHANTING = §eZauber PARTICLE_NUMBER_AT_MIN = §7→ §e{0} §8(Min)
PARTICLE_HAPPY = §2Freude PARTICLE_COLOR_EDITOR = §0Farben
PARTICLE_FLAME = §7Flammen PARTICLE_COLOR_REMOVE = §eZum Entfernen klicken
PARTICLE_END_ROD = §fEnd Rod PARTICLE_COLOR_ADD = §aFarbe hinzufügen
PARTICLE_CLOUD = §7Wolke PARTICLE_COLOR_HEX = §eHex-Code eingeben
PARTICLE_TOTEM = §aTotem PARTICLE_COLOR_HEX_EXAMPLE = §7Zum Beispiel #FF8800
PARTICLE_TOWN = §5Town PARTICLE_COLOR_HEX_TITLE = Hex-Farbe
PARTICLE_ENCHANTING_CIRCLE = §fVerzaubert PARTICLE_COLOR_HEX_INVALID = §cDas ist keine Hex-Farbe wie #FF8800
PARTICLE_WATER_FIRE = §bWasser§7/§cFeuer PARTICLE_COLOR_RANDOM = §eZufällige Farben verwenden
PARTICLE_MAGIC_ENCHANTING = §5Magie§7/§eZauber PARTICLE_SLOT_EMPTY = §7Leerer Slot
PARTICLE_WINGS_EVIL = §5Lila Flügel PARTICLE_SLOT_CREATE = §eZum Erstellen klicken
PARTICLE_SLOT_ENTRIES = §7{0} Einträge
PARTICLE_SLOT_ACTIVE = §aAktiv
PARTICLE_SLOT_VIEW = §eZum Ansehen klicken
PARTICLE_SLOT_ADD_ENTRY = §aEintrag hinzufügen
PARTICLE_SLOT_MAX_ENTRIES = §cDieser Slot ist voll
PARTICLE_SLOT_OVER_BUDGET = §cDamit würde dieser Slot zu viele Partikel erzeugen
PARTICLE_SLOT_RENAME = §eUmbenennen
PARTICLE_SLOT_RENAME_TITLE = Slotname
PARTICLE_SLOT_ICON = §eSymbol ändern
PARTICLE_SLOT_ICON_TITLE = §0Slotsymbol
PARTICLE_SLOT_DELETE = §cSlot löschen
PARTICLE_SLOT_COST = §7Partikel pro Tick§8: §e{0}§8/§7{1}
PARTICLE_ENTRY_EDITOR = §0{0}
PARTICLE_ENTRY_DESIGN = §7Design§8: {0}
PARTICLE_ENTRY_PARTICLE = §7Partikel§8: {0}
PARTICLE_ENTRY_LOCKED = §cDesign gesperrt, wird nicht angezeigt
PARTICLE_ENTRY_COST = §7Partikel pro Tick§8: §e{0}
PARTICLE_ENTRY_COST_SHARE = §7Dieser Eintrag§8: §e{0}
PARTICLE_ENTRY_EDIT = §eZum Bearbeiten klicken
PARTICLE_ENTRY_DELETE = §cEintrag löschen
PARTICLE_DESIGN_PICKER = §0Design wählen
PARTICLE_CHOICE_PICKER = §0{0}
PARTICLE_COLOR_PICKER = §0Farbe wählen
PARTICLE_VALUE = §7Wert§8: {0}
PARTICLE_VALUE_RANDOM = §7Zufällige Farben
PARTICLE_VALUE_YES = §aJa
PARTICLE_VALUE_NO = §cNein
PARTICLE_DESIGN_SIMPLE = §fEinfach
PARTICLE_DESIGN_CIRCLE = §fKreis
PARTICLE_DESIGN_WING = §fFlügel
PARTICLE_DESIGN_CLOUD = §fWolke
PARTICLE_DESIGN_BURST = §fStoß
PARTICLE_DESIGN_FOOTPRINTS = §fFußspuren
PARTICLE_DESIGN_TEAM_LINK = §fTeamband
PARTICLE_DESIGN_PULSE_SHIMMER = §fSchimmer
PARTICLE_DESIGN_HEART_BEAT = §fHerzschlag
PARTICLE_PARAM_PARTICLE = §fPartikel
PARTICLE_PARAM_GLYPH_FLIGHT = §fGlyphenflug
PARTICLE_PARAM_BURST_TRIGGER = §fWann
PARTICLE_PARAM_BURST_SHAPE = §fForm
PARTICLE_PARAM_BURST_RADIUS = §fRingradius
PARTICLE_PARAM_BURST_FALL = §fMindestfallhöhe
PARTICLE_PARAM_FOOTPRINT_LIFETIME = §fSpurdauer (Ticks)
PARTICLE_PARAM_FOOTPRINT_STEP = §fAbstand der Spuren
PARTICLE_PARAM_LINK_SPACING = §fAbstand der Partikel
PARTICLE_PARAM_PULSE_SPEED = §fPulsdauer (Ticks)
PARTICLE_PARAM_PULSE_ROTATION = §fDrehung
PARTICLE_PARAM_HEART_BEAT_SPEED = §fSchlagdauer (Ticks)
PARTICLE_PARAM_COUNT = §fAnzahl
PARTICLE_PARAM_SPREAD_X = §fStreuung X
PARTICLE_PARAM_SPREAD_Y = §fStreuung Y
PARTICLE_PARAM_SPREAD_Z = §fStreuung Z
PARTICLE_PARAM_SPEED = §fGeschwindigkeit
PARTICLE_PARAM_COLORS = §fFarben
PARTICLE_PARAM_BLOCK = §fBlock
PARTICLE_PARAM_TRAIL_DURATION = §fSpurdauer (Ticks)
PARTICLE_PARAM_TRAIL_TARGET_X = §fSpurziel X
PARTICLE_PARAM_TRAIL_TARGET_Y = §fSpurziel Y
PARTICLE_PARAM_TRAIL_TARGET_Z = §fSpurziel Z
PARTICLE_PARAM_OFFSET_X = §fVersatz X
PARTICLE_PARAM_OFFSET_Y = §fVersatz Y
PARTICLE_PARAM_OFFSET_Z = §fVersatz Z
PARTICLE_PARAM_INTERVAL = §fAlle n Ticks
PARTICLE_PARAM_PLACEMENT = §fAusgerichtet an
PARTICLE_PARAM_TICK_MODE = §fAnzeige
PARTICLE_PARAM_MOVEMENT = §fBewegung
PARTICLE_PARAM_GLIDING = §fElytra
PARTICLE_PARAM_GROUND = §fBoden
PARTICLE_PARAM_SNEAK = §fDucken
PARTICLE_PARAM_VISIBILITY = §fSichtbar für
PARTICLE_PARAM_RADIUS = §fRadius
PARTICLE_PARAM_CIRCLE_SPEED = §fDrehgeschwindigkeit
PARTICLE_PARAM_PHASE = §fStartwinkel
PARTICLE_PARAM_RISE = §fAuf und ab Höhe
PARTICLE_PARAM_RISE_SPEED = §fAuf und ab Geschwindigkeit
PARTICLE_PARAM_BOUNCE = §fZurückspringen
PARTICLE_PARAM_INVERTED = §fOben beginnen
PARTICLE_PARAM_WING_IMAGE = §fFlügelform
PARTICLE_PARAM_WING_SIZE = §fFlügelgröße
PARTICLE_PARAM_WING_DETAIL = §fPunkte pro Flügel
PARTICLE_PARAM_WING_COLORING = §fFarbmodus
PARTICLE_PARAM_WING_SPARKLE = §fFunkeln (sichtbare Punkte, %)
PARTICLE_PARAM_WING_MOTION = §fBewegung
PARTICLE_CHOICE_TICKMODE_MOVE = §fBeim Bewegen
PARTICLE_CHOICE_TICKMODE_ALWAYS = §fImmer
PARTICLE_CHOICE_PLACEMENT_WORLD = §fWelt (X Osten, Y oben, Z Süden)
PARTICLE_CHOICE_PLACEMENT_BODY = §fKörper (X rechts, Y oben, Z vorne)
PARTICLE_CHOICE_MOVEMENT_ANY = §fJede Bewegung
PARTICLE_CHOICE_MOVEMENT_MOVING = §fNur beim Bewegen
PARTICLE_CHOICE_MOVEMENT_STANDING_STILL = §fNur beim Stehen
PARTICLE_CHOICE_GLIDING_ANY = §fMit oder ohne Elytra
PARTICLE_CHOICE_GLIDING_GLIDING = §fNur beim Gleiten
PARTICLE_CHOICE_GLIDING_NOT_GLIDING = §fNicht beim Gleiten
PARTICLE_CHOICE_GROUND_ANY = §fAm Boden und in der Luft
PARTICLE_CHOICE_GROUND_ON_GROUND = §fNur am Boden
PARTICLE_CHOICE_GROUND_IN_AIR = §fNur in der Luft
PARTICLE_CHOICE_SNEAK_ANY = §fDucken egal
PARTICLE_CHOICE_SNEAK_SNEAKING = §fNur beim Ducken
PARTICLE_CHOICE_SNEAK_NOT_SNEAKING = §fNicht beim Ducken
PARTICLE_CHOICE_VISIBILITY_EVERYONE = §fAlle
PARTICLE_CHOICE_VISIBILITY_ONLY_SELF = §fNur ich
PARTICLE_CHOICE_VISIBILITY_ONLY_OTHERS = §fNur andere
PARTICLE_CHOICE_IMAGE_SIMPLE = §fEinfach
PARTICLE_CHOICE_IMAGE_COMPLEX = §fKomplex
PARTICLE_CHOICE_IMAGE_SWORD = §fSchwert
PARTICLE_CHOICE_IMAGE_BAT = §fFledermaus
PARTICLE_CHOICE_IMAGE_BUTTERFLY = §fSchmetterling
PARTICLE_CHOICE_IMAGE_FAIRY = §fFee
PARTICLE_CHOICE_IMAGE_MWGL = §fMiniWarGear Liga
PARTICLE_CHOICE_IMAGE_SW = §fSteamWar
PARTICLE_CHOICE_IMAGE_WGS = §fWarGear Saison
PARTICLE_CHOICE_IMAGE_SWORD_CROSSED = §fGekreuzte Schwerter
PARTICLE_CHOICE_COLORING_OVER_TIME = §fWechseln mit der Zeit
PARTICLE_CHOICE_COLORING_ALONG_WING = §fVom Rücken zu den Spitzen
PARTICLE_CHOICE_MOTION_NONE = §fKeine
PARTICLE_CHOICE_MOTION_FLAP_SLOW = §fLangsam schlagen
PARTICLE_CHOICE_MOTION_FLAP_FAST = §fSchnell schlagen
PARTICLE_CHOICE_MOTION_BEAT_ON_JUMP = §fSchlag beim Springen
PARTICLE_CHOICE_TRIGGER_JUMP = §fBeim Springen
PARTICLE_CHOICE_TRIGGER_LAND = §fBeim Landen
PARTICLE_CHOICE_TRIGGER_SNEAK = §fBeim Schleichen
PARTICLE_CHOICE_BURSTSHAPE_PUFF = §fWolke
PARTICLE_CHOICE_BURSTSHAPE_RING = §fRing
PARTICLE_CHOICE_GLYPHFLIGHT_NORMAL = §fHerabschweben
PARTICLE_CHOICE_GLYPHFLIGHT_INTO_PLAYER = §fIn den Spieler
PARTICLE_CHOICE_GLYPHFLIGHT_OUT_OF_PLAYER = §fAus dem Spieler
# Particle types
PARTICLE_TYPE_DUST = §fStaub
PARTICLE_TYPE_DUST_COLOR_TRANSITION = §fFarbverlaufsstaub
PARTICLE_TYPE_ENTITY_EFFECT = §fTrankwirbel
PARTICLE_TYPE_TINTED_LEAVES = §fGefärbte Blätter
PARTICLE_TYPE_BLOCK = §fBlocksplitter
PARTICLE_TYPE_BLOCK_CRUMBLE = §fBröckelnder Block
PARTICLE_TYPE_BLOCK_MARKER = §fBlockmarkierung
PARTICLE_TYPE_DUST_PILLAR = §fStaubsäule
PARTICLE_TYPE_FALLING_DUST = §fFallender Staub
PARTICLE_TYPE_ITEM = §fGegenstandssplitter
PARTICLE_TYPE_TRAIL = §fSpur
PARTICLE_TYPE_FLAME = §fFlamme
PARTICLE_TYPE_SMALL_FLAME = §fKleine Flamme
PARTICLE_TYPE_SOUL_FIRE_FLAME = §fSeelenfeuerflamme
PARTICLE_TYPE_SOUL = §fSeele
PARTICLE_TYPE_LAVA = §fLavafunken
PARTICLE_TYPE_SMOKE = §fRauch
PARTICLE_TYPE_LARGE_SMOKE = §fGroßer Rauch
PARTICLE_TYPE_WHITE_SMOKE = §fWeißer Rauch
PARTICLE_TYPE_CAMPFIRE_COSY_SMOKE = §fLagerfeuerrauch
PARTICLE_TYPE_CAMPFIRE_SIGNAL_SMOKE = §fSignalrauch
PARTICLE_TYPE_ASH = §fAsche
PARTICLE_TYPE_WHITE_ASH = §fWeiße Asche
PARTICLE_TYPE_POOF = §fPuff
PARTICLE_TYPE_CLOUD = §fWolke
PARTICLE_TYPE_SNEEZE = §fNiesen
PARTICLE_TYPE_SPIT = §fSpucke
PARTICLE_TYPE_EXPLOSION = §fExplosion
PARTICLE_TYPE_SONIC_BOOM = §fSchallwelle
PARTICLE_TYPE_SWEEP_ATTACK = §fSchwungangriff
PARTICLE_TYPE_GUST = §fWindstoß
PARTICLE_TYPE_SMALL_GUST = §fKleiner Windstoß
PARTICLE_TYPE_DRIPPING_WATER = §fTropfendes Wasser
PARTICLE_TYPE_FALLING_WATER = §fFallendes Wasser
PARTICLE_TYPE_DRIPPING_LAVA = §fTropfende Lava
PARTICLE_TYPE_FALLING_LAVA = §fFallende Lava
PARTICLE_TYPE_LANDING_LAVA = §fLandende Lava
PARTICLE_TYPE_DRIPPING_HONEY = §fTropfender Honig
PARTICLE_TYPE_FALLING_HONEY = §fFallender Honig
PARTICLE_TYPE_LANDING_HONEY = §fLandender Honig
PARTICLE_TYPE_FALLING_NECTAR = §fFallender Nektar
PARTICLE_TYPE_DRIPPING_OBSIDIAN_TEAR = §fTropfende Obsidianträne
PARTICLE_TYPE_FALLING_OBSIDIAN_TEAR = §fFallende Obsidianträne
PARTICLE_TYPE_LANDING_OBSIDIAN_TEAR = §fLandende Obsidianträne
PARTICLE_TYPE_DRIPPING_DRIPSTONE_WATER = §fTropfsteinwasser
PARTICLE_TYPE_FALLING_DRIPSTONE_WATER = §fFallendes Tropfsteinwasser
PARTICLE_TYPE_DRIPPING_DRIPSTONE_LAVA = §fTropfsteinlava
PARTICLE_TYPE_FALLING_DRIPSTONE_LAVA = §fFallende Tropfsteinlava
PARTICLE_TYPE_RAIN = §fRegen
PARTICLE_TYPE_SPLASH = §fSpritzer
PARTICLE_TYPE_FISHING = §fAngelspur
PARTICLE_TYPE_UNDERWATER = §fUnterwasser
PARTICLE_TYPE_BUBBLE_POP = §fPlatzende Blase
PARTICLE_TYPE_DOLPHIN = §fDelfinspur
PARTICLE_TYPE_NAUTILUS = §fNautilus
PARTICLE_TYPE_SQUID_INK = §fTintenfischtinte
PARTICLE_TYPE_GLOW_SQUID_INK = §fLeuchttintenfischtinte
PARTICLE_TYPE_GLOW = §fLeuchten
PARTICLE_TYPE_FALLING_SPORE_BLOSSOM = §fFallende Sporenblüte
PARTICLE_TYPE_SPORE_BLOSSOM_AIR = §fSchwebende Sporen
PARTICLE_TYPE_CHERRY_LEAVES = §fKirschblätter
PARTICLE_TYPE_PALE_OAK_LEAVES = §fBlasseichenblätter
PARTICLE_TYPE_CRIMSON_SPORE = §fKarmesinsporen
PARTICLE_TYPE_WARPED_SPORE = §fWirrsporen
PARTICLE_TYPE_MYCELIUM = §fMyzel
PARTICLE_TYPE_COMPOSTER = §fKomposter
PARTICLE_TYPE_FIREFLY = §fGlühwürmchen
PARTICLE_TYPE_SNOWFLAKE = §fSchneeflocke
PARTICLE_TYPE_ITEM_SNOWBALL = §fSchneeball
PARTICLE_TYPE_ITEM_SLIME = §fSchleim
PARTICLE_TYPE_ITEM_COBWEB = §fSpinnennetz
PARTICLE_TYPE_EGG_CRACK = §fEierschale
PARTICLE_TYPE_DUST_PLUME = §fStaubwolke
PARTICLE_TYPE_INFESTED = §fBefallen
PARTICLE_TYPE_HEART = §fHerz
PARTICLE_TYPE_NOTE = §fNote
PARTICLE_TYPE_HAPPY_VILLAGER = §fFröhlicher Dorfbewohner
PARTICLE_TYPE_ANGRY_VILLAGER = §fWütender Dorfbewohner
PARTICLE_TYPE_DAMAGE_INDICATOR = §fSchaden
PARTICLE_TYPE_CRIT = §fVolltreffer
PARTICLE_TYPE_ENCHANTED_HIT = §fVerzauberter Treffer
PARTICLE_TYPE_ENCHANT = §fZauberzeichen
PARTICLE_TYPE_EFFECT = §fTrankeffekt
PARTICLE_TYPE_INSTANT_EFFECT = §fSoforteffekt
PARTICLE_TYPE_WITCH = §fHexenmagie
PARTICLE_TYPE_END_ROD = §fEndstab
PARTICLE_TYPE_PORTAL = §fPortal
PARTICLE_TYPE_REVERSE_PORTAL = §fUmgekehrtes Portal
PARTICLE_TYPE_DRAGON_BREATH = §fDrachenatem
PARTICLE_TYPE_FIREWORK = §fFeuerwerksfunke
PARTICLE_TYPE_TOTEM_OF_UNDYING = §fTotem
PARTICLE_TYPE_ELECTRIC_SPARK = §fElektrischer Funke
PARTICLE_TYPE_SCRAPE = §fAbkratzen
PARTICLE_TYPE_WAX_ON = §fWachsen
PARTICLE_TYPE_WAX_OFF = §fEntwachsen
PARTICLE_TYPE_SCULK_SOUL = §fSculk-Seele
PARTICLE_TYPE_SCULK_CHARGE = §fSculk-Ladung
PARTICLE_TYPE_SCULK_CHARGE_POP = §fSculk-Entladung
PARTICLE_TYPE_SHRIEK = §fKreischen
PARTICLE_TYPE_RAID_OMEN = §fÜberfall-Omen
PARTICLE_TYPE_TRIAL_OMEN = §fPrüfungs-Omen
PARTICLE_TYPE_OMINOUS_SPAWNING = §fUnheilvolles Erscheinen
PARTICLE_TYPE_TRIAL_SPAWNER_DETECTION = §fPrüfungsspawner-Flamme
PARTICLE_TYPE_TRIAL_SPAWNER_DETECTION_OMINOUS = §fUnheilvolle Prüfungsspawner-Flamme
PARTICLE_TYPE_VAULT_CONNECTION = §fTresorverbindung
# Particle blocks
PARTICLE_CHOICE_BLOCK_STONE = §fStein
PARTICLE_CHOICE_BLOCK_COBBLESTONE = §fBruchstein
PARTICLE_CHOICE_BLOCK_DIRT = §fErde
PARTICLE_CHOICE_BLOCK_GRASS_BLOCK = §fGrasblock
PARTICLE_CHOICE_BLOCK_SAND = §fSand
PARTICLE_CHOICE_BLOCK_RED_SAND = §fRoter Sand
PARTICLE_CHOICE_BLOCK_GRAVEL = §fKies
PARTICLE_CHOICE_BLOCK_OAK_PLANKS = §fEichenholzbretter
PARTICLE_CHOICE_BLOCK_OAK_LEAVES = §fEichenlaub
PARTICLE_CHOICE_BLOCK_CHERRY_LEAVES = §fKirschlaub
PARTICLE_CHOICE_BLOCK_SNOW_BLOCK = §fSchneeblock
PARTICLE_CHOICE_BLOCK_ICE = §fEis
PARTICLE_CHOICE_BLOCK_GLASS = §fGlas
PARTICLE_CHOICE_BLOCK_TNT = §fTNT
PARTICLE_CHOICE_BLOCK_PUMPKIN = §fKürbis
PARTICLE_CHOICE_BLOCK_MELON = §fMelone
PARTICLE_CHOICE_BLOCK_HONEY_BLOCK = §fHonigblock
PARTICLE_CHOICE_BLOCK_SLIME_BLOCK = §fSchleimblock
PARTICLE_CHOICE_BLOCK_NETHERRACK = §fNetherrack
PARTICLE_CHOICE_BLOCK_SOUL_SAND = §fSeelensand
PARTICLE_CHOICE_BLOCK_MAGMA_BLOCK = §fMagmablock
PARTICLE_CHOICE_BLOCK_END_STONE = §fEndstein
PARTICLE_CHOICE_BLOCK_OBSIDIAN = §fObsidian
PARTICLE_CHOICE_BLOCK_PRISMARINE = §fPrismarin
PARTICLE_CHOICE_BLOCK_SCULK = §fSculk
PARTICLE_CHOICE_BLOCK_AMETHYST_BLOCK = §fAmethystblock
PARTICLE_CHOICE_BLOCK_COAL_BLOCK = §fKohleblock
PARTICLE_CHOICE_BLOCK_IRON_BLOCK = §fEisenblock
PARTICLE_CHOICE_BLOCK_GOLD_BLOCK = §fGoldblock
PARTICLE_CHOICE_BLOCK_REDSTONE_BLOCK = §fRedstone-Block
PARTICLE_CHOICE_BLOCK_LAPIS_BLOCK = §fLapislazuliblock
PARTICLE_CHOICE_BLOCK_EMERALD_BLOCK = §fSmaragdblock
PARTICLE_CHOICE_BLOCK_DIAMOND_BLOCK = §fDiamantblock
PARTICLE_CHOICE_BLOCK_WHITE_WOOL = §fWeiße Wolle
PARTICLE_CHOICE_BLOCK_RED_WOOL = §fRote Wolle
PARTICLE_CHOICE_BLOCK_ORANGE_WOOL = §fOrange Wolle
PARTICLE_CHOICE_BLOCK_YELLOW_WOOL = §fGelbe Wolle
PARTICLE_CHOICE_BLOCK_LIME_WOOL = §fHellgrüne Wolle
PARTICLE_CHOICE_BLOCK_LIGHT_BLUE_WOOL = §fHellblaue Wolle
PARTICLE_CHOICE_BLOCK_BLUE_WOOL = §fBlaue Wolle
PARTICLE_CHOICE_BLOCK_PURPLE_WOOL = §fViolette Wolle
PARTICLE_CHOICE_BLOCK_PINK_WOOL = §fRosa Wolle
PARTICLE_CHOICE_BLOCK_BLACK_WOOL = §fSchwarze Wolle
PARTICLE_PLAYER_1063 = §7Kreisende Kreise PARTICLE_PLAYER_1063 = §7Kreisende Kreise
PARTICLE_PLAYER_10697_AURA = §7Rauchgranate PARTICLE_PLAYER_10697_AURA = §7Rauchgranate
PARTICLE_TEAM_PULSE_HEART_BEAT = §cHerzschlag PARTICLE_TEAM_PULSE_HEART_BEAT = §cHerzschlag
PARTICLE_RAINCLOUD_NORMAL = §fRegenwolke
PARTICLE_RAINCLOUD_RED = §fLavawolke
PARTICLE_RAINCLOUD_YELLOW = §fNektarwolke
PARTICLE_RAINCLOUD_ORANGE = §fHonigwolke
PARTICLE_RAINCLOUD_GREEN = §fSporenwolke
PARTICLE_RAINCLOUD_PURPLE = §fObsidanwolke
PARTICLE_RAINCLOUD_NAUTILUS = §fNautiluswolke
PARTICLE_RAINCLOUD_ENCHANTMENT = §fVerzauberungswolke
PARTICLE_RAINCLOUD_SLIME = §fSchleimwolke
PARTICLE_RAINCLOUD_HAIL = §fHagelwolke
PARTICLE_RAINCLOUD_SNOW = §fSchneewolke
PARTICLE_RAINCLOUD_TOTEM = §fTotemwolke
PARTICLE_EVENT_ENCHANTING = §cVerzaubert PARTICLE_EVENT_ENCHANTING = §cVerzaubert
PARTICLE_EVENT_CLOUD = §fWolken
PARTICLE_EVENT_SMOKE = §7Rauch
PARTICLE_EVENT_WATER = §bWasser
PARTICLE_EVENT_WINGS = §fFlügel
PARTICLE_EVENT_RAIN_CLOUD = §fRegenwolke PARTICLE_EVENT_RAIN_CLOUD = §fRegenwolke
PARTICLE_EVENT_WGS = §fWGS
PARTICLE_EVENT_WARGEARCLASH = §fClash
PARTICLE_EGG_HUNT_EASY = §aGrünplfanzen
PARTICLE_EGG_HUNT_MEDIUM = §eVersteckspiel PARTICLE_EGG_HUNT_MEDIUM = §eVersteckspiel
PARTICLE_EGG_HUNT_HARD = §cOrbit PARTICLE_EGG_HUNT_HARD = §cOrbit
PARTICLE_EGG_HUNT_EXTREME = §5Lila regen
PARTICLE_EGG_HUNT_ADVANCED = §5Abheben PARTICLE_EGG_HUNT_ADVANCED = §5Abheben
PARTICLE_EGG_HUNT_HALF = §fDie Jagd ist eröffnet
JUMP_AND_RUN_FINISHED = §aBeendet in {0} mit {1} Fails JUMP_AND_RUN_FINISHED = §aBeendet in {0} mit {1} Fails
JUMP_AND_RUN_PERSONAL_BEST = §aNice! Du hast deinen Rekord um {0} verbessert! JUMP_AND_RUN_PERSONAL_BEST = §aNice! Du hast deinen Rekord um {0} verbessert!
@@ -0,0 +1,95 @@
/*
* 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 lombok.Getter;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.entity.Pose;
import org.bukkit.util.Vector;
// The player's body as a coordinate system: origin at the feet, up towards the head, forward out of the chest.
// Standing it turns with the body (as BodyMotion tracks it); gliding or swimming the body lies along the look direction.
@Getter
public class BodyFrame {
private static final Vector WORLD_UP = new Vector(0, 1, 0);
private static final double HALF_BODY = 0.9;
private final Location origin;
private final Vector right;
private final Vector up;
private final Vector forward;
// Reference directions of rings around the body; they do not turn with the body
private final Vector ringStart;
private final Vector ringSide;
private BodyFrame(Location origin, Vector right, Vector up, Vector forward) {
this.origin = origin;
this.right = right;
this.up = up;
this.forward = forward;
// World east projected onto the plane around the body, so a standing ring matches the world ring
Vector start = new Vector(1, 0, 0).subtract(up.clone().multiply(up.getX()));
if (start.lengthSquared() < 1e-6) {
start = new Vector(0, 0, 1).subtract(up.clone().multiply(up.getZ()));
}
this.ringStart = start.normalize();
this.ringSide = up.clone().crossProduct(ringStart);
}
public static BodyFrame of(Player player) {
Location location = player.getLocation();
Pose pose = player.getPose();
if (pose == Pose.FALL_FLYING || pose == Pose.SWIMMING) {
Vector up = location.getDirection().normalize();
Vector right = up.clone().crossProduct(WORLD_UP);
if (right.lengthSquared() < 1e-6) {
right = horizontal(BodyMotion.yaw(player)).crossProduct(WORLD_UP);
}
right.normalize();
Vector forward = up.clone().crossProduct(right).normalize();
// The lying body is centred in its short hitbox, its feet lie behind the centre
Location origin = location.clone().add(0, player.getHeight() / 2, 0).subtract(up.clone().multiply(HALF_BODY));
return new BodyFrame(origin, right, up, forward);
}
Vector forward = horizontal(BodyMotion.yaw(player));
Vector right = forward.clone().crossProduct(WORLD_UP).normalize();
return new BodyFrame(location, right, WORLD_UP.clone(), forward);
}
private static Vector horizontal(float yaw) {
double radians = Math.toRadians(yaw);
return new Vector(-Math.sin(radians), 0, Math.cos(radians));
}
public Vector local(double right, double up, double forward) {
return this.right.clone().multiply(right)
.add(this.up.clone().multiply(up))
.add(this.forward.clone().multiply(forward));
}
// A point on a ring around the up axis; angle 0 and the turning direction match the world rings
public Vector ring(double distance, double angle) {
return ringStart.clone().multiply(Math.cos(angle) * distance).add(ringSide.clone().multiply(Math.sin(angle) * distance));
}
}
@@ -0,0 +1,165 @@
/*
* 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 lombok.experimental.UtilityClass;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import java.util.Map;
import java.util.WeakHashMap;
// How a player's body moves: the direction it faces, worked out the way the Minecraft client turns the body, because
// the server's own body rotation for players does not follow what other players see, and how fast it moves.
// Walking turns the body towards the walking direction; standing still it only turns once the head looks more than
// 50 degrees to the side. It also notices jumps, landings and starting to sneak, which reward touches react to.
@UtilityClass
public class BodyMotion {
private final float MAX_HEAD_TURN = 50;
private final double MOVING = 0.0025;
private final Map<Player, State> states = new WeakHashMap<>();
// Counts server ticks, so events can be matched to the tick they happened in
private long tick;
private static class State {
private float body;
private double x;
private double y;
private double z;
// Blocks per tick, smoothed so single steps do not jerk the wings
private double speed;
private boolean onGround = true;
private boolean sneaking;
// Highest point of the current time in the air
private double peakY;
private long jumpTick = Long.MIN_VALUE;
private long landTick = Long.MIN_VALUE;
private double landFall;
private long sneakTick = Long.MIN_VALUE;
}
// Called once per server tick before the players are updated
public void nextTick() {
tick++;
}
// Called once per server tick for every online player
@SuppressWarnings("deprecation")
public void tick(Player player) {
Location location = player.getLocation();
State state = states.get(player);
if (state == null) {
state = new State();
state.body = location.getYaw();
state.x = location.getX();
state.y = location.getY();
state.z = location.getZ();
states.put(player, state);
return;
}
float head = location.getYaw();
double dx = location.getX() - state.x;
double dy = location.getY() - state.y;
double dz = location.getZ() - state.z;
state.x = location.getX();
state.y = location.getY();
state.z = location.getZ();
state.speed = state.speed * 0.8 + Math.sqrt(dx * dx + dy * dy + dz * dz) * 0.2;
boolean onGround = player.isOnGround();
if (state.onGround && !onGround && dy > 0) {
state.jumpTick = tick;
}
if (!onGround) {
state.peakY = state.onGround ? location.getY() : Math.max(state.peakY, location.getY());
} else if (!state.onGround) {
state.landTick = tick;
state.landFall = state.peakY - location.getY();
}
state.onGround = onGround;
if (player.isSneaking() && !state.sneaking) {
state.sneakTick = tick;
}
state.sneaking = player.isSneaking();
float target = state.body;
if (dx * dx + dz * dz > MOVING) {
target = (float) Math.toDegrees(Math.atan2(dz, dx)) - 90;
// Walking backwards keeps the body facing forwards
float away = Math.abs(wrap(head) - target);
if (away > 95 && away < 265) {
target -= 180;
}
}
state.body += wrap(target - state.body) * 0.3f;
float headTurn = wrap(head - state.body);
if (Math.abs(headTurn) > MAX_HEAD_TURN) {
state.body += headTurn - Math.signum(headTurn) * MAX_HEAD_TURN;
}
}
// The tracked body direction in degrees, or the head direction for players not tracked yet
public float yaw(Player player) {
State state = states.get(player);
return state == null ? player.getLocation().getYaw() : state.body;
}
// Smoothed speed in blocks per tick; sprinting is about 0.28, fast elytra flight well above 1
public double speed(Player player) {
State state = states.get(player);
return state == null ? 0 : state.speed;
}
public long currentTick() {
return tick;
}
public boolean jumped(Player player) {
State state = states.get(player);
return state != null && state.jumpTick == tick;
}
// Ticks since the last jump, or a large number if there was none
public long ticksSinceJump(Player player) {
State state = states.get(player);
return state == null || state.jumpTick == Long.MIN_VALUE ? Long.MAX_VALUE : tick - state.jumpTick;
}
// How far the player fell if they landed this tick, otherwise -1
public double landed(Player player) {
State state = states.get(player);
return state != null && state.landTick == tick ? state.landFall : -1;
}
public boolean startedSneaking(Player player) {
State state = states.get(player);
return state != null && state.sneakTick == tick;
}
private float wrap(float degrees) {
float wrapped = degrees % 360;
if (wrapped >= 180) wrapped -= 360;
if (wrapped < -180) wrapped += 360;
return wrapped;
}
}
@@ -0,0 +1,75 @@
/*
* 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.particle.slot.ParticleEntry;
import de.steamwar.lobby.particle.slot.ParticleSet;
import java.util.ArrayList;
import java.util.List;
public class CompiledSet {
private final List<ParticleElement> always = new ArrayList<>();
private final List<ParticleElement> move = new ArrayList<>();
public static CompiledSet of(ParticleElement... particleElements) {
CompiledSet compiledSet = new CompiledSet();
for (ParticleElement particleElement : particleElements) {
compiledSet.add(particleElement);
}
return compiledSet;
}
// Entries whose design or particle type 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.isUnlocked(context)) {
compiledSet.add(entry.build());
}
}
return compiledSet;
}
public void add(ParticleElement particleElement) {
if (particleElement.tickType() == ParticleTickType.ALWAYS) {
always.add(particleElement);
} else {
move.add(particleElement);
}
}
public boolean hasAlways() {
return !always.isEmpty();
}
public boolean hasMove() {
return !move.isEmpty();
}
public void tickAlways(ParticleTickData particleTickData) {
always.forEach(particleElement -> particleElement.tick(particleTickData));
}
public void tickMove(ParticleTickData particleTickData) {
move.forEach(particleElement -> particleElement.tick(particleTickData));
}
}
@@ -0,0 +1,160 @@
/*
* 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 org.bukkit.util.Vector;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
// Wing shapes drawn from lines and curves instead of images, so their number of points can be chosen.
// Coordinates use the same units as the wing images: x is sideways from the spine, y upwards, about 11 by 13.
public enum DrawnWings {
// An arm bone along the top, fingers spreading from the wrist and skin hanging between them
BAT {
@Override
List<List<Vector>> rightWing() {
List<List<Vector>> lines = new ArrayList<>();
lines.add(curve(p(0.5, 7), p(3, 9.5), p(6, 11), p(11, 12.5)));
lines.add(curve(p(6, 11), p(7, 4)));
lines.add(curve(p(6, 11), p(9, 6)));
lines.add(curve(p(6, 11), p(10.8, 9)));
lines.add(curve(p(0.5, 5), p(3.5, 4.6), p(5.5, 3.4), p(7, 4), p(7.6, 5.4), p(9, 6), p(9.6, 7.6), p(10.8, 9), p(11.2, 10.8), p(11, 12.5)));
return lines;
}
},
// The butterfly curve r = e^cos(t) - 2 cos(4t), which already holds both wings; its small loops around the body are left out
BUTTERFLY {
@Override
List<List<Vector>> rightWing() {
List<List<Vector>> lines = new ArrayList<>();
List<Vector> current = new ArrayList<>();
for (int i = 0; i <= 400; i++) {
double t = Math.PI * i / 400;
double r = Math.exp(Math.cos(t)) - 2 * Math.cos(4 * t);
if (Math.abs(r) < 0.9) {
if (current.size() > 1) lines.add(current);
current = new ArrayList<>();
continue;
}
current.add(p(Math.sin(t) * r * 3, Math.cos(t) * r * 2 + 6));
}
if (current.size() > 1) lines.add(current);
return lines;
}
},
// Two thin rounded pairs, a long one above and a short one below
FAIRY {
@Override
List<List<Vector>> rightWing() {
return List.of(ellipse(5.2, 9.3, 5, 1.9, 35), ellipse(3.8, 4.6, 3.6, 1.5, -30));
}
},
;
// The right wing as lines; the left wing mirrors it
abstract List<List<Vector>> rightWing();
private static final Map<String, WingDesign> CACHE = new HashMap<>();
// The shapes are drawn with the shoulder joint at y = 7; moving it to 3.3 puts it at the shoulder blades
// (about 1.3 blocks above the feet at the default size) instead of at head height
private static final double SHOULDER_SHIFT = 7 - 3.3;
// Both wings with the given number of points per wing, spread evenly along the lines
public WingDesign withDetail(int pointsPerWing) {
return CACHE.computeIfAbsent(name() + "@" + pointsPerWing, key -> {
List<Vector> right = sample(rightWing(), pointsPerWing);
right.forEach(point -> point.setY(point.getY() - SHOULDER_SHIFT));
List<Vector> both = new ArrayList<>(right);
right.forEach(point -> both.add(new Vector(-point.getX(), point.getY(), 0)));
return new WingDesign.WingDesignImpl(both.toArray(new Vector[0]));
});
}
private static List<Vector> sample(List<List<Vector>> lines, int count) {
double total = lines.stream().mapToDouble(DrawnWings::length).sum();
List<Vector> points = new ArrayList<>();
for (int i = 0; i < count; i++) {
double distance = total * (i + 0.5) / count;
for (List<Vector> line : lines) {
double length = length(line);
if (distance <= length) {
points.add(at(line, distance));
break;
}
distance -= length;
}
}
return points;
}
private static double length(List<Vector> line) {
double length = 0;
for (int i = 1; i < line.size(); i++) length += line.get(i).distance(line.get(i - 1));
return length;
}
private static Vector at(List<Vector> line, double distance) {
for (int i = 1; i < line.size(); i++) {
double segment = line.get(i).distance(line.get(i - 1));
if (distance <= segment) {
return line.get(i - 1).clone().add(line.get(i).clone().subtract(line.get(i - 1)).multiply(segment == 0 ? 0 : distance / segment));
}
distance -= segment;
}
return line.get(line.size() - 1).clone();
}
private static Vector p(double x, double y) {
return new Vector(x, y, 0);
}
// A smooth curve through the given points (Catmull-Rom), as many short segments
private static List<Vector> curve(Vector... points) {
List<Vector> result = new ArrayList<>();
for (int i = 0; i < points.length - 1; i++) {
Vector p0 = points[Math.max(0, i - 1)], p1 = points[i], p2 = points[i + 1], p3 = points[Math.min(points.length - 1, i + 2)];
for (int step = 0; step < 16; step++) {
double t = step / 16.0, t2 = t * t, t3 = t2 * t;
result.add(p1.clone().multiply(2)
.add(p2.clone().subtract(p0).multiply(t))
.add(p0.clone().multiply(2).subtract(p1.clone().multiply(5)).add(p2.clone().multiply(4)).subtract(p3).multiply(t2))
.add(p1.clone().multiply(3).subtract(p0).subtract(p2.clone().multiply(3)).add(p3).multiply(t3))
.multiply(0.5));
}
}
result.add(points[points.length - 1].clone());
return result;
}
private static List<Vector> ellipse(double centerX, double centerY, double radiusX, double radiusY, double tiltDegrees) {
double tilt = Math.toRadians(tiltDegrees);
List<Vector> result = new ArrayList<>();
for (int i = 0; i <= 64; i++) {
double a = 2 * Math.PI * i / 64;
double x = Math.cos(a) * radiusX, y = Math.sin(a) * radiusY;
result.add(p(centerX + x * Math.cos(tilt) - y * Math.sin(tilt), centerY + x * Math.sin(tilt) + y * Math.cos(tilt)));
}
return result;
}
}
@@ -1,96 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.lobby.particle;
import de.steamwar.inventory.SWItem;
import de.steamwar.lobby.LobbySystem;
import de.steamwar.sql.SteamwarUser;
import lombok.Getter;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
public class ParticleData {
private final Material material;
private final String name;
private final List<String> attributes = new ArrayList<>();
@Getter
private final ParticleRequirement requirement;
@Getter
private final ParticleElement particleElement;
public ParticleData(Material material, String name, ParticleElement particleElement) {
this.material = material;
this.name = name;
this.requirement = ParticleRequirement.NO_REQUIRMENT;
this.particleElement = particleElement;
particleElement.aggregateAttributes(this);
}
public ParticleData(Material material, String name, ParticleRequirement requirement, ParticleElement particleElement) {
this.material = material;
this.name = name;
this.requirement = requirement;
this.particleElement = particleElement;
particleElement.aggregateAttributes(this);
}
public ParticleData add(String attribute) {
attributes.add(attribute);
return this;
}
public SWItem toSWItem(Player player, SteamwarUser user, Set<Integer> eventTeilnahme, String eggHuntConfig) {
String translatedName;
try {
translatedName = LobbySystem.getMessage().parse(name, player);
} catch (Exception e) {
translatedName = name;
}
List<String> lore = new ArrayList<>();
lore.add("");
if (!attributes.isEmpty()) {
lore.add(LobbySystem.getMessage().parse("PARTICLE_ATTRIBUTE", player));
attributes.forEach(attribute -> lore.add(LobbySystem.getMessage().parse(attribute, player)));
lore.add("");
}
String unlockedBy = requirement.getRequirementName(player);
if (unlockedBy != null) {
lore.add(LobbySystem.getMessage().parse("PARTICLE_UNLOCKED_BY", player));
lore.add(unlockedBy);
lore.add("");
}
if (requirement.test(user, eventTeilnahme, eggHuntConfig)) {
lore.add(LobbySystem.getMessage().parse("PARTICLE_SELECT", player));
} else {
lore.add(LobbySystem.getMessage().parse("PARTICLE_LOCKED", player));
}
return new SWItem(material, translatedName, lore, false, clickType -> {
});
}
}
@@ -19,9 +19,7 @@
package de.steamwar.lobby.particle; package de.steamwar.lobby.particle;
import org.bukkit.Bukkit;
import org.bukkit.Color; import org.bukkit.Color;
import org.bukkit.Location;
import org.bukkit.Particle; import org.bukkit.Particle;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@@ -47,23 +45,18 @@ public interface ParticleElement {
return new Particle.DustOptions(Color.fromRGB(r, g, b), (float) (RANDOM.nextFloat() / 2 + 0.5)); return new Particle.DustOptions(Color.fromRGB(r, g, b), (float) (RANDOM.nextFloat() / 2 + 0.5));
} }
default void display(Location location, Player root, boolean onlySelf, boolean onlyOther, Consumer<Player> consumer) { default void display(ParticleTickData particleTickData, Consumer<Player> consumer) {
if (onlySelf) { Player root = particleTickData.getPlayer();
if (particleTickData.isOnlySelf()) {
consumer.accept(root); consumer.accept(root);
return; return;
} }
Bukkit.getOnlinePlayers().forEach(player -> { for (Player player : particleTickData.getViewers()) {
if (onlyOther && player == root) { if (particleTickData.isOnlyOthers() && player == root) {
return; continue;
} }
int viewDistance = player.getClientViewDistance() * 16;
if (location.distanceSquared(player.getLocation()) <= viewDistance * viewDistance) {
consumer.accept(player); consumer.accept(player);
} }
});
}
default void aggregateAttributes(ParticleData particleData) {
} }
default ParticleTickType tickType() { default ParticleTickType tickType() {
@@ -1,145 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.lobby.particle;
import de.steamwar.inventory.SWItem;
import de.steamwar.inventory.SWListInv;
import de.steamwar.lobby.LobbySystem;
import de.steamwar.lobby.particle.particles.*;
import de.steamwar.lobby.particle.particles.custom.CustomEasterParticle;
import de.steamwar.lobby.particle.particles.custom.CustomPlayerParticle;
import de.steamwar.lobby.particle.particles.custom.CustomTeamParticle;
import de.steamwar.lobby.special.easter.EggHunt;
import de.steamwar.lobby.util.LobbyPlayer;
import de.steamwar.sql.Event;
import de.steamwar.sql.SteamwarUser;
import de.steamwar.sql.TeamTeilnahme;
import de.steamwar.sql.UserConfig;
import lombok.experimental.UtilityClass;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
@UtilityClass
public class ParticleInventory {
private final Class<?>[] PARTICLES = {
PlayerParticle.class,
CustomPlayerParticle.class,
TeamParticle.class,
CustomTeamParticle.class,
RainCloudParticle.class,
ServerTeamParticle.class,
EventParticle.class,
EventParticlePlacement.class,
EventParticleParticipation.class,
EasterParticle.class,
CustomEasterParticle.class,
};
public String convertToString(ParticleEnum particleEnum) {
return particleEnum.getClass().getSimpleName() + "@" + ((Enum<?>) particleEnum).name();
}
public ParticleEnum convertFromString(String string) {
String[] split = string.split("@");
Class<?> clazz = null;
for (Class<?> aClass : PARTICLES) {
if (aClass.getSimpleName().equals(split[0])) {
clazz = aClass;
break;
}
}
if (clazz != null) {
try {
return (ParticleEnum) Enum.valueOf((Class<? extends Enum>) clazz, split[1]);
} catch (Exception e) {
return null;
}
}
return null;
}
public void openInventory(Player player, boolean onlyUnlocked) {
LobbyPlayer lobbyPlayer = LobbyPlayer.getLobbyPlayer(player.getUniqueId());
SteamwarUser user = SteamwarUser.get(player.getUniqueId());
Set<Integer> events = user.getTeam() == 0 ? new HashSet<>() : TeamTeilnahme.getEvents(user.getTeam()).stream().map(Event::getEventID).collect(Collectors.toSet());
String eggHuntConfig = UserConfig.getConfig(user.getId(), EggHunt.EGG_HUNT_CONFIG_KEY);
AtomicBoolean hasOneLocked = new AtomicBoolean(false);
List<SWListInv.SWListEntry<ParticleEnum>> particleList = new ArrayList<>();
for (Class<?> clazz : PARTICLES) {
addParticles(particleList, (ParticleEnum[]) clazz.getEnumConstants(), player, onlyUnlocked, hasOneLocked, user, events, eggHuntConfig);
}
for (int i = 0; i < particleList.size() % 45; i++) {
particleList.add(new SWListInv.SWListEntry<>(new SWItem(Material.BLACK_STAINED_GLASS_PANE, " "), null));
}
SWListInv<ParticleEnum> particleSWListInv = new SWListInv<>(player, LobbySystem.getMessage().parse("PARTICLE_INVENTORY", player), false, particleList, (clickType, particle) -> {
if (particle == null) return;
lobbyPlayer.setParticle(particle);
player.closeInventory();
if (ParticleListener.disabled(player)) {
LobbySystem.getMessage().send("PARTICLE_DEACTIVATED", player);
}
});
particleSWListInv.setItem(48, Material.BARRIER, LobbySystem.getMessage().parse("PARTICLE_DESELECT", player), new ArrayList<>(), false, clickType -> {
lobbyPlayer.setParticle(null);
});
if (!hasOneLocked.get()) {
particleSWListInv.setItem(50, Material.GRAY_BANNER, LobbySystem.getMessage().parse("PARTICLE_SHOW_HAS_ALL", player), new ArrayList<>(), false, clickType -> {
});
} else if (onlyUnlocked) {
particleSWListInv.setItem(50, Material.LIME_BANNER, LobbySystem.getMessage().parse("PARTICLE_SHOW_ALL", player), new ArrayList<>(), false, clickType -> {
openInventory(player, false);
});
} else {
particleSWListInv.setItem(50, Material.RED_BANNER, LobbySystem.getMessage().parse("PARTICLE_SHOW_UNLOCKED", player), new ArrayList<>(), false, clickType -> {
openInventory(player, true);
});
}
particleSWListInv.open();
}
private void addParticles(List<SWListInv.SWListEntry<ParticleEnum>> particleList, ParticleEnum[] particleEnums, Player player, boolean onlyUnlocked, AtomicBoolean hasOneLocked, SteamwarUser user, Set<Integer> events, String eggHuntConfig) {
for (ParticleEnum particle : particleEnums) {
ParticleData particleData = particle.getParticle();
SWItem swItem = particleData.toSWItem(player, user, events, eggHuntConfig);
if (particleData.getRequirement().test(user, events, eggHuntConfig)) {
particleList.add(new SWListInv.SWListEntry<>(swItem, particle));
} else {
hasOneLocked.set(true);
if (!onlyUnlocked) {
particleList.add(new SWListInv.SWListEntry<>(swItem, null));
}
}
}
for (int i = 0; i < particleList.size() % 9; i++) {
particleList.add(new SWListInv.SWListEntry<>(new SWItem(Material.BLACK_STAINED_GLASS_PANE, " "), null));
}
}
}
@@ -23,20 +23,25 @@ import de.steamwar.linkage.Linked;
import de.steamwar.lobby.LobbySystem; import de.steamwar.lobby.LobbySystem;
import de.steamwar.lobby.jumpandrun.JumpAndRun; import de.steamwar.lobby.jumpandrun.JumpAndRun;
import de.steamwar.lobby.listener.PlayerSpawn; import de.steamwar.lobby.listener.PlayerSpawn;
import de.steamwar.lobby.particle.gui.ParticleMenu;
import de.steamwar.lobby.util.LobbyPlayer; import de.steamwar.lobby.util.LobbyPlayer;
import de.steamwar.sql.SteamwarUser; import de.steamwar.sql.SteamwarUser;
import de.steamwar.sql.UserPerm; import de.steamwar.sql.UserPerm;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority; import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractEntityEvent; import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerMoveEvent; import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.player.PlayerQuitEvent;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
@Linked @Linked
@@ -55,17 +60,14 @@ public class ParticleListener implements Listener {
movingPlayers.entrySet().removeIf(entry -> entry.getValue() <= 0); movingPlayers.entrySet().removeIf(entry -> entry.getValue() <= 0);
deg += 0.1; deg += 0.1;
if (deg > 360) deg = 0; if (deg > 360) deg = 0;
BodyMotion.nextTick();
Bukkit.getOnlinePlayers().forEach(BodyMotion::tick);
Bukkit.getOnlinePlayers().forEach(player -> { Bukkit.getOnlinePlayers().forEach(player -> {
if (disabled(player)) return; if (disabled(player)) return;
if (JumpAndRun.isPlayerInJumpAndRun(player)) return; if (JumpAndRun.isPlayerInJumpAndRun(player)) return;
LobbyPlayer lobbyPlayer = LobbyPlayer.getLobbyPlayer(player.getUniqueId()); CompiledSet compiledSet = LobbyPlayer.getLobbyPlayer(player.getUniqueId()).getCompiledSet();
ParticleEnum particle = lobbyPlayer.getParticle(); if (compiledSet == null || !compiledSet.hasAlways()) return;
if (particle == null) return; compiledSet.tickAlways(new ParticleTickData(player.getWorld(), player, deg, movingPlayers.containsKey(player), viewers(player)));
ParticleData particleData = particle.getParticle();
ParticleElement particleElement = particleData.getParticleElement();
if (particleElement.tickType() == ParticleTickType.ALWAYS) {
particleElement.tick(new ParticleTickData(player.getWorld(), player, deg, movingPlayers.containsKey(player)));
}
}); });
}, 0, 1); }, 0, 1);
} }
@@ -77,7 +79,7 @@ public class ParticleListener implements Listener {
if (!PlayerSpawn.PARTICLE.equals(event.getItem())) return; if (!PlayerSpawn.PARTICLE.equals(event.getItem())) return;
event.setCancelled(true); event.setCancelled(true);
ParticleInventory.openInventory(player, true); ParticleMenu.open(player);
} }
@EventHandler @EventHandler
@@ -85,20 +87,23 @@ public class ParticleListener implements Listener {
Player player = event.getPlayer(); Player player = event.getPlayer();
if (disabled(player)) return; if (disabled(player)) return;
if (JumpAndRun.isPlayerInJumpAndRun(player)) return; if (JumpAndRun.isPlayerInJumpAndRun(player)) return;
LobbyPlayer lobbyPlayer = LobbyPlayer.getLobbyPlayer(player.getUniqueId()); CompiledSet compiledSet = LobbyPlayer.getLobbyPlayer(player.getUniqueId()).getCompiledSet();
ParticleEnum particle = lobbyPlayer.getParticle(); if (compiledSet == null) return;
if (particle == null) return;
ParticleData particleData = particle.getParticle(); if (compiledSet.hasMove()) {
ParticleElement particleElement = particleData.getParticleElement(); compiledSet.tickMove(new ParticleTickData(player.getWorld(), player, deg, true, viewers(player)));
if (particleElement.tickType() == ParticleTickType.MOVE) {
particleElement.tick(new ParticleTickData(player.getWorld(), player, deg, true));
} }
if (event.getFrom().getX() != event.getTo().getX() || event.getFrom().getY() != event.getTo().getY() || event.getFrom().getZ() != event.getTo().getZ()) { if (event.getFrom().getX() != event.getTo().getX() || event.getFrom().getY() != event.getTo().getY() || event.getFrom().getZ() != event.getTo().getZ()) {
movingPlayers.put(player, 5); movingPlayers.put(player, 5);
} }
} }
// 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) @EventHandler(ignoreCancelled = true)
public void onPlayerQuit(PlayerQuitEvent event) { public void onPlayerQuit(PlayerQuitEvent event) {
movingPlayers.remove(event.getPlayer()); movingPlayers.remove(event.getPlayer());
@@ -111,6 +116,18 @@ public class ParticleListener implements Listener {
} }
} }
private static List<Player> viewers(Player root) {
Location location = root.getLocation();
List<Player> viewers = new ArrayList<>();
for (Player player : root.getWorld().getPlayers()) {
int viewDistance = player.getClientViewDistance() * 16;
if (location.distanceSquared(player.getLocation()) <= viewDistance * viewDistance) {
viewers.add(player);
}
}
return viewers;
}
public static boolean disabled(Player player) { public static boolean disabled(Player player) {
SteamwarUser user = SteamwarUser.get(player.getUniqueId()); SteamwarUser user = SteamwarUser.get(player.getUniqueId());
if (Bukkit.getOnlinePlayers().size() > SERVER_TEAM_MAX_SIZE) return true; if (Bukkit.getOnlinePlayers().size() > SERVER_TEAM_MAX_SIZE) return true;
@@ -39,7 +39,13 @@ public interface ParticleRequirement {
String getRequirementName(Player player); String getRequirementName(Player player);
boolean test(SteamwarUser user, Set<Integer> eventTeilname, String eggHuntConfig); // Whether the player meets this requirement
boolean matches(UnlockContext context);
// Administrators pass every requirement; lore still names the regular requirement
default boolean test(UnlockContext context) {
return context.getUser().hasPerm(UserPerm.ADMINISTRATION) || matches(context);
}
ParticleRequirement SERVER_TEAM = new ParticleRequirement() { ParticleRequirement SERVER_TEAM = new ParticleRequirement() {
@Override @Override
@@ -48,8 +54,8 @@ public interface ParticleRequirement {
} }
@Override @Override
public boolean test(SteamwarUser user, Set<Integer> eventTeilname, String eggHuntConfig) { public boolean matches(UnlockContext context) {
return user.hasPerm(UserPerm.TEAM); return context.getUser().hasPerm(UserPerm.TEAM);
} }
}; };
@@ -60,21 +66,23 @@ public interface ParticleRequirement {
} }
@Override @Override
public boolean test(SteamwarUser user, Set<Integer> eventTeilname, String eggHuntConfig) { public boolean matches(UnlockContext context) {
return true; return true;
} }
}; };
ParticleRequirement HAS_TEAM = new ParticleRequirement() {
// Does not also pass for the server team
ParticleRequirement TEAM_MEMBER = new ParticleRequirement() {
@Override @Override
public String getRequirementName(Player player) { public String getRequirementName(Player player) {
return LobbySystem.getMessage().parse("PARTICLE_UNLOCKED_BY_TEAM", player); return LobbySystem.getMessage().parse("PARTICLE_UNLOCKED_BY_TEAM", player);
} }
@Override @Override
public boolean test(SteamwarUser user, Set<Integer> eventTeilname, String eggHuntConfig) { public boolean matches(UnlockContext context) {
return user.getTeam() != 0; return context.getUser().getTeam() != 0;
} }
}.or(SERVER_TEAM); };
ParticleRequirement EVENT_PARTICIPATION = new ParticleRequirement() { ParticleRequirement EVENT_PARTICIPATION = new ParticleRequirement() {
@Override @Override
public String getRequirementName(Player player) { public String getRequirementName(Player player) {
@@ -82,8 +90,8 @@ public interface ParticleRequirement {
} }
@Override @Override
public boolean test(SteamwarUser user, Set<Integer> eventTeilname, String eggHuntConfig) { public boolean matches(UnlockContext context) {
return !eventTeilname.isEmpty(); return !context.getEvents().isEmpty();
} }
}.or(SERVER_TEAM); }.or(SERVER_TEAM);
ParticleRequirement EGG_HUNT_EASY = _easterEggHuntDifficulty(EggDifficulty.EASY); ParticleRequirement EGG_HUNT_EASY = _easterEggHuntDifficulty(EggDifficulty.EASY);
@@ -98,7 +106,8 @@ public interface ParticleRequirement {
} }
@Override @Override
public boolean test(SteamwarUser user, Set<Integer> eventTeilname, String eggHuntConfig) { public boolean matches(UnlockContext context) {
String eggHuntConfig = context.getEggHuntConfig();
if (eggHuntConfig == null) return false; if (eggHuntConfig == null) return false;
int count = 0; int count = 0;
for (int i = 0; i < eggHuntConfig.length(); i++) { for (int i = 0; i < eggHuntConfig.length(); i++) {
@@ -120,8 +129,8 @@ public interface ParticleRequirement {
} }
@Override @Override
public boolean test(SteamwarUser user, Set<Integer> eventTeilname, String eggHuntConfig) { public boolean matches(UnlockContext context) {
return user.getTeam() == teamId; return context.getUser().getTeam() == teamId;
} }
}.or(SERVER_TEAM); }.or(SERVER_TEAM);
} }
@@ -135,8 +144,8 @@ public interface ParticleRequirement {
} }
@Override @Override
public boolean test(SteamwarUser user, Set<Integer> eventTeilname, String eggHuntConfig) { public boolean matches(UnlockContext context) {
return eventTeilname.contains(eventId); return context.getEvents().contains(eventId);
} }
}.or(SERVER_TEAM); }.or(SERVER_TEAM);
} }
@@ -152,9 +161,9 @@ public interface ParticleRequirement {
} }
@Override @Override
public boolean test(SteamwarUser user, Set<Integer> eventTeilname, String eggHuntConfig) { public boolean matches(UnlockContext context) {
if (!eventTeilname.contains(eventId)) return false; if (!teams.contains(context.getUser().getTeam())) return false;
return teams.contains(user.getTeam()); return context.getEvents().contains(eventId);
} }
}.or(SERVER_TEAM); }.or(SERVER_TEAM);
} }
@@ -181,7 +190,8 @@ public interface ParticleRequirement {
} }
@Override @Override
public boolean test(SteamwarUser user, Set<Integer> eventTeilname, String eggHuntConfig) { public boolean matches(UnlockContext context) {
String eggHuntConfig = context.getEggHuntConfig();
if (eggHuntConfig == null) return false; if (eggHuntConfig == null) return false;
return eggs.stream().allMatch(id -> eggHuntConfig.length() > id && eggHuntConfig.charAt(id) == '1'); return eggs.stream().allMatch(id -> eggHuntConfig.length() > id && eggHuntConfig.charAt(id) == '1');
} }
@@ -197,8 +207,8 @@ public interface ParticleRequirement {
} }
@Override @Override
public boolean test(SteamwarUser user, Set<Integer> eventTeilname, String eggHuntConfig) { public boolean matches(UnlockContext context) {
return user.getId() == userId; return context.getUser().getId() == userId;
} }
}.or(SERVER_TEAM); }.or(SERVER_TEAM);
} }
@@ -212,8 +222,8 @@ public interface ParticleRequirement {
} }
@Override @Override
public boolean test(SteamwarUser user, Set<Integer> eventTeilname, String eggHuntConfig) { public boolean matches(UnlockContext context) {
return user.getId() == userId; return context.getUser().getId() == userId;
} }
}.or(SERVER_TEAM); }.or(SERVER_TEAM);
} }
@@ -228,12 +238,31 @@ public interface ParticleRequirement {
} }
@Override @Override
public boolean test(SteamwarUser user, Set<Integer> eventTeilname, String eggHuntConfig) { public boolean matches(UnlockContext context) {
return user.getTeam() == teamId; return context.getUser().getTeam() == teamId;
} }
}.or(SERVER_TEAM); }.or(SERVER_TEAM);
} }
// Met by meeting any of the requirements; the lore lists them one per line
static ParticleRequirement anyOf(ParticleRequirement... requirements) {
return new ParticleRequirement() {
@Override
public String getRequirementName(Player player) {
return java.util.Arrays.stream(requirements).map(requirement -> requirement.getRequirementName(player)).filter(Objects::nonNull).collect(Collectors.joining("\n"));
}
@Override
public boolean matches(UnlockContext context) {
for (ParticleRequirement requirement : requirements) {
if (requirement.test(context)) return true;
}
return false;
}
};
}
// The alternative is checked first, as it is usually the cheap server team check
default ParticleRequirement or(ParticleRequirement particleRequirement) { default ParticleRequirement or(ParticleRequirement particleRequirement) {
return new ParticleRequirement() { return new ParticleRequirement() {
@Override @Override
@@ -242,8 +271,8 @@ public interface ParticleRequirement {
} }
@Override @Override
public boolean test(SteamwarUser user, Set<Integer> eventTeilname, String eggHuntConfig) { public boolean matches(UnlockContext context) {
return ParticleRequirement.this.test(user, eventTeilname, eggHuntConfig) || particleRequirement.test(user, eventTeilname, eggHuntConfig); return particleRequirement.test(context) || ParticleRequirement.this.test(context);
} }
}; };
} }
@@ -26,6 +26,8 @@ import org.bukkit.Location;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import java.util.List;
@RequiredArgsConstructor @RequiredArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@Getter @Getter
@@ -36,8 +38,13 @@ public class ParticleTickData {
private Location location; private Location location;
private final double deg; private final double deg;
private final boolean isMoving; private final boolean isMoving;
private final List<Player> viewers;
private boolean onlySelf = false; private boolean onlySelf = false;
private boolean onlyOthers = false; private boolean onlyOthers = false;
// Set for entries placed relative to the body, null for world placement
private BodyFrame frame;
// Where a point sits along its shape, from 0 to 1; when set, gradients colour by it instead of by time
private Double shade;
public ParticleTickData withLocation(Location location) { public ParticleTickData withLocation(Location location) {
ParticleTickData particleTickData = copy(); ParticleTickData particleTickData = copy();
@@ -45,6 +52,24 @@ public class ParticleTickData {
return particleTickData; return particleTickData;
} }
public ParticleTickData withFrame(BodyFrame frame) {
ParticleTickData particleTickData = copy();
particleTickData.frame = frame;
particleTickData.location = frame.getOrigin().clone();
return particleTickData;
}
public ParticleTickData withShade(double shade) {
ParticleTickData particleTickData = copy();
particleTickData.shade = shade;
return particleTickData;
}
// The gradient position for this tick: along the shape when a shade is set, otherwise moving with time
public double gradientPosition() {
return shade != null ? Math.min(shade, 0.9999) * 360 : (deg * 10) % 360;
}
public ParticleTickData onlySelf() { public ParticleTickData onlySelf() {
ParticleTickData particleTickData = copy(); ParticleTickData particleTickData = copy();
particleTickData.onlySelf = true; particleTickData.onlySelf = true;
@@ -65,6 +90,6 @@ public class ParticleTickData {
} }
public ParticleTickData copy() { public ParticleTickData copy() {
return new ParticleTickData(world, player, location, deg, isMoving, onlySelf, onlyOthers); return new ParticleTickData(world, player, location, deg, isMoving, viewers, onlySelf, onlyOthers, frame, shade);
} }
} }
@@ -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,102 @@
/*
* 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 de.steamwar.lobby.particle.ParticleRequirement;
import lombok.Getter;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
@Getter
public class ChoiceParameter<E> extends Parameter<E> {
// Internal choices are usable by presets but never offered in the builder or accepted from storage. Locked choices
// are offered with their requirement; the requirement is looked up lazily as it may live in a reward design.
public record Choice<E>(String id, E value, boolean internal, Supplier<ParticleRequirement> requirement) {
public Choice(String id, E value, boolean internal) {
this(id, value, internal, () -> ParticleRequirement.NO_REQUIRMENT);
}
}
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, Function<E, ParticleRequirement> requirement) {
List<Choice<E>> choices = new ArrayList<>();
for (E value : defaultValue.getDeclaringClass().getEnumConstants()) {
choices.add(new Choice<>(value.name(), value, internal.test(value), () -> requirement.apply(value)));
}
return new ChoiceParameter<>(key, nameKey, defaultValue, choices);
}
public static <E extends Enum<E>> ChoiceParameter<E> ofEnum(String key, String nameKey, E defaultValue, Predicate<E> internal) {
return ofEnum(key, nameKey, defaultValue, internal, value -> ParticleRequirement.NO_REQUIRMENT);
}
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();
}
public ParticleRequirement requirement(E value) {
return choiceOf(value).requirement().get();
}
@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,201 @@
/*
* 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 de.steamwar.lobby.particle.DrawnWings;
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 {
// Built on first use, so the design classes never depend on each other's initialisation order
private Map<String, ParticleDesign> registry;
public final ParticleDesign SIMPLE = 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 = 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));
}
};
// Images keep their old ids, so saved wings still load; preset and tournament images stay out of the builder
private final List<ChoiceParameter.Choice<WingShape>> WING_SHAPES = List.of(
new ChoiceParameter.Choice<>("simple", WingShape.image(WingDesign.SIMPLE, Material.FEATHER), false),
new ChoiceParameter.Choice<>("complex", WingShape.image(WingDesign.COMPLEX, Material.PHANTOM_MEMBRANE), false),
new ChoiceParameter.Choice<>("sword", WingShape.image(WingDesign.SWORD, Material.STONE_SWORD), false),
new ChoiceParameter.Choice<>("bat", WingShape.drawn(DrawnWings.BAT, Material.BAT_SPAWN_EGG), false),
new ChoiceParameter.Choice<>("butterfly", WingShape.drawn(DrawnWings.BUTTERFLY, Material.PINK_PETALS), false),
new ChoiceParameter.Choice<>("fairy", WingShape.drawn(DrawnWings.FAIRY, Material.ALLAY_SPAWN_EGG), false),
// Tournament wing images, unlocked by the placement that earns their reward
new ChoiceParameter.Choice<>("mwgl", WingShape.image(WingDesign.MWGL, Material.IRON_SWORD), false, Unlocks.MINI_WARGEAR_LIGA_PLACEMENT),
new ChoiceParameter.Choice<>("sw", WingShape.image(WingDesign.SW, Material.CYAN_CARPET), false, Unlocks.UNDERWATER_MWG_PLACEMENT),
new ChoiceParameter.Choice<>("wgs", WingShape.image(WingDesign.WGS, Material.DIAMOND_HELMET), false, Unlocks.WARGEAR_SEASON_2022_PLACEMENT),
new ChoiceParameter.Choice<>("sword_crossed", WingShape.image(WingDesign.SWORD_CROSSED, Material.GOLDEN_SWORD), false, Unlocks.WARGEAR_CLASH_PLACEMENT),
new ChoiceParameter.Choice<>("eclipse", WingShape.image(WingDesign.ECLIPSE), true),
new ChoiceParameter.Choice<>("pl", WingShape.image(WingDesign.PL), true),
new ChoiceParameter.Choice<>("plompa_easter", WingShape.image(WingDesign.PlompaEasterWings), true),
new ChoiceParameter.Choice<>("ely_e", WingShape.image(WingDesign.ELY_E), true),
new ChoiceParameter.Choice<>("ely_l", WingShape.image(WingDesign.ELY_L), true),
new ChoiceParameter.Choice<>("ely_y", WingShape.image(WingDesign.ELY_Y), true),
new ChoiceParameter.Choice<>("ev", WingShape.image(WingDesign.EV), true)
);
public final ChoiceParameter<WingShape> WING_SHAPE = new ChoiceParameter<>("image", "PARTICLE_PARAM_WING_IMAGE", WING_SHAPES.get(0).value(), WING_SHAPES);
// Points per wing of drawn shapes; it replaces the amount, as every point spawns one particle
public final IntParameter WING_DETAIL = new IntParameter("detail", "PARTICLE_PARAM_WING_DETAIL", 20, 6, 60);
public final DoubleParameter WING_SIZE = new DoubleParameter("size", "PARTICLE_PARAM_WING_SIZE", 0.15, 0.05, 0.3, 0.01);
public enum WingColoring {
// The whole wing changes colour through the entry's colours over time
OVER_TIME,
// The colours run from the spine to the wing tips
ALONG_WING,
}
public final ChoiceParameter<WingColoring> WING_COLORING = ChoiceParameter.ofEnum("coloring", "PARTICLE_PARAM_WING_COLORING", WingColoring.OVER_TIME);
// Percent of the points shown each tick; fewer makes the wing twinkle and costs less
public final IntParameter WING_SPARKLE = new IntParameter("sparkle", "PARTICLE_PARAM_WING_SPARKLE", 100, 10, 100);
public final ChoiceParameter<Wing.Motion> WING_MOTION = ChoiceParameter.ofEnum("motion", "PARTICLE_PARAM_WING_MOTION", Wing.Motion.NONE, motion -> false,
motion -> motion == Wing.Motion.BEAT_ON_JUMP ? Unlocks.WARGEAR_SEASON_2022.get() : ParticleRequirement.NO_REQUIRMENT);
// Moving wings need to be redrawn often to look smooth; still ones are fine once a second
private final int MOVING_WING_INTERVAL = 2;
private final int STILL_WING_INTERVAL = 20;
public final ParticleDesign WING = new ParticleDesign("wing", Material.ELYTRA, "PARTICLE_DESIGN_WING", WING_SHAPE, WING_DETAIL, WING_SIZE, WING_COLORING, WING_SPARKLE, WING_MOTION) {
@Override
public ParticleElement build(ParameterValues values, ParticleElement particle) {
WingShape shape = values.get(WING_SHAPE);
return new Wing(particle, values.get(WING_SIZE), shape.design(values.get(WING_DETAIL)),
values.get(WING_COLORING) == WingColoring.ALONG_WING, values.get(WING_SPARKLE), values.get(WING_MOTION));
}
@Override
public boolean redrawsAfterJump(ParameterValues values) {
return values.get(WING_MOTION) == Wing.Motion.BEAT_ON_JUMP;
}
@Override
public int points(ParameterValues values) {
int points = values.get(WING_SHAPE).points(values.get(WING_DETAIL));
return (int) Math.ceil(points * values.get(WING_SPARKLE) / 100.0);
}
@Override
public boolean showsSetting(Parameter<?> parameter, ParameterValues values) {
return parameter != WING_DETAIL || values.get(WING_SHAPE).isDrawn();
}
@Override
public boolean replacesAmount(ParameterValues values) {
return values.get(WING_SHAPE).isDrawn();
}
@Override
public void adjust(Parameter<?> changed, ParameterValues values) {
if (changed != WING_MOTION) return;
// A beat on jumping is redrawn every tick only while it beats
boolean moving = values.get(WING_MOTION) == Wing.Motion.FLAP_SLOW || values.get(WING_MOTION) == Wing.Motion.FLAP_FAST;
int interval = values.get(EntryParameters.INTERVAL);
if (moving && interval > MOVING_WING_INTERVAL) {
values.set(EntryParameters.INTERVAL, MOVING_WING_INTERVAL);
} else if (!moving && interval == MOVING_WING_INTERVAL) {
values.set(EntryParameters.INTERVAL, STILL_WING_INTERVAL);
}
}
// Image wings spawn a particle on every pixel, so they start sparse, and sit on the back even while gliding
@Override
public void applyDefaults(ParameterValues values) {
values.set(EntryParameters.COUNT, 1)
.set(EntryParameters.SPEED, 0.0)
.set(EntryParameters.TICK_MODE, EntryParameters.TickMode.ALWAYS)
.set(EntryParameters.INTERVAL, STILL_WING_INTERVAL)
.set(EntryParameters.PLACEMENT, EntryParameters.Placement.BODY);
}
};
// The wing shape that shows the given image, for presets
public WingShape wingShape(WingDesign image) {
return WING_SHAPE.getChoices().stream().map(ChoiceParameter.Choice::value).filter(shape -> shape.shows(image)).findFirst().orElseThrow();
}
public final ParticleDesign CLOUD = new ParticleDesign("cloud", Material.WHITE_WOOL, "PARTICLE_DESIGN_CLOUD") {
@Override
public ParticleElement build(ParameterValues values, ParticleElement particle) {
return new Cloud(particle);
}
};
private Map<String, ParticleDesign> registry() {
if (registry == null) {
Map<String, ParticleDesign> designs = new LinkedHashMap<>();
List.of(SIMPLE, CIRCLE, WING, CLOUD).forEach(design -> register(designs, design));
RewardDesigns.all().forEach(design -> register(designs, design));
InternalDesigns.all().forEach(design -> register(designs, design));
registry = designs;
}
return registry;
}
private void register(Map<String, ParticleDesign> designs, ParticleDesign design) {
if (designs.put(design.getId(), design) != null) {
throw new IllegalStateException("Duplicate particle design " + design.getId());
}
}
public ParticleDesign byId(String id) {
return registry().get(id);
}
public List<ParticleDesign> all() {
return List.copyOf(registry().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,227 @@
/*
* 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.ParticleRequirement;
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 org.bukkit.Material;
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,
}
// Body places offsets, heights, rings and wings around the player's body, even while gliding or swimming
public enum Placement {
WORLD,
BODY,
}
// Enchanting glyphs can fly between each point and the player instead of floating down
public enum GlyphFlight {
NORMAL,
INTO_PLAYER,
OUT_OF_PLAYER,
}
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(), ParticleType::getRequirement);
public final IntParameter COUNT = new IntParameter("count", "PARTICLE_PARAM_COUNT", 5, 1, 20);
// Minecraft scatters particles around their point by about this many blocks; beyond half a block a design dissolves
public final DoubleParameter SPREAD_X = new DoubleParameter("spreadX", "PARTICLE_PARAM_SPREAD_X", 0, 0, 0.5, 0.01);
public final DoubleParameter SPREAD_Y = new DoubleParameter("spreadY", "PARTICLE_PARAM_SPREAD_Y", 0, 0, 0.5, 0.01);
public final DoubleParameter SPREAD_Z = new DoubleParameter("spreadZ", "PARTICLE_PARAM_SPREAD_Z", 0, 0, 0.5, 0.01);
// For most particles the speed of their random spray; above 0.2 they shoot blocks away
public final DoubleParameter SPEED = new DoubleParameter("speed", "PARTICLE_PARAM_SPEED", 0.01, 0, 0.05, 0.005);
public final GradientParameter COLORS = new GradientParameter("colors", "PARTICLE_PARAM_COLORS", 8);
// Blocks that are also items, so they work for block and item particles alike
public final ChoiceParameter<Material> BLOCK = new ChoiceParameter<>("block", "PARTICLE_PARAM_BLOCK", Material.STONE, List.of(
new ChoiceParameter.Choice<>("stone", Material.STONE, false),
new ChoiceParameter.Choice<>("cobblestone", Material.COBBLESTONE, false),
new ChoiceParameter.Choice<>("dirt", Material.DIRT, false),
new ChoiceParameter.Choice<>("grass_block", Material.GRASS_BLOCK, false),
new ChoiceParameter.Choice<>("sand", Material.SAND, false),
new ChoiceParameter.Choice<>("red_sand", Material.RED_SAND, false),
new ChoiceParameter.Choice<>("gravel", Material.GRAVEL, false),
new ChoiceParameter.Choice<>("oak_planks", Material.OAK_PLANKS, false),
new ChoiceParameter.Choice<>("oak_leaves", Material.OAK_LEAVES, false),
new ChoiceParameter.Choice<>("cherry_leaves", Material.CHERRY_LEAVES, false),
new ChoiceParameter.Choice<>("snow_block", Material.SNOW_BLOCK, false),
new ChoiceParameter.Choice<>("ice", Material.ICE, false),
new ChoiceParameter.Choice<>("glass", Material.GLASS, false),
new ChoiceParameter.Choice<>("tnt", Material.TNT, false),
new ChoiceParameter.Choice<>("pumpkin", Material.PUMPKIN, false),
new ChoiceParameter.Choice<>("melon", Material.MELON, false),
new ChoiceParameter.Choice<>("honey_block", Material.HONEY_BLOCK, false),
new ChoiceParameter.Choice<>("slime_block", Material.SLIME_BLOCK, false),
new ChoiceParameter.Choice<>("netherrack", Material.NETHERRACK, false),
new ChoiceParameter.Choice<>("soul_sand", Material.SOUL_SAND, false),
new ChoiceParameter.Choice<>("magma_block", Material.MAGMA_BLOCK, false),
new ChoiceParameter.Choice<>("end_stone", Material.END_STONE, false),
new ChoiceParameter.Choice<>("obsidian", Material.OBSIDIAN, false),
new ChoiceParameter.Choice<>("prismarine", Material.PRISMARINE, false),
new ChoiceParameter.Choice<>("sculk", Material.SCULK, false),
new ChoiceParameter.Choice<>("amethyst_block", Material.AMETHYST_BLOCK, false),
new ChoiceParameter.Choice<>("coal_block", Material.COAL_BLOCK, false),
new ChoiceParameter.Choice<>("iron_block", Material.IRON_BLOCK, false),
new ChoiceParameter.Choice<>("gold_block", Material.GOLD_BLOCK, false),
new ChoiceParameter.Choice<>("redstone_block", Material.REDSTONE_BLOCK, false),
new ChoiceParameter.Choice<>("lapis_block", Material.LAPIS_BLOCK, false),
new ChoiceParameter.Choice<>("emerald_block", Material.EMERALD_BLOCK, false),
new ChoiceParameter.Choice<>("diamond_block", Material.DIAMOND_BLOCK, false),
new ChoiceParameter.Choice<>("white_wool", Material.WHITE_WOOL, false),
new ChoiceParameter.Choice<>("red_wool", Material.RED_WOOL, false),
new ChoiceParameter.Choice<>("orange_wool", Material.ORANGE_WOOL, false),
new ChoiceParameter.Choice<>("yellow_wool", Material.YELLOW_WOOL, false),
new ChoiceParameter.Choice<>("lime_wool", Material.LIME_WOOL, false),
new ChoiceParameter.Choice<>("light_blue_wool", Material.LIGHT_BLUE_WOOL, false),
new ChoiceParameter.Choice<>("blue_wool", Material.BLUE_WOOL, false),
new ChoiceParameter.Choice<>("purple_wool", Material.PURPLE_WOOL, false),
new ChoiceParameter.Choice<>("pink_wool", Material.PINK_WOOL, false),
new ChoiceParameter.Choice<>("black_wool", Material.BLACK_WOOL, false)
));
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 IntParameter TRAIL_DURATION = new IntParameter("trailDuration", "PARTICLE_PARAM_TRAIL_DURATION", 20, 1, 200);
// Where a trail particle flies to, relative to where it spawns
public final DoubleParameter TRAIL_TARGET_X = new DoubleParameter("trailTargetX", "PARTICLE_PARAM_TRAIL_TARGET_X", 0, -5, 5, 0.1);
public final DoubleParameter TRAIL_TARGET_Y = new DoubleParameter("trailTargetY", "PARTICLE_PARAM_TRAIL_TARGET_Y", 1, -5, 5, 0.1);
public final DoubleParameter TRAIL_TARGET_Z = new DoubleParameter("trailTargetZ", "PARTICLE_PARAM_TRAIL_TARGET_Z", 0, -5, 5, 0.1);
public final ChoiceParameter<GlyphFlight> GLYPH_FLIGHT = ChoiceParameter.ofEnum("glyphFlight", "PARTICLE_PARAM_GLYPH_FLIGHT", GlyphFlight.NORMAL, flight -> false,
flight -> switch (flight) {
case NORMAL -> ParticleRequirement.NO_REQUIRMENT;
case INTO_PLAYER -> Unlocks.WARGEAR_SEASON_PLACEMENT.get();
case OUT_OF_PLAYER -> Unlocks.WARGEAR_SEASON.get();
});
public final ChoiceParameter<Placement> PLACEMENT = ChoiceParameter.ofEnum("placement", "PARTICLE_PARAM_PLACEMENT", Placement.WORLD);
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, GLYPH_FLIGHT, COUNT, SPREAD_X, SPREAD_Y, SPREAD_Z, SPEED, COLORS, BLOCK,
TRAIL_DURATION, TRAIL_TARGET_X, TRAIL_TARGET_Y, TRAIL_TARGET_Z,
OFFSET_X, OFFSET_Y, OFFSET_Z, PLACEMENT, INTERVAL, TICK_MODE,
MOVEMENT, GLIDING, GROUND, SNEAK, VISIBILITY
);
}
@@ -0,0 +1,62 @@
/*
* 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 org.bukkit.Material;
import java.util.function.Consumer;
import java.util.function.Supplier;
// A design whose look, including its particles, is fixed; only the entry's conditions, timing and offset apply
public class FixedDesign extends ParticleDesign {
private final int points;
private final Consumer<ParameterValues> defaults;
private final Supplier<ParticleElement> shape;
public FixedDesign(String id, Material icon, String nameKey, ParticleRequirement requirement, int points, Consumer<ParameterValues> defaults, Supplier<ParticleElement> shape) {
super(id, icon, nameKey, DesignVisibility.REWARD, requirement);
this.points = points;
this.defaults = defaults;
this.shape = shape;
}
@Override
public ParticleElement build(ParameterValues values, ParticleElement particle) {
return shape.get();
}
@Override
public int points(ParameterValues values) {
return points;
}
@Override
public boolean usesEntryParticle() {
return false;
}
@Override
public void applyDefaults(ParameterValues values) {
defaults.accept(values);
}
}
@@ -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,85 @@
/*
* 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 de.steamwar.lobby.particle.elements.Circle;
import de.steamwar.lobby.particle.elements.custom.HearthBeat;
import de.steamwar.lobby.particle.elements.custom.PulseShimmer;
import de.steamwar.lobby.particle.elements.custom.YOffset;
import lombok.experimental.UtilityClass;
import org.bukkit.Material;
import java.util.List;
// Shapes only presets use; they are never offered in the builder
@UtilityClass
public class InternalDesigns {
public final IntParameter PULSE_SPEED = new IntParameter("pulseSpeed", "PARTICLE_PARAM_PULSE_SPEED", 80, 1, 200);
public final IntParameter PULSE_ROTATION = new IntParameter("pulseRotation", "PARTICLE_PARAM_PULSE_ROTATION", 2, 0, 20);
public final ParticleDesign PULSE_SHIMMER = new ParticleDesign("pulse_shimmer", Material.GRAY_CANDLE, "PARTICLE_DESIGN_PULSE_SHIMMER", DesignVisibility.INTERNAL, ParticleRequirement.NO_REQUIRMENT,
PULSE_SPEED, PULSE_ROTATION) {
@Override
public ParticleElement build(ParameterValues values, ParticleElement particle) {
return new PulseShimmer(particle, values.get(PULSE_SPEED), values.get(PULSE_ROTATION));
}
@Override
public int points(ParameterValues values) {
return 3;
}
};
public final DoubleParameter HEART_BEAT_SPEED = new DoubleParameter("heartBeatSpeed", "PARTICLE_PARAM_HEART_BEAT_SPEED", 80, 1, 200, 5);
public final ParticleDesign HEART_BEAT = new ParticleDesign("heart_beat", Material.APPLE, "PARTICLE_DESIGN_HEART_BEAT", DesignVisibility.INTERNAL, ParticleRequirement.NO_REQUIRMENT,
HEART_BEAT_SPEED) {
@Override
public ParticleElement build(ParameterValues values, ParticleElement particle) {
return new HearthBeat(particle, values.get(HEART_BEAT_SPEED));
}
};
public final DoubleParameter ORBIT_RADIUS = new DoubleParameter("orbitRadius", "PARTICLE_PARAM_ORBIT_RADIUS", 1.5, 0.2, 3, 0.1);
public final DoubleParameter ORBIT_SPEED = new DoubleParameter("orbitSpeed", "PARTICLE_PARAM_ORBIT_SPEED", 1, -20, 20, 0.5);
public final DoubleParameter ORBIT_PHASE = new DoubleParameter("orbitPhase", "PARTICLE_PARAM_ORBIT_PHASE", 0, 0, 345, 15);
// A circle whose centre itself moves on a circle around the player
public final ParticleDesign ORBIT = new ParticleDesign("orbit", Material.ENDER_PEARL, "PARTICLE_DESIGN_ORBIT", DesignVisibility.INTERNAL, ParticleRequirement.NO_REQUIRMENT,
ORBIT_RADIUS, ORBIT_SPEED, ORBIT_PHASE,
Designs.CIRCLE_RADIUS, Designs.CIRCLE_SPEED, Designs.CIRCLE_PHASE, Designs.CIRCLE_RISE, Designs.CIRCLE_RISE_SPEED, Designs.CIRCLE_BOUNCE, Designs.CIRCLE_INVERTED) {
@Override
public ParticleElement build(ParameterValues values, ParticleElement particle) {
double rise = values.get(Designs.CIRCLE_RISE);
if (rise > 0) {
particle = new YOffset(particle, values.get(Designs.CIRCLE_RISE_SPEED), 0, rise, values.get(Designs.CIRCLE_INVERTED), values.get(Designs.CIRCLE_BOUNCE));
}
ParticleElement inner = new Circle(particle, values.get(Designs.CIRCLE_RADIUS), values.get(Designs.CIRCLE_SPEED), values.get(Designs.CIRCLE_PHASE));
return new Circle(inner, values.get(ORBIT_RADIUS), values.get(ORBIT_SPEED), values.get(ORBIT_PHASE));
}
};
public List<ParticleDesign> all() {
return List.of(PULSE_SHIMMER, HEART_BEAT, ORBIT);
}
}
@@ -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,76 @@
/*
* 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;
}
// Skips the builder's limits; only for presets, which are never stored
public <T> ParameterValues setUnchecked(Parameter<T> parameter, T value) {
values.put(parameter.getKey(), value);
return this;
}
// Puts back values saved with copy()
public void restore(ParameterValues saved) {
values.clear();
values.putAll(saved.values);
}
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,88 @@
/*
* 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;
}
// False for designs with a fixed look that ignore the entry's particle type, count, spread, speed and colours
public boolean usesEntryParticle() {
return true;
}
// Whether the entry editor shows this design setting; some only matter for some values of others
public boolean showsSetting(Parameter<?> parameter, ParameterValues values) {
return true;
}
// True when the design decides how many particles each point spawns, so the amount setting is hidden and counts as 1
public boolean replacesAmount(ParameterValues values) {
return false;
}
// Whether the design must be drawn every tick for a moment after a jump, whatever its interval; these extra
// redraws do not count towards the budget
public boolean redrawsAfterJump(ParameterValues values) {
return false;
}
// Called after a setting changed, to adjust others that depend on it
public void adjust(Parameter<?> changed, ParameterValues values) {
}
// Sets the conditions and timing a new entry of this design starts with
public void applyDefaults(ParameterValues values) {
}
}
@@ -0,0 +1,200 @@
/*
* 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.ParticleRequirement;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.bukkit.Material;
import org.bukkit.Particle;
// Left out: ELDER_GUARDIAN, FLASH and the explosion/gust emitters cover other players' screens,
// BUBBLE, BUBBLE_COLUMN_UP and CURRENT_DOWN only show under water, VIBRATION needs a block or entity to fly to.
// Not buildable: particles a block or more in size or rising for seconds, which wall in players once a design
// spawns many of them (a wing spawns over a hundred); presets may still use them.
@AllArgsConstructor
@Getter
public enum ParticleType {
DUST(Material.REDSTONE, Particle.DUST, Data.DUST, true),
DUST_COLOR_TRANSITION(Material.REDSTONE_TORCH, Particle.DUST_COLOR_TRANSITION, Data.COLOR_TRANSITION, true),
ENTITY_EFFECT(Material.BREWING_STAND, Particle.ENTITY_EFFECT, Data.COLOR, true),
TINTED_LEAVES(Material.OAK_LEAVES, Particle.TINTED_LEAVES, Data.COLOR, true),
BLOCK(Material.STONE, Particle.BLOCK, Data.BLOCK, true),
BLOCK_CRUMBLE(Material.CRACKED_STONE_BRICKS, Particle.BLOCK_CRUMBLE, Data.BLOCK, true),
BLOCK_MARKER(Material.GLASS, Particle.BLOCK_MARKER, Data.BLOCK, false),
DUST_PILLAR(Material.MUD, Particle.DUST_PILLAR, Data.BLOCK, false),
FALLING_DUST(Material.SAND, Particle.FALLING_DUST, Data.BLOCK, true),
ITEM(Material.ITEM_FRAME, Particle.ITEM, Data.ITEM, true),
TRAIL(Material.LEAD, Particle.TRAIL, Data.TRAIL, true),
FLAME(Material.FLINT_AND_STEEL, Particle.FLAME, Data.NONE, true),
SMALL_FLAME(Material.TORCH, Particle.SMALL_FLAME, Data.NONE, true),
SOUL_FIRE_FLAME(Material.SOUL_TORCH, Particle.SOUL_FIRE_FLAME, Data.NONE, true),
SOUL(Material.ZOMBIE_HEAD, Particle.SOUL, Data.NONE, true),
LAVA(Material.MAGMA_CREAM, Particle.LAVA, Data.NONE, true),
SMOKE(Material.COBWEB, Particle.SMOKE, Data.NONE, true),
LARGE_SMOKE(Material.GRAY_WOOL, Particle.LARGE_SMOKE, Data.NONE, false),
WHITE_SMOKE(Material.LIGHT_GRAY_DYE, Particle.WHITE_SMOKE, Data.NONE, true),
CAMPFIRE_COSY_SMOKE(Material.CAMPFIRE, Particle.CAMPFIRE_COSY_SMOKE, Data.NONE, true),
CAMPFIRE_SIGNAL_SMOKE(Material.HAY_BLOCK, Particle.CAMPFIRE_SIGNAL_SMOKE, Data.NONE, false),
ASH(Material.BASALT, Particle.ASH, Data.NONE, true),
WHITE_ASH(Material.BONE_MEAL, Particle.WHITE_ASH, Data.NONE, true),
POOF(Material.WHITE_DYE, Particle.POOF, Data.NONE, true),
CLOUD(Material.WHITE_WOOL, Particle.CLOUD, Data.NONE, true),
SNEEZE(Material.SLIME_BLOCK, Particle.SNEEZE, Data.NONE, true),
SPIT(Material.LLAMA_SPAWN_EGG, Particle.SPIT, Data.NONE, true),
EXPLOSION(Material.TNT, Particle.EXPLOSION, Data.NONE, false),
SONIC_BOOM(Material.ECHO_SHARD, Particle.SONIC_BOOM, Data.NONE, false),
SWEEP_ATTACK(Material.IRON_SWORD, Particle.SWEEP_ATTACK, Data.NONE, false),
GUST(Material.WIND_CHARGE, Particle.GUST, Data.NONE, false),
SMALL_GUST(Material.BREEZE_ROD, Particle.SMALL_GUST, Data.NONE, false),
DRIPPING_WATER(Material.WATER_BUCKET, Particle.DRIPPING_WATER, Data.NONE, true),
FALLING_WATER(Material.LIGHT_BLUE_DYE, Particle.FALLING_WATER, Data.NONE, true),
DRIPPING_LAVA(Material.LAVA_BUCKET, Particle.DRIPPING_LAVA, Data.NONE, true),
FALLING_LAVA(Material.ORANGE_DYE, Particle.FALLING_LAVA, Data.NONE, true),
LANDING_LAVA(Material.FIRE_CORAL, Particle.LANDING_LAVA, Data.NONE, true),
DRIPPING_HONEY(Material.HONEY_BOTTLE, Particle.DRIPPING_HONEY, Data.NONE, true),
FALLING_HONEY(Material.HONEY_BLOCK, Particle.FALLING_HONEY, Data.NONE, true),
LANDING_HONEY(Material.HONEYCOMB_BLOCK, Particle.LANDING_HONEY, Data.NONE, true),
FALLING_NECTAR(Material.HONEYCOMB, Particle.FALLING_NECTAR, Data.NONE, true),
DRIPPING_OBSIDIAN_TEAR(Material.OBSIDIAN, Particle.DRIPPING_OBSIDIAN_TEAR, Data.NONE, true),
FALLING_OBSIDIAN_TEAR(Material.CRYING_OBSIDIAN, Particle.FALLING_OBSIDIAN_TEAR, Data.NONE, true),
LANDING_OBSIDIAN_TEAR(Material.RESPAWN_ANCHOR, Particle.LANDING_OBSIDIAN_TEAR, Data.NONE, true),
DRIPPING_DRIPSTONE_WATER(Material.DRIPSTONE_BLOCK, Particle.DRIPPING_DRIPSTONE_WATER, Data.NONE, true),
FALLING_DRIPSTONE_WATER(Material.BLUE_ICE, Particle.FALLING_DRIPSTONE_WATER, Data.NONE, true),
DRIPPING_DRIPSTONE_LAVA(Material.POINTED_DRIPSTONE, Particle.DRIPPING_DRIPSTONE_LAVA, Data.NONE, true),
FALLING_DRIPSTONE_LAVA(Material.MAGMA_BLOCK, Particle.FALLING_DRIPSTONE_LAVA, Data.NONE, true),
RAIN(Material.CYAN_DYE, Particle.RAIN, Data.NONE, true),
SPLASH(Material.KELP, Particle.SPLASH, Data.NONE, true),
FISHING(Material.FISHING_ROD, Particle.FISHING, Data.NONE, true),
UNDERWATER(Material.PRISMARINE_SHARD, Particle.UNDERWATER, Data.NONE, true),
BUBBLE_POP(Material.TUBE_CORAL, Particle.BUBBLE_POP, Data.NONE, true),
DOLPHIN(Material.BLUE_DYE, Particle.DOLPHIN, Data.NONE, true),
NAUTILUS(Material.NAUTILUS_SHELL, Particle.NAUTILUS, Data.NONE, true),
SQUID_INK(Material.INK_SAC, Particle.SQUID_INK, Data.NONE, true),
GLOW_SQUID_INK(Material.GLOW_INK_SAC, Particle.GLOW_SQUID_INK, Data.NONE, true),
GLOW(Material.GLOW_BERRIES, Particle.GLOW, Data.NONE, true),
FALLING_SPORE_BLOSSOM(Material.SPORE_BLOSSOM, Particle.FALLING_SPORE_BLOSSOM, Data.NONE, true),
SPORE_BLOSSOM_AIR(Material.PINK_PETALS, Particle.SPORE_BLOSSOM_AIR, Data.NONE, true),
CHERRY_LEAVES(Material.CHERRY_LEAVES, Particle.CHERRY_LEAVES, Data.NONE, true),
PALE_OAK_LEAVES(Material.PALE_OAK_LEAVES, Particle.PALE_OAK_LEAVES, Data.NONE, true),
CRIMSON_SPORE(Material.CRIMSON_FUNGUS, Particle.CRIMSON_SPORE, Data.NONE, true),
WARPED_SPORE(Material.WARPED_FUNGUS, Particle.WARPED_SPORE, Data.NONE, true),
MYCELIUM(Material.MYCELIUM, Particle.MYCELIUM, Data.NONE, true),
COMPOSTER(Material.COMPOSTER, Particle.COMPOSTER, Data.NONE, true),
FIREFLY(Material.FIREFLY_BUSH, Particle.FIREFLY, Data.NONE, true),
SNOWFLAKE(Material.SNOWBALL, Particle.SNOWFLAKE, Data.NONE, true),
ITEM_SNOWBALL(Material.SNOW_BLOCK, Particle.ITEM_SNOWBALL, Data.NONE, true),
ITEM_SLIME(Material.SLIME_BALL, Particle.ITEM_SLIME, Data.NONE, true),
ITEM_COBWEB(Material.STRING, Particle.ITEM_COBWEB, Data.NONE, true),
EGG_CRACK(Material.TURTLE_EGG, Particle.EGG_CRACK, Data.NONE, true),
DUST_PLUME(Material.SUSPICIOUS_SAND, Particle.DUST_PLUME, Data.NONE, true),
INFESTED(Material.INFESTED_STONE, Particle.INFESTED, Data.NONE, true),
HEART(Material.RED_DYE, Particle.HEART, Data.NONE, true),
NOTE(Material.NOTE_BLOCK, Particle.NOTE, Data.NONE, true),
HAPPY_VILLAGER(Material.EMERALD, Particle.HAPPY_VILLAGER, Data.NONE, true),
ANGRY_VILLAGER(Material.REDSTONE_BLOCK, Particle.ANGRY_VILLAGER, Data.NONE, true),
DAMAGE_INDICATOR(Material.SPIDER_EYE, Particle.DAMAGE_INDICATOR, Data.NONE, true),
CRIT(Material.CAULDRON, Particle.CRIT, Data.NONE, true),
ENCHANTED_HIT(Material.GLOWSTONE_DUST, Particle.ENCHANTED_HIT, Data.NONE, true),
ENCHANT(Material.ENCHANTING_TABLE, Particle.ENCHANT, Data.NONE, true),
EFFECT(Material.POTION, Particle.EFFECT, Data.NONE, true),
INSTANT_EFFECT(Material.SPLASH_POTION, Particle.INSTANT_EFFECT, Data.NONE, true),
WITCH(Material.EXPERIENCE_BOTTLE, Particle.WITCH, Data.NONE, true),
END_ROD(Material.END_ROD, Particle.END_ROD, Data.NONE, true),
PORTAL(Material.ENDER_PEARL, Particle.PORTAL, Data.NONE, true),
REVERSE_PORTAL(Material.ENDER_EYE, Particle.REVERSE_PORTAL, Data.NONE, true),
DRAGON_BREATH(Material.DRAGON_BREATH, Particle.DRAGON_BREATH, Data.NONE, true),
FIREWORK(Material.FIRE_CHARGE, Particle.FIREWORK, Data.NONE, true),
TOTEM_OF_UNDYING(Material.TOTEM_OF_UNDYING, Particle.TOTEM_OF_UNDYING, Data.NONE, true),
ELECTRIC_SPARK(Material.LIGHTNING_ROD, Particle.ELECTRIC_SPARK, Data.NONE, true),
SCRAPE(Material.COPPER_BLOCK, Particle.SCRAPE, Data.NONE, true),
WAX_ON(Material.WAXED_COPPER_BLOCK, Particle.WAX_ON, Data.NONE, true),
WAX_OFF(Material.WAXED_EXPOSED_COPPER, Particle.WAX_OFF, Data.NONE, true),
SCULK_SOUL(Material.SCULK_CATALYST, Particle.SCULK_SOUL, Data.NONE, true),
SCULK_CHARGE(Material.SCULK_SENSOR, Particle.SCULK_CHARGE, Data.ZERO_FLOAT, true),
SCULK_CHARGE_POP(Material.SCULK_VEIN, Particle.SCULK_CHARGE_POP, Data.NONE, true),
SHRIEK(Material.SCULK_SHRIEKER, Particle.SHRIEK, Data.ZERO_INT, false),
RAID_OMEN(Material.FIREWORK_STAR, Particle.RAID_OMEN, Data.NONE, true),
TRIAL_OMEN(Material.TRIAL_KEY, Particle.TRIAL_OMEN, Data.NONE, true),
OMINOUS_SPAWNING(Material.OMINOUS_TRIAL_KEY, Particle.OMINOUS_SPAWNING, Data.NONE, true),
TRIAL_SPAWNER_DETECTION(Material.TRIAL_SPAWNER, Particle.TRIAL_SPAWNER_DETECTION, Data.NONE, true),
TRIAL_SPAWNER_DETECTION_OMINOUS(Material.VAULT, Particle.TRIAL_SPAWNER_DETECTION_OMINOUS, Data.NONE, true),
VAULT_CONNECTION(Material.HEAVY_CORE, Particle.VAULT_CONNECTION, Data.NONE, true),
;
// The extra data a particle needs when it is spawned
public enum Data {
NONE,
// Colour and size, handled by the dust element
DUST,
// A colour from the entry's colours
COLOR,
// Fades between the entry's first two colours
COLOR_TRANSITION,
// Block data of the entry's block
BLOCK,
// An item of the entry's block
ITEM,
// Flies to the entry's trail target in its colour, taking the trail duration
TRAIL,
ZERO_INT,
ZERO_FLOAT,
}
private final Material icon;
private final Particle particle;
private final Data data;
private final boolean buildable;
public String getNameKey() {
return "PARTICLE_TYPE_" + name();
}
public boolean isDust() {
return data == Data.DUST;
}
public boolean usesColors() {
return data == Data.DUST || data == Data.COLOR || data == Data.COLOR_TRANSITION || data == Data.TRAIL;
}
// Leaves drift down slowly and pile up around a player, so only the server team may build with them. Cosy smoke is
// earned by taking part in Hells Bells.
public ParticleRequirement getRequirement() {
return switch (this) {
case CHERRY_LEAVES, PALE_OAK_LEAVES, TINTED_LEAVES -> ParticleRequirement.SERVER_TEAM;
case CAMPFIRE_COSY_SMOKE -> Unlocks.HELLS_BELLS.get();
default -> ParticleRequirement.NO_REQUIRMENT;
};
}
public boolean isTrail() {
return data == Data.TRAIL;
}
public boolean usesBlock() {
return data == Data.BLOCK || data == Data.ITEM;
}
}
@@ -0,0 +1,222 @@
/*
* 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 de.steamwar.lobby.particle.elements.*;
import de.steamwar.lobby.particle.elements.custom.HearthBeat;
import de.steamwar.lobby.particle.elements.custom.PulseShimmer;
import de.steamwar.lobby.particle.elements.touch.EventBurst;
import de.steamwar.lobby.particle.elements.touch.Footprints;
import de.steamwar.lobby.particle.elements.touch.Ring;
import de.steamwar.lobby.particle.elements.touch.TeamLink;
import lombok.experimental.UtilityClass;
import org.bukkit.Material;
import org.bukkit.Particle;
import org.bukkit.entity.Player;
import org.bukkit.util.Vector;
import java.util.List;
import java.util.function.Consumer;
import static de.steamwar.lobby.particle.design.EntryParameters.*;
// Designs unlocked by event and egg hunt rewards. Most are styled like the basic designs, with a particle of the
// player's choice; the others have a fixed look. Rewards whose look the basic designs can already build unlock
// ingredients instead: wing images, a wing motion and particles.
@UtilityClass
public class RewardDesigns {
private final Consumer<ParameterValues> ALWAYS = values -> values.set(TICK_MODE, TickMode.ALWAYS);
public enum BurstShape {
PUFF,
RING,
}
private final int RING_POINTS = 16;
public final ChoiceParameter<EventBurst.Trigger> BURST_TRIGGER = ChoiceParameter.ofEnum("trigger", "PARTICLE_PARAM_BURST_TRIGGER", EventBurst.Trigger.JUMP, trigger -> false,
trigger -> switch (trigger) {
case JUMP -> Unlocks.AIRSHIP_PLACEMENT.get();
case LAND -> ParticleRequirement.anyOf(Unlocks.UNDERWATER_PLACEMENT.get(), Unlocks.ABSTURZ_PLACEMENT.get(), Unlocks.ANY_EVENT.get(), Unlocks.EGG_HUNT_EXTREME.get());
case SNEAK -> Unlocks.WARGEAR_SEASON.get();
});
public final ChoiceParameter<BurstShape> BURST_SHAPE = ChoiceParameter.ofEnum("burstShape", "PARTICLE_PARAM_BURST_SHAPE", BurstShape.PUFF);
public final DoubleParameter BURST_RADIUS = new DoubleParameter("burstRadius", "PARTICLE_PARAM_BURST_RADIUS", 1, 0.3, 2, 0.1);
public final DoubleParameter BURST_FALL = new DoubleParameter("minimumFall", "PARTICLE_PARAM_BURST_FALL", 1, 0, 10, 0.5);
// Once at the feet when the player jumps, lands or starts sneaking
public final ParticleDesign BURST = new ParticleDesign("burst", Material.FIREWORK_ROCKET, "PARTICLE_DESIGN_BURST", DesignVisibility.REWARD,
ParticleRequirement.anyOf(Unlocks.AIRSHIP_PLACEMENT.get(), Unlocks.UNDERWATER_PLACEMENT.get(), Unlocks.ABSTURZ_PLACEMENT.get(),
Unlocks.ANY_EVENT.get(), Unlocks.EGG_HUNT_EXTREME.get(), Unlocks.WARGEAR_SEASON.get()),
BURST_TRIGGER, BURST_SHAPE, BURST_RADIUS, BURST_FALL) {
@Override
public ParticleElement build(ParameterValues values, ParticleElement particle) {
ParticleElement burst = values.get(BURST_SHAPE) == BurstShape.RING ? new Ring(particle, values.get(BURST_RADIUS), RING_POINTS) : particle;
double minimumFall = values.get(BURST_TRIGGER) == EventBurst.Trigger.LAND ? values.get(BURST_FALL) : 0;
return new EventBurst(values.get(BURST_TRIGGER), minimumFall, burst);
}
// Shows only now and then, so it counts as a single point
@Override
public int points(ParameterValues values) {
return 1;
}
@Override
public boolean showsSetting(Parameter<?> parameter, ParameterValues values) {
if (parameter == BURST_RADIUS) return values.get(BURST_SHAPE) == BurstShape.RING;
if (parameter == BURST_FALL) return values.get(BURST_TRIGGER) == EventBurst.Trigger.LAND;
return true;
}
@Override
public void applyDefaults(ParameterValues values) {
values.set(TICK_MODE, TickMode.ALWAYS).set(COUNT, 8).set(SPREAD_X, 0.3).set(SPREAD_Y, 0.05).set(SPREAD_Z, 0.3).set(SPEED, 0.02);
}
};
public final IntParameter FOOTPRINT_LIFETIME = new IntParameter("printLifetime", "PARTICLE_PARAM_FOOTPRINT_LIFETIME", 40, 10, 100);
public final DoubleParameter FOOTPRINT_STEP = new DoubleParameter("printStep", "PARTICLE_PARAM_FOOTPRINT_STEP", 0.6, 0.3, 2, 0.1);
// Blocks per tick while walking, to estimate how many prints are shown at once
private final double WALKING_SPEED = 0.22;
public final ParticleDesign FOOTPRINTS = new ParticleDesign("footprints", Material.LEATHER_BOOTS, "PARTICLE_DESIGN_FOOTPRINTS", DesignVisibility.REWARD,
ParticleRequirement.anyOf(Unlocks.ADVENT_WAR_SHIP_PLACEMENT.get(), Unlocks.ADVENT_WAR_SHIP.get(), Unlocks.AIRSHIP.get(), Unlocks.EGG_HUNT_EASY.get()),
FOOTPRINT_LIFETIME, FOOTPRINT_STEP) {
@Override
public ParticleElement build(ParameterValues values, ParticleElement particle) {
return new Footprints(particle, values.get(FOOTPRINT_LIFETIME), values.get(FOOTPRINT_STEP));
}
@Override
public int points(ParameterValues values) {
return (int) Math.ceil(values.get(FOOTPRINT_LIFETIME) * WALKING_SPEED / values.get(FOOTPRINT_STEP));
}
@Override
public void applyDefaults(ParameterValues values) {
values.set(TICK_MODE, TickMode.ALWAYS).set(COUNT, 1).set(SPREAD_X, 0.05).set(SPREAD_Z, 0.05).set(SPEED, 0.0);
}
};
public final DoubleParameter LINK_SPACING = new DoubleParameter("linkSpacing", "PARTICLE_PARAM_LINK_SPACING", 0.5, 0.3, 2, 0.1);
// A line to every nearby player who shows this design too
public final ParticleDesign TEAM_LINK = new ParticleDesign("team_link", Material.LEAD, "PARTICLE_DESIGN_TEAM_LINK", DesignVisibility.REWARD,
ParticleRequirement.anyOf(Unlocks.ANY_EVENT.get(), Unlocks.UNDERWATER.get()),
LINK_SPACING) {
@Override
public ParticleElement build(ParameterValues values, ParticleElement particle) {
return new TeamLink("team_link", particle, values.get(LINK_SPACING));
}
// A line to one partner half the range away
@Override
public int points(ParameterValues values) {
return (int) Math.ceil(TeamLink.RANGE / 2 / values.get(LINK_SPACING));
}
@Override
public void applyDefaults(ParameterValues values) {
values.set(TICK_MODE, TickMode.ALWAYS).set(COUNT, 1).set(SPEED, 0.0);
}
};
public final ParticleDesign SHIMMER = new ParticleDesign("shimmer", Material.AMETHYST_CLUSTER, "PARTICLE_DESIGN_PULSE_SHIMMER", DesignVisibility.REWARD,
ParticleRequirement.anyOf(Unlocks.MINI_WARGEAR_LIGA.get(), Unlocks.WARGEAR_SEASON_PLACEMENT.get()),
InternalDesigns.PULSE_SPEED, InternalDesigns.PULSE_ROTATION) {
@Override
public ParticleElement build(ParameterValues values, ParticleElement particle) {
return new PulseShimmer(particle, values.get(InternalDesigns.PULSE_SPEED), values.get(InternalDesigns.PULSE_ROTATION));
}
@Override
public int points(ParameterValues values) {
return 3;
}
@Override
public void applyDefaults(ParameterValues values) {
values.set(TICK_MODE, TickMode.ALWAYS).set(MOVEMENT, Movement.STANDING_STILL).set(COUNT, 1).set(SPEED, 0.0);
}
};
public final ParticleDesign HEARTBEAT = new ParticleDesign("heartbeat", Material.RED_DYE, "PARTICLE_DESIGN_HEART_BEAT", DesignVisibility.REWARD,
Unlocks.EGG_HUNT_HALF.get(),
InternalDesigns.HEART_BEAT_SPEED) {
@Override
public ParticleElement build(ParameterValues values, ParticleElement particle) {
return new HearthBeat(particle, values.get(InternalDesigns.HEART_BEAT_SPEED));
}
@Override
public void applyDefaults(ParameterValues values) {
values.set(TICK_MODE, TickMode.ALWAYS).set(MOVEMENT, Movement.STANDING_STILL).set(COUNT, 1).set(SPEED, 0.0);
}
};
public final ParticleDesign EVENT_PARTICIPATION_UNDERWATER_MWG = new FixedDesign("event_participation_underwater_mwg", Material.BLUE_CARPET, "PARTICLE_EVENT_RAIN_CLOUD",
ParticleRequirement.eventParticipation(35), 15, ALWAYS,
() -> new RandomParticle(
rainCloud(Particle.RAIN, Particle.DRIPPING_WATER, 5),
rainCloud(null, Particle.DRIPPING_LAVA, 5),
rainCloud(null, Particle.FALLING_NECTAR, 5),
rainCloud(null, Particle.FALLING_HONEY, 5),
rainCloud(null, Particle.FALLING_SPORE_BLOSSOM, 5),
rainCloud(null, Particle.FALLING_OBSIDIAN_TEAR, 5),
rainCloud(null, Particle.NAUTILUS, 5),
rainCloud(null, Particle.ENCHANT, 5),
rainCloud(null, Particle.ITEM_SLIME, 5),
rainCloud(null, Particle.ITEM_SNOWBALL, 5),
rainCloud(null, Particle.SNOWFLAKE, 5),
rainCloud(null, Particle.TOTEM_OF_UNDYING, 2)
));
public final ParticleDesign EGG_HUNT_MEDIUM = new FixedDesign("egg_hunt_medium", Material.YELLOW_CONCRETE_POWDER, "PARTICLE_EGG_HUNT_MEDIUM",
ParticleRequirement.EGG_HUNT_MEDIUM, 5, ALWAYS.andThen(values -> values.set(GROUND, Ground.ON_GROUND).set(SNEAK, Sneak.SNEAKING)),
() -> new SimpleParticle(Particle.GLOW_SQUID_INK, 0.4F, 0.9F, 0.4F, 0.01));
public final ParticleDesign EGG_HUNT_HARD = new FixedDesign("egg_hunt_hard", Material.RED_CONCRETE_POWDER, "PARTICLE_EGG_HUNT_HARD",
ParticleRequirement.EGG_HUNT_HARD, 2, ALWAYS,
() -> new LocationMutator(new Circle(new DoubleCircle(new DustParticle(Particle.DUST, 0, 0, 0, 0.01, 1, null), new DustParticle(Particle.DUST, 0, 0, 0, 0.01, 1, null)), 0.8, 0.5), 0, 0.5, 0));
public final ParticleDesign EGG_HUNT_ADVANCED = new FixedDesign("egg_hunt_advanced", Material.BLACK_CONCRETE_POWDER, "PARTICLE_EGG_HUNT_ADVANCED",
ParticleRequirement.EGG_HUNT_ADVANCED, 2, ALWAYS.andThen(values -> values.set(SNEAK, Sneak.SNEAKING)),
() -> new LocationMutator(new Group(
new SimpleParticle(Particle.SMOKE, 0.02F, 0, 0.02F, 0.05, 1),
new SimpleParticle(Particle.LARGE_SMOKE, 0.02F, 0, 0.02F, 0.05, 1),
particleTickData -> {
Player player = particleTickData.getPlayer();
player.setVelocity(player.getVelocity().add(new Vector(0, 0.1, 0)));
}
), 0, -0.2, 0));
// A cloud above the head that drips the given particle; rain is an optional second particle inside the cloud
private ParticleElement rainCloud(Particle rain, Particle drip, int dripCount) {
ParticleElement cloud = new SimpleParticle(Particle.CLOUD, 0.3F, 0.1F, 0.3F, 0.01);
ParticleElement drops = new LocationMutator(new SimpleParticle(drip, 0.3F, 0.0F, 0.3F, 0.01, dripCount), 0, -0.3, 0);
ParticleElement group = rain == null ? new Group(cloud, drops) : new Group(cloud, new SimpleParticle(rain, 0.3F, 0.1F, 0.3F, 0.01), drops);
return new LocationMutator(group, 0, 2.4, 0);
}
public List<ParticleDesign> all() {
return List.of(BURST, FOOTPRINTS, TEAM_LINK, SHIMMER, HEARTBEAT, EVENT_PARTICIPATION_UNDERWATER_MWG, EGG_HUNT_MEDIUM, EGG_HUNT_HARD, EGG_HUNT_ADVANCED);
}
}
@@ -0,0 +1,65 @@
/*
* 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.ParticleRequirement;
import lombok.experimental.UtilityClass;
import java.util.function.Supplier;
// Requirements of the reward ingredients: wing images, particles and touches. Each is created on first use, as event
// requirements look up the event's name.
@UtilityClass
public class Unlocks {
public final Supplier<ParticleRequirement> WARGEAR_SEASON_PLACEMENT = lazy(() -> ParticleRequirement.eventPlacement(22, 12, 285, 54));
public final Supplier<ParticleRequirement> AIRSHIP_PLACEMENT = lazy(() -> ParticleRequirement.eventPlacement(26, 205, 9, 54, 120, 292));
public final Supplier<ParticleRequirement> UNDERWATER_PLACEMENT = lazy(() -> ParticleRequirement.eventPlacement(31, 9, 210, 520));
public final Supplier<ParticleRequirement> ADVENT_WAR_SHIP_PLACEMENT = lazy(() -> ParticleRequirement.eventPlacement(32, 9, 205, 210));
public final Supplier<ParticleRequirement> MINI_WARGEAR_LIGA_PLACEMENT = lazy(() -> ParticleRequirement.eventPlacement(33, 9, 34, 205));
public final Supplier<ParticleRequirement> ABSTURZ_PLACEMENT = lazy(() -> ParticleRequirement.eventPlacement(34, 210, 205, 527, 286));
public final Supplier<ParticleRequirement> UNDERWATER_MWG_PLACEMENT = lazy(() -> ParticleRequirement.eventPlacement(35, 9, 210));
public final Supplier<ParticleRequirement> WARGEAR_SEASON_2022_PLACEMENT = lazy(() -> ParticleRequirement.eventPlacement(37, 285, 210, 122));
public final Supplier<ParticleRequirement> WARGEAR_CLASH_PLACEMENT = lazy(() -> ParticleRequirement.eventPlacement(38, 210, 158, 167, 286));
public final Supplier<ParticleRequirement> ANY_EVENT = () -> ParticleRequirement.EVENT_PARTICIPATION;
public final Supplier<ParticleRequirement> WARGEAR_SEASON = lazy(() -> ParticleRequirement.eventParticipation(22));
public final Supplier<ParticleRequirement> AIRSHIP = lazy(() -> ParticleRequirement.eventParticipation(26));
public final Supplier<ParticleRequirement> HELLS_BELLS = lazy(() -> ParticleRequirement.eventParticipation(28));
public final Supplier<ParticleRequirement> UNDERWATER = lazy(() -> ParticleRequirement.eventParticipation(31));
public final Supplier<ParticleRequirement> ADVENT_WAR_SHIP = lazy(() -> ParticleRequirement.eventParticipation(32));
public final Supplier<ParticleRequirement> MINI_WARGEAR_LIGA = lazy(() -> ParticleRequirement.eventParticipation(33));
public final Supplier<ParticleRequirement> WARGEAR_SEASON_2022 = lazy(() -> ParticleRequirement.eventParticipation(37));
public final Supplier<ParticleRequirement> EGG_HUNT_EASY = () -> ParticleRequirement.EGG_HUNT_EASY;
public final Supplier<ParticleRequirement> EGG_HUNT_EXTREME = () -> ParticleRequirement.EGG_HUNT_EXTREME;
public final Supplier<ParticleRequirement> EGG_HUNT_HALF = () -> ParticleRequirement.EGG_HUNT_FOUND_HALF;
private Supplier<ParticleRequirement> lazy(Supplier<ParticleRequirement> create) {
return new Supplier<>() {
private ParticleRequirement requirement;
@Override
public ParticleRequirement get() {
if (requirement == null) requirement = create.get();
return requirement;
}
};
}
}
@@ -0,0 +1,69 @@
/*
* 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.DrawnWings;
import de.steamwar.lobby.particle.WingDesign;
import lombok.Getter;
import org.bukkit.Material;
// A wing shape of the wings design: either a fixed image or a shape drawn with a chosen number of points
public class WingShape {
private final WingDesign image;
private final DrawnWings drawn;
@Getter
private final Material icon;
private WingShape(WingDesign image, DrawnWings drawn, Material icon) {
this.image = image;
this.drawn = drawn;
this.icon = icon;
}
public static WingShape image(WingDesign image, Material icon) {
return new WingShape(image, null, icon);
}
// Shapes only presets use, never shown in the builder
public static WingShape image(WingDesign image) {
return image(image, Material.PHANTOM_MEMBRANE);
}
public static WingShape drawn(DrawnWings drawn, Material icon) {
return new WingShape(null, drawn, icon);
}
public boolean isDrawn() {
return drawn != null;
}
public boolean shows(WingDesign image) {
return this.image == image;
}
public WingDesign design(int pointsPerWing) {
return isDrawn() ? drawn.withDetail(pointsPerWing) : image;
}
public int points(int pointsPerWing) {
return isDrawn() ? 2 * pointsPerWing : image.getVectors().length;
}
}
@@ -29,11 +29,6 @@ public class Always extends DelegatingParticleElement {
super(particleElement); super(particleElement);
} }
@Override
public String attribute() {
return "PARTICLE_ATTRIBUTE_TICK";
}
@Override @Override
public ParticleTickType tickType() { public ParticleTickType tickType() {
return ParticleTickType.ALWAYS; return ParticleTickType.ALWAYS;
@@ -1,7 +1,7 @@
/* /*
* This file is a part of the SteamWar software. * This file is a part of the SteamWar software.
* *
* Copyright (C) 2025 SteamWar.de-Serverteam * Copyright (C) 2026 SteamWar.de-Serverteam
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU Affero General Public License as published by
@@ -19,19 +19,19 @@
package de.steamwar.lobby.particle.elements; package de.steamwar.lobby.particle.elements;
import de.steamwar.lobby.particle.ParticleData; import de.steamwar.lobby.particle.BodyFrame;
import de.steamwar.lobby.particle.ParticleElement; import de.steamwar.lobby.particle.ParticleElement;
import de.steamwar.lobby.particle.ParticleTickData; import de.steamwar.lobby.particle.ParticleTickData;
public class Separator implements ParticleElement { // Places everything inside relative to the player's body instead of the world
public class BodyRelative extends DelegatingParticleElement {
@Override public BodyRelative(ParticleElement particleElement) {
public void aggregateAttributes(ParticleData particleData) { super(particleElement);
particleData.add("PARTICLE_ATTRIBUTE_SEPARATOR");
} }
@Override @Override
public void tick(ParticleTickData particleTickData) { public void tick(ParticleTickData particleTickData) {
// do nothing particleElement.tick(particleTickData.withFrame(BodyFrame.of(particleTickData.getPlayer())));
} }
} }
@@ -19,6 +19,7 @@
package de.steamwar.lobby.particle.elements; package de.steamwar.lobby.particle.elements;
import de.steamwar.lobby.particle.BodyFrame;
import de.steamwar.lobby.particle.ParticleElement; import de.steamwar.lobby.particle.ParticleElement;
import de.steamwar.lobby.particle.ParticleTickData; import de.steamwar.lobby.particle.ParticleTickData;
import org.bukkit.Location; import org.bukkit.Location;
@@ -28,6 +29,7 @@ public class Circle extends DelegatingParticleElement {
private double distance; private double distance;
private double speed; private double speed;
private double phase;
public Circle(ParticleElement particleElement) { public Circle(ParticleElement particleElement) {
super(particleElement); super(particleElement);
@@ -41,17 +43,22 @@ public class Circle extends DelegatingParticleElement {
this.speed = speed; this.speed = speed;
} }
@Override public Circle(ParticleElement particleElement, double distance, double speed, double phaseDegrees) {
public String attribute() { this(particleElement, distance, speed);
return "PARTICLE_ATTRIBUTE_CIRCLE"; this.phase = Math.toRadians(phaseDegrees);
} }
@Override @Override
public void tick(ParticleTickData particleTickData) { public void tick(ParticleTickData particleTickData) {
Location location = particleTickData.getLocation(); Location location = particleTickData.getLocation();
BodyFrame frame = particleTickData.getFrame();
if (frame != null) {
particleElement.tick(particleTickData.withLocation(location.clone().add(frame.ring(distance, (particleTickData.getDeg() * speed) % 360 + phase))));
return;
}
Vector vector = new Vector(distance, 0, 0); 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)); ParticleTickData nParticleTickData = particleTickData.withLocation(location.clone().add(vector));
particleElement.tick(nParticleTickData); particleElement.tick(nParticleTickData);
} }
@@ -31,11 +31,6 @@ public class Cloud extends DelegatingParticleElement {
super(particleElement); super(particleElement);
} }
@Override
public String attribute() {
return "PARTICLE_ATTRIBUTE_CLOUD";
}
@Override @Override
public void tick(ParticleTickData particleTickData) { public void tick(ParticleTickData particleTickData) {
if (particleTickData.getWorld().getBlockAt(particleTickData.getPlayer().getLocation().subtract(0, 0.5, 0)).getType() == Material.AIR) { if (particleTickData.getWorld().getBlockAt(particleTickData.getPlayer().getLocation().subtract(0, 0.5, 0)).getType() == Material.AIR) {
@@ -0,0 +1,67 @@
/*
* 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.elements;
import de.steamwar.lobby.particle.Gradient;
import de.steamwar.lobby.particle.ParticleElement;
import de.steamwar.lobby.particle.ParticleTickData;
import org.bukkit.Color;
import org.bukkit.Location;
import org.bukkit.Particle;
import java.util.function.BiFunction;
// A particle that needs extra data, computed per tick from the tick and spawn location so colours can follow a gradient
public class DataParticle implements ParticleElement {
private final Particle particle;
private final float vx;
private final float vy;
private final float vz;
private final double speed;
private final int count;
private final BiFunction<ParticleTickData, Location, Object> data;
public DataParticle(Particle particle, float vx, float vy, float vz, double speed, int count, BiFunction<ParticleTickData, Location, Object> data) {
this.particle = particle;
this.vx = vx;
this.vy = vy;
this.vz = vz;
this.speed = speed;
this.count = count;
this.data = data;
}
// The gradient's colour at this tick, the same way dust particles pick it; a random colour without gradient
public static Color color(Gradient gradient, ParticleTickData particleTickData) {
if (gradient == null) {
return Color.fromRGB(RANDOM.nextInt(256), RANDOM.nextInt(256), RANDOM.nextInt(256));
}
java.awt.Color color = gradient.getColor(particleTickData.gradientPosition(), 360);
return Color.fromRGB(color.getRed(), color.getGreen(), color.getBlue());
}
@Override
public void tick(ParticleTickData particleTickData) {
Location location = particleTickData.getLocation().add(0.0, 0.2, 0.0);
Object value = data.apply(particleTickData, location);
display(particleTickData, player -> player.spawnParticle(particle, location, count, vx, vy, vz, speed, value));
}
}
@@ -19,50 +19,31 @@
package de.steamwar.lobby.particle.elements; package de.steamwar.lobby.particle.elements;
import de.steamwar.lobby.LobbySystem;
import de.steamwar.lobby.particle.ParticleElement; import de.steamwar.lobby.particle.ParticleElement;
import de.steamwar.lobby.particle.ParticleTickData; import de.steamwar.lobby.particle.ParticleTickData;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.WeakHashMap;
public class Delayed extends DelegatingParticleElement { public class Delayed extends DelegatingParticleElement {
private DelayedData delayedData = new DelayedData(); // Weak keys drop players once they quit, so no quit listener has to be registered per instance
private final Map<Player, Integer> data = new WeakHashMap<>();
private int interval; private int interval;
private static class DelayedData implements Listener {
private Map<Player, Integer> data = new HashMap<>();
@EventHandler(ignoreCancelled = true)
public void onPlayerQuit(PlayerQuitEvent event) {
data.remove(event.getPlayer());
}
}
public Delayed(ParticleElement particleElement, int interval) { public Delayed(ParticleElement particleElement, int interval) {
super(particleElement); super(particleElement);
Bukkit.getPluginManager().registerEvents(delayedData, LobbySystem.getInstance());
this.interval = interval; this.interval = interval;
if (interval <= 0) { if (interval <= 0) {
throw new IllegalArgumentException("Interval must be greater than 0"); throw new IllegalArgumentException("Interval must be greater than 0");
} }
} }
@Override
public String attribute() {
return null;
}
@Override @Override
public void tick(ParticleTickData particleTickData) { public void tick(ParticleTickData particleTickData) {
int currentNumber = delayedData.data.getOrDefault(particleTickData.getPlayer(), 0) % interval; int currentNumber = data.getOrDefault(particleTickData.getPlayer(), 0) % interval;
delayedData.data.put(particleTickData.getPlayer(), currentNumber + 1); data.put(particleTickData.getPlayer(), currentNumber + 1);
if (currentNumber != 0) { if (currentNumber != 0) {
return; return;
} }
@@ -19,7 +19,6 @@
package de.steamwar.lobby.particle.elements; package de.steamwar.lobby.particle.elements;
import de.steamwar.lobby.particle.ParticleData;
import de.steamwar.lobby.particle.ParticleElement; import de.steamwar.lobby.particle.ParticleElement;
import de.steamwar.lobby.particle.ParticleTickType; import de.steamwar.lobby.particle.ParticleTickType;
@@ -31,17 +30,6 @@ public abstract class DelegatingParticleElement implements ParticleElement {
this.particleElement = particleElement; this.particleElement = particleElement;
} }
public abstract String attribute();
@Override
public void aggregateAttributes(ParticleData particleData) {
String attribute = attribute();
if (attribute != null) {
particleData.add(attribute);
}
particleElement.aggregateAttributes(particleData);
}
@Override @Override
public ParticleTickType tickType() { public ParticleTickType tickType() {
return particleElement.tickType(); return particleElement.tickType();
@@ -19,6 +19,7 @@
package de.steamwar.lobby.particle.elements; package de.steamwar.lobby.particle.elements;
import de.steamwar.lobby.particle.BodyFrame;
import de.steamwar.lobby.particle.ParticleElement; import de.steamwar.lobby.particle.ParticleElement;
import de.steamwar.lobby.particle.ParticleTickData; import de.steamwar.lobby.particle.ParticleTickData;
import de.steamwar.lobby.particle.ParticleTickType; import de.steamwar.lobby.particle.ParticleTickType;
@@ -46,11 +47,6 @@ public class DoubleCircle extends Circle {
this.speed = speed; this.speed = speed;
} }
@Override
public String attribute() {
return "PARTICLE_ATTRIBUTE_BI_CIRCLE";
}
@Override @Override
public ParticleTickType tickType() { public ParticleTickType tickType() {
if (particleElement.tickType() == second.tickType()) { if (particleElement.tickType() == second.tickType()) {
@@ -62,6 +58,13 @@ public class DoubleCircle extends Circle {
@Override @Override
public void tick(ParticleTickData particleTickData) { public void tick(ParticleTickData particleTickData) {
Location location = particleTickData.getLocation(); Location location = particleTickData.getLocation();
BodyFrame frame = particleTickData.getFrame();
if (frame != null) {
double angle = (particleTickData.getDeg() * speed) % 360;
particleElement.tick(particleTickData.withLocation(location.clone().add(frame.ring(distance, angle))));
second.tick(particleTickData.withLocation(location.clone().add(frame.ring(distance, angle + Math.PI))));
return;
}
Vector vector = new Vector(distance, 0, 0); Vector vector = new Vector(distance, 0, 0);
vector.rotateAroundY((particleTickData.getDeg() * speed) % 360); vector.rotateAroundY((particleTickData.getDeg() * speed) % 360);
@@ -65,13 +65,12 @@ public class DustParticle implements ParticleElement {
Location location = particleTickData.getLocation().add(0.0, 0.2, 0.0); Location location = particleTickData.getLocation().add(0.0, 0.2, 0.0);
Particle.DustOptions dustOptions; Particle.DustOptions dustOptions;
if (gradient != null) { if (gradient != null) {
double result = (particleTickData.getDeg() * 10) % 360; Color color = gradient.getColor(particleTickData.gradientPosition(), 360);
Color color = gradient.getColor(result, 360);
dustOptions = withColor(color.getRed(), color.getGreen(), color.getBlue()); dustOptions = withColor(color.getRed(), color.getGreen(), color.getBlue());
} else { } else {
dustOptions = randomParticleDust(); dustOptions = randomParticleDust();
} }
display(location, particleTickData.getPlayer(), particleTickData.isOnlySelf(), particleTickData.isOnlyOthers(), player -> { display(particleTickData, player -> {
player.spawnParticle(particle, location, count, vx, vy, vz, speed, dustOptions); player.spawnParticle(particle, location, count, vx, vy, vz, speed, dustOptions);
}); });
} }
@@ -28,11 +28,6 @@ public class Floor extends DelegatingParticleElement {
super(particleElement); super(particleElement);
} }
@Override
public String attribute() {
return "PARTICLE_ATTRIBUTE_FLOOR";
}
@Override @Override
public void tick(ParticleTickData particleTickData) { public void tick(ParticleTickData particleTickData) {
if (!particleTickData.getWorld().getBlockAt(particleTickData.getPlayer().getLocation().subtract(0, 0.5, 0)).getType().isAir()) { if (!particleTickData.getWorld().getBlockAt(particleTickData.getPlayer().getLocation().subtract(0, 0.5, 0)).getType().isAir()) {
@@ -28,11 +28,6 @@ public class Flying extends DelegatingParticleElement {
super(particleElement); super(particleElement);
} }
@Override
public String attribute() {
return "PARTICLE_ATTRIBUTE_FLYING";
}
@Override @Override
public void tick(ParticleTickData particleTickData) { public void tick(ParticleTickData particleTickData) {
if (!particleTickData.getPlayer().isGliding()) { if (!particleTickData.getPlayer().isGliding()) {
@@ -19,7 +19,6 @@
package de.steamwar.lobby.particle.elements; package de.steamwar.lobby.particle.elements;
import de.steamwar.lobby.particle.ParticleData;
import de.steamwar.lobby.particle.ParticleElement; import de.steamwar.lobby.particle.ParticleElement;
import de.steamwar.lobby.particle.ParticleTickData; import de.steamwar.lobby.particle.ParticleTickData;
@@ -32,19 +31,6 @@ public class Group extends DelegatingParticleElement {
this.rest = rest; this.rest = rest;
} }
@Override
public String attribute() {
return null;
}
@Override
public void aggregateAttributes(ParticleData particleData) {
particleElement.aggregateAttributes(particleData);
for (ParticleElement particleElement : rest) {
particleElement.aggregateAttributes(particleData);
}
}
@Override @Override
public void tick(ParticleTickData particleTickData) { public void tick(ParticleTickData particleTickData) {
particleElement.tick(particleTickData); particleElement.tick(particleTickData);
@@ -19,15 +19,19 @@
package de.steamwar.lobby.particle.elements; package de.steamwar.lobby.particle.elements;
import de.steamwar.lobby.particle.BodyFrame;
import de.steamwar.lobby.particle.ParticleElement; import de.steamwar.lobby.particle.ParticleElement;
import de.steamwar.lobby.particle.ParticleTickData; import de.steamwar.lobby.particle.ParticleTickData;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.util.Vector;
import java.util.function.UnaryOperator; import java.util.function.UnaryOperator;
public class LocationMutator extends DelegatingParticleElement { public class LocationMutator extends DelegatingParticleElement {
private UnaryOperator<Location> mutator; private UnaryOperator<Location> mutator;
// Set for plain offsets, which turn into right/up/forward when placed relative to the body
private Vector offset;
public LocationMutator(ParticleElement particleElement, UnaryOperator<Location> mutator) { public LocationMutator(ParticleElement particleElement, UnaryOperator<Location> mutator) {
super(particleElement); super(particleElement);
@@ -36,15 +40,17 @@ public class LocationMutator extends DelegatingParticleElement {
public LocationMutator(ParticleElement particleElement, double vx, double vy, double vz) { public LocationMutator(ParticleElement particleElement, double vx, double vy, double vz) {
this(particleElement, location -> location.add(vx, vy, vz)); this(particleElement, location -> location.add(vx, vy, vz));
} this.offset = new Vector(vx, vy, vz);
@Override
public String attribute() {
return null;
} }
@Override @Override
public void tick(ParticleTickData particleTickData) { public void tick(ParticleTickData particleTickData) {
BodyFrame frame = particleTickData.getFrame();
if (frame != null && offset != null) {
Location location = particleTickData.getLocation().clone().add(frame.local(offset.getX(), offset.getY(), offset.getZ()));
particleElement.tick(particleTickData.withLocation(location));
return;
}
ParticleTickData nParticleTickData = particleTickData.withLocation(mutator.apply(particleTickData.getLocation())); ParticleTickData nParticleTickData = particleTickData.withLocation(mutator.apply(particleTickData.getLocation()));
particleElement.tick(nParticleTickData); particleElement.tick(nParticleTickData);
} }
@@ -28,11 +28,6 @@ public class NonFloor extends DelegatingParticleElement {
super(particleElement); super(particleElement);
} }
@Override
public String attribute() {
return "PARTICLE_ATTRIBUTE_NON_FLOOR";
}
@Override @Override
public void tick(ParticleTickData particleTickData) { public void tick(ParticleTickData particleTickData) {
if (particleTickData.getWorld().getBlockAt(particleTickData.getPlayer().getLocation().subtract(0, 0.5, 0)).getType().isAir()) { if (particleTickData.getWorld().getBlockAt(particleTickData.getPlayer().getLocation().subtract(0, 0.5, 0)).getType().isAir()) {
@@ -28,11 +28,6 @@ public class NonFlying extends DelegatingParticleElement {
super(particleElement); super(particleElement);
} }
@Override
public String attribute() {
return "PARTICLE_ATTRIBUTE_NON_FLYING";
}
@Override @Override
public void tick(ParticleTickData particleTickData) { public void tick(ParticleTickData particleTickData) {
if (particleTickData.getPlayer().isGliding()) { if (particleTickData.getPlayer().isGliding()) {
@@ -28,11 +28,6 @@ public class OnlyOthers extends DelegatingParticleElement {
super(particleElement); super(particleElement);
} }
@Override
public String attribute() {
return null;
}
@Override @Override
public void tick(ParticleTickData particleTickData) { public void tick(ParticleTickData particleTickData) {
particleElement.tick(particleTickData.onlyOthers()); particleElement.tick(particleTickData.onlyOthers());
@@ -28,11 +28,6 @@ public class OnlySelf extends DelegatingParticleElement {
super(particleElement); super(particleElement);
} }
@Override
public String attribute() {
return null;
}
@Override @Override
public void tick(ParticleTickData particleTickData) { public void tick(ParticleTickData particleTickData) {
particleElement.tick(particleTickData.onlySelf()); particleElement.tick(particleTickData.onlySelf());
@@ -19,58 +19,29 @@
package de.steamwar.lobby.particle.elements; package de.steamwar.lobby.particle.elements;
import de.steamwar.lobby.LobbySystem;
import de.steamwar.lobby.particle.ParticleElement; import de.steamwar.lobby.particle.ParticleElement;
import de.steamwar.lobby.particle.ParticleEnum;
import de.steamwar.lobby.particle.ParticleTickData; import de.steamwar.lobby.particle.ParticleTickData;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Random; import java.util.WeakHashMap;
public class RandomParticle implements ParticleElement { public class RandomParticle implements ParticleElement {
private RandomData randomData; // Weak keys drop players once they quit, so each player rolls again on their next session
private ParticleEnum[] particleEnums; private final Map<Player, Integer> data = new WeakHashMap<>();
private final ParticleElement[] particleElements;
private static class RandomData implements Listener { public RandomParticle(ParticleElement... particleElements) {
private Random random = new Random(); if (particleElements.length == 0) {
private int max; throw new IllegalArgumentException("ParticleElements must not be empty");
private Map<Player, Integer> data = new HashMap<>();
public RandomData(int max) {
this.max = max;
} }
this.particleElements = particleElements;
@EventHandler(ignoreCancelled = true)
public void onPlayerJoin(PlayerJoinEvent event) {
data.put(event.getPlayer(), random.nextInt(max));
}
@EventHandler(ignoreCancelled = true)
public void onPlayerQuit(PlayerQuitEvent event) {
data.remove(event.getPlayer());
}
}
public RandomParticle(ParticleEnum... particleEnums) {
if (particleEnums.length == 0) {
throw new IllegalArgumentException("ParticleEnums must not be empty");
}
this.randomData = new RandomData(particleEnums.length);
Bukkit.getPluginManager().registerEvents(randomData, LobbySystem.getInstance());
this.particleEnums = particleEnums;
} }
@Override @Override
public void tick(ParticleTickData particleTickData) { public void tick(ParticleTickData particleTickData) {
int currentNumber = randomData.data.getOrDefault(particleTickData.getPlayer(), 0); int currentNumber = data.computeIfAbsent(particleTickData.getPlayer(), player -> RANDOM.nextInt(particleElements.length));
particleEnums[currentNumber].getParticle().getParticleElement().tick(particleTickData); particleElements[currentNumber].tick(particleTickData);
} }
} }
@@ -74,7 +74,7 @@ public class SimpleParticle implements ParticleElement {
@Override @Override
public void tick(ParticleTickData particleTickData) { public void tick(ParticleTickData particleTickData) {
Location location = particleTickData.getLocation().add(0.0, 0.2, 0.0); Location location = particleTickData.getLocation().add(0.0, 0.2, 0.0);
display(location, particleTickData.getPlayer(), particleTickData.isOnlySelf(), particleTickData.isOnlyOthers(), player -> { display(particleTickData, player -> {
if (customVelocity) { if (customVelocity) {
player.spawnParticle(particle, location, count, vx, vy, vz, time); player.spawnParticle(particle, location, count, vx, vy, vz, time);
} else { } else {
@@ -28,11 +28,6 @@ public class Sneaking extends DelegatingParticleElement {
super(particleElement); super(particleElement);
} }
@Override
public String attribute() {
return "PARTICLE_ATTRIBUTE_SNEAKING";
}
@Override @Override
public void tick(ParticleTickData particleTickData) { public void tick(ParticleTickData particleTickData) {
if (!particleTickData.getPlayer().isSneaking()) { if (!particleTickData.getPlayer().isSneaking()) {
@@ -19,6 +19,7 @@
package de.steamwar.lobby.particle.elements; package de.steamwar.lobby.particle.elements;
import de.steamwar.lobby.particle.BodyFrame;
import de.steamwar.lobby.particle.ParticleElement; import de.steamwar.lobby.particle.ParticleElement;
import de.steamwar.lobby.particle.ParticleTickData; import de.steamwar.lobby.particle.ParticleTickData;
import de.steamwar.lobby.particle.ParticleTickType; import de.steamwar.lobby.particle.ParticleTickType;
@@ -48,11 +49,6 @@ public class TrippleCircle extends Circle {
this.speed = speed; this.speed = speed;
} }
@Override
public String attribute() {
return "PARTICLE_ATTRIBUTE_TRI_CIRCLE";
}
@Override @Override
public ParticleTickType tickType() { public ParticleTickType tickType() {
if (particleElement.tickType() == second.tickType() && particleElement.tickType() == third.tickType()) { if (particleElement.tickType() == second.tickType() && particleElement.tickType() == third.tickType()) {
@@ -64,6 +60,14 @@ public class TrippleCircle extends Circle {
@Override @Override
public void tick(ParticleTickData particleTickData) { public void tick(ParticleTickData particleTickData) {
Location location = particleTickData.getLocation(); Location location = particleTickData.getLocation();
BodyFrame frame = particleTickData.getFrame();
if (frame != null) {
double angle = (particleTickData.getDeg() * speed) % 360;
particleElement.tick(particleTickData.withLocation(location.clone().add(frame.ring(distance, angle))));
second.tick(particleTickData.withLocation(location.clone().add(frame.ring(distance, angle + Math.toRadians(120)))));
third.tick(particleTickData.withLocation(location.clone().add(frame.ring(distance, angle + Math.toRadians(240)))));
return;
}
Vector vector = new Vector(distance, 0, 0); Vector vector = new Vector(distance, 0, 0);
vector.rotateAroundY((particleTickData.getDeg() * speed) % 360); vector.rotateAroundY((particleTickData.getDeg() * speed) % 360);
@@ -19,6 +19,8 @@
package de.steamwar.lobby.particle.elements; package de.steamwar.lobby.particle.elements;
import de.steamwar.lobby.particle.BodyFrame;
import de.steamwar.lobby.particle.BodyMotion;
import de.steamwar.lobby.particle.ParticleElement; import de.steamwar.lobby.particle.ParticleElement;
import de.steamwar.lobby.particle.ParticleTickData; import de.steamwar.lobby.particle.ParticleTickData;
import de.steamwar.lobby.particle.WingDesign; import de.steamwar.lobby.particle.WingDesign;
@@ -27,8 +29,29 @@ import org.bukkit.util.Vector;
public class Wing extends DelegatingParticleElement { public class Wing extends DelegatingParticleElement {
public enum Motion {
NONE,
FLAP_SLOW,
FLAP_FAST,
// One wing beat after each jump; unlocked by a reward
BEAT_ON_JUMP,
}
// Wings stand slightly open when idle and open further the faster the player moves
private static final double IDLE_OPEN_DEGREES = 5;
private static final double MOVING_OPEN_DEGREES = 15;
private static final double FULL_OPEN_SPEED = 0.6;
private static final double FLAP_DEGREES = 25;
public static final int JUMP_BEAT_TICKS = 10;
private double size; private double size;
private WingDesign wingDesign; private WingDesign wingDesign;
// Colour each point by its distance from the spine instead of by time
private boolean colorAlongWing;
// Share of the points shown each tick, in percent; below 100 the wing twinkles
private int density = 100;
private Motion motion = Motion.NONE;
private double widest;
public Wing(ParticleElement particleElement, double size, WingDesign wingDesign) { public Wing(ParticleElement particleElement, double size, WingDesign wingDesign) {
super(particleElement); super(particleElement);
@@ -36,21 +59,75 @@ public class Wing extends DelegatingParticleElement {
this.wingDesign = wingDesign; this.wingDesign = wingDesign;
} }
@Override public Wing(ParticleElement particleElement, double size, WingDesign wingDesign, boolean colorAlongWing, int density, Motion motion) {
public String attribute() { this(particleElement, size, wingDesign);
return "PARTICLE_ATTRIBUTE_WING"; this.colorAlongWing = colorAlongWing;
this.density = density;
this.motion = motion;
for (Vector vector : wingDesign.getVectors()) {
widest = Math.max(widest, Math.abs(vector.getX()));
}
} }
@Override @Override
public void tick(ParticleTickData particleTickData) { public void tick(ParticleTickData particleTickData) {
double ticks = particleTickData.getDeg() * 10;
double open = Math.toRadians(openDegrees(particleTickData, ticks));
double cos = Math.cos(open), sin = Math.sin(open);
double sneak = particleTickData.getPlayer().isSneaking() ? 0.5 : 0;
BodyFrame frame = particleTickData.getFrame();
if (frame != null) {
// Half a block behind the back, so the wings stay on the back even while gliding
Location base = particleTickData.getLocation();
for (Vector dVector : wingDesign.getVectors()) { for (Vector dVector : wingDesign.getVectors()) {
Vector vector = new Vector(dVector.getX() * size, 0.6 + dVector.getY() * size - (particleTickData.getPlayer().isSneaking() ? 0.5 : 0), 0.5); if (hidden()) continue;
vector.rotateAroundY(Math.toRadians(particleTickData.getPlayer().getLocation().getYaw() * -1)); double out = dVector.getX() * size;
Vector local = frame.local(out * cos, 0.6 + dVector.getY() * size - sneak, -0.5 - Math.abs(out) * sin);
particleElement.tick(shaded(particleTickData, dVector).withLocation(base.clone().add(local)));
}
return;
}
for (Vector dVector : wingDesign.getVectors()) {
if (hidden()) continue;
double out = dVector.getX() * size;
Vector vector = new Vector(out * cos, 0.6 + dVector.getY() * size - sneak, 0.5 + Math.abs(out) * sin);
vector.rotateAroundY(Math.toRadians(BodyMotion.yaw(particleTickData.getPlayer()) * -1));
vector.setX(-vector.getX()); vector.setX(-vector.getX());
vector.setZ(-vector.getZ()); vector.setZ(-vector.getZ());
Location location = particleTickData.getPlayer().getLocation().clone().add(vector); Location location = particleTickData.getPlayer().getLocation().clone().add(vector);
ParticleTickData current = particleTickData.withLocation(location); ParticleTickData current = shaded(particleTickData, dVector).withLocation(location);
particleElement.tick(current); particleElement.tick(current);
} }
} }
// How far each half is turned back around the spine; flapping swings it back and forth
private double openDegrees(ParticleTickData particleTickData, double ticks) {
double speed = Math.min(1, BodyMotion.speed(particleTickData.getPlayer()) / FULL_OPEN_SPEED);
double open = IDLE_OPEN_DEGREES + (MOVING_OPEN_DEGREES - IDLE_OPEN_DEGREES) * speed;
return switch (motion) {
case FLAP_SLOW -> open + FLAP_DEGREES * Math.sin(2 * Math.PI * ticks / 20);
case FLAP_FAST -> open + FLAP_DEGREES * Math.sin(2 * Math.PI * ticks / 10);
case BEAT_ON_JUMP -> open + jumpBeat(particleTickData);
default -> open;
};
}
// One swing back and forth during the ticks after a jump
private double jumpBeat(ParticleTickData particleTickData) {
long sinceJump = BodyMotion.ticksSinceJump(particleTickData.getPlayer());
return sinceJump > JUMP_BEAT_TICKS ? 0 : FLAP_DEGREES * Math.sin(Math.PI * sinceJump / JUMP_BEAT_TICKS);
}
// Full wings never draw a random number, so presets keep their exact look
private boolean hidden() {
return density < 100 && RANDOM.nextInt(100) >= density;
}
private ParticleTickData shaded(ParticleTickData particleTickData, Vector point) {
if (!colorAlongWing || widest == 0) {
return particleTickData;
}
return particleTickData.withShade(Math.abs(point.getX()) / widest);
}
} }
@@ -19,6 +19,7 @@
package de.steamwar.lobby.particle.elements.custom; package de.steamwar.lobby.particle.elements.custom;
import de.steamwar.lobby.particle.BodyMotion;
import de.steamwar.lobby.particle.ParticleElement; import de.steamwar.lobby.particle.ParticleElement;
import de.steamwar.lobby.particle.ParticleTickData; import de.steamwar.lobby.particle.ParticleTickData;
import de.steamwar.lobby.particle.elements.DelegatingParticleElement; import de.steamwar.lobby.particle.elements.DelegatingParticleElement;
@@ -41,11 +42,6 @@ public class HearthBeat extends DelegatingParticleElement {
this.speed = speed; this.speed = speed;
} }
@Override
public String attribute() {
return null;
}
@Override @Override
public void tick(ParticleTickData particleTickData) { public void tick(ParticleTickData particleTickData) {
double time = ((particleTickData.getDeg() * speed) % 360) / 360.0; double time = ((particleTickData.getDeg() * speed) % 360) / 360.0;
@@ -53,7 +49,7 @@ public class HearthBeat extends DelegatingParticleElement {
double x = (time - 0.5) * 2; double x = (time - 0.5) * 2;
Vector vector = new Vector(x, 1.1 + y - (particleTickData.getPlayer().isSneaking() ? 0.5 : 0), 0.7); Vector vector = new Vector(x, 1.1 + y - (particleTickData.getPlayer().isSneaking() ? 0.5 : 0), 0.7);
vector.rotateAroundY(Math.toRadians(particleTickData.getPlayer().getLocation().getYaw() * -1)); vector.rotateAroundY(Math.toRadians(BodyMotion.yaw(particleTickData.getPlayer()) * -1));
vector.setX(-vector.getX()); vector.setX(-vector.getX());
vector.setZ(-vector.getZ()); vector.setZ(-vector.getZ());
Location location = particleTickData.getPlayer().getLocation().clone().add(vector); Location location = particleTickData.getPlayer().getLocation().clone().add(vector);
@@ -29,11 +29,6 @@ public class NonMoving extends DelegatingParticleElement {
super(particleElement); super(particleElement);
} }
@Override
public String attribute() {
return "PARTICLE_ATTRIBUTE_NON_MOVING";
}
@Override @Override
public void tick(ParticleTickData particleTickData) { public void tick(ParticleTickData particleTickData) {
if (particleTickData.isMoving()) { if (particleTickData.isMoving()) {
@@ -47,11 +47,6 @@ public class PulseShimmer extends DelegatingParticleElement {
this.rotationSpeed = rotationSpeed; this.rotationSpeed = rotationSpeed;
} }
@Override
public String attribute() {
return null;
}
@Override @Override
public void tick(ParticleTickData particleTickData) { public void tick(ParticleTickData particleTickData) {
double time = ((particleTickData.getDeg() * speed) % 360) / 360.0; double time = ((particleTickData.getDeg() * speed) % 360) / 360.0;
@@ -19,9 +19,11 @@
package de.steamwar.lobby.particle.elements.custom; package de.steamwar.lobby.particle.elements.custom;
import de.steamwar.lobby.particle.BodyFrame;
import de.steamwar.lobby.particle.ParticleElement; import de.steamwar.lobby.particle.ParticleElement;
import de.steamwar.lobby.particle.ParticleTickData; import de.steamwar.lobby.particle.ParticleTickData;
import de.steamwar.lobby.particle.elements.DelegatingParticleElement; import de.steamwar.lobby.particle.elements.DelegatingParticleElement;
import org.bukkit.Location;
public class YOffset extends DelegatingParticleElement { public class YOffset extends DelegatingParticleElement {
@@ -40,11 +42,6 @@ public class YOffset extends DelegatingParticleElement {
this.bounce = bounce; this.bounce = bounce;
} }
@Override
public String attribute() {
return null;
}
@Override @Override
public void tick(ParticleTickData particleTickData) { public void tick(ParticleTickData particleTickData) {
double y; double y;
@@ -71,7 +68,9 @@ public class YOffset extends DelegatingParticleElement {
y = minY + (maxY - minY) * value; y = minY + (maxY - minY) * value;
} }
} }
ParticleTickData current = particleTickData.withLocation(particleTickData.getLocation().clone().add(0, y, 0)); BodyFrame frame = particleTickData.getFrame();
Location location = particleTickData.getLocation().clone();
ParticleTickData current = particleTickData.withLocation(frame == null ? location.add(0, y, 0) : location.add(frame.getUp().clone().multiply(y)));
particleElement.tick(current); particleElement.tick(current);
} }
} }
@@ -0,0 +1,46 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.lobby.particle.elements.touch;
import de.steamwar.lobby.particle.BodyMotion;
import de.steamwar.lobby.particle.ParticleElement;
import de.steamwar.lobby.particle.ParticleTickData;
import de.steamwar.lobby.particle.elements.DelegatingParticleElement;
// Ticks the element every tick for a while after each jump, and as usual otherwise
public class AfterJump extends DelegatingParticleElement {
private final ParticleElement otherwise;
private final int ticks;
public AfterJump(ParticleElement particleElement, ParticleElement otherwise, int ticks) {
super(particleElement);
this.otherwise = otherwise;
this.ticks = ticks;
}
@Override
public void tick(ParticleTickData particleTickData) {
if (BodyMotion.ticksSinceJump(particleTickData.getPlayer()) <= ticks) {
particleElement.tick(particleTickData);
} else {
otherwise.tick(particleTickData);
}
}
}
@@ -0,0 +1,63 @@
/*
* 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.elements.touch;
import de.steamwar.lobby.particle.BodyMotion;
import de.steamwar.lobby.particle.ParticleElement;
import de.steamwar.lobby.particle.ParticleTickData;
import de.steamwar.lobby.particle.elements.DelegatingParticleElement;
import org.bukkit.entity.Player;
// Shows the burst once at the player's feet in the tick the event happens
public class EventBurst extends DelegatingParticleElement {
public enum Trigger {
JUMP,
LAND,
SNEAK,
}
private final Trigger trigger;
// Landings from lower than this many blocks are ignored
private final double minimumFall;
public EventBurst(Trigger trigger, ParticleElement burst) {
this(trigger, 0, burst);
}
public EventBurst(Trigger trigger, double minimumFall, ParticleElement burst) {
super(burst);
this.trigger = trigger;
this.minimumFall = minimumFall;
}
@Override
public void tick(ParticleTickData particleTickData) {
Player player = particleTickData.getPlayer();
boolean happened = switch (trigger) {
case JUMP -> BodyMotion.jumped(player);
case LAND -> BodyMotion.landed(player) >= minimumFall;
case SNEAK -> BodyMotion.startedSneaking(player);
};
if (happened) {
particleElement.tick(particleTickData.withLocation(player.getLocation()));
}
}
}
@@ -0,0 +1,69 @@
/*
* 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.elements.touch;
import de.steamwar.lobby.particle.ParticleElement;
import de.steamwar.lobby.particle.ParticleTickData;
import de.steamwar.lobby.particle.elements.DelegatingParticleElement;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map;
import java.util.WeakHashMap;
// Leaves a print every given distance on the ground and keeps showing it for a while after the player walked on
public class Footprints extends DelegatingParticleElement {
private record Print(Location location, int age) {
}
private final int lifetime;
private final double step;
// Weak keys drop players once they quit
private final Map<Player, Deque<Print>> prints = new WeakHashMap<>();
public Footprints(ParticleElement particleElement, int lifetime, double step) {
super(particleElement);
this.lifetime = lifetime;
this.step = step;
}
@Override
@SuppressWarnings("deprecation")
public void tick(ParticleTickData particleTickData) {
Player player = particleTickData.getPlayer();
Deque<Print> playerPrints = prints.computeIfAbsent(player, key -> new ArrayDeque<>());
Location feet = player.getLocation();
Print last = playerPrints.peekLast();
if (player.isOnGround() && (last == null || last.location().getWorld() != feet.getWorld() || last.location().distanceSquared(feet) > step * step)) {
playerPrints.addLast(new Print(feet, 0));
}
int size = playerPrints.size();
for (int i = 0; i < size; i++) {
Print print = playerPrints.pollFirst();
if (print.age() >= lifetime) continue;
particleElement.tick(particleTickData.withLocation(print.location().clone()));
playerPrints.addLast(new Print(print.location(), print.age() + 1));
}
}
}
@@ -0,0 +1,65 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.lobby.particle.elements.touch;
import de.steamwar.lobby.particle.ParticleElement;
import de.steamwar.lobby.particle.ParticleTickData;
import org.bukkit.Location;
import org.bukkit.Particle;
import org.bukkit.util.Vector;
// Enchanting glyphs that fly between each point and the player's chest. Spawned with an amount of 0, the offset becomes
// the glyph's path: it appears at location + offset and flies to the location, like glyphs flying into an enchanting
// table.
public class GlyphPath implements ParticleElement {
public enum Direction {
INTO_PLAYER,
OUT_OF_PLAYER,
}
private static final double CHEST = 1.2;
private final Direction direction;
private final double spreadX;
private final double spreadY;
private final double spreadZ;
private final int count;
public GlyphPath(Direction direction, double spreadX, double spreadY, double spreadZ, int count) {
this.direction = direction;
this.spreadX = spreadX;
this.spreadY = spreadY;
this.spreadZ = spreadZ;
this.count = count;
}
@Override
public void tick(ParticleTickData particleTickData) {
Location chest = particleTickData.getPlayer().getLocation().add(0, CHEST, 0);
for (int i = 0; i < count; i++) {
// Spread like the game spreads particles, as the game does not spread those spawned with an amount of 0
Location point = particleTickData.getLocation().clone().add(RANDOM.nextGaussian() * spreadX, RANDOM.nextGaussian() * spreadY, RANDOM.nextGaussian() * spreadZ);
Vector fromChest = point.toVector().subtract(chest.toVector());
Location target = direction == Direction.INTO_PLAYER ? chest : point;
Vector path = direction == Direction.INTO_PLAYER ? fromChest : fromChest.multiply(-1);
display(particleTickData, player -> player.spawnParticle(Particle.ENCHANT, target, 0, path.getX(), path.getY(), path.getZ(), 1));
}
}
}
@@ -0,0 +1,47 @@
/*
* 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.elements.touch;
import de.steamwar.lobby.particle.ParticleElement;
import de.steamwar.lobby.particle.ParticleTickData;
import de.steamwar.lobby.particle.elements.DelegatingParticleElement;
import org.bukkit.Location;
// A flat ring of points around the location, all at once, for bursts
public class Ring extends DelegatingParticleElement {
private final double radius;
private final int points;
public Ring(ParticleElement particleElement, double radius, int points) {
super(particleElement);
this.radius = radius;
this.points = points;
}
@Override
public void tick(ParticleTickData particleTickData) {
Location center = particleTickData.getLocation();
for (int i = 0; i < points; i++) {
double angle = 2 * Math.PI * i / points;
particleElement.tick(particleTickData.withLocation(center.clone().add(Math.cos(angle) * radius, 0, Math.sin(angle) * radius)));
}
}
}
@@ -0,0 +1,82 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.lobby.particle.elements.touch;
import de.steamwar.lobby.particle.BodyMotion;
import de.steamwar.lobby.particle.ParticleElement;
import de.steamwar.lobby.particle.ParticleTickData;
import de.steamwar.lobby.particle.elements.DelegatingParticleElement;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.util.Vector;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
// A line of particles between nearby players who show the same link. It starts in one player's particle and blends
// into the other's towards them. Each pair is drawn by only one of them.
public class TeamLink extends DelegatingParticleElement {
public static final double RANGE = 8;
private static final double CHEST = 1.2;
// Spreads the blend evenly along the line, so it does not flicker from tick to tick
private static final double GOLDEN_RATIO = 0.6180339887;
private record Showing(long tick, ParticleElement particle) {
}
// Per link, who showed it in which tick with which particle
private static final Map<String, Map<Player, Showing>> SHOWING = new HashMap<>();
private final String link;
private final double spacing;
public TeamLink(String link, ParticleElement particleElement, double spacing) {
super(particleElement);
this.link = link;
this.spacing = spacing;
}
@Override
public void tick(ParticleTickData particleTickData) {
Player player = particleTickData.getPlayer();
long now = BodyMotion.currentTick();
Map<Player, Showing> showing = SHOWING.computeIfAbsent(link, key -> new WeakHashMap<>());
showing.put(player, new Showing(now, particleElement));
Location from = player.getLocation().add(0, CHEST, 0);
for (Map.Entry<Player, Showing> other : showing.entrySet()) {
Player partner = other.getKey();
// Still showing it, and drawn by the player with the smaller id only
if (partner == player || other.getValue().tick() < now - 1 || player.getUniqueId().compareTo(partner.getUniqueId()) > 0) continue;
Location to = partner.getLocation().add(0, CHEST, 0);
if (to.getWorld() != from.getWorld() || to.distanceSquared(from) > RANGE * RANGE) continue;
Vector step = to.toVector().subtract(from.toVector());
int points = (int) Math.ceil(step.length() / spacing);
step.multiply(1.0 / points);
for (int i = 1; i < points; i++) {
double towardsPartner = (double) i / points;
ParticleElement particle = (i * GOLDEN_RATIO) % 1 < towardsPartner ? other.getValue().particle() : particleElement;
particle.tick(particleTickData.withLocation(from.clone().add(step.clone().multiply(i))));
}
}
}
}
@@ -0,0 +1,60 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.lobby.particle.gui;
import de.steamwar.inventory.SWInventory;
import de.steamwar.inventory.SWItem;
import lombok.experimental.UtilityClass;
import org.bukkit.Material;
// Every particle menu puts the same button in the same column of its last row
@UtilityClass
class BottomBar {
// Taken by the page arrows of list menus
final int PREVIOUS_PAGE = 0;
final int BACK = 1;
// Shows why a locked slot cannot be edited
final int STATUS = 2;
final int ADD = 3;
final int RENAME = 4;
final int ICON = 5;
final int COST = 6;
// Delete in editors, reset in value screens; no menu has both
final int DELETE = 7;
final int RESET = 7;
final int NEXT_PAGE = 8;
int position(int inventorySize, int column) {
return inventorySize - 9 + column;
}
// Fills the columns this menu does not use, so the buttons it does use stay recognisable by position.
// List menus keep the outer columns free for the page arrows they add when opened.
void fillUnused(SWInventory inventory, int inventorySize, boolean listMenu) {
for (int column = listMenu ? 1 : 0; column < (listMenu ? 8 : 9); column++) {
int position = position(inventorySize, column);
if (!inventory.hasItem(position)) {
inventory.setItem(position, new SWItem(Material.BLACK_STAINED_GLASS_PANE, " ", clickType -> {
}));
}
}
}
}
@@ -0,0 +1,309 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.lobby.particle.gui;
import de.steamwar.inventory.SWInventory;
import de.steamwar.inventory.SWItem;
import de.steamwar.lobby.LobbySystem;
import de.steamwar.lobby.particle.UnlockContext;
import de.steamwar.lobby.particle.design.*;
import de.steamwar.lobby.particle.slot.ParticleEntry;
import de.steamwar.lobby.particle.slot.ParticleSlot;
import de.steamwar.lobby.particle.slot.ParticleSlots;
import de.steamwar.lobby.util.LobbyPlayer;
import de.steamwar.sql.SteamwarUser;
import lombok.experimental.UtilityClass;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static de.steamwar.lobby.particle.design.EntryParameters.*;
import static de.steamwar.lobby.particle.gui.ParticleGuiText.*;
// Settings of one entry in labelled rows; a click on a setting toggles it or opens its own screen
@UtilityClass
class EntryEditor {
private final int ROW_DESIGN = 0;
private final int ROW_PARTICLE = 1;
private final int ROW_SHAPE = 2;
private final int ROW_TIMING = 3;
private final int ROW_CONDITIONS = 4;
// Settings start after the row header and one column of space
private final int FIRST_SETTING = 2;
private final int SIZE = 54;
private final int BACK = BottomBar.position(SIZE, BottomBar.BACK);
private final int COST = BottomBar.position(SIZE, BottomBar.COST);
private final int DELETE = BottomBar.position(SIZE, BottomBar.DELETE);
private final int MAX_CYCLED_OPTIONS = 5;
private final Map<Parameter<?>, Material> ICONS = Map.ofEntries(
Map.entry(COUNT, Material.GLOWSTONE_DUST),
Map.entry(SPEED, Material.FEATHER),
Map.entry(SPREAD_X, Material.GUNPOWDER),
Map.entry(SPREAD_Y, Material.GUNPOWDER),
Map.entry(SPREAD_Z, Material.GUNPOWDER),
Map.entry(OFFSET_X, Material.SPECTRAL_ARROW),
Map.entry(OFFSET_Y, Material.SPECTRAL_ARROW),
Map.entry(OFFSET_Z, Material.SPECTRAL_ARROW),
Map.entry(INTERVAL, Material.CLOCK),
Map.entry(TICK_MODE, Material.REPEATER),
Map.entry(PLACEMENT, Material.PLAYER_HEAD),
Map.entry(TRAIL_DURATION, Material.CLOCK),
Map.entry(TRAIL_TARGET_X, Material.LEAD),
Map.entry(TRAIL_TARGET_Y, Material.LEAD),
Map.entry(TRAIL_TARGET_Z, Material.LEAD),
Map.entry(MOVEMENT, Material.LEATHER_BOOTS),
Map.entry(GLIDING, Material.ELYTRA),
Map.entry(GROUND, Material.GRASS_BLOCK),
Map.entry(SNEAK, Material.LEATHER_LEGGINGS),
Map.entry(VISIBILITY, Material.SPYGLASS),
Map.entry(Designs.CIRCLE_RADIUS, Material.COMPASS),
Map.entry(Designs.CIRCLE_SPEED, Material.RECOVERY_COMPASS),
Map.entry(Designs.CIRCLE_PHASE, Material.TARGET),
Map.entry(Designs.CIRCLE_RISE, Material.LADDER),
Map.entry(Designs.CIRCLE_RISE_SPEED, Material.RABBIT_FOOT),
Map.entry(Designs.WING_SHAPE, Material.PHANTOM_MEMBRANE),
Map.entry(Designs.WING_SIZE, Material.SCAFFOLDING),
Map.entry(Designs.WING_DETAIL, Material.AMETHYST_SHARD),
Map.entry(Designs.WING_COLORING, Material.CYAN_DYE),
Map.entry(Designs.WING_SPARKLE, Material.AMETHYST_CLUSTER),
Map.entry(Designs.WING_MOTION, Material.FEATHER)
);
private record Context(Player player, SteamwarUser user, int index, int entryIndex, ParticleSlot slot, ParticleEntry entry) {
void reopen() {
open(player, index, entryIndex);
}
}
void open(Player player, int index, int entryIndex) {
LobbyPlayer lobbyPlayer = LobbyPlayer.getLobbyPlayer(player);
ParticleSlot slot = lobbyPlayer.getSlot(index);
SteamwarUser user = SteamwarUser.get(player.getUniqueId());
if (slot == null || entryIndex >= slot.getEntries().size() || !ParticleSlots.requirement(index).test(new UnlockContext(user))) {
SlotEditor.open(player, index);
return;
}
Context context = new Context(player, user, index, entryIndex, slot, slot.getEntries().get(entryIndex));
ParticleEntry entry = context.entry();
ParticleDesign design = entry.getDesign();
SWInventory inventory = new SWInventory(player, SIZE, msg("PARTICLE_ENTRY_EDITOR", player, designName(design, player)));
header(inventory, ROW_DESIGN, Material.PAINTING, "PARTICLE_SECTION_DESIGN", player);
header(inventory, ROW_PARTICLE, Material.FIREWORK_STAR, "PARTICLE_SECTION_PARTICLE", player);
header(inventory, ROW_SHAPE, Material.ARMOR_STAND, "PARTICLE_SECTION_SHAPE", player);
header(inventory, ROW_TIMING, Material.DAYLIGHT_DETECTOR, "PARTICLE_SECTION_TIMING", player);
header(inventory, ROW_CONDITIONS, Material.OBSERVER, "PARTICLE_SECTION_CONDITIONS", player);
inventory.setItem(ROW_DESIGN * 9 + FIRST_SETTING, new SWItem(design.getIcon(), designName(design, player), List.of(msg("PARTICLE_VALUE_CLICK", player)), false,
clickType -> Pickers.design(player, design, picked -> changeDesign(context, picked), context::reopen)));
if (design.usesEntryParticle()) {
inventory.setItem(ROW_DESIGN * 9 + FIRST_SETTING + 1, parameterItem(context, PARTICLE));
ParticleType type = entry.getValues().get(PARTICLE);
if (type.usesColors()) {
inventory.setItem(ROW_DESIGN * 9 + FIRST_SETTING + 2, parameterItem(context, COLORS));
} else if (type.usesBlock()) {
inventory.setItem(ROW_DESIGN * 9 + FIRST_SETTING + 2, parameterItem(context, BLOCK));
} else if (type == ParticleType.ENCHANT) {
inventory.setItem(ROW_DESIGN * 9 + FIRST_SETTING + 2, parameterItem(context, GLYPH_FLIGHT));
}
// The trail target follows the particle; its duration goes to the end of the particle row
List<Parameter<?>> particleRow = new ArrayList<>(design.replacesAmount(entry.getValues()) ? List.of(SPEED, SPREAD_X, SPREAD_Y, SPREAD_Z) : List.of(COUNT, SPEED, SPREAD_X, SPREAD_Y, SPREAD_Z));
if (type.isTrail()) {
List<Parameter<?>> target = List.of(TRAIL_TARGET_X, TRAIL_TARGET_Y, TRAIL_TARGET_Z);
for (int i = 0; i < target.size(); i++) {
inventory.setItem(ROW_DESIGN * 9 + FIRST_SETTING + 3 + i, parameterItem(context, target.get(i)));
}
particleRow.add(TRAIL_DURATION);
}
row(inventory, context, ROW_PARTICLE, particleRow);
} else {
inventory.setItem(ROW_PARTICLE * 9 + FIRST_SETTING, new SWItem(Material.STRUCTURE_VOID, msg("PARTICLE_FIXED_LOOK", player), clickType -> {
}));
}
row(inventory, context, ROW_SHAPE, design.getParameters().stream().filter(parameter -> design.showsSetting(parameter, entry.getValues())).toList());
row(inventory, context, ROW_TIMING, List.of(TICK_MODE, INTERVAL, PLACEMENT, OFFSET_X, OFFSET_Y, OFFSET_Z));
row(inventory, context, ROW_CONDITIONS, List.of(MOVEMENT, GLIDING, GROUND, SNEAK, VISIBILITY));
inventory.setItem(BACK, new SWItem(Material.ARROW, msg("PARTICLE_BACK", player), clickType -> SlotEditor.open(player, index)));
inventory.setItem(COST, SlotEditor.costItem(player, user, slot, entry));
inventory.setItem(DELETE, new SWItem(Material.LAVA_BUCKET, msg("PARTICLE_ENTRY_DELETE", player), List.of(msg("PARTICLE_SHIFT_TO_DELETE", player)), false, clickType -> {
if (!clickType.isShiftClick()) return;
slot.getEntries().remove(entryIndex);
SlotEditor.save(player, index, slot);
SlotEditor.open(player, index);
}));
BottomBar.fillUnused(inventory, SIZE, false);
SlotEditor.fill(inventory, 0, SIZE);
inventory.open();
}
// Glowing so the row labels stand apart from the settings next to them
private void header(SWInventory inventory, int row, Material icon, String key, Player player) {
inventory.setItem(row * 9, new SWItem(icon, msg(key, player), List.of(), true, clickType -> {
}));
}
private void row(SWInventory inventory, Context context, int row, List<Parameter<?>> parameters) {
for (int i = 0; i < parameters.size() && FIRST_SETTING + i < 9; i++) {
inventory.setItem(row * 9 + FIRST_SETTING + i, parameterItem(context, parameters.get(i)));
}
}
@SuppressWarnings("unchecked")
private SWItem parameterItem(Context context, Parameter<?> parameter) {
Player player = context.player();
Object value = context.entry().getValues().get(parameter);
List<String> lore = new ArrayList<>();
if (parameter instanceof ChoiceParameter<?> choice && isCycled(choice)) {
lore.addAll(optionLines((ChoiceParameter<Object>) choice, value, player));
} else {
lore.add(msg("PARTICLE_VALUE", player, valueText(parameter, value, player)));
}
lore.add("");
lore.add(msg(isToggle(parameter) ? "PARTICLE_VALUE_TOGGLE" : isCycled(parameter) ? "PARTICLE_VALUE_CYCLE" : "PARTICLE_VALUE_CLICK", player));
Material icon = ICONS.getOrDefault(parameter, Material.PAPER);
if (parameter instanceof BooleanParameter) icon = (Boolean) value ? Material.LIME_DYE : Material.GRAY_DYE;
if (parameter == PARTICLE) icon = ((ParticleType) value).getIcon();
if (parameter == COLORS) icon = Material.RED_DYE;
if (parameter == BLOCK) icon = (Material) value;
if (value instanceof WingShape shape) icon = shape.getIcon();
return new SWItem(icon, name(parameter.getNameKey(), player), lore, false, clickType -> {
if (parameter instanceof BooleanParameter toggle) {
apply(context, toggle, !(Boolean) value);
context.reopen();
} else if (parameter instanceof DoubleParameter number) {
ValueEditors.editNumber(player, name(number.getNameKey(), player), () -> context.entry().getValues().get(number),
number.getStep(), number.getMin(), number.getMax(), number.getDefaultValue(), false,
next -> apply(context, number, next), context::reopen);
} else if (parameter instanceof IntParameter number) {
ValueEditors.editNumber(player, name(number.getNameKey(), player), () -> (double) context.entry().getValues().get(number),
1, number.getMin(), number.getMax(), number.getDefaultValue(), true,
next -> apply(context, number, (int) Math.round(next)), context::reopen);
} else if (parameter instanceof GradientParameter gradient) {
ValueEditors.editColors(player, () -> context.entry().getValues().get(gradient), gradient.getMaxColors(),
colors -> apply(context, gradient, colors), context::reopen);
} else if (parameter instanceof ChoiceParameter<?> choice) {
if (isCycled(choice)) {
cycle(context, (ChoiceParameter<Object>) choice, value, clickType.isRightClick() ? -1 : 1);
} else {
pick(context, (ChoiceParameter<Object>) choice, value);
}
}
});
}
// Choices with a few options cycle through them on click instead of opening a list
private boolean isCycled(Parameter<?> parameter) {
return parameter instanceof ChoiceParameter<?> choice && choice.getBuildableValues().size() <= MAX_CYCLED_OPTIONS;
}
// With two options both click directions lead to the same value, so it reads as a plain toggle
private boolean isToggle(Parameter<?> parameter) {
return parameter instanceof BooleanParameter || (isCycled(parameter) && ((ChoiceParameter<?>) parameter).getBuildableValues().size() == 2);
}
// All options of a cycled choice, the current one highlighted
private <E> List<String> optionLines(ChoiceParameter<E> parameter, E current, Player player) {
List<String> lines = new ArrayList<>();
for (E option : parameter.getBuildableValues()) {
String name = choiceName(parameter, option, player).replaceAll("§.", "");
lines.add(option == current ? "§a» " + name : "§8» §7" + name);
}
return lines;
}
// Left click moves forward, right click backward, skipping locked options
private <E> void cycle(Context context, ChoiceParameter<E> parameter, E current, int direction) {
UnlockContext unlock = new UnlockContext(SteamwarUser.get(context.player().getUniqueId()));
List<E> values = parameter.getBuildableValues().stream()
.filter(value -> value == current || parameter.requirement(value).test(unlock))
.toList();
apply(context, parameter, values.get(Math.floorMod(values.indexOf(current) + direction, values.size())));
context.reopen();
}
private <E> void pick(Context context, ChoiceParameter<E> parameter, E current) {
Pickers.choice(context.player(), parameter, current, EntryEditor::choiceIcon,
parameter::requirement, picked -> {
apply(context, parameter, picked);
context.reopen();
}, context::reopen);
}
private Material choiceIcon(Object value) {
if (value instanceof ParticleType type) return type.getIcon();
if (value instanceof WingShape shape) return shape.getIcon();
if (value instanceof Material material) return material;
return Material.LEVER;
}
// Saves the change, or reverts it and tells the player when the slot would go over its budget
private <T> boolean apply(Context context, Parameter<T> parameter, T value) {
T previous = context.entry().getValues().get(parameter);
if (Objects.equals(previous, parameter.validate(value))) {
return true;
}
double previousCost = context.slot().cost();
ParameterValues saved = context.entry().getValues().copy();
context.entry().getValues().set(parameter, value);
context.entry().getDesign().adjust(parameter, context.entry().getValues());
if (!SlotEditor.allows(context.user(), context.slot(), previousCost)) {
context.entry().getValues().restore(saved);
LobbySystem.getMessage().send("PARTICLE_SLOT_OVER_BUDGET", context.player());
return false;
}
SlotEditor.save(context.player(), context.index(), context.slot());
return true;
}
// Keeps the particle, offset and conditions; a fixed design brings its own conditions and timing
private void changeDesign(Context context, ParticleDesign design) {
ParticleEntry previous = context.entry();
ParticleEntry next = new ParticleEntry(design);
for (Parameter<?> parameter : EntryParameters.ALL) {
copy(parameter, previous.getValues(), next.getValues());
}
if (!design.usesEntryParticle()) {
design.applyDefaults(next.getValues());
}
List<ParticleEntry> entries = context.slot().getEntries();
double previousCost = context.slot().cost();
entries.set(context.entryIndex(), next);
if (!SlotEditor.allows(context.user(), context.slot(), previousCost)) {
entries.set(context.entryIndex(), previous);
LobbySystem.getMessage().send("PARTICLE_SLOT_OVER_BUDGET", context.player());
} else {
SlotEditor.save(context.player(), context.index(), context.slot());
}
context.reopen();
}
private <T> void copy(Parameter<T> parameter, ParameterValues from, ParameterValues to) {
to.set(parameter, from.get(parameter));
}
}
@@ -0,0 +1,160 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.lobby.particle.gui;
import de.steamwar.lobby.LobbySystem;
import de.steamwar.lobby.particle.ParticleRequirement;
import de.steamwar.lobby.particle.UnlockContext;
import de.steamwar.lobby.particle.design.*;
import de.steamwar.lobby.particle.slot.ParticleEntry;
import de.steamwar.lobby.particle.slot.ParticleSlot;
import de.steamwar.lobby.particle.slot.ParticleSlots;
import de.steamwar.sql.SteamwarUser;
import lombok.experimental.UtilityClass;
import org.bukkit.entity.Player;
import java.awt.Color;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.MissingResourceException;
import static de.steamwar.lobby.particle.design.EntryParameters.*;
@UtilityClass
class ParticleGuiText {
String msg(String key, Player player, Object... params) {
return LobbySystem.getMessage().parse(key, player, params);
}
// Falls back to a readable form of the key for names without a translation
String name(String key, Player player) {
try {
return LobbySystem.getMessage().parse(key, player);
} catch (MissingResourceException e) {
return "§f" + prettify(key.replaceFirst("^PARTICLE_(TYPE_|DESIGN_|PARAM_)?", ""));
}
}
String prettify(String id) {
StringBuilder builder = new StringBuilder();
for (String word : id.toLowerCase(Locale.ROOT).split("_")) {
if (word.isEmpty()) continue;
if (!builder.isEmpty()) builder.append(' ');
builder.append(Character.toUpperCase(word.charAt(0))).append(word.substring(1));
}
return builder.toString();
}
String designName(ParticleDesign design, Player player) {
return name(design.getNameKey(), player);
}
String particleName(ParticleType type, Player player) {
return name(type.getNameKey(), player);
}
<E> String choiceName(ChoiceParameter<E> parameter, E value, Player player) {
for (ChoiceParameter.Choice<E> choice : parameter.getChoices()) {
if (choice.value() != value) continue;
if (value instanceof ParticleType type) return particleName(type, player);
return name("PARTICLE_CHOICE_" + parameter.getKey().toUpperCase(Locale.ROOT) + "_" + choice.id().toUpperCase(Locale.ROOT), player);
}
return String.valueOf(value);
}
String number(double value) {
return value == Math.rint(value) ? String.valueOf((long) value) : String.format(Locale.ROOT, "%.3f", value).replaceAll("0+$", "");
}
@SuppressWarnings("unchecked")
String valueText(Parameter<?> parameter, Object value, Player player) {
if (parameter instanceof DoubleParameter) return "§e" + number((Double) value);
if (parameter instanceof IntParameter) return "§e" + value;
if (parameter instanceof BooleanParameter) return msg((Boolean) value ? "PARTICLE_VALUE_YES" : "PARTICLE_VALUE_NO", player);
if (parameter instanceof ChoiceParameter<?> choice) return choiceName((ChoiceParameter<Object>) choice, value, player);
if (parameter instanceof GradientParameter) {
List<Color> colors = (List<Color>) value;
if (colors.isEmpty()) return msg("PARTICLE_VALUE_RANDOM", player);
StringBuilder builder = new StringBuilder();
colors.forEach(color -> builder.append(net.md_5.bungee.api.ChatColor.of(color)).append('█'));
return builder.toString();
}
return String.valueOf(value);
}
// Requirement lines for an item's lore, empty when there is no requirement
List<String> requirement(ParticleRequirement requirement, Player player) {
List<String> lore = new ArrayList<>();
String name = requirement.getRequirementName(player);
if (name != null) {
lore.add(msg("PARTICLE_UNLOCKED_BY", player));
lore.addAll(List.of(name.split("\n")));
}
return lore;
}
// Short description of an entry: design, particle and the conditions that are set
List<String> entrySummary(ParticleEntry entry, UnlockContext context, Player player) {
List<String> lore = new ArrayList<>();
ParameterValues values = entry.getValues();
lore.add(msg("PARTICLE_ENTRY_DESIGN", player, designName(entry.getDesign(), player)));
if (entry.getDesign().usesEntryParticle()) {
lore.add(msg("PARTICLE_ENTRY_PARTICLE", player, particleName(values.get(PARTICLE), player)));
}
lore.add("§8- " + choiceName(TICK_MODE, values.get(TICK_MODE), player));
addCondition(lore, MOVEMENT, values.get(MOVEMENT), Movement.ANY, player);
addCondition(lore, GLIDING, values.get(GLIDING), Gliding.ANY, player);
addCondition(lore, GROUND, values.get(GROUND), Ground.ANY, player);
addCondition(lore, SNEAK, values.get(SNEAK), Sneak.ANY, player);
addCondition(lore, VISIBILITY, values.get(VISIBILITY), Visibility.EVERYONE, player);
lore.add(msg("PARTICLE_ENTRY_COST", player, number(entry.cost())));
if (!entry.isUnlocked(context)) {
lore.add(msg("PARTICLE_ENTRY_LOCKED", player));
}
return lore;
}
private <E> void addCondition(List<String> lore, ChoiceParameter<E> parameter, E value, E none, Player player) {
if (value != none) {
lore.add("§8- " + choiceName(parameter, value, player));
}
}
// The slot's name followed by how many of its allowed particles per tick it uses
String slotName(ParticleSlot slot, SteamwarUser user, Player player) {
return msg("PARTICLE_SLOT_NAME", player, slot.getName(), number(slot.cost()), number(ParticleSlots.maxCost(user)));
}
String slotTitle(ParticleSlot slot, SteamwarUser user, Player player) {
return msg("PARTICLE_SLOT_TITLE", player, slot.getName(), number(slot.cost()), number(ParticleSlots.maxCost(user)));
}
List<String> slotSummary(ParticleSlot slot, Player player) {
List<String> lore = new ArrayList<>();
lore.add(msg("PARTICLE_SLOT_ENTRIES", player, slot.getEntries().size()));
for (ParticleEntry entry : slot.getEntries()) {
String particle = entry.getDesign().usesEntryParticle() ? " §8(" + particleName(entry.getValues().get(PARTICLE), player) + "§8)" : "";
lore.add("§8- " + designName(entry.getDesign(), player) + particle);
}
return lore;
}
}
@@ -0,0 +1,135 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.lobby.particle.gui;
import de.steamwar.inventory.SWInventory;
import de.steamwar.inventory.SWItem;
import de.steamwar.lobby.LobbySystem;
import de.steamwar.lobby.particle.ParticleListener;
import de.steamwar.lobby.particle.ParticleRequirement;
import de.steamwar.lobby.particle.UnlockContext;
import de.steamwar.lobby.particle.slot.ParticlePreset;
import de.steamwar.lobby.particle.slot.ParticleSlot;
import de.steamwar.lobby.particle.slot.ParticleSlots;
import de.steamwar.lobby.particle.slot.Presets;
import de.steamwar.lobby.util.LobbyPlayer;
import de.steamwar.sql.SteamwarUser;
import lombok.experimental.UtilityClass;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.List;
import static de.steamwar.lobby.particle.gui.ParticleGuiText.*;
// The particle item's menu: the player's slots in the top row, their unlocked presets below
@UtilityClass
public class ParticleMenu {
private final int NO_PARTICLE_POSITION = 8;
private final int PRESET_START = 18;
public void open(Player player) {
LobbyPlayer lobbyPlayer = LobbyPlayer.getLobbyPlayer(player);
UnlockContext context = new UnlockContext(SteamwarUser.get(player.getUniqueId()));
List<ParticlePreset> presets = Presets.all().stream().filter(preset -> preset.getRequirement().test(context)).toList();
int rows = presets.isEmpty() ? 1 : Math.min(6, 2 + (presets.size() + 8) / 9);
SWInventory inventory = new SWInventory(player, rows * 9, msg("PARTICLE_INVENTORY", player));
for (int index = 0; index < ParticleSlots.COUNT; index++) {
inventory.setItem(index, slotItem(player, lobbyPlayer, context, index));
}
inventory.setItem(NO_PARTICLE_POSITION, new SWItem(Material.BARRIER, msg("PARTICLE_DESELECT", player), clickType -> {
lobbyPlayer.deselect();
player.closeInventory();
}));
for (int i = 0; i < presets.size() && PRESET_START + i < rows * 9; i++) {
ParticlePreset preset = presets.get(i);
List<String> lore = new ArrayList<>();
lore.add("");
lore.add(msg("PARTICLE_SELECT", player));
inventory.setItem(PRESET_START + i, new SWItem(preset.getIcon(), ParticleGuiText.name(preset.getNameKey(), player), lore, preset == lobbyPlayer.getActivePreset(), clickType -> {
lobbyPlayer.selectPreset(preset);
selected(player);
}));
}
inventory.open();
}
private SWItem slotItem(Player player, LobbyPlayer lobbyPlayer, UnlockContext context, int index) {
ParticleSlot slot = lobbyPlayer.getSlot(index);
ParticleRequirement requirement = ParticleSlots.requirement(index);
boolean unlocked = requirement.test(context);
if (slot == null) {
if (!unlocked) {
List<String> lore = new ArrayList<>(ParticleGuiText.requirement(requirement, player));
return new SWItem(Material.BARRIER, msg("PARTICLE_LOCKED", player), lore, false, clickType -> {
});
}
List<String> lore = List.of(msg("PARTICLE_SLOT_CREATE", player));
return new SWItem(Material.FIREWORK_STAR, msg("PARTICLE_SLOT_EMPTY", player), lore, false, clickType -> {
lobbyPlayer.setSlot(index, new ParticleSlot(ParticleSlots.defaultName(index)));
SlotEditor.open(player, index);
});
}
List<String> lore = new ArrayList<>(ParticleGuiText.slotSummary(slot, player));
lore.add("");
String name = slotName(slot, context.getUser(), player);
if (!unlocked) {
lore.add(msg("PARTICLE_LOCKED", player));
lore.addAll(ParticleGuiText.requirement(requirement, player));
lore.add(msg("PARTICLE_SLOT_VIEW", player));
return new SWItem(slot.getDisplayIcon(), name, lore, false, clickType -> SlotEditor.open(player, index));
}
if (slot.getEntries().isEmpty()) {
lore.add(msg("PARTICLE_SLOT_OPEN", player));
return new SWItem(slot.getDisplayIcon(), name, lore, false, clickType -> SlotEditor.open(player, index));
}
// A slot with entries is switched on and off with a click; shift click edits it
boolean active = lobbyPlayer.getActiveSlot() == index;
if (active) {
lore.add(msg("PARTICLE_SLOT_ACTIVE", player));
}
lore.add(msg(active ? "PARTICLE_SLOT_CLICK_STOP" : "PARTICLE_SLOT_CLICK_USE", player));
lore.add(msg("PARTICLE_SLOT_SHIFT_EDIT", player));
return new SWItem(slot.getDisplayIcon(), name, lore, active, clickType -> {
if (clickType.isShiftClick()) {
SlotEditor.open(player, index);
} else if (active) {
lobbyPlayer.deselect();
open(player);
} else if (lobbyPlayer.selectSlot(index)) {
selected(player);
}
});
}
void selected(Player player) {
player.closeInventory();
if (ParticleListener.disabled(player)) {
LobbySystem.getMessage().send("PARTICLE_DEACTIVATED", player);
}
}
}
@@ -0,0 +1,135 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.lobby.particle.gui;
import de.steamwar.inventory.SWInventory;
import de.steamwar.inventory.SWItem;
import de.steamwar.inventory.SWListInv;
import de.steamwar.lobby.particle.ParticleRequirement;
import de.steamwar.lobby.particle.UnlockContext;
import de.steamwar.lobby.particle.design.*;
import de.steamwar.sql.SteamwarUser;
import lombok.experimental.UtilityClass;
import org.bukkit.DyeColor;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import java.awt.Color;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
import static de.steamwar.lobby.particle.gui.ParticleGuiText.*;
@UtilityClass
class Pickers {
private final int SIZE = 54;
private final int BACK = BottomBar.position(SIZE, BottomBar.BACK);
// Basic designs first, then rewards; locked rewards are shown with their requirement but cannot be picked.
// The current design, if any, glows.
void design(Player player, ParticleDesign current, Consumer<ParticleDesign> onPick, Runnable onBack) {
UnlockContext context = new UnlockContext(SteamwarUser.get(player.getUniqueId()));
List<SWListInv.SWListEntry<ParticleDesign>> list = new ArrayList<>();
for (DesignVisibility visibility : List.of(DesignVisibility.BASIC, DesignVisibility.REWARD)) {
for (ParticleDesign design : Designs.all()) {
if (design.getVisibility() != visibility) continue;
List<String> lore = new ArrayList<>(requirement(design.getRequirement(), player));
boolean unlocked = design.getRequirement().test(context);
if (!lore.isEmpty()) lore.add("");
lore.add(msg(design == current ? "PARTICLE_VALUE_SELECTED" : unlocked ? "PARTICLE_SELECT" : "PARTICLE_LOCKED", player));
list.add(new SWListInv.SWListEntry<>(new SWItem(design.getIcon(), designName(design, player), lore, design == current, clickType -> {
}), unlocked ? design : null));
}
}
open(player, msg("PARTICLE_DESIGN_PICKER", player), list, onPick, onBack);
}
// Lists every option; the current one glows, locked ones show their requirement and cannot be picked
<E> void choice(Player player, ChoiceParameter<E> parameter, E current, Function<E, Material> icon, Function<E, ParticleRequirement> requirement,
Consumer<E> onPick, Runnable onBack) {
UnlockContext context = new UnlockContext(SteamwarUser.get(player.getUniqueId()));
List<SWListInv.SWListEntry<E>> list = new ArrayList<>();
for (E value : parameter.getBuildableValues()) {
boolean unlocked = requirement.apply(value).test(context);
List<String> lore = new ArrayList<>(requirement(requirement.apply(value), player));
if (!lore.isEmpty()) lore.add("");
lore.add(msg(value == current ? "PARTICLE_VALUE_SELECTED" : unlocked ? "PARTICLE_SELECT" : "PARTICLE_LOCKED", player));
list.add(new SWListInv.SWListEntry<>(new SWItem(icon.apply(value), choiceName(parameter, value, player), lore, value == current, clickType -> {
}), unlocked ? value : null));
}
open(player, msg("PARTICLE_CHOICE_PICKER", player, name(parameter.getNameKey(), player)), list, onPick, onBack);
}
void color(Player player, Consumer<Color> onPick, Runnable onBack) {
List<SWListInv.SWListEntry<Color>> list = new ArrayList<>();
for (DyeColor dye : DyeColor.values()) {
Color color = new Color(dye.getColor().asRGB());
list.add(new SWListInv.SWListEntry<>(new SWItem(Material.valueOf(dye.name() + "_DYE"), net.md_5.bungee.api.ChatColor.of(color) + prettify(dye.name()), clickType -> {
}), color));
}
open(player, msg("PARTICLE_COLOR_PICKER", player), list, onPick, onBack);
}
void icon(Player player, Consumer<Material> onPick, Runnable onBack) {
Set<Material> icons = new LinkedHashSet<>();
for (ParticleType type : ParticleType.values()) {
if (type.isBuildable()) icons.add(type.getIcon());
}
for (ParticleDesign design : Designs.all()) {
if (design.getVisibility() == DesignVisibility.BASIC) icons.add(design.getIcon());
}
icons.addAll(List.of(Material.DIAMOND, Material.EMERALD, Material.GOLD_INGOT, Material.AMETHYST_SHARD, Material.HEART_OF_THE_SEA, Material.NETHER_STAR, Material.FEATHER, Material.TNT));
List<SWListInv.SWListEntry<Material>> list = new ArrayList<>();
for (Material material : icons) {
list.add(new SWListInv.SWListEntry<>(new SWItem(material, "§f" + prettify(material.name()), clickType -> {
}), material));
}
open(player, msg("PARTICLE_SLOT_ICON_TITLE", player), list, onPick, onBack);
}
// Lists that fit on one page get just enough rows for their options plus the bottom bar
private <T> void open(Player player, String title, List<SWListInv.SWListEntry<T>> list, Consumer<T> onPick, Runnable onBack) {
if (list.size() <= SIZE - 9) {
int size = (Math.max(1, (list.size() + 8) / 9) + 1) * 9;
SWInventory inventory = new SWInventory(player, size, title);
for (int i = 0; i < list.size(); i++) {
T value = list.get(i).getObject();
inventory.setItem(i, list.get(i).getItem().getItemStack(), clickType -> {
if (value != null) onPick.accept(value);
});
}
inventory.setItem(BottomBar.position(size, BottomBar.BACK), new SWItem(Material.ARROW, msg("PARTICLE_BACK", player), clickType -> onBack.run()));
BottomBar.fillUnused(inventory, size, false);
SlotEditor.fill(inventory, 0, size);
inventory.open();
return;
}
SWListInv<T> inventory = new SWListInv<>(player, title, false, list, (clickType, value) -> {
if (value != null) onPick.accept(value);
});
inventory.setItem(BACK, new SWItem(Material.ARROW, msg("PARTICLE_BACK", player), clickType -> onBack.run()));
BottomBar.fillUnused(inventory, SIZE, true);
inventory.open();
}
}
@@ -0,0 +1,171 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.lobby.particle.gui;
import de.steamwar.inventory.SWAnvilInv;
import de.steamwar.inventory.SWInventory;
import de.steamwar.inventory.SWItem;
import de.steamwar.lobby.LobbySystem;
import de.steamwar.lobby.particle.UnlockContext;
import de.steamwar.lobby.particle.slot.ParticleEntry;
import de.steamwar.lobby.particle.slot.ParticleSlot;
import de.steamwar.lobby.particle.slot.ParticleSlots;
import de.steamwar.lobby.util.LobbyPlayer;
import de.steamwar.sql.SteamwarUser;
import lombok.experimental.UtilityClass;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.List;
import static de.steamwar.lobby.particle.gui.ParticleGuiText.*;
// Lists a slot's entries; slots the player has lost access to are shown read-only but can still be deleted
@UtilityClass
public class SlotEditor {
// One row of entries above the bottom bar
private final int SIZE = (ParticleSlots.MAX_ENTRIES + 8) / 9 * 9 + 9;
private final int BACK = BottomBar.position(SIZE, BottomBar.BACK);
private final int STATUS = BottomBar.position(SIZE, BottomBar.STATUS);
private final int ADD_ENTRY = BottomBar.position(SIZE, BottomBar.ADD);
private final int RENAME = BottomBar.position(SIZE, BottomBar.RENAME);
private final int ICON = BottomBar.position(SIZE, BottomBar.ICON);
private final int COST = BottomBar.position(SIZE, BottomBar.COST);
private final int DELETE = BottomBar.position(SIZE, BottomBar.DELETE);
public void open(Player player, int index) {
LobbyPlayer lobbyPlayer = LobbyPlayer.getLobbyPlayer(player);
ParticleSlot slot = lobbyPlayer.getSlot(index);
if (slot == null) {
ParticleMenu.open(player);
return;
}
SteamwarUser user = SteamwarUser.get(player.getUniqueId());
UnlockContext context = new UnlockContext(user);
boolean editable = ParticleSlots.requirement(index).test(context);
SWInventory inventory = new SWInventory(player, SIZE, slotTitle(slot, user, player));
List<ParticleEntry> entries = slot.getEntries();
for (int i = 0; i < entries.size(); i++) {
ParticleEntry entry = entries.get(i);
List<String> lore = new ArrayList<>(entrySummary(entry, context, player));
if (editable) {
lore.add("");
lore.add(msg("PARTICLE_ENTRY_EDIT", player));
}
int entryIndex = i;
inventory.setItem(i, new SWItem(entry.getDesign().getIcon(), designName(entry.getDesign(), player), lore, false, clickType -> {
if (editable) EntryEditor.open(player, index, entryIndex);
}));
}
inventory.setItem(BACK, new SWItem(Material.ARROW, msg("PARTICLE_BACK", player), clickType -> ParticleMenu.open(player)));
inventory.setItem(COST, costItem(player, user, slot));
// Locked slots can still be deleted, so players can clear what they made before losing the rank
inventory.setItem(DELETE, new SWItem(Material.LAVA_BUCKET, msg("PARTICLE_SLOT_DELETE", player), List.of(msg("PARTICLE_SHIFT_TO_DELETE", player)), false, clickType -> {
if (!clickType.isShiftClick()) return;
if (lobbyPlayer.getActiveSlot() == index) {
lobbyPlayer.deselect();
}
lobbyPlayer.setSlot(index, null);
ParticleMenu.open(player);
}));
if (!editable) {
List<String> lore = new ArrayList<>(requirement(ParticleSlots.requirement(index), player));
inventory.setItem(STATUS, new SWItem(Material.BARRIER, msg("PARTICLE_LOCKED", player), lore, false, clickType -> {
}));
BottomBar.fillUnused(inventory, SIZE, false);
fill(inventory, 0, SIZE);
inventory.open();
return;
}
inventory.setItem(ADD_ENTRY, new SWItem(Material.NETHER_STAR, msg("PARTICLE_SLOT_ADD_ENTRY", player), clickType -> {
if (entries.size() >= ParticleSlots.MAX_ENTRIES) {
LobbySystem.getMessage().send("PARTICLE_SLOT_MAX_ENTRIES", player);
return;
}
Pickers.design(player, null, design -> {
double previousCost = slot.cost();
entries.add(new ParticleEntry(design));
if (!allows(user, slot, previousCost)) {
entries.remove(entries.size() - 1);
LobbySystem.getMessage().send("PARTICLE_SLOT_OVER_BUDGET", player);
open(player, index);
return;
}
save(player, index, slot);
EntryEditor.open(player, index, entries.size() - 1);
}, () -> open(player, index));
}));
inventory.setItem(RENAME, new SWItem(Material.NAME_TAG, msg("PARTICLE_SLOT_RENAME", player), clickType -> {
SWAnvilInv anvil = new SWAnvilInv(player, msg("PARTICLE_SLOT_RENAME_TITLE", player), slot.getName());
anvil.setItem(Material.NAME_TAG);
anvil.setCallback(name -> {
String cleaned = name.replace("§", "").trim();
slot.setName(cleaned.isEmpty() ? ParticleSlots.defaultName(index) : cleaned);
save(player, index, slot);
Bukkit.getScheduler().runTask(LobbySystem.getInstance(), () -> open(player, index));
});
anvil.open();
}));
inventory.setItem(ICON, new SWItem(slot.getDisplayIcon(), msg("PARTICLE_SLOT_ICON", player), clickType -> Pickers.icon(player, icon -> {
slot.setIcon(icon);
save(player, index, slot);
open(player, index);
}, () -> open(player, index))));
BottomBar.fillUnused(inventory, SIZE, false);
fill(inventory, 0, SIZE);
inventory.open();
}
SWItem costItem(Player player, SteamwarUser user, ParticleSlot slot) {
return new SWItem(Material.PAPER, msg("PARTICLE_SLOT_COST", player, number(slot.cost()), number(ParticleSlots.maxCost(user))), clickType -> {
});
}
// The slot's particles per tick, with the share of the entry being edited
SWItem costItem(Player player, SteamwarUser user, ParticleSlot slot, ParticleEntry entry) {
return costItem(player, user, slot).setLore(msg("PARTICLE_ENTRY_COST_SHARE", player, number(entry.cost())));
}
// A change is fine within the budget, and also when it does not add particles to a slot already over it
boolean allows(SteamwarUser user, ParticleSlot slot, double previousCost) {
double cost = slot.cost();
return cost <= ParticleSlots.maxCost(user) || cost <= previousCost;
}
void save(Player player, int index, ParticleSlot slot) {
LobbyPlayer.getLobbyPlayer(player).setSlot(index, slot);
}
// Puts gray filler panes on the empty positions in the given range; the bottom bar uses black ones
void fill(SWInventory inventory, int from, int to) {
for (int position = from; position < to; position++) {
if (!inventory.hasItem(position)) {
inventory.setItem(position, new SWItem(Material.GRAY_STAINED_GLASS_PANE, " ", clickType -> {
}));
}
}
}
}
@@ -0,0 +1,195 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.lobby.particle.gui;
import de.steamwar.inventory.SWAnvilInv;
import de.steamwar.inventory.SWInventory;
import de.steamwar.inventory.SWItem;
import de.steamwar.lobby.LobbySystem;
import lombok.experimental.UtilityClass;
import org.bukkit.Bukkit;
import org.bukkit.DyeColor;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import java.awt.Color;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import java.util.function.Supplier;
import static de.steamwar.lobby.particle.gui.ParticleGuiText.*;
// Screens for settings that need more than a click: numbers and colour lists
@UtilityClass
class ValueEditors {
private final int SIZE = 27;
private final int BACK = BottomBar.position(SIZE, BottomBar.BACK);
private final int ADD = BottomBar.position(SIZE, BottomBar.ADD);
private final int RESET = BottomBar.position(SIZE, BottomBar.RESET);
// Buttons step the value down or up; clicking the value itself lets the player type one
void editNumber(Player player, String title, Supplier<Double> current, double step, double min, double max, double defaultValue, boolean integer,
Predicate<Double> apply, Runnable back) {
double value = current.get();
Runnable reopen = () -> editNumber(player, title, current, step, min, max, defaultValue, integer, apply, back);
SWInventory inventory = new SWInventory(player, SIZE, "§0" + strip(title));
inventory.setItem(10, stepItem(player, Material.RED_CONCRETE, -10 * step, step, value, min, max, integer, apply, reopen));
inventory.setItem(11, stepItem(player, Material.RED_STAINED_GLASS_PANE, -step, step, value, min, max, integer, apply, reopen));
inventory.setItem(15, stepItem(player, Material.LIME_STAINED_GLASS_PANE, step, step, value, min, max, integer, apply, reopen));
inventory.setItem(16, stepItem(player, Material.LIME_CONCRETE, 10 * step, step, value, min, max, integer, apply, reopen));
List<String> lore = List.of(msg("PARTICLE_NUMBER_RANGE", player, number(min), number(max)), "", msg("PARTICLE_NUMBER_TYPE", player));
inventory.setItem(13, new SWItem(Material.PAPER, "§e" + number(value), lore, false, clickType -> {
SWAnvilInv anvil = new SWAnvilInv(player, strip(title), number(value));
anvil.setItem(Material.PAPER);
anvil.setCallback(input -> Bukkit.getScheduler().runTask(LobbySystem.getInstance(), () -> {
try {
double typed = Double.parseDouble(input.trim().replace(',', '.'));
apply.test(integer ? Math.round(typed) : typed);
} catch (NumberFormatException e) {
// Keep the old value
}
reopen.run();
}));
anvil.open();
}));
inventory.setItem(BACK, new SWItem(Material.ARROW, msg("PARTICLE_BACK", player), clickType -> back.run()));
inventory.setItem(RESET, new SWItem(Material.STRUCTURE_VOID, msg("PARTICLE_VALUE_RESET", player, number(defaultValue)), clickType -> {
apply.test(defaultValue);
reopen.run();
}));
BottomBar.fillUnused(inventory, SIZE, false);
SlotEditor.fill(inventory, 0, SIZE);
inventory.open();
}
private SWItem stepItem(Player player, Material material, double delta, double step, double value, double min, double max, boolean integer, Predicate<Double> apply, Runnable reopen) {
String label = (delta < 0 ? "§c-" : "§a+") + number(Math.abs(delta));
// Snap to the step grid and drop floating point noise, so repeated steps do not drift
double unclamped = integer ? Math.round(value + delta) : Math.round(Math.round((value + delta) / step) * step * 1e6) / 1e6;
double next = Math.max(min, Math.min(max, unclamped));
String limit = next == max && unclamped >= max ? "PARTICLE_NUMBER_AT_MAX" : next == min && unclamped <= min ? "PARTICLE_NUMBER_AT_MIN" : "PARTICLE_NUMBER_RESULT";
return new SWItem(material, label, List.of(msg(limit, player, number(next))), false, clickType -> {
// Stepping past the minimum or maximum changes nothing, so it does nothing
if (next == value) return;
apply.test(next);
reopen.run();
});
}
// Shows the colours in order; a click removes one, the buttons below add one or switch to random colours
void editColors(Player player, Supplier<List<Color>> current, int maxColors, Predicate<List<Color>> apply, Runnable back) {
List<Color> colors = current.get();
Runnable reopen = () -> editColors(player, current, maxColors, apply, back);
SWInventory inventory = new SWInventory(player, SIZE, msg("PARTICLE_COLOR_EDITOR", player));
for (int i = 0; i < colors.size() && i < 9; i++) {
int position = i;
Color color = colors.get(i);
inventory.setItem(i, new SWItem(dye(color), net.md_5.bungee.api.ChatColor.of(color) + "█ #" + String.format("%06X", color.getRGB() & 0xFFFFFF),
List.of(msg("PARTICLE_COLOR_REMOVE", player)), false, clickType -> {
List<Color> next = new ArrayList<>(colors);
next.remove(position);
apply.test(next);
reopen.run();
}));
}
if (colors.isEmpty()) {
inventory.setItem(4, new SWItem(Material.WHITE_DYE, msg("PARTICLE_VALUE_RANDOM", player), clickType -> {
}));
}
inventory.setItem(BACK, new SWItem(Material.ARROW, msg("PARTICLE_BACK", player), clickType -> back.run()));
if (colors.size() < maxColors) {
inventory.setItem(ADD, new SWItem(Material.NETHER_STAR, msg("PARTICLE_COLOR_ADD", player), clickType -> Pickers.color(player, color -> {
List<Color> next = new ArrayList<>(colors);
next.add(color);
apply.test(next);
reopen.run();
}, reopen)));
// Same position as typing a value on the number screen
inventory.setItem(13, new SWItem(Material.NAME_TAG, msg("PARTICLE_COLOR_HEX", player), List.of(msg("PARTICLE_COLOR_HEX_EXAMPLE", player)), false, clickType -> {
SWAnvilInv anvil = new SWAnvilInv(player, msg("PARTICLE_COLOR_HEX_TITLE", player), "#");
anvil.setItem(Material.NAME_TAG);
anvil.setCallback(input -> Bukkit.getScheduler().runTask(LobbySystem.getInstance(), () -> {
Color color = parseHex(input);
if (color == null) {
LobbySystem.getMessage().send("PARTICLE_COLOR_HEX_INVALID", player);
} else {
List<Color> next = new ArrayList<>(colors);
next.add(color);
apply.test(next);
}
reopen.run();
}));
anvil.open();
}));
}
inventory.setItem(RESET, new SWItem(Material.WHITE_DYE, msg("PARTICLE_COLOR_RANDOM", player), clickType -> {
apply.test(List.of());
reopen.run();
}));
BottomBar.fillUnused(inventory, SIZE, false);
SlotEditor.fill(inventory, 0, SIZE);
inventory.open();
}
// Accepts #RRGGBB and #RGB, with or without the #; null for anything else
Color parseHex(String input) {
String hex = input.trim();
if (hex.startsWith("#")) hex = hex.substring(1);
if (hex.length() == 3) {
hex = "" + hex.charAt(0) + hex.charAt(0) + hex.charAt(1) + hex.charAt(1) + hex.charAt(2) + hex.charAt(2);
}
if (!hex.matches("[0-9a-fA-F]{6}")) {
return null;
}
return new Color(Integer.parseInt(hex, 16));
}
// The dye whose colour is closest to the given one, so typed hex colours get a matching icon
private Material dye(Color color) {
DyeColor closest = DyeColor.WHITE;
double best = Double.MAX_VALUE;
for (DyeColor dye : DyeColor.values()) {
double distance = distance(color, new Color(dye.getColor().asRGB()));
if (distance < best) {
best = distance;
closest = dye;
}
}
return Material.valueOf(closest.name() + "_DYE");
}
// Colour distance weighted by how sensitive the eye is to red, green and blue ("redmean")
private double distance(Color a, Color b) {
double redMean = (a.getRed() + b.getRed()) / 2.0;
int dr = a.getRed() - b.getRed(), dg = a.getGreen() - b.getGreen(), db = a.getBlue() - b.getBlue();
return (2 + redMean / 256) * dr * dr + 4 * dg * dg + (2 + (255 - redMean) / 256) * db * db;
}
private String strip(String text) {
return text.replaceAll("§.", "");
}
}
@@ -1,70 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.lobby.particle.particles;
import de.steamwar.lobby.particle.ParticleData;
import de.steamwar.lobby.particle.ParticleEnum;
import de.steamwar.lobby.particle.ParticleRequirement;
import de.steamwar.lobby.particle.elements.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.bukkit.Material;
import org.bukkit.Particle;
import org.bukkit.entity.Player;
import org.bukkit.util.Vector;
@AllArgsConstructor
public enum EasterParticle implements ParticleEnum {
EGG_HUNT_EASY(new ParticleData(Material.LIME_CONCRETE_POWDER, "PARTICLE_EGG_HUNT_EASY", ParticleRequirement.EGG_HUNT_EASY,
new Always(new LocationMutator(new SimpleParticle(Particle.FALLING_SPORE_BLOSSOM, 0.5F, 0.1f, 0.5F, 1, 1), 0, 2.2, 0)))
),
EGG_HUNT_MEDIUM(new ParticleData(Material.YELLOW_CONCRETE_POWDER, "PARTICLE_EGG_HUNT_MEDIUM", ParticleRequirement.EGG_HUNT_MEDIUM,
new Always(new Floor(new Sneaking(new SimpleParticle(Particle.GLOW_SQUID_INK, 0.4F, 0.9F, 0.4F, 0.01)))))
),
EGG_HUNT_HARD(new ParticleData(Material.RED_CONCRETE_POWDER, "PARTICLE_EGG_HUNT_HARD", ParticleRequirement.EGG_HUNT_HARD,
new Always(new LocationMutator(new DoubleCircle(new DustParticle(Particle.DUST, 0, 0, 0, 0.01, 1, null), new DustParticle(Particle.DUST, 0, 0, 0, 0.01, 1, null)), 0, 0.5, 0)))
),
EGG_HUNT_EXTREME(new ParticleData(Material.PURPLE_CONCRETE_POWDER, "PARTICLE_EGG_HUNT_EXTREME", ParticleRequirement.EGG_HUNT_EXTREME,
new Always(new LocationMutator(new SimpleParticle(Particle.FALLING_OBSIDIAN_TEAR, 0.5F, 0.1F, 0.5F, 1, 1), 0, 2.2, 0)))
),
EGG_HUNT_ADVANCED(new ParticleData(Material.BLACK_CONCRETE_POWDER, "PARTICLE_EGG_HUNT_ADVANCED", ParticleRequirement.EGG_HUNT_ADVANCED,
new Always(new Sneaking(new LocationMutator(new Group(
new SimpleParticle(Particle.SMOKE, 0.02F, 0, 0.02F, 0.05, 1),
new SimpleParticle(Particle.LARGE_SMOKE, 0.02F, 0, 0.02F, 0.05, 1),
particleTickData -> {
Player player = particleTickData.getPlayer();
player.setVelocity(player.getVelocity().add(new Vector(0, 0.1, 0)));
}
), 0, -0.2, 0))))
),
EGG_HUNT_HALF(new ParticleData(Material.EGG, "PARTICLE_EGG_HUNT_HALF", ParticleRequirement.EGG_HUNT_FOUND_HALF,
new Group(
new Always(new Sneaking(new Floor(new LocationMutator(new SimpleParticle(Particle.HEART, 0, 0, 0, 0.01), 0, 1.7, 0)))),
new Separator(),
new Always(new NonFloor(new NonFlying(new LocationMutator(new Cloud(new Circle(new SimpleParticle(Particle.HEART, 0, 0, 0, 0.01))), 0, 0.5, 0))))
))
),
;
public static ParticleEnum[] particles = values();
@Getter
private ParticleData particle;
}
@@ -1,48 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.lobby.particle.particles;
import de.steamwar.lobby.particle.ParticleData;
import de.steamwar.lobby.particle.ParticleEnum;
import de.steamwar.lobby.particle.ParticleRequirement;
import de.steamwar.lobby.particle.elements.Always;
import de.steamwar.lobby.particle.elements.Circle;
import de.steamwar.lobby.particle.elements.LocationMutator;
import de.steamwar.lobby.particle.elements.SimpleParticle;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.bukkit.Material;
import org.bukkit.Particle;
@AllArgsConstructor
public enum EventParticle implements ParticleEnum {
WATER(new ParticleData(Material.WATER_BUCKET, "PARTICLE_WATER", ParticleRequirement.EVENT_PARTICIPATION,
new Always(new Circle(new LocationMutator(new SimpleParticle(Particle.DRIPPING_WATER), 0, 1.1, 0))))
),
LAVA(new ParticleData(Material.LAVA_BUCKET, "PARTICLE_FIRE", ParticleRequirement.EVENT_PARTICIPATION,
new Always(new Circle(new LocationMutator(new SimpleParticle(Particle.DRIPPING_LAVA), 0, 1.1, 0))))
),
;
public static ParticleEnum[] particles = values();
@Getter
private ParticleData particle;
}
@@ -1,64 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.lobby.particle.particles;
import de.steamwar.lobby.particle.ParticleData;
import de.steamwar.lobby.particle.ParticleEnum;
import de.steamwar.lobby.particle.ParticleRequirement;
import de.steamwar.lobby.particle.WingDesign;
import de.steamwar.lobby.particle.elements.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.bukkit.Material;
import org.bukkit.Particle;
@AllArgsConstructor
public enum EventParticleParticipation implements ParticleEnum {
WarGearSeason(new ParticleData(Material.BOOK, "PARTICLE_EVENT_ENCHANTING", ParticleRequirement.eventParticipation(22),
new SimpleParticle(Particle.ENCHANT))
),
AirshipEvent(new ParticleData(Material.SNOW_BLOCK, "PARTICLE_EVENT_CLOUD", ParticleRequirement.eventParticipation(26),
new SimpleParticle(Particle.CLOUD))
),
HellsBellsWs(new ParticleData(Material.TNT, "PARTICLE_EVENT_SMOKE", ParticleRequirement.eventParticipation(28),
new Circle(new SimpleParticle(Particle.CAMPFIRE_COSY_SMOKE, 0, 0, 0, 0.01)))
),
Underwater(new ParticleData(Material.PRISMARINE_BRICKS, "PARTICLE_EVENT_WATER", ParticleRequirement.eventParticipation(31),
new SimpleParticle(Particle.DRIPPING_WATER))
),
AdventWarShip(new ParticleData(Material.PRISMARINE_WALL, "PARTICLE_EVENT_WATER", ParticleRequirement.eventParticipation(32),
new Group(new SimpleParticle(Particle.DRIPPING_WATER), new SimpleParticle(Particle.UNDERWATER, 0.2F, 0.2F, 0.2F, 0.01)))
),
MiniWarGearLiga(new ParticleData(Material.PRISMARINE_SLAB, "PARTICLE_EVENT_WATER", ParticleRequirement.eventParticipation(33),
new Circle(new SimpleParticle(Particle.UNDERWATER, 0, 0, 0, 0)))
),
UnderwaterMWG(new ParticleData(Material.BLUE_CARPET, "PARTICLE_EVENT_RAIN_CLOUD", ParticleRequirement.eventParticipation(35),
new Always(new RandomParticle(RainCloudParticle.values())))
),
WarGearSeason2022(new ParticleData(Material.DIAMOND_SWORD, "PARTICLE_EVENT_WINGS", ParticleRequirement.eventParticipation(37),
new Always(new Delayed(new NonFlying(new Wing(new SimpleParticle(Particle.DRAGON_BREATH, 0, 0, 0, 0, 1), 0.15, WingDesign.COMPLEX)), 20)))
),
;
public static ParticleEnum[] particles = values();
@Getter
private ParticleData particle;
}
@@ -1,70 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.lobby.particle.particles;
import de.steamwar.lobby.particle.ParticleData;
import de.steamwar.lobby.particle.ParticleEnum;
import de.steamwar.lobby.particle.ParticleRequirement;
import de.steamwar.lobby.particle.WingDesign;
import de.steamwar.lobby.particle.elements.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.bukkit.Material;
import org.bukkit.Particle;
@AllArgsConstructor
public enum EventParticlePlacement implements ParticleEnum {
WarGearSeason(new ParticleData(Material.ENCHANTING_TABLE, "PARTICLE_EVENT_ENCHANTING", ParticleRequirement.eventPlacement(22, 12, 285, 54),
new Cloud(new Circle(new LocationMutator(new SimpleParticle(Particle.ENCHANT, 0, 0, 0, 0.01), 0, 1.1, 0)))
)),
AirshipEvent(new ParticleData(Material.SNOWBALL, "PARTICLE_EVENT_CLOUD", ParticleRequirement.eventPlacement(26, 205, 9, 54, 120, 292),
new Circle(new LocationMutator(new SimpleParticle(Particle.CLOUD, 0, 0, 0, 0.01), 0, 2.2, 0))
)),
HellsBellsWs(new ParticleData(Material.TNT_MINECART, "PARTICLE_EVENT_SMOKE", ParticleRequirement.eventPlacement(28, 205, 9, 11),
new Cloud(new Circle(new LocationMutator(new SimpleParticle(Particle.CAMPFIRE_COSY_SMOKE, 0, 0, 0, 0.01), 0, 2.2, 0)))
)),
Underwater(new ParticleData(Material.PRISMARINE_SHARD, "PARTICLE_EVENT_WATER", ParticleRequirement.eventPlacement(31, 9, 210, 520),
new Cloud(new SimpleParticle(Particle.DRIPPING_WATER)))
),
AdventWarShip(new ParticleData(Material.PRISMARINE_CRYSTALS, "PARTICLE_EVENT_WATER", ParticleRequirement.eventPlacement(32, 9, 205, 210),
new Always(new LocationMutator(new Circle(new Group(new SimpleParticle(Particle.DRIPPING_WATER), new SimpleParticle(Particle.UNDERWATER, 0.2F, 0.2F, 0.2F, 0.01))), 0, 1.1, 0)))
),
MiniWarGearLiga(new ParticleData(Material.IRON_SWORD, "PARTICLE_EVENT_WINGS", ParticleRequirement.eventPlacement(33, 9, 34, 205),
new Always(new Delayed(new NonFlying(new Wing(new SimpleParticle(Particle.UNDERWATER, 0, 0, 0, 0, 1), 0.15, WingDesign.MWGL)), 20)))
),
Absturz(new ParticleData(Material.FEATHER, "PARTICLE_EVENT_WINGS", ParticleRequirement.eventPlacement(34, 210, 205, 527, 286),
new Always(new Delayed(new NonFlying(new Wing(new SimpleParticle(Particle.CRIT, 0, 0, 0, 0, 1), 0.15, WingDesign.SIMPLE)), 20)))
),
UnderwaterMWG(new ParticleData(Material.CYAN_CARPET, "PARTICLE_EVENT_RAIN_CLOUD", ParticleRequirement.eventPlacement(35, 9, 210),
new Always(new Delayed(new NonFlying(new Wing(new SimpleParticle(Particle.CRIT, 0, 0, 0, 0, 1), 0.15, WingDesign.SW)), 20)))
),
WarGearSeason2022(new ParticleData(Material.DIAMOND_HELMET, "PARTICLE_EVENT_WGS", ParticleRequirement.eventPlacement(37, 285, 210, 122),
new Always(new Delayed(new NonFlying(new Wing(new SimpleParticle(Particle.FIREWORK, 0, 0, 0, 0, 1), 0.15, WingDesign.WGS)), 20)))
),
WargearClash(new ParticleData(Material.GOLDEN_SWORD, "PARTICLE_EVENT_WARGEARCLASH", ParticleRequirement.eventPlacement(38, 210, 158, 167, 286),
new Always(new Delayed(new NonFlying(new Wing(new SimpleParticle(Particle.CRIT, 0, 0, 0, 0, 1), 0.1, WingDesign.SWORD_CROSSED)), 20)))
),
;
public static ParticleEnum[] particles = values();
@Getter
private ParticleData particle;
}
@@ -1,82 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.lobby.particle.particles;
import de.steamwar.lobby.particle.ParticleData;
import de.steamwar.lobby.particle.ParticleEnum;
import de.steamwar.lobby.particle.elements.DustParticle;
import de.steamwar.lobby.particle.elements.LocationMutator;
import de.steamwar.lobby.particle.elements.SimpleParticle;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.bukkit.Material;
import org.bukkit.Particle;
@AllArgsConstructor
public enum PlayerParticle implements ParticleEnum {
SNEEZE(new ParticleData(Material.SLIME_BLOCK, "PARTICLE_SNEEZE",
new SimpleParticle(Particle.SNEEZE, 0.2F, 0.2F, 0.2F, 0.01))
),
SMOKE(new ParticleData(Material.COBWEB, "PARTICLE_SMOKE",
new SimpleParticle(Particle.SMOKE, 0.2F, 0.2F, 0.2F, 0.01))
),
FIRE(new ParticleData(Material.LAVA_BUCKET, "PARTICLE_FIRE",
new SimpleParticle(Particle.DRIPPING_LAVA))
),
WATER(new ParticleData(Material.WATER_BUCKET, "PARTICLE_WATER",
new SimpleParticle(Particle.DRIPPING_WATER))
),
HEARTH(new ParticleData(Material.RED_DYE, "PARTICLE_HEART",
new LocationMutator(new SimpleParticle(Particle.HEART), 0, 2.2, 0))
),
NOTES(new ParticleData(Material.NOTE_BLOCK, "PARTICLE_NOTES",
new LocationMutator(new SimpleParticle(Particle.NOTE), 0, 2.2, 0))
),
NAUTILUS(new ParticleData(Material.NAUTILUS_SHELL, "PARTICLE_NAUTILUS",
new SimpleParticle(Particle.NAUTILUS, 0.2F, 0.2F, 0.2F, 0.01))
),
SNOWBALL(new ParticleData(Material.SNOWBALL, "PARTICLE_SNOWBALL",
new SimpleParticle(Particle.SNOWFLAKE, 0.2F, 0.2F, 0.2F, 0.01))
),
EFFECT(new ParticleData(Material.GLASS_BOTTLE, "PARTICLE_EFFECT",
new DustParticle(Particle.DUST, 0, 0.2F, 0, 0.01, 5, null))
),
CAMPFIRE(new ParticleData(Material.CAMPFIRE, "PARTICLE_CAMPFIRE",
new SimpleParticle(Particle.CAMPFIRE_COSY_SMOKE, 0, 0.2F, 0, 0.01))
),
MAGIC(new ParticleData(Material.CAULDRON, "PARTICLE_MAGIC",
new SimpleParticle(Particle.CRIT, 0.2F, 0.2F, 0.2F, 0.01))
),
ANGRY(new ParticleData(Material.REDSTONE_BLOCK, "PARTICLE_ANGRY",
new SimpleParticle(Particle.ANGRY_VILLAGER, 0.2F, 0.2F, 0.2F, 0.01))
),
SLIME(new ParticleData(Material.SLIME_BALL, "PARTICLE_SLIME",
new SimpleParticle(Particle.ITEM_SLIME))
),
MOB(new ParticleData(Material.ZOMBIE_HEAD, "PARTICLE_MOB",
new SimpleParticle(Particle.SOUL))
),
;
public static ParticleEnum[] particles = values();
@Getter
private ParticleData particle;
}
@@ -1,78 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.lobby.particle.particles;
import de.steamwar.lobby.particle.ParticleData;
import de.steamwar.lobby.particle.ParticleEnum;
import de.steamwar.lobby.particle.ParticleRequirement;
import de.steamwar.lobby.particle.elements.Always;
import de.steamwar.lobby.particle.elements.Group;
import de.steamwar.lobby.particle.elements.LocationMutator;
import de.steamwar.lobby.particle.elements.SimpleParticle;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.bukkit.Material;
import org.bukkit.Particle;
@AllArgsConstructor
public enum RainCloudParticle implements ParticleEnum {
Raincloud(new ParticleData(Material.BLUE_CARPET, "PARTICLE_RAINCLOUD_NORMAL", ParticleRequirement.HAS_TEAM,
new Always(new LocationMutator(new Group(new SimpleParticle(Particle.CLOUD, 0.3F, 0.1F, 0.3F, 0.01), new SimpleParticle(Particle.RAIN, 0.3F, 0.1F, 0.3F, 0.01), new LocationMutator(new SimpleParticle(Particle.DRIPPING_WATER, 0.3F, 0.0F, 0.3F, 0.01), 0, -0.3, 0)), 0, 2.4, 0)))
),
Red_Raincloud(new ParticleData(Material.RED_CARPET, "PARTICLE_RAINCLOUD_RED", ParticleRequirement.HAS_TEAM,
new Always(new LocationMutator(new Group(new SimpleParticle(Particle.CLOUD, 0.3F, 0.1F, 0.3F, 0.01), new LocationMutator(new SimpleParticle(Particle.DRIPPING_LAVA, 0.3F, 0.0F, 0.3F, 0.01), 0, -0.3, 0)), 0, 2.4, 0)))
),
Yellow_Raincloud(new ParticleData(Material.YELLOW_CARPET, "PARTICLE_RAINCLOUD_YELLOW", ParticleRequirement.HAS_TEAM,
new Always(new LocationMutator(new Group(new SimpleParticle(Particle.CLOUD, 0.3F, 0.1F, 0.3F, 0.01), new LocationMutator(new SimpleParticle(Particle.FALLING_NECTAR, 0.3F, 0.0F, 0.3F, 0.01), 0, -0.3, 0)), 0, 2.4, 0)))
),
Orange_Raincloud(new ParticleData(Material.ORANGE_CARPET, "PARTICLE_RAINCLOUD_ORANGE", ParticleRequirement.HAS_TEAM,
new Always(new LocationMutator(new Group(new SimpleParticle(Particle.CLOUD, 0.3F, 0.1F, 0.3F, 0.01), new LocationMutator(new SimpleParticle(Particle.FALLING_HONEY, 0.3F, 0.0F, 0.3F, 0.01), 0, -0.3, 0)), 0, 2.4, 0)))
),
Green_Raincloud(new ParticleData(Material.LIME_CARPET, "PARTICLE_RAINCLOUD_GREEN", ParticleRequirement.HAS_TEAM,
new Always(new LocationMutator(new Group(new SimpleParticle(Particle.CLOUD, 0.3F, 0.1F, 0.3F, 0.01), new LocationMutator(new SimpleParticle(Particle.FALLING_SPORE_BLOSSOM, 0.3F, 0.0F, 0.3F, 0.01), 0, -0.3, 0)), 0, 2.4, 0)))
),
Purple_Raincloud(new ParticleData(Material.PURPLE_CARPET, "PARTICLE_RAINCLOUD_PURPLE", ParticleRequirement.HAS_TEAM,
new Always(new LocationMutator(new Group(new SimpleParticle(Particle.CLOUD, 0.3F, 0.1F, 0.3F, 0.01), new LocationMutator(new SimpleParticle(Particle.FALLING_OBSIDIAN_TEAR, 0.3F, 0.0F, 0.3F, 0.01), 0, -0.3, 0)), 0, 2.4, 0)))
),
Nautilus_Raincloud(new ParticleData(Material.NAUTILUS_SHELL, "PARTICLE_RAINCLOUD_NAUTILUS", ParticleRequirement.HAS_TEAM,
new Always(new LocationMutator(new Group(new SimpleParticle(Particle.CLOUD, 0.3F, 0.1F, 0.3F, 0.01), new LocationMutator(new SimpleParticle(Particle.NAUTILUS, 0.3F, 0.0F, 0.3F, 0.01), 0, -0.3, 0)), 0, 2.4, 0)))
),
Enchantment_Raincloud(new ParticleData(Material.ENCHANTED_BOOK, "PARTICLE_RAINCLOUD_ENCHANTMENT", ParticleRequirement.HAS_TEAM,
new Always(new LocationMutator(new Group(new SimpleParticle(Particle.CLOUD, 0.3F, 0.1F, 0.3F, 0.01), new LocationMutator(new SimpleParticle(Particle.ENCHANT, 0.3F, 0.0F, 0.3F, 0.01), 0, -0.3, 0)), 0, 2.4, 0)))
),
Slime_Raincloud(new ParticleData(Material.GREEN_CARPET, "PARTICLE_RAINCLOUD_SLIME", ParticleRequirement.HAS_TEAM,
new Always(new LocationMutator(new Group(new SimpleParticle(Particle.CLOUD, 0.3F, 0.1F, 0.3F, 0.01), new LocationMutator(new SimpleParticle(Particle.ITEM_SLIME, 0.3F, 0.0F, 0.3F, 0.01), 0, -0.3, 0)), 0, 2.4, 0)))
),
Hail_Raincloud(new ParticleData(Material.LIGHT_GRAY_CARPET, "PARTICLE_RAINCLOUD_HAIL", ParticleRequirement.HAS_TEAM,
new Always(new LocationMutator(new Group(new SimpleParticle(Particle.CLOUD, 0.3F, 0.1F, 0.3F, 0.01), new LocationMutator(new SimpleParticle(Particle.ITEM_SNOWBALL, 0.3F, 0.0F, 0.3F, 0.01), 0, -0.3, 0)), 0, 2.4, 0)))
),
Snow_Raincloud(new ParticleData(Material.WHITE_CARPET, "PARTICLE_RAINCLOUD_SNOW", ParticleRequirement.HAS_TEAM,
new Always(new LocationMutator(new Group(new SimpleParticle(Particle.CLOUD, 0.3F, 0.1F, 0.3F, 0.01), new LocationMutator(new SimpleParticle(Particle.SNOWFLAKE, 0.3F, 0.0F, 0.3F, 0.01), 0, -0.3, 0)), 0, 2.4, 0)))
),
Totem_Raincloud(new ParticleData(Material.TOTEM_OF_UNDYING, "PARTICLE_RAINCLOUD_TOTEM", ParticleRequirement.HAS_TEAM,
new Always(new LocationMutator(new Group(new SimpleParticle(Particle.CLOUD, 0.3F, 0.1F, 0.3F, 0.01), new LocationMutator(new SimpleParticle(Particle.TOTEM_OF_UNDYING, 0.3F, 0.0F, 0.3F, 0.01, 2), 0, -0.3, 0)), 0, 2.4, 0)))
),
;
public static ParticleEnum[] particles = values();
@Getter
private ParticleData particle;
}
@@ -1,118 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.lobby.particle.particles;
import de.steamwar.lobby.particle.ParticleData;
import de.steamwar.lobby.particle.ParticleEnum;
import de.steamwar.lobby.particle.ParticleRequirement;
import de.steamwar.lobby.particle.WingDesign;
import de.steamwar.lobby.particle.elements.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.bukkit.Material;
import org.bukkit.Particle;
@AllArgsConstructor
public enum ServerTeamParticle implements ParticleEnum {
WITCH(new ParticleData(Material.EXPERIENCE_BOTTLE, "PARTICLE_WITCH", ParticleRequirement.SERVER_TEAM,
new SimpleParticle(Particle.WITCH))
),
ENCHANTING(new ParticleData(Material.ENCHANTING_TABLE, "PARTICLE_ENCHANTING", ParticleRequirement.SERVER_TEAM,
new SimpleParticle(Particle.ENCHANT))
),
HAPPY(new ParticleData(Material.EMERALD_BLOCK, "PARTICLE_HAPPY", ParticleRequirement.SERVER_TEAM,
new SimpleParticle(Particle.HEART, 0.2F, 0.2F, 0.2F, 0.01))
),
FLAME(new ParticleData(Material.FLINT_AND_STEEL, "PARTICLE_FLAME", ParticleRequirement.SERVER_TEAM,
new SimpleParticle(Particle.FLAME, 0, 0.2F, 0, 0.01))
),
END_ROD(new ParticleData(Material.END_ROD, "PARTICLE_END_ROD", ParticleRequirement.SERVER_TEAM,
new SimpleParticle(Particle.END_ROD, 0.2F, 0.2F, 0.2F, 0.01))
),
CLOUD(new ParticleData(Material.WHITE_WOOL, "PARTICLE_CLOUD", ParticleRequirement.SERVER_TEAM,
new Cloud(new SimpleParticle(Particle.CLOUD)))
),
TOTEM(new ParticleData(Material.TOTEM_OF_UNDYING, "PARTICLE_TOTEM", ParticleRequirement.SERVER_TEAM,
new Cloud(new SimpleParticle(Particle.TOTEM_OF_UNDYING, 0.2F, 0.2F, 0.2F, 0.01)))
),
ENCHANTING_CLOUD(new ParticleData(Material.WHITE_DYE, "PARTICLE_ENCHANTING", ParticleRequirement.SERVER_TEAM,
new Cloud(new SimpleParticle(Particle.ENCHANT)))
),
FLAME_CLOUD(new ParticleData(Material.FIRE_CORAL_BLOCK, "PARTICLE_FLAME", ParticleRequirement.SERVER_TEAM,
new Cloud(new SimpleParticle(Particle.FLAME)))
),
SNEEZE_CLOUD(new ParticleData(Material.LIME_SHULKER_BOX, "PARTICLE_SNEEZE", ParticleRequirement.SERVER_TEAM,
new Cloud(new SimpleParticle(Particle.SNEEZE)))
),
SLIME_CLOUD(new ParticleData(Material.GREEN_SHULKER_BOX, "PARTICLE_SLIME", ParticleRequirement.SERVER_TEAM,
new Cloud(new SimpleParticle(Particle.ITEM_SLIME)))
),
SMOKE_CLOUD(new ParticleData(Material.DEAD_BRAIN_CORAL_BLOCK, "PARTICLE_SMOKE", ParticleRequirement.SERVER_TEAM,
new Cloud(new SimpleParticle(Particle.CAMPFIRE_COSY_SMOKE, 0.2F, 0.2F, 0.2F, 0.01)))
),
TOWN_CLOUD(new ParticleData(Material.FIREWORK_STAR, "PARTICLE_TOWN", ParticleRequirement.SERVER_TEAM,
new Cloud(new SimpleParticle(Particle.RAID_OMEN)))
),
FLAME_CIRCLE(new ParticleData(Material.MAGMA_BLOCK, "PARTICLE_FLAME", ParticleRequirement.SERVER_TEAM,
new Circle(new LocationMutator(new SimpleParticle(Particle.FLAME, 0, 0, 0, 0.01), 0, 1.1, 0)))
),
ENCHANTING_CIRCLE(new ParticleData(Material.WHITE_DYE, "PARTICLE_ENCHANTING_CIRCLE", ParticleRequirement.SERVER_TEAM,
new Circle(new LocationMutator(new SimpleParticle(Particle.ENCHANT, 0.2F, 0.2F, 0.2F, 0.01), 0, 1.1, 0)))
),
NOTES_CIRCLE(new ParticleData(Material.NOTE_BLOCK, "PARTICLE_NOTES", ParticleRequirement.SERVER_TEAM,
new Circle(new LocationMutator(new SimpleParticle(Particle.NOTE, 0, 0, 0, 0.01), 0, 2.2, 0)))
),
WATER_FIRE(new ParticleData(Material.GUARDIAN_SPAWN_EGG, "PARTICLE_WATER_FIRE", ParticleRequirement.SERVER_TEAM,
new LocationMutator(new DoubleCircle(new SimpleParticle(Particle.DRIPPING_WATER, 0, 0, 0, 0.01), new SimpleParticle(Particle.DRIPPING_LAVA, 0, 0, 0, 0.01)), 0, 1.1, 0))
),
WATER_FIRE_ALWAYS(new ParticleData(Material.GUARDIAN_SPAWN_EGG, "PARTICLE_WATER_FIRE", ParticleRequirement.SERVER_TEAM,
new Always(new LocationMutator(new DoubleCircle(new SimpleParticle(Particle.DRIPPING_WATER, 0, 0, 0, 0.01), new SimpleParticle(Particle.DRIPPING_LAVA, 0, 0, 0, 0.01)), 0, 1.1, 0)))
),
MAGIC_ENCHANTING(new ParticleData(Material.DIAMOND_SWORD, "PARTICLE_MAGIC_ENCHANTING", ParticleRequirement.SERVER_TEAM,
new LocationMutator(new DoubleCircle(new SimpleParticle(Particle.CRIT, 0, 0, 0, 0.01), new SimpleParticle(Particle.ENCHANT, 0, 0, 0, 0.01)), 0, 1.1, 0))
),
MAGIC_ENCHANTING_ALWAYS(new ParticleData(Material.WOODEN_SWORD, "PARTICLE_MAGIC_ENCHANTING", ParticleRequirement.SERVER_TEAM,
new Always(new LocationMutator(new DoubleCircle(new SimpleParticle(Particle.CRIT, 0, 0, 0, 0.01), new SimpleParticle(Particle.ENCHANT, 0, 0, 0, 0.01)), 0, 1.1, 0)))
),
MAGIC_CLOUD_CIRCLE(new ParticleData(Material.GLOWSTONE_DUST, "PARTICLE_MAGIC", ParticleRequirement.SERVER_TEAM,
new Cloud(new Circle(new LocationMutator(new SimpleParticle(Particle.ENCHANTED_HIT, 0, 0, 0, 0.01), 0, 1.1, 0))))
),
FLAME_CLOUD_CIRCLE(new ParticleData(Material.FIRE_CORAL, "PARTICLE_FLAME", ParticleRequirement.SERVER_TEAM,
new Cloud(new Circle(new LocationMutator(new SimpleParticle(Particle.FLAME, 0, 0, 0, 0.01), 0, 1.1, 0))))
),
FIREWORK_CLOUD_CIRCLE(new ParticleData(Material.FIREWORK_ROCKET, "PARTICLE_FIREWORK", ParticleRequirement.SERVER_TEAM,
new Cloud(new Circle(new LocationMutator(new SimpleParticle(Particle.FIREWORK, 0, 0, 0, 0.01), 0, 1.1, 0))))
),
WATER_CLOUD_CIRCLE(new ParticleData(Material.CYAN_DYE, "PARTICLE_WATER_FIRE", ParticleRequirement.SERVER_TEAM,
new Cloud(new Circle(new LocationMutator(new SimpleParticle(Particle.RAIN, 0, 0, 0, 0.01), 0, 1.1, 0))))
),
ENCHANTING_DOUBLE_CIRCLE(new ParticleData(Material.BUBBLE_CORAL_BLOCK, "PARTICLE_ENCHANTING", ParticleRequirement.SERVER_TEAM,
new Always(new LocationMutator(new DoubleCircle(new SimpleParticle(Particle.ENCHANT, 0, 0, 0, 0.01), new SimpleParticle(Particle.ENCHANT, 0, 0, 0, 0.01)), 0, 2.2, 0)))
),
EVIL_WINGS(new ParticleData(Material.PURPUR_SLAB, "PARTICLE_WINGS_EVIL", ParticleRequirement.SERVER_TEAM,
new Always(new Delayed(new NonFlying(new Wing(new SimpleParticle(Particle.WITCH, 0, 0, 0, 0, 1), 0.15, WingDesign.SIMPLE)), 20)))
),
;
public static ParticleEnum[] particles = values();
@Getter
private ParticleData particle;
}
@@ -1,69 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.lobby.particle.particles;
import de.steamwar.lobby.particle.ParticleData;
import de.steamwar.lobby.particle.ParticleEnum;
import de.steamwar.lobby.particle.ParticleRequirement;
import de.steamwar.lobby.particle.elements.Circle;
import de.steamwar.lobby.particle.elements.LocationMutator;
import de.steamwar.lobby.particle.elements.NonFloor;
import de.steamwar.lobby.particle.elements.SimpleParticle;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.bukkit.Material;
import org.bukkit.Particle;
@AllArgsConstructor
public enum TeamParticle implements ParticleEnum {
SQUID(new ParticleData(Material.INK_SAC, "PARTICLE_SQUID", ParticleRequirement.HAS_TEAM,
new SimpleParticle(Particle.SQUID_INK, 0.2F, 0.2F, 0.2F, 0.01))
),
BUBBLE(new ParticleData(Material.TUBE_CORAL, "PARTICLE_BUBBLE", ParticleRequirement.HAS_TEAM,
new SimpleParticle(Particle.BUBBLE_POP, 0.2F, 0.2F, 0.2F, 0.01))
),
HONEY(new ParticleData(Material.HONEY_BOTTLE, "PARTICLE_HONEY", ParticleRequirement.HAS_TEAM,
new SimpleParticle(Particle.DRIPPING_HONEY, 0.2F, 0.2F, 0.2F, 1))
),
NECTAR(new ParticleData(Material.HONEYCOMB, "PARTICLE_NECTAR", ParticleRequirement.HAS_TEAM,
new SimpleParticle(Particle.FALLING_NECTAR, 0.2F, 0.2F, 0.2F, 1))
),
FIREWORK(new ParticleData(Material.FIRE_CHARGE, "PARTICLE_FIREWORK", ParticleRequirement.HAS_TEAM,
new LocationMutator(new NonFloor(new SimpleParticle(Particle.FIREWORK, 0.1F, 0.1F, 0.1F, 0.2, 2)), 0, -0.2, 0))
),
DRAGON_BREATH(new ParticleData(Material.DRAGON_BREATH, "PARTICLE_DRAGON_BREATH", ParticleRequirement.HAS_TEAM,
new SimpleParticle(Particle.DRAGON_BREATH, 1F, 0.2F, 1F, 0.01))
),
DAMAGE(new ParticleData(Material.SPIDER_EYE, "PARTICLE_DAMAGE", ParticleRequirement.HAS_TEAM,
new SimpleParticle(Particle.DAMAGE_INDICATOR, 0.2F, 0, 0.2F, 0.01))
),
DOLPHIN(new ParticleData(Material.BLUE_DYE, "PARTICLE_DOLPHIN", ParticleRequirement.HAS_TEAM,
new SimpleParticle(Particle.DOLPHIN, 0.2F, 0, 0.2F, 0.01))
),
HEART(new ParticleData(Material.RED_CONCRETE, "PARTICLE_HEART", ParticleRequirement.HAS_TEAM,
new Circle(new LocationMutator(new SimpleParticle(Particle.HEART), 0, 2.2, 0)))
),
;
public static ParticleEnum[] particles = values();
@Getter
private ParticleData particle;
}
@@ -1,126 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.lobby.particle.particles.custom;
import de.steamwar.lobby.particle.*;
import de.steamwar.lobby.particle.elements.*;
import de.steamwar.lobby.particle.elements.custom.HearthBeat;
import de.steamwar.lobby.particle.elements.custom.NonMoving;
import de.steamwar.lobby.particle.elements.custom.PulseShimmer;
import de.steamwar.lobby.particle.elements.custom.YOffset;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.bukkit.Material;
import org.bukkit.Particle;
import java.awt.*;
@AllArgsConstructor
public enum CustomEasterParticle implements ParticleEnum {
PLAYER_1063(new ParticleData(Material.ENDER_PEARL, "PARTICLE_PLAYER_1063", ParticleRequirement.easterEventSpecificPlayer(1063),
new Always(
new DoubleCircle(
new DoubleCircle(
new YOffset(new SimpleParticle(Particle.CRIT, 0, 0, 0, 0.01, 1), 20, 0, 2, true, true),
new YOffset(new SimpleParticle(Particle.CRIT, 0, 0, 0, 0.01, 1), 20, 0, 2, false, true), 0.5, 4),
new DoubleCircle(
new YOffset(new SimpleParticle(Particle.CRIT, 0, 0, 0, 0.01, 1), 20, 0, 2, false, true),
new YOffset(new SimpleParticle(Particle.CRIT, 0, 0, 0, 0.01, 1), 20, 0, 2, true, true), 0.5, 4), 1.5, 1)
))
),
PLAYER_10916(new ParticleData(Material.TORCHFLOWER, "PARTICLE_PLAYER_10916_AURA", ParticleRequirement.easterEventSpecificPlayer(10916),
new Always(new NonMoving(new Group(
new LocationMutator(new DoubleCircle(new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), 0.5, 10), location -> location.add(0, 0.2, 0)),
new LocationMutator(new DoubleCircle(new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), 0.5, 10), location -> location.add(0, 0.3, 0)),
new LocationMutator(new DoubleCircle(new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), 0.55, 10), location -> location.add(0, 0.4, 0)),
new LocationMutator(new DoubleCircle(new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), 0.55, 10), location -> location.add(0, 0.5, 0)),
new LocationMutator(new DoubleCircle(new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), 0.65, 10), location -> location.add(0, 0.6, 0)),
new LocationMutator(new DoubleCircle(new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), 0.65, 10), location -> location.add(0, 0.7, 0)),
new LocationMutator(new DoubleCircle(new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), 0.7, 10), location -> location.add(0, 0.8, 0)),
new LocationMutator(new DoubleCircle(new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), 0.7, 10), location -> location.add(0, 0.9, 0)),
new LocationMutator(new DoubleCircle(new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), 0.7, 10), location -> location.add(0, 1.0, 0)),
new LocationMutator(new DoubleCircle(new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), 0.75, 10), location -> location.add(0, 1.1, 0)),
new LocationMutator(new DoubleCircle(new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), 0.75, 10), location -> location.add(0, 1.2, 0)),
new LocationMutator(new DoubleCircle(new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), 0.75, 10), location -> location.add(0, 1.3, 0)),
new LocationMutator(new DoubleCircle(new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), 0.8, 10), location -> location.add(0, 1.4, 0)),
new LocationMutator(new DoubleCircle(new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), 0.8, 10), location -> location.add(0, 1.5, 0)),
new LocationMutator(new DoubleCircle(new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), 0.8, 10), location -> location.add(0, 1.6, 0)),
new LocationMutator(new DoubleCircle(new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), 0.85, 10), location -> location.add(0, 1.7, 0)),
new LocationMutator(new DoubleCircle(new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), 0.85, 10), location -> location.add(0, 1.8, 0)),
new LocationMutator(new DoubleCircle(new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), 0.85, 10), location -> location.add(0, 1.9, 0)),
new LocationMutator(new DoubleCircle(new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.RED)), 0.9, 10), location -> location.add(0, 2.0, 0))
))))
),
PLAYER_10697(new ParticleData(Material.GUNPOWDER, "PARTICLE_PLAYER_10697_AURA", ParticleRequirement.easterEventSpecificPlayer(10697),
new Always(new Sneaking(new Group(
new OnlySelf(new Delayed(new SimpleParticle(Particle.EXPLOSION, 0, 0, 0, 0.01, 1), 20)),
new OnlyOthers(new Delayed(new SimpleParticle(Particle.EXPLOSION, 0, 0, 0, 0.01, 1), 2))
))))
),
PLAYER_64(new ParticleData(Material.PUFFERFISH_BUCKET, "PARTICLE_PLAYER_64", ParticleRequirement.easterEventSpecificPlayer(64),
new Always(new Delayed(new NonFlying(new Wing(new SimpleParticle(Particle.END_ROD, 0, 0, 0, 0, 1), 0.15, WingDesign.PlompaEasterWings)), 80)))
),
TEAM_210_1(new ParticleData(Material.RED_CANDLE, "PARTICLE_TEAM_PULSE_AURA", ParticleRequirement.easterEventSpecificTeam(210),
new Always(new NonMoving(new PulseShimmer(new SimpleParticle(Particle.FLAME, 0, 0, 0, 0, 1), 80, 2))))
),
TEAM_210_2(new ParticleData(Material.APPLE, "PARTICLE_TEAM_PULSE_HEART_BEAT", ParticleRequirement.easterEventSpecificTeam(210),
new Always(new NonFlying(new HearthBeat(new SimpleParticle(Particle.FLAME, 0, 0, 0, 0, 1), 80))))
),
PLAYER_153(new ParticleData(Material.OAK_SIGN, "PARTICLE_PLAYER_153", ParticleRequirement.easterEventSpecificPlayer(153),
new Always(new Delayed(new NonFlying(new Group(
new Wing(new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.DARK_GRAY)), 0.15, WingDesign.ELY_E),
new Wing(new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.GREEN)), 0.15, WingDesign.ELY_L),
new Wing(new DustParticle(Particle.DUST, 0, 0, 0, 0, 1, new Gradient(Color.GRAY)), 0.15, WingDesign.ELY_Y)
)), 10)))
),
// TODO: Implement zSalos! -> Team
// TODO: Implement GroupXyz!
// TODO: Implement minecrafter_133 1817!
// TODO: Implement TheReaper22122!
// TODO: Implement Bosslar!
// TODO: Implement ATOM65!
PLAYER_3266(new ParticleData(Material.BREAD, "PARTICLE_PLAYER_3266", ParticleRequirement.easterEventSpecificPlayer(3266),
new Group(
new Always(new Sneaking(new LocationMutator(new None(), location -> location))),
new Always(new NonFlying(new LocationMutator(new TrippleCircle(
new DustParticle(Particle.DUST, new Gradient(Color.CYAN, Color.BLUE, Color.MAGENTA.darker(), Color.RED, Color.YELLOW, Color.GREEN, Color.CYAN)),
new DustParticle(Particle.DUST, new Gradient(Color.CYAN, Color.BLUE, Color.MAGENTA.darker(), Color.RED, Color.YELLOW, Color.GREEN, Color.CYAN)),
new DustParticle(Particle.DUST, new Gradient(Color.CYAN, Color.BLUE, Color.MAGENTA.darker(), Color.RED, Color.YELLOW, Color.GREEN, Color.CYAN)),
0.7,
0.5), location -> location.add(0, 0.6, 0)
))))
)
),
// TODO: Implement Gehfxhler!
// TODO: Implement SchwarzerFuerst
// TODO: Implement byVallu
TEAM_158_1(new ParticleData(Material.ENCHANTED_BOOK, "PARTICLE_TEAM_158_1", ParticleRequirement.easterEventSpecificTeam(158),
new Always(new NonFlying(new Delayed(new Wing(new SimpleParticle(Particle.CRIT, 0, 0, 0, 0, 1), 0.2, WingDesign.EV), 15))))
),
TEAM_158_2(new ParticleData(Material.ENDER_EYE, "PARTICLE_TEAM_158_2", ParticleRequirement.easterEventSpecificTeam(158),
new Always(new NonFlying(new YOffset(new DoubleCircle(new DustParticle(Particle.CRIT, 0.01f, 0.01f, 0.01f, 0.1, 1, new Gradient(Color.RED)), new DustParticle(Particle.DUST, 0.01f, 0.01f, 0.01f, 0.1, 1, new Gradient(Color.BLUE)), 0.7, 1), 40, 0, 2, false, false))))
),
;
public static ParticleEnum[] particles = values();
@Getter
private ParticleData particle;
}
@@ -1,47 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.lobby.particle.particles.custom;
import de.steamwar.lobby.particle.ParticleData;
import de.steamwar.lobby.particle.ParticleEnum;
import de.steamwar.lobby.particle.ParticleRequirement;
import de.steamwar.lobby.particle.elements.*;
import de.steamwar.lobby.particle.elements.custom.NonMoving;
import de.steamwar.lobby.particle.elements.custom.YOffset;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.bukkit.Material;
import org.bukkit.Particle;
@AllArgsConstructor
public enum CustomPlayerParticle implements ParticleEnum {
Haylim_(new ParticleData(Material.WHITE_CANDLE, "PARTICLE_PLAYER_HAYLIM_AURA", ParticleRequirement.specificPlayer(9426),
new Always(new NonMoving(new NonFlying(new Group(
new DoubleCircle(new YOffset(new SimpleParticle(Particle.ENCHANT, 0, 0, 0, 0.01, 5), 40, 0, 2, false, false), new YOffset(new SimpleParticle(Particle.ENCHANT, 0, 0, 0, 0.0, 5), 40, 0, 2, true, false)),
new DoubleCircle(new YOffset(new SimpleParticle(Particle.ENCHANT, 0, 0, 0, 0.01, 5), 40, 0, 2, true, false), new YOffset(new SimpleParticle(Particle.ENCHANT, 0, 0, 0, 0.0, 5), 40, 0, 2, false, false))
)))))
),
;
public static ParticleEnum[] particles = values();
@Getter
private ParticleData particle;
}
@@ -1,54 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.lobby.particle.particles.custom;
import de.steamwar.lobby.particle.ParticleData;
import de.steamwar.lobby.particle.ParticleEnum;
import de.steamwar.lobby.particle.ParticleRequirement;
import de.steamwar.lobby.particle.WingDesign;
import de.steamwar.lobby.particle.elements.*;
import de.steamwar.lobby.particle.elements.custom.NonMoving;
import de.steamwar.lobby.particle.elements.custom.PulseShimmer;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.bukkit.Material;
import org.bukkit.Particle;
@AllArgsConstructor
public enum CustomTeamParticle implements ParticleEnum {
Eclipse(new ParticleData(Material.ENDER_CHEST, "PARTICLE_EVENT_ENCHANTING", ParticleRequirement.specificTeam(34),
new Always(new Delayed(new NonFlying(new Wing(new SimpleParticle(Particle.END_ROD, 0, 0, 0, 0, 1), 0.15, WingDesign.ECLIPSE)), 80)))
),
Pulse_1(new ParticleData(Material.GRAY_CANDLE, "PARTICLE_TEAM_PULSE_AURA_2", ParticleRequirement.specificTeam(210),
new Always(new NonMoving(new PulseShimmer(new SimpleParticle(Particle.END_ROD, 0, 0, 0, 0, 1), 80, 2))))
),
Pulse_2(new ParticleData(Material.WHITE_CANDLE, "PARTICLE_TEAM_PULSE_AURA_3", ParticleRequirement.specificTeam(210),
new Always(new NonMoving(new PulseShimmer(new SimpleParticle(Particle.ENCHANT, 0, 0, 0, 0, 5), 80, 2))))
),
Pulse_Logo(new ParticleData(Material.ENDER_CHEST, "PARTICLE_TEAM_PULSE_LOGO", ParticleRequirement.specificTeam(210),
new Always(new Delayed(new NonFlying(new Wing(new SimpleParticle(Particle.END_ROD, 0, 0, 0, 0, 1), 0.15, WingDesign.PL)), 80)))
),
;
public static ParticleEnum[] particles = values();
@Getter
private ParticleData particle;
}
@@ -0,0 +1,230 @@
/*
* 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.ParticleTickData;
import de.steamwar.lobby.particle.UnlockContext;
import de.steamwar.lobby.particle.design.*;
import de.steamwar.lobby.particle.elements.*;
import de.steamwar.lobby.particle.elements.touch.AfterJump;
import de.steamwar.lobby.particle.elements.touch.GlyphPath;
import lombok.Getter;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Particle;
import org.bukkit.block.data.BlockData;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.Vector;
import java.awt.Color;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;
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;
}
// A new entry with the design's default conditions and timing
public ParticleEntry(ParticleDesign design) {
this(design, new ParameterValues());
design.applyDefaults(values);
}
public <T> ParticleEntry with(Parameter<T> parameter, T value) {
values.set(parameter, value);
return this;
}
// For presets that go beyond what the builder allows
public <T> ParticleEntry withUnchecked(Parameter<T> parameter, T value) {
values.setUnchecked(parameter, value);
return this;
}
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() {
ParticleElement element;
if (design.usesEntryParticle()) {
element = design.build(values, withOffset(particle()));
} else {
element = withOffset(design.build(values, null));
}
if (values.get(PLACEMENT) == Placement.BODY) {
element = new BodyRelative(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 && design.redrawsAfterJump(values)) {
element = new AfterJump(element, new Delayed(element, interval), Wing.JUMP_BEAT_TICKS);
} else if (interval > 1) {
element = new Delayed(element, interval);
}
if (values.get(TICK_MODE) == TickMode.ALWAYS) {
element = new Always(element);
}
return element;
}
private ParticleElement particle() {
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 = amount();
List<Color> colors = values.get(COLORS);
Gradient gradient = colors.isEmpty() ? null : new Gradient(colors.toArray(new Color[0]));
Material block = values.get(BLOCK);
BiFunction<ParticleTickData, Location, Object> data = switch (type.getData()) {
// Dust keeps its own element below, so presets look exactly as before
case NONE, DUST -> null;
case COLOR -> (tick, location) -> DataParticle.color(gradient, tick);
case COLOR_TRANSITION -> (tick, location) -> new Particle.DustTransition(
colors.isEmpty() ? DataParticle.color(null, tick) : bukkitColor(colors.get(0)),
colors.size() < 2 ? DataParticle.color(null, tick) : bukkitColor(colors.get(1)),
1);
case BLOCK -> {
BlockData blockData = block.createBlockData();
yield (tick, location) -> blockData;
}
case ITEM -> {
ItemStack item = new ItemStack(block);
yield (tick, location) -> item;
}
case TRAIL -> {
int duration = values.get(TRAIL_DURATION);
Vector target = new Vector(values.get(TRAIL_TARGET_X), values.get(TRAIL_TARGET_Y), values.get(TRAIL_TARGET_Z));
// The target follows the body like offsets do when the entry is placed relative to it
yield (tick, location) -> new Particle.Trail(
location.clone().add(tick.getFrame() == null ? target : tick.getFrame().local(target.getX(), target.getY(), target.getZ())),
DataParticle.color(gradient, tick), duration);
}
case ZERO_INT -> (tick, location) -> 0;
case ZERO_FLOAT -> (tick, location) -> 0f;
};
if (type == ParticleType.ENCHANT && values.get(GLYPH_FLIGHT) != GlyphFlight.NORMAL) {
GlyphPath.Direction direction = values.get(GLYPH_FLIGHT) == GlyphFlight.INTO_PLAYER ? GlyphPath.Direction.INTO_PLAYER : GlyphPath.Direction.OUT_OF_PLAYER;
return new GlyphPath(direction, spreadX, spreadY, spreadZ, count);
}
if (type.isDust()) {
return new DustParticle(type.getParticle(), spreadX, spreadY, spreadZ, speed, count, gradient);
}
if (data != null) {
return new DataParticle(type.getParticle(), spreadX, spreadY, spreadZ, speed, count, data);
}
return new SimpleParticle(type.getParticle(), spreadX, spreadY, spreadZ, speed, count);
}
private static org.bukkit.Color bukkitColor(Color color) {
return org.bukkit.Color.fromRGB(color.getRed(), color.getGreen(), color.getBlue());
}
private ParticleElement withOffset(ParticleElement element) {
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) {
return element;
}
return new LocationMutator(element, offsetX, offsetY, offsetZ);
}
// Shown only when the design, its chosen options and, for designs that use it, the particle type are unlocked
@SuppressWarnings("unchecked")
public boolean isUnlocked(UnlockContext context) {
if (!design.getRequirement().test(context)) {
return false;
}
for (Parameter<?> parameter : design.getParameters()) {
if (parameter instanceof ChoiceParameter<?> choice && !((ChoiceParameter<Object>) choice).requirement(values.get(choice)).test(context)) {
return false;
}
}
if (!design.usesEntryParticle()) {
return true;
}
ParticleType type = values.get(PARTICLE);
return type.getRequirement().test(context) && (type != ParticleType.ENCHANT || GLYPH_FLIGHT.requirement(values.get(GLYPH_FLIGHT)).test(context));
}
// Particles each point spawns; designs that set their own point count replace the amount with 1
private int amount() {
return design.replacesAmount(values) ? 1 : values.get(COUNT);
}
// Particle positions spawned per tick, used for the slot budget
public double cost() {
double perTick = design.usesEntryParticle() ? (double) design.points(values) * amount() : design.points(values);
return perTick / 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,49 @@
/*
* 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 de.steamwar.lobby.particle.ParticleRequirement;
import lombok.Getter;
import org.bukkit.Material;
import java.util.List;
// A read-only particle for a specific team or user, selectable without using a slot
@Getter
public class ParticlePreset implements ParticleSet {
private final String id;
private final Material icon;
private final String nameKey;
private final ParticleRequirement requirement;
private final List<ParticleEntry> entries;
public ParticlePreset(String id, Material icon, String nameKey, ParticleRequirement requirement, List<ParticleEntry> entries) {
this.id = id;
this.icon = icon;
this.nameKey = nameKey;
this.requirement = requirement;
this.entries = List.copyOf(entries);
}
public ParticlePreset(String id, Material icon, String nameKey, ParticleRequirement requirement, ParticleEntry... entries) {
this(id, icon, nameKey, requirement, List.of(entries));
}
}
@@ -1,7 +1,7 @@
/* /*
* This file is a part of the SteamWar software. * This file is a part of the SteamWar software.
* *
* Copyright (C) 2025 SteamWar.de-Serverteam * Copyright (C) 2026 SteamWar.de-Serverteam
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU Affero General Public License as published by
@@ -17,8 +17,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package de.steamwar.lobby.particle; package de.steamwar.lobby.particle.slot;
public interface ParticleEnum { import java.util.List;
ParticleData getParticle();
// A list of entries that are shown together: a player's slot or a preset
public interface ParticleSet {
List<ParticleEntry> getEntries();
} }
@@ -0,0 +1,108 @@
/*
* 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 double cost() {
return entries.stream().mapToDouble(ParticleEntry::cost).sum();
}
public void setName(String name) {
this.name = name.length() > MAX_NAME_LENGTH ? name.substring(0, MAX_NAME_LENGTH) : name;
}
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 (slot.entries.size() >= ParticleSlots.MAX_ENTRIES) break;
if (!element.isJsonObject()) continue;
ParticleEntry entry = ParticleEntry.fromJson(element.getAsJsonObject());
if (entry != null) {
slot.entries.add(entry);
}
}
}
return slot;
}
}
@@ -0,0 +1,83 @@
/*
* 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.SteamwarUser;
import de.steamwar.sql.UserConfig;
import de.steamwar.sql.UserPerm;
import lombok.experimental.UtilityClass;
import java.util.logging.Level;
@UtilityClass
public class ParticleSlots {
public final int COUNT = 7;
// One row in the slot editor
public final int MAX_ENTRIES = 9;
private final String KEY_PREFIX = "lobby-particle-slot-";
// Particles per tick a slot may spawn. A default entry costs 5; the largest preset costs 38.
public double maxCost(SteamwarUser user) {
return user.hasPerm(UserPerm.TEAM) ? 90 : 30;
}
// 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);
}
}
@@ -0,0 +1,219 @@
/*
* 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 de.steamwar.lobby.particle.ParticleRequirement;
import de.steamwar.lobby.particle.WingDesign;
import de.steamwar.lobby.particle.design.Designs;
import de.steamwar.lobby.particle.design.InternalDesigns;
import de.steamwar.lobby.particle.design.ParticleDesign;
import de.steamwar.lobby.particle.design.ParticleType;
import lombok.experimental.UtilityClass;
import org.bukkit.Material;
import java.awt.Color;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static de.steamwar.lobby.particle.design.EntryParameters.*;
// Particles made for specific teams and users
@UtilityClass
public class Presets {
private final Map<String, ParticlePreset> PRESETS = new LinkedHashMap<>();
public final ParticlePreset TEAM_34_ECLIPSE = register(new ParticlePreset("team_34_eclipse", Material.ENDER_CHEST, "PARTICLE_EVENT_ENCHANTING", ParticleRequirement.specificTeam(34),
wing(ParticleType.END_ROD, 0.15, WingDesign.ECLIPSE, 80)));
public final ParticlePreset TEAM_210_PULSE_1 = register(new ParticlePreset("team_210_pulse_1", Material.GRAY_CANDLE, "PARTICLE_TEAM_PULSE_AURA_2", ParticleRequirement.specificTeam(210),
pulse(ParticleType.END_ROD, 1)));
public final ParticlePreset TEAM_210_PULSE_2 = register(new ParticlePreset("team_210_pulse_2", Material.WHITE_CANDLE, "PARTICLE_TEAM_PULSE_AURA_3", ParticleRequirement.specificTeam(210),
pulse(ParticleType.ENCHANT, 5)));
public final ParticlePreset TEAM_210_PULSE_LOGO = register(new ParticlePreset("team_210_pulse_logo", Material.ENDER_CHEST, "PARTICLE_TEAM_PULSE_LOGO", ParticleRequirement.specificTeam(210),
wing(ParticleType.END_ROD, 0.15, WingDesign.PL, 80)));
public final ParticlePreset PLAYER_9426_AURA = register(new ParticlePreset("player_9426_aura", Material.WHITE_CANDLE, "PARTICLE_PLAYER_HAYLIM_AURA", ParticleRequirement.specificPlayer(9426),
haylimRing(0, 0.01, false),
haylimRing(180, 0, true),
haylimRing(0, 0.01, true),
haylimRing(180, 0, false)));
public final ParticlePreset EASTER_PLAYER_1063 = register(new ParticlePreset("easter_player_1063", Material.ENDER_PEARL, "PARTICLE_PLAYER_1063", ParticleRequirement.easterEventSpecificPlayer(1063),
orbit(0, 0, true),
orbit(0, 180, false),
orbit(180, 0, false),
orbit(180, 180, true)));
public final ParticlePreset EASTER_PLAYER_10916 = register(new ParticlePreset("easter_player_10916", Material.TORCHFLOWER, "PARTICLE_PLAYER_10916_AURA", ParticleRequirement.easterEventSpecificPlayer(10916),
redRingTower()));
public final ParticlePreset EASTER_PLAYER_10697 = register(new ParticlePreset("easter_player_10697", Material.GUNPOWDER, "PARTICLE_PLAYER_10697_AURA", ParticleRequirement.easterEventSpecificPlayer(10697),
explosion(Visibility.ONLY_SELF, 20),
explosion(Visibility.ONLY_OTHERS, 2)));
public final ParticlePreset EASTER_PLAYER_64 = register(new ParticlePreset("easter_player_64", Material.PUFFERFISH_BUCKET, "PARTICLE_PLAYER_64", ParticleRequirement.easterEventSpecificPlayer(64),
wing(ParticleType.END_ROD, 0.15, WingDesign.PlompaEasterWings, 80)));
public final ParticlePreset EASTER_TEAM_210_AURA = register(new ParticlePreset("easter_team_210_aura", Material.RED_CANDLE, "PARTICLE_TEAM_PULSE_AURA", ParticleRequirement.easterEventSpecificTeam(210),
pulse(ParticleType.FLAME, 1)));
public final ParticlePreset EASTER_TEAM_210_HEART_BEAT = register(new ParticlePreset("easter_team_210_heart_beat", Material.APPLE, "PARTICLE_TEAM_PULSE_HEART_BEAT", ParticleRequirement.easterEventSpecificTeam(210),
particle(InternalDesigns.HEART_BEAT, ParticleType.FLAME, 0, 0, 1)
.with(InternalDesigns.HEART_BEAT_SPEED, 80.0)
.with(TICK_MODE, TickMode.ALWAYS)
.with(GLIDING, Gliding.NOT_GLIDING)));
public final ParticlePreset EASTER_PLAYER_153 = register(new ParticlePreset("easter_player_153", Material.OAK_SIGN, "PARTICLE_PLAYER_153", ParticleRequirement.easterEventSpecificPlayer(153),
dustWing(Color.DARK_GRAY, WingDesign.ELY_E),
dustWing(Color.GREEN, WingDesign.ELY_L),
dustWing(Color.GRAY, WingDesign.ELY_Y)));
public final ParticlePreset EASTER_PLAYER_3266 = register(new ParticlePreset("easter_player_3266", Material.BREAD, "PARTICLE_PLAYER_3266", ParticleRequirement.easterEventSpecificPlayer(3266),
rainbowRing(0),
rainbowRing(120),
rainbowRing(240)));
public final ParticlePreset EASTER_TEAM_158_WINGS = register(new ParticlePreset("easter_team_158_wings", Material.ENCHANTED_BOOK, "PARTICLE_TEAM_158_1", ParticleRequirement.easterEventSpecificTeam(158),
wing(ParticleType.CRIT, 0.2, WingDesign.EV, 15)));
// The red half used to pass dust options to CRIT, which CRIT does not take; it is a plain CRIT now
public final ParticlePreset EASTER_TEAM_158_RINGS = register(new ParticlePreset("easter_team_158_rings", Material.ENDER_EYE, "PARTICLE_TEAM_158_2", ParticleRequirement.easterEventSpecificTeam(158),
team158Ring(particle(Designs.CIRCLE, ParticleType.CRIT, 0.01, 0.1, 1), 0),
team158Ring(particle(Designs.CIRCLE, ParticleType.DUST, 0.01, 0.1, 1).with(COLORS, List.of(Color.BLUE)), 180)));
private ParticlePreset register(ParticlePreset preset) {
if (PRESETS.put(preset.getId(), preset) != null) {
throw new IllegalStateException("Duplicate particle preset " + preset.getId());
}
return preset;
}
public ParticlePreset byId(String id) {
return PRESETS.get(id);
}
public List<ParticlePreset> all() {
return List.copyOf(PRESETS.values());
}
private ParticleEntry particle(ParticleDesign design, ParticleType type, double spread, double speed, int count) {
return new ParticleEntry(design)
.with(PARTICLE, type)
.with(SPREAD_X, spread)
.with(SPREAD_Y, spread)
.with(SPREAD_Z, spread)
.withUnchecked(SPEED, speed)
.with(COUNT, count);
}
private ParticleEntry wing(ParticleType type, double size, WingDesign image, int interval) {
return particle(Designs.WING, type, 0, 0, 1)
.with(Designs.WING_SHAPE, Designs.wingShape(image))
.with(Designs.WING_SIZE, size)
.with(TICK_MODE, TickMode.ALWAYS)
.with(INTERVAL, interval)
.with(GLIDING, Gliding.NOT_GLIDING)
.with(PLACEMENT, Placement.WORLD);
}
private ParticleEntry dustWing(Color color, WingDesign image) {
return wing(ParticleType.DUST, 0.15, image, 10)
.with(COLORS, List.of(color));
}
private ParticleEntry pulse(ParticleType type, int count) {
return particle(InternalDesigns.PULSE_SHIMMER, type, 0, 0, count)
.with(InternalDesigns.PULSE_SPEED, 80)
.with(InternalDesigns.PULSE_ROTATION, 2)
.with(TICK_MODE, TickMode.ALWAYS)
.with(MOVEMENT, Movement.STANDING_STILL);
}
private ParticleEntry haylimRing(double phase, double speed, boolean inverted) {
return particle(Designs.CIRCLE, ParticleType.ENCHANT, 0, speed, 5)
.with(Designs.CIRCLE_PHASE, phase)
.with(Designs.CIRCLE_RISE, 2.0)
.with(Designs.CIRCLE_RISE_SPEED, 40.0)
.with(Designs.CIRCLE_BOUNCE, false)
.with(Designs.CIRCLE_INVERTED, inverted)
.with(TICK_MODE, TickMode.ALWAYS)
.with(MOVEMENT, Movement.STANDING_STILL)
.with(GLIDING, Gliding.NOT_GLIDING);
}
private ParticleEntry orbit(double orbitPhase, double phase, boolean inverted) {
return particle(InternalDesigns.ORBIT, ParticleType.CRIT, 0, 0.01, 1)
.with(InternalDesigns.ORBIT_RADIUS, 1.5)
.with(InternalDesigns.ORBIT_SPEED, 1.0)
.with(InternalDesigns.ORBIT_PHASE, orbitPhase)
.with(Designs.CIRCLE_RADIUS, 0.5)
.with(Designs.CIRCLE_SPEED, 4.0)
.with(Designs.CIRCLE_PHASE, phase)
.with(Designs.CIRCLE_RISE, 2.0)
.with(Designs.CIRCLE_RISE_SPEED, 20.0)
.with(Designs.CIRCLE_BOUNCE, true)
.with(Designs.CIRCLE_INVERTED, inverted)
.with(TICK_MODE, TickMode.ALWAYS);
}
// Two opposite red rings on each of 19 heights, widening towards the top
private List<ParticleEntry> redRingTower() {
double[] radii = {0.5, 0.5, 0.55, 0.55, 0.65, 0.65, 0.7, 0.7, 0.7, 0.75, 0.75, 0.75, 0.8, 0.8, 0.8, 0.85, 0.85, 0.85, 0.9};
List<ParticleEntry> entries = new ArrayList<>();
for (int i = 0; i < radii.length; i++) {
for (double phase : new double[]{0, 180}) {
entries.add(particle(Designs.CIRCLE, ParticleType.DUST, 0, 0, 1)
.with(COLORS, List.of(Color.RED))
.with(Designs.CIRCLE_RADIUS, radii[i])
.with(Designs.CIRCLE_SPEED, 10.0)
.with(Designs.CIRCLE_PHASE, phase)
.with(OFFSET_Y, 0.2 + i * 0.1)
.with(TICK_MODE, TickMode.ALWAYS)
.with(MOVEMENT, Movement.STANDING_STILL));
}
}
return entries;
}
private ParticleEntry explosion(Visibility visibility, int interval) {
return particle(Designs.SIMPLE, ParticleType.EXPLOSION, 0, 0.01, 1)
.with(TICK_MODE, TickMode.ALWAYS)
.with(SNEAK, Sneak.SNEAKING)
.with(VISIBILITY, visibility)
.with(INTERVAL, interval);
}
private ParticleEntry rainbowRing(double phase) {
return particle(Designs.CIRCLE, ParticleType.DUST, 0.01, 0.01, 5)
.with(COLORS, List.of(Color.CYAN, Color.BLUE, Color.MAGENTA.darker(), Color.RED, Color.YELLOW, Color.GREEN, Color.CYAN))
.with(Designs.CIRCLE_RADIUS, 0.7)
.with(Designs.CIRCLE_SPEED, 0.5)
.with(Designs.CIRCLE_PHASE, phase)
.with(OFFSET_Y, 0.6)
.with(TICK_MODE, TickMode.ALWAYS)
.with(GLIDING, Gliding.NOT_GLIDING);
}
private ParticleEntry team158Ring(ParticleEntry entry, double phase) {
return entry
.with(Designs.CIRCLE_RADIUS, 0.7)
.with(Designs.CIRCLE_SPEED, 1.0)
.with(Designs.CIRCLE_PHASE, phase)
.with(Designs.CIRCLE_RISE, 2.0)
.with(Designs.CIRCLE_RISE_SPEED, 40.0)
.with(Designs.CIRCLE_BOUNCE, false)
.with(Designs.CIRCLE_INVERTED, false)
.with(TICK_MODE, TickMode.ALWAYS)
.with(GLIDING, Gliding.NOT_GLIDING);
}
}
@@ -19,8 +19,12 @@
package de.steamwar.lobby.util; package de.steamwar.lobby.util;
import de.steamwar.lobby.particle.ParticleEnum; import de.steamwar.lobby.particle.CompiledSet;
import de.steamwar.lobby.particle.ParticleInventory; import de.steamwar.lobby.particle.UnlockContext;
import de.steamwar.lobby.particle.slot.ParticlePreset;
import de.steamwar.lobby.particle.slot.ParticleSlot;
import de.steamwar.lobby.particle.slot.ParticleSlots;
import de.steamwar.lobby.particle.slot.Presets;
import de.steamwar.sql.SteamwarUser; import de.steamwar.sql.SteamwarUser;
import de.steamwar.sql.UserConfig; import de.steamwar.sql.UserConfig;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@@ -31,21 +35,41 @@ import java.util.UUID;
public class LobbyPlayer { public class LobbyPlayer {
private static final String ACTIVE_KEY = "lobby-particle-active";
private static final String ACTIVE_SLOT_PREFIX = "slot:";
private static final String ACTIVE_PRESET_PREFIX = "preset:";
private static Map<UUID, LobbyPlayer> cache = new HashMap<>(); private static Map<UUID, LobbyPlayer> cache = new HashMap<>();
private ParticleEnum particle; private final UUID uuid;
private final ParticleSlot[] slots = new ParticleSlot[ParticleSlots.COUNT];
private final boolean[] loadedSlots = new boolean[ParticleSlots.COUNT];
// At most one of activeSlot and activePreset is set
private int activeSlot = -1;
private ParticlePreset activePreset;
private CompiledSet compiledSet;
private int userId; private int userId;
private boolean fly; private boolean fly;
private LobbyPlayer(UUID uuid) { private LobbyPlayer(UUID uuid) {
this.uuid = uuid;
userId = SteamwarUser.get(uuid).getId(); userId = SteamwarUser.get(uuid).getId();
cache.put(uuid, this); cache.put(uuid, this);
particle = null; String active = UserConfig.getConfig(userId, ACTIVE_KEY);
String saved = UserConfig.getConfig(userId, "lobby-particle"); if (active != null && active.startsWith(ACTIVE_SLOT_PREFIX)) {
if (saved != null) { try {
particle = ParticleInventory.convertFromString(saved); 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 particle selected
}
} else if (active != null && active.startsWith(ACTIVE_PRESET_PREFIX)) {
activePreset = Presets.byId(active.substring(ACTIVE_PRESET_PREFIX.length()));
}
recompile();
} }
public boolean isFlying() { public boolean isFlying() {
@@ -56,18 +80,92 @@ public class LobbyPlayer {
this.fly = fly; this.fly = fly;
} }
public ParticleEnum getParticle() { public ParticleSlot getSlot(int index) {
return particle; if (!loadedSlots[index]) {
slots[index] = ParticleSlots.load(userId, index);
loadedSlots[index] = true;
}
return slots[index];
} }
public void setParticle(ParticleEnum particle) { // Null deletes the slot
if (particle == null) { public void setSlot(int index, ParticleSlot slot) {
UserConfig.removePlayerConfig(userId, "lobby-particle"); ParticleSlots.save(userId, index, slot);
} else { slots[index] = slot;
String saved = ParticleInventory.convertToString(particle); loadedSlots[index] = true;
UserConfig.updatePlayerConfig(userId, "lobby-particle", saved); if (index == activeSlot) {
recompile();
} }
this.particle = particle; }
public int getActiveSlot() {
return activeSlot;
}
public ParticlePreset getActivePreset() {
return activePreset;
}
// 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;
}
activeSlot = index;
activePreset = null;
saveSelection();
recompile();
return true;
}
// Returns false when the preset is locked for this player
public boolean selectPreset(ParticlePreset preset) {
if (!preset.getRequirement().test(unlockContext())) {
return false;
}
activeSlot = -1;
activePreset = preset;
saveSelection();
recompile();
return true;
}
public void deselect() {
activeSlot = -1;
activePreset = null;
saveSelection();
recompile();
}
public CompiledSet getCompiledSet() {
return compiledSet;
}
// A locked selection shows nothing but stays selected, so it comes back once the rank does
public void recompile() {
UnlockContext context = unlockContext();
if (activeSlot >= 0) {
ParticleSlot slot = getSlot(activeSlot);
compiledSet = slot != null && ParticleSlots.requirement(activeSlot).test(context) ? CompiledSet.of(slot, context) : null;
} else if (activePreset != null) {
compiledSet = activePreset.getRequirement().test(context) ? CompiledSet.of(activePreset, context) : null;
} else {
compiledSet = null;
}
}
private void saveSelection() {
if (activeSlot >= 0) {
UserConfig.updatePlayerConfig(userId, ACTIVE_KEY, ACTIVE_SLOT_PREFIX + (activeSlot + 1));
} else if (activePreset != null) {
UserConfig.updatePlayerConfig(userId, ACTIVE_KEY, ACTIVE_PRESET_PREFIX + activePreset.getId());
} else {
UserConfig.removePlayerConfig(userId, ACTIVE_KEY);
}
}
private UnlockContext unlockContext() {
return new UnlockContext(SteamwarUser.get(uuid));
} }
public static LobbyPlayer getLobbyPlayer(UUID uuid) { public static LobbyPlayer getLobbyPlayer(UUID uuid) {
View File
@@ -467,7 +467,7 @@ public class SchematicSelector {
return nodes; return nodes;
} }
private List<SchematicNode> getSchematicList(SchematicNode parent) { protected List<SchematicNode> getSchematicList(SchematicNode parent) {
List<SchematicNode> nodes = new ArrayList<>(); List<SchematicNode> nodes = new ArrayList<>();
switch (target.target) { switch (target.target) {
case DIRECTORY: case DIRECTORY:
@@ -156,8 +156,7 @@ public class CheckCommand extends SWCommand {
} }
SchematicNode schem = SchematicNode.getSchematicNode(Integer.parseInt(schemID)); SchematicNode schem = SchematicNode.getSchematicNode(Integer.parseInt(schemID));
GameModeConfig<String, String> gameModeConfig = ArenaMode.getBySchemType(schem.getSchemtype()); if (!mayCheck(sender.user(), schem)) {
if (!gameModeConfig.Checkers.isEmpty() && !gameModeConfig.Checkers.contains(sender.user().getId())) {
sender.system("CHECK_SCHEMATIC_EVENT"); sender.system("CHECK_SCHEMATIC_EVENT");
return; return;
} }
@@ -313,60 +312,6 @@ public class CheckCommand extends SWCommand {
} }
private void accept() { private void accept() {
// TODO: This Code is only for the WGS and because YoyoNow is not available this can be removed or changed after the WGS!
if (schematic.getSchemtype().toDB().equals("cwargearseason26")) {
int userId = schematic.getOwner();
SteamwarUser user = SteamwarUser.byId(userId);
int teamId = user.getTeam();
SchematicNode teamFolder = SchematicNode.getSchematicNodeInNode(172325)
.stream()
.filter(schematicNode -> schematicNode.getName().startsWith(teamId + "_"))
.findFirst()
.orElse(null);
if (teamFolder == null) {
internalAccept();
return;
}
// Remove any added players from the schematic in the folder
for (SchematicNode schematicNode : SchematicNode.getSchematicNodeInNode(teamFolder.getNodeId())) {
for (NodeMember nodeMember : NodeMember.getNodeMembers(schematicNode.getNodeId())) {
nodeMember.delete();
}
}
// Copy Schem into team folder of -1 user
String name = DateTimeFormatter.ofPattern("yyyy.MM.dd_HH:mm:ss").format(schematic.getLastUpdate().toLocalDateTime());
NodeData data = NodeData.getLatest(schematic);
SchematicNode node = SchematicNode.createSchematic(-1, name, teamFolder.getNodeId());
NodeData.saveFromStream(node, data.schemData(false), data.getNodeFormat());
// Accept the team folder schematic and set other to Normal as well as adding the original owner on the schematic
node.setSchemtype(GameModeConfig.getBySchematicType(schematic.getSchemtype()).Schematic.Type);
NodeMember.createNodeMember(node.getNodeId(), schematic.getOwner());
for (NodeMember nodeMember : NodeMember.getNodeMembers(schematic.getId())) {
NodeMember.createNodeMember(node.getNodeId(), nodeMember.getMember());
}
// Conclude by setting send in schematic to normal and broadcast
concludeCheckSession("freigegeben", SchematicType.Normal, () -> {
Chatter owner = Chatter.of(SteamwarUser.byId(schematic.getOwner()).getUUID());
owner.withPlayerOrOffline(
player -> owner.system("CHECK_ACCEPTED", schematic.getSchemtype().name(), schematic.getName()),
() -> DiscordAlert.send(owner, Color.GREEN, new Message("DC_TITLE_SCHEMINFO"), new Message("DC_SCHEM_ACCEPT", schematic.getName()), true)
);
notifyTeam(new Message("CHECK_ACCEPTED_TEAM", schematic.getName(), owner.user().getUserName()));
return owner.getPlayer() != null;
});
return;
}
internalAccept();
}
private void internalAccept() {
concludeCheckSession("freigegeben", GameModeConfig.getBySchematicType(schematic.getSchemtype()).Schematic.Type, () -> { concludeCheckSession("freigegeben", GameModeConfig.getBySchematicType(schematic.getSchemtype()).Schematic.Type, () -> {
Chatter owner = Chatter.of(SteamwarUser.byId(schematic.getOwner()).getUUID()); Chatter owner = Chatter.of(SteamwarUser.byId(schematic.getOwner()).getUUID());
owner.withPlayerOrOffline( owner.withPlayerOrOffline(