From a5b8f00c58fba4ec41e79e153ed2ad4af3eae583 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Mon, 26 Oct 2026 10:48:32 +0100 Subject: [PATCH] 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. --- .../de/steamwar/lobby/LobbySystem.properties | 8 + .../steamwar/lobby/LobbySystem_de.properties | 8 + .../steamwar/lobby/particle/DrawnWings.java | 186 ++++++++++++++++++ .../lobby/particle/design/Designs.java | 24 ++- .../lobby/particle/gui/EntryEditor.java | 4 +- 5 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 LobbySystem/src/de/steamwar/lobby/particle/DrawnWings.java diff --git a/LobbySystem/src/de/steamwar/lobby/LobbySystem.properties b/LobbySystem/src/de/steamwar/lobby/LobbySystem.properties index 5af7324d..40477799 100644 --- a/LobbySystem/src/de/steamwar/lobby/LobbySystem.properties +++ b/LobbySystem/src/de/steamwar/lobby/LobbySystem.properties @@ -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 diff --git a/LobbySystem/src/de/steamwar/lobby/LobbySystem_de.properties b/LobbySystem/src/de/steamwar/lobby/LobbySystem_de.properties index 14879b40..f1da7ad6 100644 --- a/LobbySystem/src/de/steamwar/lobby/LobbySystem_de.properties +++ b/LobbySystem/src/de/steamwar/lobby/LobbySystem_de.properties @@ -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 diff --git a/LobbySystem/src/de/steamwar/lobby/particle/DrawnWings.java b/LobbySystem/src/de/steamwar/lobby/particle/DrawnWings.java new file mode 100644 index 00000000..ec31c586 --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/DrawnWings.java @@ -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 . + */ + +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> rightWing() { + List> 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> rightWing() { + List> 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> rightWing() { + List> lines = new ArrayList<>(); + List 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> 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> rightWing() { + List> 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> rightWing(); + + private static final Map 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 right = sample(rightWing(), pointsPerWing); + List 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 sample(List> lines, int count) { + double total = lines.stream().mapToDouble(DrawnWings::length).sum(); + List points = new ArrayList<>(); + for (int i = 0; i < count; i++) { + double distance = total * (i + 0.5) / count; + for (List line : lines) { + double length = length(line); + if (distance <= length) { + points.add(at(line, distance)); + break; + } + distance -= length; + } + } + return points; + } + + private static double length(List 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 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 curve(Vector... points) { + List 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 lineThrough(Vector... points) { + return new ArrayList<>(List.of(points)); + } + + private static List ellipse(double centerX, double centerY, double radiusX, double radiusY, double tiltDegrees) { + double tilt = Math.toRadians(tiltDegrees); + List 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; + } +} diff --git a/LobbySystem/src/de/steamwar/lobby/particle/design/Designs.java b/LobbySystem/src/de/steamwar/lobby/particle/design/Designs.java index 4210da24..c6a351bb 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/design/Designs.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/design/Designs.java @@ -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 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 registry() { if (registry == null) { Map 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; diff --git a/LobbySystem/src/de/steamwar/lobby/particle/gui/EntryEditor.java b/LobbySystem/src/de/steamwar/lobby/particle/gui/EntryEditor.java index f9e2d135..10e79cca 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/gui/EntryEditor.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/gui/EntryEditor.java @@ -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) {