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_CIRCLE = §fCircle
|
||||
PARTICLE_DESIGN_WING = §fWings
|
||||
PARTICLE_DESIGN_DRAWN_WING = §fDrawn wings
|
||||
PARTICLE_DESIGN_CLOUD = §fCloud
|
||||
|
||||
PARTICLE_PARAM_PARTICLE = §fParticle
|
||||
@@ -169,7 +168,6 @@ 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_PARAM_WING_COLORING = §fColour mode
|
||||
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_COMPLEX = §fComplex
|
||||
PARTICLE_CHOICE_IMAGE_SWORD = §fSword
|
||||
PARTICLE_CHOICE_SHAPE_BAT = §fBat
|
||||
PARTICLE_CHOICE_SHAPE_BUTTERFLY = §fButterfly
|
||||
PARTICLE_CHOICE_SHAPE_FAIRY = §fFairy
|
||||
PARTICLE_CHOICE_IMAGE_BAT = §fBat
|
||||
PARTICLE_CHOICE_IMAGE_BUTTERFLY = §fButterfly
|
||||
PARTICLE_CHOICE_IMAGE_FAIRY = §fFairy
|
||||
PARTICLE_CHOICE_COLORING_OVER_TIME = §fChange over time
|
||||
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_CIRCLE = §fKreis
|
||||
PARTICLE_DESIGN_WING = §fFlügel
|
||||
PARTICLE_DESIGN_DRAWN_WING = §fGezeichnete Flügel
|
||||
PARTICLE_DESIGN_CLOUD = §fWolke
|
||||
|
||||
PARTICLE_PARAM_PARTICLE = §fPartikel
|
||||
@@ -169,7 +168,6 @@ 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_PARAM_WING_COLORING = §fFarbmodus
|
||||
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_COMPLEX = §fKomplex
|
||||
PARTICLE_CHOICE_IMAGE_SWORD = §fSchwert
|
||||
PARTICLE_CHOICE_SHAPE_BAT = §fFledermaus
|
||||
PARTICLE_CHOICE_SHAPE_BUTTERFLY = §fSchmetterling
|
||||
PARTICLE_CHOICE_SHAPE_FAIRY = §fFee
|
||||
PARTICLE_CHOICE_IMAGE_BAT = §fFledermaus
|
||||
PARTICLE_CHOICE_IMAGE_BUTTERFLY = §fSchmetterling
|
||||
PARTICLE_CHOICE_IMAGE_FAIRY = §fFee
|
||||
PARTICLE_CHOICE_COLORING_OVER_TIME = §fWechseln mit der Zeit
|
||||
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;
|
||||
|
||||
// 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
|
||||
public class BodyFrame {
|
||||
|
||||
@@ -63,7 +63,7 @@ public class BodyFrame {
|
||||
Vector up = location.getDirection().normalize();
|
||||
Vector right = up.clone().crossProduct(WORLD_UP);
|
||||
if (right.lengthSquared() < 1e-6) {
|
||||
right = horizontal(player.getBodyYaw()).crossProduct(WORLD_UP);
|
||||
right = horizontal(BodyYaw.of(player)).crossProduct(WORLD_UP);
|
||||
}
|
||||
right.normalize();
|
||||
Vector forward = up.clone().crossProduct(right).normalize();
|
||||
@@ -72,7 +72,7 @@ public class BodyFrame {
|
||||
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();
|
||||
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;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
CompiledSet compiledSet = new CompiledSet();
|
||||
for (ParticleEntry entry : particleSet.getEntries()) {
|
||||
if (entry.getDesign().getRequirement().test(context)) {
|
||||
if (entry.isUnlocked(context)) {
|
||||
compiledSet.add(entry.build());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ public class ParticleListener implements Listener {
|
||||
movingPlayers.entrySet().removeIf(entry -> entry.getValue() <= 0);
|
||||
deg += 0.1;
|
||||
if (deg > 360) deg = 0;
|
||||
Bukkit.getOnlinePlayers().forEach(BodyYaw::tick);
|
||||
Bukkit.getOnlinePlayers().forEach(player -> {
|
||||
if (disabled(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(
|
||||
new ChoiceParameter.Choice<>("simple", WingDesign.SIMPLE, false),
|
||||
new ChoiceParameter.Choice<>("complex", WingDesign.COMPLEX, false),
|
||||
new ChoiceParameter.Choice<>("sword", WingDesign.SWORD, false),
|
||||
new ChoiceParameter.Choice<>("sw", WingDesign.SW, true),
|
||||
new ChoiceParameter.Choice<>("wgs", WingDesign.WGS, true),
|
||||
new ChoiceParameter.Choice<>("sword_crossed", WingDesign.SWORD_CROSSED, true),
|
||||
new ChoiceParameter.Choice<>("mwgl", WingDesign.MWGL, true),
|
||||
new ChoiceParameter.Choice<>("eclipse", WingDesign.ECLIPSE, true),
|
||||
new ChoiceParameter.Choice<>("pl", WingDesign.PL, true),
|
||||
new ChoiceParameter.Choice<>("plompa_easter", WingDesign.PlompaEasterWings, true),
|
||||
new ChoiceParameter.Choice<>("ely_e", WingDesign.ELY_E, true),
|
||||
new ChoiceParameter.Choice<>("ely_l", WingDesign.ELY_L, true),
|
||||
new ChoiceParameter.Choice<>("ely_y", WingDesign.ELY_Y, true),
|
||||
new ChoiceParameter.Choice<>("ev", WingDesign.EV, true)
|
||||
));
|
||||
// Images keep their old ids, so saved wings still load; preset and tournament images stay out of the builder
|
||||
private final List<ChoiceParameter.Choice<WingShape>> WING_SHAPES = List.of(
|
||||
new ChoiceParameter.Choice<>("simple", WingShape.image(WingDesign.SIMPLE), false),
|
||||
new ChoiceParameter.Choice<>("complex", WingShape.image(WingDesign.COMPLEX), false),
|
||||
new ChoiceParameter.Choice<>("sword", WingShape.image(WingDesign.SWORD), false),
|
||||
new ChoiceParameter.Choice<>("bat", WingShape.drawn(DrawnWings.BAT), false),
|
||||
new ChoiceParameter.Choice<>("butterfly", WingShape.drawn(DrawnWings.BUTTERFLY), false),
|
||||
new ChoiceParameter.Choice<>("fairy", WingShape.drawn(DrawnWings.FAIRY), false),
|
||||
new ChoiceParameter.Choice<>("sw", WingShape.image(WingDesign.SW), true),
|
||||
new ChoiceParameter.Choice<>("wgs", WingShape.image(WingDesign.WGS), true),
|
||||
new ChoiceParameter.Choice<>("sword_crossed", WingShape.image(WingDesign.SWORD_CROSSED), true),
|
||||
new ChoiceParameter.Choice<>("mwgl", WingShape.image(WingDesign.MWGL), true),
|
||||
new ChoiceParameter.Choice<>("eclipse", WingShape.image(WingDesign.ECLIPSE), true),
|
||||
new ChoiceParameter.Choice<>("pl", WingShape.image(WingDesign.PL), 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 enum WingColoring {
|
||||
@@ -95,18 +102,31 @@ public class Designs {
|
||||
// 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) {
|
||||
public final ParticleDesign WING = new ParticleDesign("wing", Material.ELYTRA, "PARTICLE_DESIGN_WING", WING_SHAPE, WING_DETAIL, WING_SIZE, WING_COLORING, WING_SPARKLE) {
|
||||
@Override
|
||||
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
|
||||
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
|
||||
public void applyDefaults(ParameterValues values) {
|
||||
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);
|
||||
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, 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);
|
||||
}
|
||||
};
|
||||
// The wing shape that shows the given image, for presets
|
||||
public WingShape wingShape(WingDesign image) {
|
||||
return WING_SHAPE.getChoices().stream().map(ChoiceParameter.Choice::value).filter(shape -> shape.shows(image)).findFirst().orElseThrow();
|
||||
}
|
||||
|
||||
public final ParticleDesign CLOUD = new ParticleDesign("cloud", Material.WHITE_WOOL, "PARTICLE_DESIGN_CLOUD") {
|
||||
@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() {
|
||||
if (registry == null) {
|
||||
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));
|
||||
InternalDesigns.all().forEach(design -> register(designs, design));
|
||||
registry = designs;
|
||||
|
||||
@@ -62,6 +62,16 @@ public abstract class ParticleDesign {
|
||||
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
|
||||
public void applyDefaults(ParameterValues values) {
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
package de.steamwar.lobby.particle.design;
|
||||
|
||||
import de.steamwar.lobby.particle.ParticleRequirement;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
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;
|
||||
}
|
||||
|
||||
// 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() {
|
||||
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;
|
||||
|
||||
import de.steamwar.lobby.particle.BodyFrame;
|
||||
import de.steamwar.lobby.particle.BodyYaw;
|
||||
import de.steamwar.lobby.particle.ParticleElement;
|
||||
import de.steamwar.lobby.particle.ParticleTickData;
|
||||
import de.steamwar.lobby.particle.WingDesign;
|
||||
@@ -68,7 +69,7 @@ public class Wing extends DelegatingParticleElement {
|
||||
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.rotateAroundY(Math.toRadians(BodyYaw.of(particleTickData.getPlayer()) * -1));
|
||||
vector.setX(-vector.getX());
|
||||
vector.setZ(-vector.getZ());
|
||||
Location location = particleTickData.getPlayer().getLocation().clone().add(vector);
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
package de.steamwar.lobby.particle.elements.custom;
|
||||
|
||||
import de.steamwar.lobby.particle.BodyYaw;
|
||||
import de.steamwar.lobby.particle.ParticleElement;
|
||||
import de.steamwar.lobby.particle.ParticleTickData;
|
||||
import de.steamwar.lobby.particle.elements.DelegatingParticleElement;
|
||||
@@ -48,7 +49,7 @@ public class HearthBeat extends DelegatingParticleElement {
|
||||
double x = (time - 0.5) * 2;
|
||||
|
||||
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.setZ(-vector.getZ());
|
||||
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.SWItem;
|
||||
import de.steamwar.lobby.LobbySystem;
|
||||
import de.steamwar.lobby.particle.ParticleRequirement;
|
||||
import de.steamwar.lobby.particle.UnlockContext;
|
||||
import de.steamwar.lobby.particle.WingDesign;
|
||||
import de.steamwar.lobby.particle.design.*;
|
||||
import de.steamwar.lobby.particle.slot.ParticleEntry;
|
||||
import de.steamwar.lobby.particle.slot.ParticleSlot;
|
||||
@@ -85,9 +85,8 @@ class EntryEditor {
|
||||
Map.entry(Designs.CIRCLE_PHASE, Material.TARGET),
|
||||
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_SHAPE, 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_COLORING, Material.CYAN_DYE),
|
||||
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)));
|
||||
}
|
||||
}
|
||||
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 {
|
||||
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_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) {
|
||||
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);
|
||||
context.reopen();
|
||||
}, context::reopen);
|
||||
@@ -248,7 +249,7 @@ class EntryEditor {
|
||||
|
||||
private Material choiceIcon(Object value) {
|
||||
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;
|
||||
return Material.LEVER;
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ class ParticleGuiText {
|
||||
addCondition(lore, SNEAK, values.get(SNEAK), Sneak.ANY, player);
|
||||
addCondition(lore, VISIBILITY, values.get(VISIBILITY), Visibility.EVERYONE, player);
|
||||
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));
|
||||
}
|
||||
return lore;
|
||||
|
||||
@@ -22,6 +22,7 @@ package de.steamwar.lobby.particle.gui;
|
||||
import de.steamwar.inventory.SWInventory;
|
||||
import de.steamwar.inventory.SWItem;
|
||||
import de.steamwar.inventory.SWListInv;
|
||||
import de.steamwar.lobby.particle.ParticleRequirement;
|
||||
import de.steamwar.lobby.particle.UnlockContext;
|
||||
import de.steamwar.lobby.particle.design.*;
|
||||
import de.steamwar.sql.SteamwarUser;
|
||||
@@ -62,13 +63,18 @@ class Pickers {
|
||||
open(player, msg("PARTICLE_DESIGN_PICKER", player), list, onPick, onBack);
|
||||
}
|
||||
|
||||
// Lists every option; the current one glows
|
||||
<E> void choice(Player player, ChoiceParameter<E> parameter, E current, Function<E, Material> icon, Consumer<E> onPick, Runnable onBack) {
|
||||
// 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, Function<E, ParticleRequirement> requirement,
|
||||
Consumer<E> onPick, Runnable onBack) {
|
||||
UnlockContext context = new UnlockContext(SteamwarUser.get(player.getUniqueId()));
|
||||
List<SWListInv.SWListEntry<E>> list = new ArrayList<>();
|
||||
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 -> {
|
||||
}), value));
|
||||
}), unlocked ? value : null));
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
// The dye whose colour is closest to the given one, so typed hex colours get a matching icon
|
||||
private Material dye(Color color) {
|
||||
DyeColor closest = DyeColor.WHITE;
|
||||
double best = Double.MAX_VALUE;
|
||||
for (DyeColor dye : DyeColor.values()) {
|
||||
if ((dye.getColor().asRGB() & 0xFFFFFF) == (color.getRGB() & 0xFFFFFF)) {
|
||||
return Material.valueOf(dye.name() + "_DYE");
|
||||
double distance = distance(color, new Color(dye.getColor().asRGB()));
|
||||
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) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import com.google.gson.JsonObject;
|
||||
import de.steamwar.lobby.particle.Gradient;
|
||||
import de.steamwar.lobby.particle.ParticleElement;
|
||||
import de.steamwar.lobby.particle.ParticleTickData;
|
||||
import de.steamwar.lobby.particle.UnlockContext;
|
||||
import de.steamwar.lobby.particle.design.*;
|
||||
import de.steamwar.lobby.particle.elements.*;
|
||||
import lombok.Getter;
|
||||
@@ -106,7 +107,7 @@ public class ParticleEntry {
|
||||
float spreadY = values.get(SPREAD_Y).floatValue();
|
||||
float spreadZ = values.get(SPREAD_Z).floatValue();
|
||||
double speed = values.get(SPEED);
|
||||
int count = values.get(COUNT);
|
||||
int count = amount();
|
||||
|
||||
List<Color> colors = values.get(COLORS);
|
||||
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);
|
||||
}
|
||||
|
||||
// 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
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ public class Presets {
|
||||
|
||||
private ParticleEntry wing(ParticleType type, double size, WingDesign image, int interval) {
|
||||
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(TICK_MODE, TickMode.ALWAYS)
|
||||
.with(INTERVAL, interval)
|
||||
|
||||
Reference in New Issue
Block a user