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.
This commit is contained in:
2026-10-26 15:13:39 +01:00
parent db939e27d9
commit f63390915b
7 changed files with 120 additions and 15 deletions
@@ -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
@@ -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
@@ -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<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);
@@ -205,7 +219,7 @@ public class EntryParameters {
public final ChoiceParameter<Visibility> VISIBILITY = ChoiceParameter.ofEnum("visibility", "PARTICLE_PARAM_VISIBILITY", Visibility.EVERYONE);
public final List<Parameter<?>> ALL = List.of(
PARTICLE, COUNT, SPREAD_X, SPREAD_Y, SPREAD_Z, SPEED, COLORS, 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
@@ -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));
}
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<String, Map<Player, Long>> 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<String, Map<Player, Showing>> 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<Player, Long> showing = SHOWING.computeIfAbsent(reward, key -> new WeakHashMap<>());
showing.put(player, now);
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, Long> other : showing.entrySet()) {
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() < 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))));
}
}
}
@@ -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<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));
@@ -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