/* * 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<>(); // The shapes are drawn with the shoulder joint at y = 7; moving it to 3.3 puts it at the shoulder blades // (about 1.3 blocks above the feet at the default size) instead of at head height private static final double SHOULDER_SHIFT = 7 - 3.3; // 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); right.forEach(point -> point.setY(point.getY() - SHOULDER_SHIFT)); 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; } }