forked from SteamWar/SteamWar
Merge wing designs, turn wings with the body, limit leaves
Wing and Drawn wings become one Wings design whose shapes include the images and the drawn bat, butterfly and fairy; for drawn shapes the points per wing replace the amount. Body rotation is tracked per player the way the client turns the body, and wings, the heart beat and body placement follow it instead of the head. Leaves particles are for the server team only, and typed hex colours show the closest dye.
This commit is contained in:
@@ -134,7 +134,6 @@ PARTICLE_VALUE_NO = §cNo
|
|||||||
PARTICLE_DESIGN_SIMPLE = §fSimple
|
PARTICLE_DESIGN_SIMPLE = §fSimple
|
||||||
PARTICLE_DESIGN_CIRCLE = §fCircle
|
PARTICLE_DESIGN_CIRCLE = §fCircle
|
||||||
PARTICLE_DESIGN_WING = §fWings
|
PARTICLE_DESIGN_WING = §fWings
|
||||||
PARTICLE_DESIGN_DRAWN_WING = §fDrawn wings
|
|
||||||
PARTICLE_DESIGN_CLOUD = §fCloud
|
PARTICLE_DESIGN_CLOUD = §fCloud
|
||||||
|
|
||||||
PARTICLE_PARAM_PARTICLE = §fParticle
|
PARTICLE_PARAM_PARTICLE = §fParticle
|
||||||
@@ -169,7 +168,6 @@ PARTICLE_PARAM_BOUNCE = §fBounce back
|
|||||||
PARTICLE_PARAM_INVERTED = §fStart at the top
|
PARTICLE_PARAM_INVERTED = §fStart at the top
|
||||||
PARTICLE_PARAM_WING_IMAGE = §fWing shape
|
PARTICLE_PARAM_WING_IMAGE = §fWing shape
|
||||||
PARTICLE_PARAM_WING_SIZE = §fWing size
|
PARTICLE_PARAM_WING_SIZE = §fWing size
|
||||||
PARTICLE_PARAM_DRAWN_SHAPE = §fShape
|
|
||||||
PARTICLE_PARAM_WING_DETAIL = §fPoints per wing
|
PARTICLE_PARAM_WING_DETAIL = §fPoints per wing
|
||||||
PARTICLE_PARAM_WING_COLORING = §fColour mode
|
PARTICLE_PARAM_WING_COLORING = §fColour mode
|
||||||
PARTICLE_PARAM_WING_SPARKLE = §fSparkle (points shown, %)
|
PARTICLE_PARAM_WING_SPARKLE = §fSparkle (points shown, %)
|
||||||
@@ -196,9 +194,9 @@ PARTICLE_CHOICE_VISIBILITY_ONLY_OTHERS = §fOnly others
|
|||||||
PARTICLE_CHOICE_IMAGE_SIMPLE = §fSimple
|
PARTICLE_CHOICE_IMAGE_SIMPLE = §fSimple
|
||||||
PARTICLE_CHOICE_IMAGE_COMPLEX = §fComplex
|
PARTICLE_CHOICE_IMAGE_COMPLEX = §fComplex
|
||||||
PARTICLE_CHOICE_IMAGE_SWORD = §fSword
|
PARTICLE_CHOICE_IMAGE_SWORD = §fSword
|
||||||
PARTICLE_CHOICE_SHAPE_BAT = §fBat
|
PARTICLE_CHOICE_IMAGE_BAT = §fBat
|
||||||
PARTICLE_CHOICE_SHAPE_BUTTERFLY = §fButterfly
|
PARTICLE_CHOICE_IMAGE_BUTTERFLY = §fButterfly
|
||||||
PARTICLE_CHOICE_SHAPE_FAIRY = §fFairy
|
PARTICLE_CHOICE_IMAGE_FAIRY = §fFairy
|
||||||
PARTICLE_CHOICE_COLORING_OVER_TIME = §fChange over time
|
PARTICLE_CHOICE_COLORING_OVER_TIME = §fChange over time
|
||||||
PARTICLE_CHOICE_COLORING_ALONG_WING = §fFrom the back to the tips
|
PARTICLE_CHOICE_COLORING_ALONG_WING = §fFrom the back to the tips
|
||||||
|
|
||||||
|
|||||||
@@ -134,7 +134,6 @@ PARTICLE_VALUE_NO = §cNein
|
|||||||
PARTICLE_DESIGN_SIMPLE = §fEinfach
|
PARTICLE_DESIGN_SIMPLE = §fEinfach
|
||||||
PARTICLE_DESIGN_CIRCLE = §fKreis
|
PARTICLE_DESIGN_CIRCLE = §fKreis
|
||||||
PARTICLE_DESIGN_WING = §fFlügel
|
PARTICLE_DESIGN_WING = §fFlügel
|
||||||
PARTICLE_DESIGN_DRAWN_WING = §fGezeichnete Flügel
|
|
||||||
PARTICLE_DESIGN_CLOUD = §fWolke
|
PARTICLE_DESIGN_CLOUD = §fWolke
|
||||||
|
|
||||||
PARTICLE_PARAM_PARTICLE = §fPartikel
|
PARTICLE_PARAM_PARTICLE = §fPartikel
|
||||||
@@ -169,7 +168,6 @@ PARTICLE_PARAM_BOUNCE = §fZurückspringen
|
|||||||
PARTICLE_PARAM_INVERTED = §fOben beginnen
|
PARTICLE_PARAM_INVERTED = §fOben beginnen
|
||||||
PARTICLE_PARAM_WING_IMAGE = §fFlügelform
|
PARTICLE_PARAM_WING_IMAGE = §fFlügelform
|
||||||
PARTICLE_PARAM_WING_SIZE = §fFlügelgröße
|
PARTICLE_PARAM_WING_SIZE = §fFlügelgröße
|
||||||
PARTICLE_PARAM_DRAWN_SHAPE = §fForm
|
|
||||||
PARTICLE_PARAM_WING_DETAIL = §fPunkte pro Flügel
|
PARTICLE_PARAM_WING_DETAIL = §fPunkte pro Flügel
|
||||||
PARTICLE_PARAM_WING_COLORING = §fFarbmodus
|
PARTICLE_PARAM_WING_COLORING = §fFarbmodus
|
||||||
PARTICLE_PARAM_WING_SPARKLE = §fFunkeln (sichtbare Punkte, %)
|
PARTICLE_PARAM_WING_SPARKLE = §fFunkeln (sichtbare Punkte, %)
|
||||||
@@ -196,9 +194,9 @@ PARTICLE_CHOICE_VISIBILITY_ONLY_OTHERS = §fNur andere
|
|||||||
PARTICLE_CHOICE_IMAGE_SIMPLE = §fEinfach
|
PARTICLE_CHOICE_IMAGE_SIMPLE = §fEinfach
|
||||||
PARTICLE_CHOICE_IMAGE_COMPLEX = §fKomplex
|
PARTICLE_CHOICE_IMAGE_COMPLEX = §fKomplex
|
||||||
PARTICLE_CHOICE_IMAGE_SWORD = §fSchwert
|
PARTICLE_CHOICE_IMAGE_SWORD = §fSchwert
|
||||||
PARTICLE_CHOICE_SHAPE_BAT = §fFledermaus
|
PARTICLE_CHOICE_IMAGE_BAT = §fFledermaus
|
||||||
PARTICLE_CHOICE_SHAPE_BUTTERFLY = §fSchmetterling
|
PARTICLE_CHOICE_IMAGE_BUTTERFLY = §fSchmetterling
|
||||||
PARTICLE_CHOICE_SHAPE_FAIRY = §fFee
|
PARTICLE_CHOICE_IMAGE_FAIRY = §fFee
|
||||||
PARTICLE_CHOICE_COLORING_OVER_TIME = §fWechseln mit der Zeit
|
PARTICLE_CHOICE_COLORING_OVER_TIME = §fWechseln mit der Zeit
|
||||||
PARTICLE_CHOICE_COLORING_ALONG_WING = §fVom Rücken zu den Spitzen
|
PARTICLE_CHOICE_COLORING_ALONG_WING = §fVom Rücken zu den Spitzen
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import org.bukkit.entity.Pose;
|
|||||||
import org.bukkit.util.Vector;
|
import org.bukkit.util.Vector;
|
||||||
|
|
||||||
// The player's body as a coordinate system: origin at the feet, up towards the head, forward out of the chest.
|
// The player's body as a coordinate system: origin at the feet, up towards the head, forward out of the chest.
|
||||||
// Standing it turns with the body; gliding or swimming the body lies along the look direction.
|
// Standing it turns with the body (as BodyYaw tracks it); gliding or swimming the body lies along the look direction.
|
||||||
@Getter
|
@Getter
|
||||||
public class BodyFrame {
|
public class BodyFrame {
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ public class BodyFrame {
|
|||||||
Vector up = location.getDirection().normalize();
|
Vector up = location.getDirection().normalize();
|
||||||
Vector right = up.clone().crossProduct(WORLD_UP);
|
Vector right = up.clone().crossProduct(WORLD_UP);
|
||||||
if (right.lengthSquared() < 1e-6) {
|
if (right.lengthSquared() < 1e-6) {
|
||||||
right = horizontal(player.getBodyYaw()).crossProduct(WORLD_UP);
|
right = horizontal(BodyYaw.of(player)).crossProduct(WORLD_UP);
|
||||||
}
|
}
|
||||||
right.normalize();
|
right.normalize();
|
||||||
Vector forward = up.clone().crossProduct(right).normalize();
|
Vector forward = up.clone().crossProduct(right).normalize();
|
||||||
@@ -72,7 +72,7 @@ public class BodyFrame {
|
|||||||
return new BodyFrame(origin, right, up, forward);
|
return new BodyFrame(origin, right, up, forward);
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector forward = horizontal(player.getBodyYaw());
|
Vector forward = horizontal(BodyYaw.of(player));
|
||||||
Vector right = forward.clone().crossProduct(WORLD_UP).normalize();
|
Vector right = forward.clone().crossProduct(WORLD_UP).normalize();
|
||||||
return new BodyFrame(location, right, WORLD_UP.clone(), forward);
|
return new BodyFrame(location, right, WORLD_UP.clone(), forward);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
* 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 lombok.experimental.UtilityClass;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.WeakHashMap;
|
||||||
|
|
||||||
|
// The direction a player's body faces, worked out the way the Minecraft client turns the body, because the server's
|
||||||
|
// own body rotation for players does not follow what other players see. Walking turns the body towards the walking
|
||||||
|
// direction, and standing still it only turns once the head looks more than 50 degrees to the side.
|
||||||
|
@UtilityClass
|
||||||
|
public class BodyYaw {
|
||||||
|
|
||||||
|
private final float MAX_HEAD_TURN = 50;
|
||||||
|
private final double MOVING = 0.0025;
|
||||||
|
|
||||||
|
private final Map<Player, State> states = new WeakHashMap<>();
|
||||||
|
|
||||||
|
private static class State {
|
||||||
|
private float body;
|
||||||
|
private double x;
|
||||||
|
private double z;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called once per server tick for every online player
|
||||||
|
public void tick(Player player) {
|
||||||
|
Location location = player.getLocation();
|
||||||
|
State state = states.get(player);
|
||||||
|
if (state == null) {
|
||||||
|
state = new State();
|
||||||
|
state.body = location.getYaw();
|
||||||
|
state.x = location.getX();
|
||||||
|
state.z = location.getZ();
|
||||||
|
states.put(player, state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float head = location.getYaw();
|
||||||
|
double dx = location.getX() - state.x;
|
||||||
|
double dz = location.getZ() - state.z;
|
||||||
|
state.x = location.getX();
|
||||||
|
state.z = location.getZ();
|
||||||
|
|
||||||
|
float target = state.body;
|
||||||
|
if (dx * dx + dz * dz > MOVING) {
|
||||||
|
target = (float) Math.toDegrees(Math.atan2(dz, dx)) - 90;
|
||||||
|
// Walking backwards keeps the body facing forwards
|
||||||
|
float away = Math.abs(wrap(head) - target);
|
||||||
|
if (away > 95 && away < 265) {
|
||||||
|
target -= 180;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.body += wrap(target - state.body) * 0.3f;
|
||||||
|
float headTurn = wrap(head - state.body);
|
||||||
|
if (Math.abs(headTurn) > MAX_HEAD_TURN) {
|
||||||
|
state.body += headTurn - Math.signum(headTurn) * MAX_HEAD_TURN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The tracked body direction in degrees, or the head direction for players not tracked yet
|
||||||
|
public float of(Player player) {
|
||||||
|
State state = states.get(player);
|
||||||
|
return state == null ? player.getLocation().getYaw() : state.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float wrap(float degrees) {
|
||||||
|
float wrapped = degrees % 360;
|
||||||
|
if (wrapped >= 180) wrapped -= 360;
|
||||||
|
if (wrapped < -180) wrapped += 360;
|
||||||
|
return wrapped;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,11 +38,11 @@ public class CompiledSet {
|
|||||||
return compiledSet;
|
return compiledSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Entries whose design is not unlocked are skipped, not removed
|
// Entries whose design or particle type is not unlocked are skipped, not removed
|
||||||
public static CompiledSet of(ParticleSet particleSet, UnlockContext context) {
|
public static CompiledSet of(ParticleSet particleSet, UnlockContext context) {
|
||||||
CompiledSet compiledSet = new CompiledSet();
|
CompiledSet compiledSet = new CompiledSet();
|
||||||
for (ParticleEntry entry : particleSet.getEntries()) {
|
for (ParticleEntry entry : particleSet.getEntries()) {
|
||||||
if (entry.getDesign().getRequirement().test(context)) {
|
if (entry.isUnlocked(context)) {
|
||||||
compiledSet.add(entry.build());
|
compiledSet.add(entry.build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ public class ParticleListener implements Listener {
|
|||||||
movingPlayers.entrySet().removeIf(entry -> entry.getValue() <= 0);
|
movingPlayers.entrySet().removeIf(entry -> entry.getValue() <= 0);
|
||||||
deg += 0.1;
|
deg += 0.1;
|
||||||
if (deg > 360) deg = 0;
|
if (deg > 360) deg = 0;
|
||||||
|
Bukkit.getOnlinePlayers().forEach(BodyYaw::tick);
|
||||||
Bukkit.getOnlinePlayers().forEach(player -> {
|
Bukkit.getOnlinePlayers().forEach(player -> {
|
||||||
if (disabled(player)) return;
|
if (disabled(player)) return;
|
||||||
if (JumpAndRun.isPlayerInJumpAndRun(player)) return;
|
if (JumpAndRun.isPlayerInJumpAndRun(player)) return;
|
||||||
|
|||||||
@@ -66,22 +66,29 @@ public class Designs {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public final ChoiceParameter<WingDesign> WING_IMAGE = new ChoiceParameter<>("image", "PARTICLE_PARAM_WING_IMAGE", WingDesign.SIMPLE, List.of(
|
// Images keep their old ids, so saved wings still load; preset and tournament images stay out of the builder
|
||||||
new ChoiceParameter.Choice<>("simple", WingDesign.SIMPLE, false),
|
private final List<ChoiceParameter.Choice<WingShape>> WING_SHAPES = List.of(
|
||||||
new ChoiceParameter.Choice<>("complex", WingDesign.COMPLEX, false),
|
new ChoiceParameter.Choice<>("simple", WingShape.image(WingDesign.SIMPLE), false),
|
||||||
new ChoiceParameter.Choice<>("sword", WingDesign.SWORD, false),
|
new ChoiceParameter.Choice<>("complex", WingShape.image(WingDesign.COMPLEX), false),
|
||||||
new ChoiceParameter.Choice<>("sw", WingDesign.SW, true),
|
new ChoiceParameter.Choice<>("sword", WingShape.image(WingDesign.SWORD), false),
|
||||||
new ChoiceParameter.Choice<>("wgs", WingDesign.WGS, true),
|
new ChoiceParameter.Choice<>("bat", WingShape.drawn(DrawnWings.BAT), false),
|
||||||
new ChoiceParameter.Choice<>("sword_crossed", WingDesign.SWORD_CROSSED, true),
|
new ChoiceParameter.Choice<>("butterfly", WingShape.drawn(DrawnWings.BUTTERFLY), false),
|
||||||
new ChoiceParameter.Choice<>("mwgl", WingDesign.MWGL, true),
|
new ChoiceParameter.Choice<>("fairy", WingShape.drawn(DrawnWings.FAIRY), false),
|
||||||
new ChoiceParameter.Choice<>("eclipse", WingDesign.ECLIPSE, true),
|
new ChoiceParameter.Choice<>("sw", WingShape.image(WingDesign.SW), true),
|
||||||
new ChoiceParameter.Choice<>("pl", WingDesign.PL, true),
|
new ChoiceParameter.Choice<>("wgs", WingShape.image(WingDesign.WGS), true),
|
||||||
new ChoiceParameter.Choice<>("plompa_easter", WingDesign.PlompaEasterWings, true),
|
new ChoiceParameter.Choice<>("sword_crossed", WingShape.image(WingDesign.SWORD_CROSSED), true),
|
||||||
new ChoiceParameter.Choice<>("ely_e", WingDesign.ELY_E, true),
|
new ChoiceParameter.Choice<>("mwgl", WingShape.image(WingDesign.MWGL), true),
|
||||||
new ChoiceParameter.Choice<>("ely_l", WingDesign.ELY_L, true),
|
new ChoiceParameter.Choice<>("eclipse", WingShape.image(WingDesign.ECLIPSE), true),
|
||||||
new ChoiceParameter.Choice<>("ely_y", WingDesign.ELY_Y, true),
|
new ChoiceParameter.Choice<>("pl", WingShape.image(WingDesign.PL), true),
|
||||||
new ChoiceParameter.Choice<>("ev", WingDesign.EV, true)
|
new ChoiceParameter.Choice<>("plompa_easter", WingShape.image(WingDesign.PlompaEasterWings), true),
|
||||||
));
|
new ChoiceParameter.Choice<>("ely_e", WingShape.image(WingDesign.ELY_E), true),
|
||||||
|
new ChoiceParameter.Choice<>("ely_l", WingShape.image(WingDesign.ELY_L), true),
|
||||||
|
new ChoiceParameter.Choice<>("ely_y", WingShape.image(WingDesign.ELY_Y), true),
|
||||||
|
new ChoiceParameter.Choice<>("ev", WingShape.image(WingDesign.EV), true)
|
||||||
|
);
|
||||||
|
public final ChoiceParameter<WingShape> WING_SHAPE = new ChoiceParameter<>("image", "PARTICLE_PARAM_WING_IMAGE", WING_SHAPES.get(0).value(), WING_SHAPES);
|
||||||
|
// Points per wing of drawn shapes; it replaces the amount, as every point spawns one particle
|
||||||
|
public final IntParameter WING_DETAIL = new IntParameter("detail", "PARTICLE_PARAM_WING_DETAIL", 20, 6, 60);
|
||||||
public final DoubleParameter WING_SIZE = new DoubleParameter("size", "PARTICLE_PARAM_WING_SIZE", 0.15, 0.05, 0.3, 0.01);
|
public final DoubleParameter WING_SIZE = new DoubleParameter("size", "PARTICLE_PARAM_WING_SIZE", 0.15, 0.05, 0.3, 0.01);
|
||||||
|
|
||||||
public enum WingColoring {
|
public enum WingColoring {
|
||||||
@@ -95,18 +102,31 @@ public class Designs {
|
|||||||
// Percent of the points shown each tick; fewer makes the wing twinkle and costs less
|
// 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 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) {
|
public final ParticleDesign WING = new ParticleDesign("wing", Material.ELYTRA, "PARTICLE_DESIGN_WING", WING_SHAPE, WING_DETAIL, WING_SIZE, WING_COLORING, WING_SPARKLE) {
|
||||||
@Override
|
@Override
|
||||||
public ParticleElement build(ParameterValues values, ParticleElement particle) {
|
public ParticleElement build(ParameterValues values, ParticleElement particle) {
|
||||||
return wing(values, particle, values.get(WING_IMAGE));
|
WingShape shape = values.get(WING_SHAPE);
|
||||||
|
return new Wing(particle, values.get(WING_SIZE), shape.design(values.get(WING_DETAIL)),
|
||||||
|
values.get(WING_COLORING) == WingColoring.ALONG_WING, values.get(WING_SPARKLE));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int points(ParameterValues values) {
|
public int points(ParameterValues values) {
|
||||||
return shown(values, values.get(WING_IMAGE).getVectors().length);
|
int points = values.get(WING_SHAPE).points(values.get(WING_DETAIL));
|
||||||
|
return (int) Math.ceil(points * values.get(WING_SPARKLE) / 100.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wings spawn a particle on every pixel of the image, so they start sparse, and sit on the back even while gliding
|
@Override
|
||||||
|
public boolean showsSetting(Parameter<?> parameter, ParameterValues values) {
|
||||||
|
return parameter != WING_DETAIL || values.get(WING_SHAPE).isDrawn();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean replacesAmount(ParameterValues values) {
|
||||||
|
return values.get(WING_SHAPE).isDrawn();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Image wings spawn a particle on every pixel, so they start sparse, and sit on the back even while gliding
|
||||||
@Override
|
@Override
|
||||||
public void applyDefaults(ParameterValues values) {
|
public void applyDefaults(ParameterValues values) {
|
||||||
values.set(EntryParameters.COUNT, 1)
|
values.set(EntryParameters.COUNT, 1)
|
||||||
@@ -117,27 +137,10 @@ public class Designs {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public final ChoiceParameter<DrawnWings> DRAWN_SHAPE = ChoiceParameter.ofEnum("shape", "PARTICLE_PARAM_DRAWN_SHAPE", DrawnWings.BAT);
|
// The wing shape that shows the given image, for presets
|
||||||
public final IntParameter WING_DETAIL = new IntParameter("detail", "PARTICLE_PARAM_WING_DETAIL", 20, 6, 60);
|
public WingShape wingShape(WingDesign image) {
|
||||||
|
return WING_SHAPE.getChoices().stream().map(ChoiceParameter.Choice::value).filter(shape -> shape.shows(image)).findFirst().orElseThrow();
|
||||||
// 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, WING_COLORING, WING_SPARKLE) {
|
|
||||||
@Override
|
|
||||||
public ParticleElement build(ParameterValues values, ParticleElement particle) {
|
|
||||||
return wing(values, particle, values.get(DRAWN_SHAPE).withDetail(values.get(WING_DETAIL)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int points(ParameterValues values) {
|
|
||||||
return shown(values, 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") {
|
public final ParticleDesign CLOUD = new ParticleDesign("cloud", Material.WHITE_WOOL, "PARTICLE_DESIGN_CLOUD") {
|
||||||
@Override
|
@Override
|
||||||
@@ -146,19 +149,10 @@ 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() {
|
private Map<String, ParticleDesign> registry() {
|
||||||
if (registry == null) {
|
if (registry == null) {
|
||||||
Map<String, ParticleDesign> designs = new LinkedHashMap<>();
|
Map<String, ParticleDesign> designs = new LinkedHashMap<>();
|
||||||
List.of(SIMPLE, CIRCLE, WING, DRAWN_WING, CLOUD).forEach(design -> register(designs, design));
|
List.of(SIMPLE, CIRCLE, WING, CLOUD).forEach(design -> register(designs, design));
|
||||||
RewardDesigns.all().forEach(design -> register(designs, design));
|
RewardDesigns.all().forEach(design -> register(designs, design));
|
||||||
InternalDesigns.all().forEach(design -> register(designs, design));
|
InternalDesigns.all().forEach(design -> register(designs, design));
|
||||||
registry = designs;
|
registry = designs;
|
||||||
|
|||||||
@@ -62,6 +62,16 @@ public abstract class ParticleDesign {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Whether the entry editor shows this design setting; some only matter for some values of others
|
||||||
|
public boolean showsSetting(Parameter<?> parameter, ParameterValues values) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// True when the design decides how many particles each point spawns, so the amount setting is hidden and counts as 1
|
||||||
|
public boolean replacesAmount(ParameterValues values) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Sets the conditions and timing a new entry of this design starts with
|
// Sets the conditions and timing a new entry of this design starts with
|
||||||
public void applyDefaults(ParameterValues values) {
|
public void applyDefaults(ParameterValues values) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
package de.steamwar.lobby.particle.design;
|
package de.steamwar.lobby.particle.design;
|
||||||
|
|
||||||
|
import de.steamwar.lobby.particle.ParticleRequirement;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
@@ -179,6 +180,14 @@ public enum ParticleType {
|
|||||||
return data == Data.DUST || data == Data.COLOR || data == Data.COLOR_TRANSITION || data == Data.TRAIL;
|
return data == Data.DUST || data == Data.COLOR || data == Data.COLOR_TRANSITION || data == Data.TRAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Leaves drift down slowly and pile up around a player, so only the server team may build with them
|
||||||
|
public ParticleRequirement getRequirement() {
|
||||||
|
return switch (this) {
|
||||||
|
case CHERRY_LEAVES, PALE_OAK_LEAVES, TINTED_LEAVES -> ParticleRequirement.SERVER_TEAM;
|
||||||
|
default -> ParticleRequirement.NO_REQUIRMENT;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isTrail() {
|
public boolean isTrail() {
|
||||||
return data == Data.TRAIL;
|
return data == Data.TRAIL;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* 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.design;
|
||||||
|
|
||||||
|
import de.steamwar.lobby.particle.DrawnWings;
|
||||||
|
import de.steamwar.lobby.particle.WingDesign;
|
||||||
|
|
||||||
|
// A wing shape of the wings design: either a fixed image or a shape drawn with a chosen number of points
|
||||||
|
public class WingShape {
|
||||||
|
|
||||||
|
private final WingDesign image;
|
||||||
|
private final DrawnWings drawn;
|
||||||
|
|
||||||
|
private WingShape(WingDesign image, DrawnWings drawn) {
|
||||||
|
this.image = image;
|
||||||
|
this.drawn = drawn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WingShape image(WingDesign image) {
|
||||||
|
return new WingShape(image, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WingShape drawn(DrawnWings drawn) {
|
||||||
|
return new WingShape(null, drawn);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDrawn() {
|
||||||
|
return drawn != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shows(WingDesign image) {
|
||||||
|
return this.image == image;
|
||||||
|
}
|
||||||
|
|
||||||
|
public WingDesign design(int pointsPerWing) {
|
||||||
|
return isDrawn() ? drawn.withDetail(pointsPerWing) : image;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int points(int pointsPerWing) {
|
||||||
|
return isDrawn() ? 2 * pointsPerWing : image.getVectors().length;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
package de.steamwar.lobby.particle.elements;
|
package de.steamwar.lobby.particle.elements;
|
||||||
|
|
||||||
import de.steamwar.lobby.particle.BodyFrame;
|
import de.steamwar.lobby.particle.BodyFrame;
|
||||||
|
import de.steamwar.lobby.particle.BodyYaw;
|
||||||
import de.steamwar.lobby.particle.ParticleElement;
|
import de.steamwar.lobby.particle.ParticleElement;
|
||||||
import de.steamwar.lobby.particle.ParticleTickData;
|
import de.steamwar.lobby.particle.ParticleTickData;
|
||||||
import de.steamwar.lobby.particle.WingDesign;
|
import de.steamwar.lobby.particle.WingDesign;
|
||||||
@@ -68,7 +69,7 @@ public class Wing extends DelegatingParticleElement {
|
|||||||
for (Vector dVector : wingDesign.getVectors()) {
|
for (Vector dVector : wingDesign.getVectors()) {
|
||||||
if (hidden()) continue;
|
if (hidden()) continue;
|
||||||
Vector vector = new Vector(dVector.getX() * size, 0.6 + dVector.getY() * size - (particleTickData.getPlayer().isSneaking() ? 0.5 : 0), 0.5);
|
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.rotateAroundY(Math.toRadians(BodyYaw.of(particleTickData.getPlayer()) * -1));
|
||||||
vector.setX(-vector.getX());
|
vector.setX(-vector.getX());
|
||||||
vector.setZ(-vector.getZ());
|
vector.setZ(-vector.getZ());
|
||||||
Location location = particleTickData.getPlayer().getLocation().clone().add(vector);
|
Location location = particleTickData.getPlayer().getLocation().clone().add(vector);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
package de.steamwar.lobby.particle.elements.custom;
|
package de.steamwar.lobby.particle.elements.custom;
|
||||||
|
|
||||||
|
import de.steamwar.lobby.particle.BodyYaw;
|
||||||
import de.steamwar.lobby.particle.ParticleElement;
|
import de.steamwar.lobby.particle.ParticleElement;
|
||||||
import de.steamwar.lobby.particle.ParticleTickData;
|
import de.steamwar.lobby.particle.ParticleTickData;
|
||||||
import de.steamwar.lobby.particle.elements.DelegatingParticleElement;
|
import de.steamwar.lobby.particle.elements.DelegatingParticleElement;
|
||||||
@@ -48,7 +49,7 @@ public class HearthBeat extends DelegatingParticleElement {
|
|||||||
double x = (time - 0.5) * 2;
|
double x = (time - 0.5) * 2;
|
||||||
|
|
||||||
Vector vector = new Vector(x, 1.1 + y - (particleTickData.getPlayer().isSneaking() ? 0.5 : 0), 0.7);
|
Vector vector = new Vector(x, 1.1 + y - (particleTickData.getPlayer().isSneaking() ? 0.5 : 0), 0.7);
|
||||||
vector.rotateAroundY(Math.toRadians(particleTickData.getPlayer().getLocation().getYaw() * -1));
|
vector.rotateAroundY(Math.toRadians(BodyYaw.of(particleTickData.getPlayer()) * -1));
|
||||||
vector.setX(-vector.getX());
|
vector.setX(-vector.getX());
|
||||||
vector.setZ(-vector.getZ());
|
vector.setZ(-vector.getZ());
|
||||||
Location location = particleTickData.getPlayer().getLocation().clone().add(vector);
|
Location location = particleTickData.getPlayer().getLocation().clone().add(vector);
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ package de.steamwar.lobby.particle.gui;
|
|||||||
import de.steamwar.inventory.SWInventory;
|
import de.steamwar.inventory.SWInventory;
|
||||||
import de.steamwar.inventory.SWItem;
|
import de.steamwar.inventory.SWItem;
|
||||||
import de.steamwar.lobby.LobbySystem;
|
import de.steamwar.lobby.LobbySystem;
|
||||||
|
import de.steamwar.lobby.particle.ParticleRequirement;
|
||||||
import de.steamwar.lobby.particle.UnlockContext;
|
import de.steamwar.lobby.particle.UnlockContext;
|
||||||
import de.steamwar.lobby.particle.WingDesign;
|
|
||||||
import de.steamwar.lobby.particle.design.*;
|
import de.steamwar.lobby.particle.design.*;
|
||||||
import de.steamwar.lobby.particle.slot.ParticleEntry;
|
import de.steamwar.lobby.particle.slot.ParticleEntry;
|
||||||
import de.steamwar.lobby.particle.slot.ParticleSlot;
|
import de.steamwar.lobby.particle.slot.ParticleSlot;
|
||||||
@@ -85,9 +85,8 @@ class EntryEditor {
|
|||||||
Map.entry(Designs.CIRCLE_PHASE, Material.TARGET),
|
Map.entry(Designs.CIRCLE_PHASE, Material.TARGET),
|
||||||
Map.entry(Designs.CIRCLE_RISE, Material.LADDER),
|
Map.entry(Designs.CIRCLE_RISE, Material.LADDER),
|
||||||
Map.entry(Designs.CIRCLE_RISE_SPEED, Material.RABBIT_FOOT),
|
Map.entry(Designs.CIRCLE_RISE_SPEED, Material.RABBIT_FOOT),
|
||||||
Map.entry(Designs.WING_IMAGE, Material.PHANTOM_MEMBRANE),
|
Map.entry(Designs.WING_SHAPE, 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),
|
Map.entry(Designs.WING_DETAIL, Material.AMETHYST_SHARD),
|
||||||
Map.entry(Designs.WING_COLORING, Material.CYAN_DYE),
|
Map.entry(Designs.WING_COLORING, Material.CYAN_DYE),
|
||||||
Map.entry(Designs.WING_SPARKLE, Material.AMETHYST_CLUSTER)
|
Map.entry(Designs.WING_SPARKLE, Material.AMETHYST_CLUSTER)
|
||||||
@@ -134,12 +133,13 @@ class EntryEditor {
|
|||||||
inventory.setItem(ROW_DESIGN * 9 + FIRST_SETTING + 3 + i, parameterItem(context, trail.get(i)));
|
inventory.setItem(ROW_DESIGN * 9 + FIRST_SETTING + 3 + i, parameterItem(context, trail.get(i)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
row(inventory, context, ROW_PARTICLE, List.of(COUNT, SPEED, SPREAD_X, SPREAD_Y, SPREAD_Z));
|
List<Parameter<?>> particleRow = design.replacesAmount(entry.getValues()) ? List.of(SPEED, SPREAD_X, SPREAD_Y, SPREAD_Z) : List.of(COUNT, SPEED, SPREAD_X, SPREAD_Y, SPREAD_Z);
|
||||||
|
row(inventory, context, ROW_PARTICLE, particleRow);
|
||||||
} else {
|
} else {
|
||||||
inventory.setItem(ROW_PARTICLE * 9 + FIRST_SETTING, new SWItem(Material.STRUCTURE_VOID, msg("PARTICLE_FIXED_LOOK", player), clickType -> {
|
inventory.setItem(ROW_PARTICLE * 9 + FIRST_SETTING, new SWItem(Material.STRUCTURE_VOID, msg("PARTICLE_FIXED_LOOK", player), clickType -> {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
row(inventory, context, ROW_SHAPE, design.getParameters());
|
row(inventory, context, ROW_SHAPE, design.getParameters().stream().filter(parameter -> design.showsSetting(parameter, entry.getValues())).toList());
|
||||||
row(inventory, context, ROW_TIMING, List.of(TICK_MODE, INTERVAL, PLACEMENT, OFFSET_X, OFFSET_Y, OFFSET_Z));
|
row(inventory, context, ROW_TIMING, List.of(TICK_MODE, INTERVAL, PLACEMENT, OFFSET_X, OFFSET_Y, OFFSET_Z));
|
||||||
row(inventory, context, ROW_CONDITIONS, List.of(MOVEMENT, GLIDING, GROUND, SNEAK, VISIBILITY));
|
row(inventory, context, ROW_CONDITIONS, List.of(MOVEMENT, GLIDING, GROUND, SNEAK, VISIBILITY));
|
||||||
|
|
||||||
@@ -240,7 +240,8 @@ class EntryEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private <E> void pick(Context context, ChoiceParameter<E> parameter, E current) {
|
private <E> void pick(Context context, ChoiceParameter<E> parameter, E current) {
|
||||||
Pickers.choice(context.player(), parameter, current, EntryEditor::choiceIcon, picked -> {
|
Pickers.choice(context.player(), parameter, current, EntryEditor::choiceIcon,
|
||||||
|
value -> value instanceof ParticleType type ? type.getRequirement() : ParticleRequirement.NO_REQUIRMENT, picked -> {
|
||||||
apply(context, parameter, picked);
|
apply(context, parameter, picked);
|
||||||
context.reopen();
|
context.reopen();
|
||||||
}, context::reopen);
|
}, context::reopen);
|
||||||
@@ -248,7 +249,7 @@ class EntryEditor {
|
|||||||
|
|
||||||
private Material choiceIcon(Object value) {
|
private Material choiceIcon(Object value) {
|
||||||
if (value instanceof ParticleType type) return type.getIcon();
|
if (value instanceof ParticleType type) return type.getIcon();
|
||||||
if (value instanceof WingDesign) return Material.PHANTOM_MEMBRANE;
|
if (value instanceof WingShape) return Material.PHANTOM_MEMBRANE;
|
||||||
if (value instanceof Material material) return material;
|
if (value instanceof Material material) return material;
|
||||||
return Material.LEVER;
|
return Material.LEVER;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ class ParticleGuiText {
|
|||||||
addCondition(lore, SNEAK, values.get(SNEAK), Sneak.ANY, player);
|
addCondition(lore, SNEAK, values.get(SNEAK), Sneak.ANY, player);
|
||||||
addCondition(lore, VISIBILITY, values.get(VISIBILITY), Visibility.EVERYONE, player);
|
addCondition(lore, VISIBILITY, values.get(VISIBILITY), Visibility.EVERYONE, player);
|
||||||
lore.add(msg("PARTICLE_ENTRY_COST", player, number(entry.cost())));
|
lore.add(msg("PARTICLE_ENTRY_COST", player, number(entry.cost())));
|
||||||
if (!entry.getDesign().getRequirement().test(context)) {
|
if (!entry.isUnlocked(context)) {
|
||||||
lore.add(msg("PARTICLE_ENTRY_LOCKED", player));
|
lore.add(msg("PARTICLE_ENTRY_LOCKED", player));
|
||||||
}
|
}
|
||||||
return lore;
|
return lore;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ package de.steamwar.lobby.particle.gui;
|
|||||||
import de.steamwar.inventory.SWInventory;
|
import de.steamwar.inventory.SWInventory;
|
||||||
import de.steamwar.inventory.SWItem;
|
import de.steamwar.inventory.SWItem;
|
||||||
import de.steamwar.inventory.SWListInv;
|
import de.steamwar.inventory.SWListInv;
|
||||||
|
import de.steamwar.lobby.particle.ParticleRequirement;
|
||||||
import de.steamwar.lobby.particle.UnlockContext;
|
import de.steamwar.lobby.particle.UnlockContext;
|
||||||
import de.steamwar.lobby.particle.design.*;
|
import de.steamwar.lobby.particle.design.*;
|
||||||
import de.steamwar.sql.SteamwarUser;
|
import de.steamwar.sql.SteamwarUser;
|
||||||
@@ -62,13 +63,18 @@ class Pickers {
|
|||||||
open(player, msg("PARTICLE_DESIGN_PICKER", player), list, onPick, onBack);
|
open(player, msg("PARTICLE_DESIGN_PICKER", player), list, onPick, onBack);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lists every option; the current one glows
|
// Lists every option; the current one glows, locked ones show their requirement and cannot be picked
|
||||||
<E> void choice(Player player, ChoiceParameter<E> parameter, E current, Function<E, Material> icon, Consumer<E> onPick, Runnable onBack) {
|
<E> void choice(Player player, ChoiceParameter<E> parameter, E current, Function<E, Material> icon, Function<E, ParticleRequirement> requirement,
|
||||||
|
Consumer<E> onPick, Runnable onBack) {
|
||||||
|
UnlockContext context = new UnlockContext(SteamwarUser.get(player.getUniqueId()));
|
||||||
List<SWListInv.SWListEntry<E>> list = new ArrayList<>();
|
List<SWListInv.SWListEntry<E>> list = new ArrayList<>();
|
||||||
for (E value : parameter.getBuildableValues()) {
|
for (E value : parameter.getBuildableValues()) {
|
||||||
List<String> lore = List.of(msg(value == current ? "PARTICLE_VALUE_SELECTED" : "PARTICLE_SELECT", player));
|
boolean unlocked = requirement.apply(value).test(context);
|
||||||
|
List<String> lore = new ArrayList<>(requirement(requirement.apply(value), player));
|
||||||
|
if (!lore.isEmpty()) lore.add("");
|
||||||
|
lore.add(msg(value == current ? "PARTICLE_VALUE_SELECTED" : unlocked ? "PARTICLE_SELECT" : "PARTICLE_LOCKED", player));
|
||||||
list.add(new SWListInv.SWListEntry<>(new SWItem(icon.apply(value), choiceName(parameter, value, player), lore, value == current, clickType -> {
|
list.add(new SWListInv.SWListEntry<>(new SWItem(icon.apply(value), choiceName(parameter, value, player), lore, value == current, clickType -> {
|
||||||
}), value));
|
}), unlocked ? value : null));
|
||||||
}
|
}
|
||||||
open(player, msg("PARTICLE_CHOICE_PICKER", player, name(parameter.getNameKey(), player)), list, onPick, onBack);
|
open(player, msg("PARTICLE_CHOICE_PICKER", player, name(parameter.getNameKey(), player)), list, onPick, onBack);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,13 +168,25 @@ class ValueEditors {
|
|||||||
return new Color(Integer.parseInt(hex, 16));
|
return new Color(Integer.parseInt(hex, 16));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The dye whose colour is closest to the given one, so typed hex colours get a matching icon
|
||||||
private Material dye(Color color) {
|
private Material dye(Color color) {
|
||||||
|
DyeColor closest = DyeColor.WHITE;
|
||||||
|
double best = Double.MAX_VALUE;
|
||||||
for (DyeColor dye : DyeColor.values()) {
|
for (DyeColor dye : DyeColor.values()) {
|
||||||
if ((dye.getColor().asRGB() & 0xFFFFFF) == (color.getRGB() & 0xFFFFFF)) {
|
double distance = distance(color, new Color(dye.getColor().asRGB()));
|
||||||
return Material.valueOf(dye.name() + "_DYE");
|
if (distance < best) {
|
||||||
|
best = distance;
|
||||||
|
closest = dye;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Material.WHITE_DYE;
|
return Material.valueOf(closest.name() + "_DYE");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Colour distance weighted by how sensitive the eye is to red, green and blue ("redmean")
|
||||||
|
private double distance(Color a, Color b) {
|
||||||
|
double redMean = (a.getRed() + b.getRed()) / 2.0;
|
||||||
|
int dr = a.getRed() - b.getRed(), dg = a.getGreen() - b.getGreen(), db = a.getBlue() - b.getBlue();
|
||||||
|
return (2 + redMean / 256) * dr * dr + 4 * dg * dg + (2 + (255 - redMean) / 256) * db * db;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String strip(String text) {
|
private String strip(String text) {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import com.google.gson.JsonObject;
|
|||||||
import de.steamwar.lobby.particle.Gradient;
|
import de.steamwar.lobby.particle.Gradient;
|
||||||
import de.steamwar.lobby.particle.ParticleElement;
|
import de.steamwar.lobby.particle.ParticleElement;
|
||||||
import de.steamwar.lobby.particle.ParticleTickData;
|
import de.steamwar.lobby.particle.ParticleTickData;
|
||||||
|
import de.steamwar.lobby.particle.UnlockContext;
|
||||||
import de.steamwar.lobby.particle.design.*;
|
import de.steamwar.lobby.particle.design.*;
|
||||||
import de.steamwar.lobby.particle.elements.*;
|
import de.steamwar.lobby.particle.elements.*;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@@ -106,7 +107,7 @@ public class ParticleEntry {
|
|||||||
float spreadY = values.get(SPREAD_Y).floatValue();
|
float spreadY = values.get(SPREAD_Y).floatValue();
|
||||||
float spreadZ = values.get(SPREAD_Z).floatValue();
|
float spreadZ = values.get(SPREAD_Z).floatValue();
|
||||||
double speed = values.get(SPEED);
|
double speed = values.get(SPEED);
|
||||||
int count = values.get(COUNT);
|
int count = amount();
|
||||||
|
|
||||||
List<Color> colors = values.get(COLORS);
|
List<Color> colors = values.get(COLORS);
|
||||||
Gradient gradient = colors.isEmpty() ? null : new Gradient(colors.toArray(new Color[0]));
|
Gradient gradient = colors.isEmpty() ? null : new Gradient(colors.toArray(new Color[0]));
|
||||||
@@ -162,9 +163,22 @@ public class ParticleEntry {
|
|||||||
return new LocationMutator(element, offsetX, offsetY, offsetZ);
|
return new LocationMutator(element, offsetX, offsetY, offsetZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shown only when both the design and, for designs that use it, the particle type are unlocked
|
||||||
|
public boolean isUnlocked(UnlockContext context) {
|
||||||
|
if (!design.getRequirement().test(context)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !design.usesEntryParticle() || values.get(PARTICLE).getRequirement().test(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Particles each point spawns; designs that set their own point count replace the amount with 1
|
||||||
|
private int amount() {
|
||||||
|
return design.replacesAmount(values) ? 1 : values.get(COUNT);
|
||||||
|
}
|
||||||
|
|
||||||
// Particle positions spawned per tick, used for the slot budget
|
// Particle positions spawned per tick, used for the slot budget
|
||||||
public double cost() {
|
public double cost() {
|
||||||
double perTick = design.usesEntryParticle() ? (double) design.points(values) * values.get(COUNT) : design.points(values);
|
double perTick = design.usesEntryParticle() ? (double) design.points(values) * amount() : design.points(values);
|
||||||
return perTick / values.get(INTERVAL);
|
return perTick / values.get(INTERVAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ public class Presets {
|
|||||||
|
|
||||||
private ParticleEntry wing(ParticleType type, double size, WingDesign image, int interval) {
|
private ParticleEntry wing(ParticleType type, double size, WingDesign image, int interval) {
|
||||||
return particle(Designs.WING, type, 0, 0, 1)
|
return particle(Designs.WING, type, 0, 0, 1)
|
||||||
.with(Designs.WING_IMAGE, image)
|
.with(Designs.WING_SHAPE, Designs.wingShape(image))
|
||||||
.with(Designs.WING_SIZE, size)
|
.with(Designs.WING_SIZE, size)
|
||||||
.with(TICK_MODE, TickMode.ALWAYS)
|
.with(TICK_MODE, TickMode.ALWAYS)
|
||||||
.with(INTERVAL, interval)
|
.with(INTERVAL, interval)
|
||||||
|
|||||||
Reference in New Issue
Block a user