From f63390915b6be2d29c227edd128a99469528a23d Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Mon, 26 Oct 2026 15:13:39 +0100 Subject: [PATCH] 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. --- .../de/steamwar/lobby/LobbySystem.properties | 4 ++ .../steamwar/lobby/LobbySystem_de.properties | 4 ++ .../particle/design/EntryParameters.java | 16 ++++- .../particle/elements/touch/GlyphPath.java | 65 +++++++++++++++++++ .../particle/elements/touch/TeamLink.java | 33 ++++++---- .../lobby/particle/gui/EntryEditor.java | 2 + .../lobby/particle/slot/ParticleEntry.java | 11 +++- 7 files changed, 120 insertions(+), 15 deletions(-) create mode 100644 LobbySystem/src/de/steamwar/lobby/particle/elements/touch/GlyphPath.java diff --git a/LobbySystem/src/de/steamwar/lobby/LobbySystem.properties b/LobbySystem/src/de/steamwar/lobby/LobbySystem.properties index 8e7388e6..b43920e9 100644 --- a/LobbySystem/src/de/steamwar/lobby/LobbySystem.properties +++ b/LobbySystem/src/de/steamwar/lobby/LobbySystem.properties @@ -142,6 +142,7 @@ 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 @@ -228,6 +229,9 @@ 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 diff --git a/LobbySystem/src/de/steamwar/lobby/LobbySystem_de.properties b/LobbySystem/src/de/steamwar/lobby/LobbySystem_de.properties index 0fe6c771..cecc7084 100644 --- a/LobbySystem/src/de/steamwar/lobby/LobbySystem_de.properties +++ b/LobbySystem/src/de/steamwar/lobby/LobbySystem_de.properties @@ -142,6 +142,7 @@ 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 @@ -228,6 +229,9 @@ 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 diff --git a/LobbySystem/src/de/steamwar/lobby/particle/design/EntryParameters.java b/LobbySystem/src/de/steamwar/lobby/particle/design/EntryParameters.java index 29ba76e0..1bd972c4 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/design/EntryParameters.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/design/EntryParameters.java @@ -19,6 +19,7 @@ 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; @@ -43,6 +44,13 @@ public class EntryParameters { 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 -> { @@ -196,6 +204,12 @@ public class EntryParameters { 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 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 = ChoiceParameter.ofEnum("placement", "PARTICLE_PARAM_PLACEMENT", Placement.WORLD); public final ChoiceParameter TICK_MODE = ChoiceParameter.ofEnum("tickMode", "PARTICLE_PARAM_TICK_MODE", TickMode.MOVE); public final ChoiceParameter MOVEMENT = ChoiceParameter.ofEnum("movement", "PARTICLE_PARAM_MOVEMENT", Movement.ANY); @@ -205,7 +219,7 @@ public class EntryParameters { public final ChoiceParameter VISIBILITY = ChoiceParameter.ofEnum("visibility", "PARTICLE_PARAM_VISIBILITY", Visibility.EVERYONE); public final List> ALL = List.of( - PARTICLE, COUNT, SPREAD_X, SPREAD_Y, SPREAD_Z, SPEED, COLORS, BLOCK, + 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 diff --git a/LobbySystem/src/de/steamwar/lobby/particle/elements/touch/GlyphPath.java b/LobbySystem/src/de/steamwar/lobby/particle/elements/touch/GlyphPath.java new file mode 100644 index 00000000..836a07a3 --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/elements/touch/GlyphPath.java @@ -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 . + */ +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)); + } + } +} diff --git a/LobbySystem/src/de/steamwar/lobby/particle/elements/touch/TeamLink.java b/LobbySystem/src/de/steamwar/lobby/particle/elements/touch/TeamLink.java index 96b5c020..c904e09c 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/elements/touch/TeamLink.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/elements/touch/TeamLink.java @@ -1,7 +1,7 @@ /* * This file is a part of the SteamWar software. * - * Copyright (C) 2026 SteamWar.de-Serverteam + * 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 @@ -16,7 +16,6 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ - package de.steamwar.lobby.particle.elements.touch; import de.steamwar.lobby.particle.BodyMotion; @@ -31,21 +30,27 @@ import java.util.HashMap; import java.util.Map; import java.util.WeakHashMap; -// A line of particles between nearby players who show the same reward. Each pair is drawn by only one of them. +// 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; - // Per reward, who showed it in which tick - private static final Map> SHOWING = new HashMap<>(); + private record Showing(long tick, ParticleElement particle) { + } - private final String reward; + // Per link, who showed it in which tick with which particle + private static final Map> SHOWING = new HashMap<>(); + + private final String link; private final double spacing; - public TeamLink(String reward, ParticleElement particleElement, double spacing) { + public TeamLink(String link, ParticleElement particleElement, double spacing) { super(particleElement); - this.reward = reward; + this.link = link; this.spacing = spacing; } @@ -53,14 +58,14 @@ public class TeamLink extends DelegatingParticleElement { public void tick(ParticleTickData particleTickData) { Player player = particleTickData.getPlayer(); long now = BodyMotion.currentTick(); - Map showing = SHOWING.computeIfAbsent(reward, key -> new WeakHashMap<>()); - showing.put(player, now); + Map 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 other : showing.entrySet()) { + for (Map.Entry 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() < now - 1 || player.getUniqueId().compareTo(partner.getUniqueId()) > 0) continue; + 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; @@ -68,7 +73,9 @@ public class TeamLink extends DelegatingParticleElement { int points = (int) Math.ceil(step.length() / spacing); step.multiply(1.0 / points); for (int i = 1; i < points; i++) { - particleElement.tick(particleTickData.withLocation(from.clone().add(step.clone().multiply(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)))); } } } diff --git a/LobbySystem/src/de/steamwar/lobby/particle/gui/EntryEditor.java b/LobbySystem/src/de/steamwar/lobby/particle/gui/EntryEditor.java index 8184f74a..cfa20384 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/gui/EntryEditor.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/gui/EntryEditor.java @@ -126,6 +126,8 @@ class EntryEditor { 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> 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)); diff --git a/LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleEntry.java b/LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleEntry.java index 358f380f..e6b63492 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleEntry.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleEntry.java @@ -28,6 +28,7 @@ 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; @@ -149,6 +150,10 @@ public class ParticleEntry { 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); } @@ -183,7 +188,11 @@ public class ParticleEntry { return false; } } - return !design.usesEntryParticle() || PARTICLE.requirement(values.get(PARTICLE)).test(context); + 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