forked from SteamWar/SteamWar
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.
This commit is contained in:
@@ -134,6 +134,7 @@ PARTICLE_VALUE_NO = §cNo
|
||||
PARTICLE_DESIGN_SIMPLE = §fSimple
|
||||
PARTICLE_DESIGN_CIRCLE = §fCircle
|
||||
PARTICLE_DESIGN_WING = §fWings
|
||||
PARTICLE_DESIGN_DRAWN_WING = §fDrawn wings
|
||||
PARTICLE_DESIGN_CLOUD = §fCloud
|
||||
|
||||
PARTICLE_PARAM_PARTICLE = §fParticle
|
||||
@@ -168,6 +169,8 @@ 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_DRAWN_SHAPE = §fShape
|
||||
PARTICLE_PARAM_WING_DETAIL = §fPoints per wing
|
||||
|
||||
PARTICLE_CHOICE_TICKMODE_MOVE = §fWhile moving
|
||||
PARTICLE_CHOICE_TICKMODE_ALWAYS = §fAlways
|
||||
@@ -191,6 +194,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 types
|
||||
PARTICLE_TYPE_DUST = §fDust
|
||||
|
||||
@@ -134,6 +134,7 @@ PARTICLE_VALUE_NO = §cNein
|
||||
PARTICLE_DESIGN_SIMPLE = §fEinfach
|
||||
PARTICLE_DESIGN_CIRCLE = §fKreis
|
||||
PARTICLE_DESIGN_WING = §fFlügel
|
||||
PARTICLE_DESIGN_DRAWN_WING = §fGezeichnete Flügel
|
||||
PARTICLE_DESIGN_CLOUD = §fWolke
|
||||
|
||||
PARTICLE_PARAM_PARTICLE = §fPartikel
|
||||
@@ -168,6 +169,8 @@ 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_DRAWN_SHAPE = §fForm
|
||||
PARTICLE_PARAM_WING_DETAIL = §fPunkte pro Flügel
|
||||
|
||||
PARTICLE_CHOICE_TICKMODE_MOVE = §fBeim Bewegen
|
||||
PARTICLE_CHOICE_TICKMODE_ALWAYS = §fImmer
|
||||
@@ -191,6 +194,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 types
|
||||
PARTICLE_TYPE_DUST = §fStaub
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* 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 {
|
||||
|
||||
// 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
|
||||
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));
|
||||
}
|
||||
},
|
||||
// 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
|
||||
abstract List<List<Vector>> rightWing();
|
||||
|
||||
private static final Map<String, WingDesign> CACHE = new HashMap<>();
|
||||
|
||||
// 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);
|
||||
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;
|
||||
}
|
||||
|
||||
// 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<>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
package de.steamwar.lobby.particle.design;
|
||||
|
||||
import de.steamwar.lobby.particle.ParticleElement;
|
||||
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;
|
||||
@@ -105,6 +106,27 @@ public class Designs {
|
||||
}
|
||||
};
|
||||
|
||||
public final ChoiceParameter<DrawnWings> DRAWN_SHAPE = ChoiceParameter.ofEnum("shape", "PARTICLE_PARAM_DRAWN_SHAPE", DrawnWings.ANGEL);
|
||||
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) {
|
||||
@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)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int points(ParameterValues values) {
|
||||
return 2 * values.get(WING_DETAIL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyDefaults(ParameterValues values) {
|
||||
WING.applyDefaults(values);
|
||||
}
|
||||
};
|
||||
|
||||
public final ParticleDesign CLOUD = new ParticleDesign("cloud", Material.WHITE_WOOL, "PARTICLE_DESIGN_CLOUD") {
|
||||
@Override
|
||||
public ParticleElement build(ParameterValues values, ParticleElement particle) {
|
||||
@@ -115,7 +137,7 @@ public class Designs {
|
||||
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));
|
||||
List.of(SIMPLE, CIRCLE, WING, DRAWN_WING, CLOUD).forEach(design -> register(designs, design));
|
||||
RewardDesigns.all().forEach(design -> register(designs, design));
|
||||
InternalDesigns.all().forEach(design -> register(designs, design));
|
||||
registry = designs;
|
||||
|
||||
@@ -86,7 +86,9 @@ class EntryEditor {
|
||||
Map.entry(Designs.CIRCLE_RISE, Material.LADDER),
|
||||
Map.entry(Designs.CIRCLE_RISE_SPEED, Material.RABBIT_FOOT),
|
||||
Map.entry(Designs.WING_IMAGE, Material.PHANTOM_MEMBRANE),
|
||||
Map.entry(Designs.WING_SIZE, Material.SCAFFOLDING)
|
||||
Map.entry(Designs.WING_SIZE, Material.SCAFFOLDING),
|
||||
Map.entry(Designs.DRAWN_SHAPE, Material.FEATHER),
|
||||
Map.entry(Designs.WING_DETAIL, Material.AMETHYST_SHARD)
|
||||
);
|
||||
|
||||
private record Context(Player player, SteamwarUser user, int index, int entryIndex, ParticleSlot slot, ParticleEntry entry) {
|
||||
|
||||
Reference in New Issue
Block a user