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.
This commit is contained in:
2026-10-26 13:34:23 +01:00
parent 4e5945d2bc
commit b4661ea345
9 changed files with 85 additions and 48 deletions
@@ -171,6 +171,8 @@ PARTICLE_PARAM_WING_IMAGE = §fWing shape
PARTICLE_PARAM_WING_SIZE = §fWing size
PARTICLE_PARAM_DRAWN_SHAPE = §fShape
PARTICLE_PARAM_WING_DETAIL = §fPoints per wing
PARTICLE_PARAM_WING_COLORING = §fColour mode
PARTICLE_PARAM_WING_SPARKLE = §fSparkle (points shown, %)
PARTICLE_CHOICE_TICKMODE_MOVE = §fWhile moving
PARTICLE_CHOICE_TICKMODE_ALWAYS = §fAlways
@@ -194,11 +196,11 @@ PARTICLE_CHOICE_VISIBILITY_ONLY_OTHERS = §fOnly others
PARTICLE_CHOICE_IMAGE_SIMPLE = §fSimple
PARTICLE_CHOICE_IMAGE_COMPLEX = §fComplex
PARTICLE_CHOICE_IMAGE_SWORD = §fSword
PARTICLE_CHOICE_SHAPE_ANGEL = §fAngel
PARTICLE_CHOICE_SHAPE_BAT = §fBat
PARTICLE_CHOICE_SHAPE_BUTTERFLY = §fButterfly
PARTICLE_CHOICE_SHAPE_FAIRY = §fFairy
PARTICLE_CHOICE_SHAPE_PHOENIX = §fPhoenix
PARTICLE_CHOICE_COLORING_OVER_TIME = §fChange over time
PARTICLE_CHOICE_COLORING_ALONG_WING = §fFrom the back to the tips
# Particle types
PARTICLE_TYPE_DUST = §fDust
@@ -171,6 +171,8 @@ PARTICLE_PARAM_WING_IMAGE = §fFlügelform
PARTICLE_PARAM_WING_SIZE = §fFlügelgröße
PARTICLE_PARAM_DRAWN_SHAPE = §fForm
PARTICLE_PARAM_WING_DETAIL = §fPunkte pro Flügel
PARTICLE_PARAM_WING_COLORING = §fFarbmodus
PARTICLE_PARAM_WING_SPARKLE = §fFunkeln (sichtbare Punkte, %)
PARTICLE_CHOICE_TICKMODE_MOVE = §fBeim Bewegen
PARTICLE_CHOICE_TICKMODE_ALWAYS = §fImmer
@@ -194,11 +196,11 @@ PARTICLE_CHOICE_VISIBILITY_ONLY_OTHERS = §fNur andere
PARTICLE_CHOICE_IMAGE_SIMPLE = §fEinfach
PARTICLE_CHOICE_IMAGE_COMPLEX = §fKomplex
PARTICLE_CHOICE_IMAGE_SWORD = §fSchwert
PARTICLE_CHOICE_SHAPE_ANGEL = §fEngel
PARTICLE_CHOICE_SHAPE_BAT = §fFledermaus
PARTICLE_CHOICE_SHAPE_BUTTERFLY = §fSchmetterling
PARTICLE_CHOICE_SHAPE_FAIRY = §fFee
PARTICLE_CHOICE_SHAPE_PHOENIX = §fPhönix
PARTICLE_CHOICE_COLORING_OVER_TIME = §fWechseln mit der Zeit
PARTICLE_CHOICE_COLORING_ALONG_WING = §fVom Rücken zu den Spitzen
# Particle types
PARTICLE_TYPE_DUST = §fStaub
@@ -30,18 +30,6 @@ import java.util.Map;
// Coordinates use the same units as the wing images: x is sideways from the spine, y upwards, about 11 by 13.
public enum DrawnWings {
// A curved upper edge, a row of covert feathers and long flight feathers ending in a scalloped edge
ANGEL {
@Override
List<List<Vector>> rightWing() {
List<List<Vector>> lines = new ArrayList<>();
lines.add(curve(p(0.5, 7), p(3, 10), p(6, 11.8), p(9, 12.4), p(10.8, 11.2), p(11, 8.5)));
lines.add(curve(p(1, 6.6), p(4, 8.3), p(7, 9.3), p(10, 9.8)));
lines.add(lineThrough(p(0.5, 5.5), p(2.2, 3.8), p(3.4, 4.6), p(4.2, 2.8), p(5.4, 3.9), p(6.3, 2.6), p(7.5, 4),
p(8.3, 3.4), p(9.4, 5.2), p(10, 5), p(10.7, 6.6), p(11, 7.5), p(11, 8.5)));
return lines;
}
},
// An arm bone along the top, fingers spreading from the wrist and skin hanging between them
BAT {
@Override
@@ -82,19 +70,6 @@ public enum DrawnWings {
return List.of(ellipse(5.2, 9.3, 5, 1.9, 35), ellipse(3.8, 4.6, 3.6, 1.5, -30));
}
},
// An upswept lower edge with flame tongues rising from it, each bending out and curling back at the tip
PHOENIX {
@Override
List<List<Vector>> rightWing() {
List<List<Vector>> lines = new ArrayList<>();
lines.add(curve(p(0.5, 5), p(4, 5.2), p(8, 7), p(11, 10), p(12, 13)));
double[][] tongues = {{1.5, 5.2, 2.5, 10}, {4, 5.4, 5.5, 11.5}, {6.5, 6.2, 8.2, 12.8}, {9, 8, 10.5, 14}};
for (double[] tongue : tongues) {
lines.add(curve(p(tongue[0], tongue[1]), p(tongue[0] + 1.2, tongue[1] + 1.8), p(tongue[2] - 0.8, tongue[3] - 1.5), p(tongue[2], tongue[3])));
}
return lines;
}
},
;
// The right wing as lines; the left wing mirrors it
@@ -172,11 +147,6 @@ public enum DrawnWings {
return result;
}
// Straight segments between the points, for sharp corners like feather tips
private static List<Vector> lineThrough(Vector... points) {
return new ArrayList<>(List.of(points));
}
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<>();
@@ -43,6 +43,8 @@ public class ParticleTickData {
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) {
ParticleTickData particleTickData = copy();
@@ -57,6 +59,17 @@ public class ParticleTickData {
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() {
ParticleTickData particleTickData = copy();
particleTickData.onlySelf = true;
@@ -77,6 +90,6 @@ public class ParticleTickData {
}
public ParticleTickData copy() {
return new ParticleTickData(world, player, location, deg, isMoving, viewers, onlySelf, onlyOthers, frame);
return new ParticleTickData(world, player, location, deg, isMoving, viewers, onlySelf, onlyOthers, frame, shade);
}
}
@@ -84,15 +84,26 @@ public class Designs {
));
public final DoubleParameter WING_SIZE = new DoubleParameter("size", "PARTICLE_PARAM_WING_SIZE", 0.15, 0.05, 0.3, 0.01);
public final ParticleDesign WING = new ParticleDesign("wing", Material.ELYTRA, "PARTICLE_DESIGN_WING", WING_IMAGE, WING_SIZE) {
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 ParticleDesign WING = new ParticleDesign("wing", Material.ELYTRA, "PARTICLE_DESIGN_WING", WING_IMAGE, WING_SIZE, WING_COLORING, WING_SPARKLE) {
@Override
public ParticleElement build(ParameterValues values, ParticleElement particle) {
return new Wing(particle, values.get(WING_SIZE), values.get(WING_IMAGE));
return wing(values, particle, values.get(WING_IMAGE));
}
@Override
public int points(ParameterValues values) {
return values.get(WING_IMAGE).getVectors().length;
return shown(values, values.get(WING_IMAGE).getVectors().length);
}
// Wings spawn a particle on every pixel of the image, so they start sparse, and sit on the back even while gliding
@@ -106,19 +117,20 @@ public class Designs {
}
};
public final ChoiceParameter<DrawnWings> DRAWN_SHAPE = ChoiceParameter.ofEnum("shape", "PARTICLE_PARAM_DRAWN_SHAPE", DrawnWings.ANGEL);
public final ChoiceParameter<DrawnWings> DRAWN_SHAPE = ChoiceParameter.ofEnum("shape", "PARTICLE_PARAM_DRAWN_SHAPE", DrawnWings.BAT);
public final IntParameter WING_DETAIL = new IntParameter("detail", "PARTICLE_PARAM_WING_DETAIL", 20, 6, 60);
// Wings drawn from lines, with a chosen number of points per wing; sparse ones stay cheap enough to show often
public final ParticleDesign DRAWN_WING = new ParticleDesign("drawn_wing", Material.FEATHER, "PARTICLE_DESIGN_DRAWN_WING", DRAWN_SHAPE, WING_DETAIL, WING_SIZE) {
public final ParticleDesign DRAWN_WING = new ParticleDesign("drawn_wing", Material.FEATHER, "PARTICLE_DESIGN_DRAWN_WING",
DRAWN_SHAPE, WING_DETAIL, WING_SIZE, WING_COLORING, WING_SPARKLE) {
@Override
public ParticleElement build(ParameterValues values, ParticleElement particle) {
return new Wing(particle, values.get(WING_SIZE), values.get(DRAWN_SHAPE).withDetail(values.get(WING_DETAIL)));
return wing(values, particle, values.get(DRAWN_SHAPE).withDetail(values.get(WING_DETAIL)));
}
@Override
public int points(ParameterValues values) {
return 2 * values.get(WING_DETAIL);
return shown(values, 2 * values.get(WING_DETAIL));
}
@Override
@@ -134,6 +146,15 @@ public class Designs {
}
};
private ParticleElement wing(ParameterValues values, ParticleElement particle, WingDesign shape) {
return new Wing(particle, values.get(WING_SIZE), shape, values.get(WING_COLORING) == WingColoring.ALONG_WING, values.get(WING_SPARKLE));
}
// Points shown per tick on average with the sparkle setting
private int shown(ParameterValues values, int points) {
return (int) Math.ceil(points * values.get(WING_SPARKLE) / 100.0);
}
private Map<String, ParticleDesign> registry() {
if (registry == null) {
Map<String, ParticleDesign> designs = new LinkedHashMap<>();
@@ -54,7 +54,7 @@ public class DataParticle implements ParticleElement {
if (gradient == null) {
return Color.fromRGB(RANDOM.nextInt(256), RANDOM.nextInt(256), RANDOM.nextInt(256));
}
java.awt.Color color = gradient.getColor((particleTickData.getDeg() * 10) % 360, 360);
java.awt.Color color = gradient.getColor(particleTickData.gradientPosition(), 360);
return Color.fromRGB(color.getRed(), color.getGreen(), color.getBlue());
}
@@ -65,8 +65,7 @@ public class DustParticle implements ParticleElement {
Location location = particleTickData.getLocation().add(0.0, 0.2, 0.0);
Particle.DustOptions dustOptions;
if (gradient != null) {
double result = (particleTickData.getDeg() * 10) % 360;
Color color = gradient.getColor(result, 360);
Color color = gradient.getColor(particleTickData.gradientPosition(), 360);
dustOptions = withColor(color.getRed(), color.getGreen(), color.getBlue());
} else {
dustOptions = randomParticleDust();
@@ -30,6 +30,11 @@ public class Wing extends DelegatingParticleElement {
private double size;
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 double widest;
public Wing(ParticleElement particleElement, double size, WingDesign wingDesign) {
super(particleElement);
@@ -37,6 +42,15 @@ public class Wing extends DelegatingParticleElement {
this.wingDesign = wingDesign;
}
public Wing(ParticleElement particleElement, double size, WingDesign wingDesign, boolean colorAlongWing, int density) {
this(particleElement, size, wingDesign);
this.colorAlongWing = colorAlongWing;
this.density = density;
for (Vector vector : wingDesign.getVectors()) {
widest = Math.max(widest, Math.abs(vector.getX()));
}
}
@Override
public void tick(ParticleTickData particleTickData) {
BodyFrame frame = particleTickData.getFrame();
@@ -45,19 +59,33 @@ public class Wing extends DelegatingParticleElement {
double sneak = particleTickData.getPlayer().isSneaking() ? 0.5 : 0;
Location base = particleTickData.getLocation();
for (Vector dVector : wingDesign.getVectors()) {
if (hidden()) continue;
Vector local = frame.local(dVector.getX() * size, 0.6 + dVector.getY() * size - sneak, -0.5);
particleElement.tick(particleTickData.withLocation(base.clone().add(local)));
particleElement.tick(shaded(particleTickData, dVector).withLocation(base.clone().add(local)));
}
return;
}
for (Vector dVector : wingDesign.getVectors()) {
if (hidden()) continue;
Vector vector = new Vector(dVector.getX() * size, 0.6 + dVector.getY() * size - (particleTickData.getPlayer().isSneaking() ? 0.5 : 0), 0.5);
vector.rotateAroundY(Math.toRadians(particleTickData.getPlayer().getLocation().getYaw() * -1));
vector.setX(-vector.getX());
vector.setZ(-vector.getZ());
Location location = particleTickData.getPlayer().getLocation().clone().add(vector);
ParticleTickData current = particleTickData.withLocation(location);
ParticleTickData current = shaded(particleTickData, dVector).withLocation(location);
particleElement.tick(current);
}
}
// 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);
}
}
@@ -88,7 +88,9 @@ class EntryEditor {
Map.entry(Designs.WING_IMAGE, Material.PHANTOM_MEMBRANE),
Map.entry(Designs.WING_SIZE, Material.SCAFFOLDING),
Map.entry(Designs.DRAWN_SHAPE, Material.FEATHER),
Map.entry(Designs.WING_DETAIL, Material.AMETHYST_SHARD)
Map.entry(Designs.WING_DETAIL, Material.AMETHYST_SHARD),
Map.entry(Designs.WING_COLORING, Material.CYAN_DYE),
Map.entry(Designs.WING_SPARKLE, Material.AMETHYST_CLUSTER)
);
private record Context(Player player, SteamwarUser user, int index, int entryIndex, ParticleSlot slot, ParticleEntry entry) {