From e830d087c9b8e12e13e0047a95e986ffc337d8bc Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Mon, 26 Oct 2026 13:39:28 +0100 Subject: [PATCH] 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. --- .../de/steamwar/lobby/LobbySystem.properties | 8 +- .../steamwar/lobby/LobbySystem_de.properties | 8 +- .../de/steamwar/lobby/particle/BodyFrame.java | 6 +- .../de/steamwar/lobby/particle/BodyYaw.java | 93 ++++++++++++++++++ .../steamwar/lobby/particle/CompiledSet.java | 4 +- .../lobby/particle/ParticleListener.java | 1 + .../lobby/particle/design/Designs.java | 96 +++++++++---------- .../lobby/particle/design/ParticleDesign.java | 10 ++ .../lobby/particle/design/ParticleType.java | 9 ++ .../lobby/particle/design/WingShape.java | 59 ++++++++++++ .../lobby/particle/elements/Wing.java | 3 +- .../particle/elements/custom/HearthBeat.java | 3 +- .../lobby/particle/gui/EntryEditor.java | 15 +-- .../lobby/particle/gui/ParticleGuiText.java | 2 +- .../steamwar/lobby/particle/gui/Pickers.java | 14 ++- .../lobby/particle/gui/ValueEditors.java | 18 +++- .../lobby/particle/slot/ParticleEntry.java | 18 +++- .../steamwar/lobby/particle/slot/Presets.java | 2 +- 18 files changed, 283 insertions(+), 86 deletions(-) create mode 100644 LobbySystem/src/de/steamwar/lobby/particle/BodyYaw.java create mode 100644 LobbySystem/src/de/steamwar/lobby/particle/design/WingShape.java diff --git a/LobbySystem/src/de/steamwar/lobby/LobbySystem.properties b/LobbySystem/src/de/steamwar/lobby/LobbySystem.properties index 15f50cb3..cc61a9ff 100644 --- a/LobbySystem/src/de/steamwar/lobby/LobbySystem.properties +++ b/LobbySystem/src/de/steamwar/lobby/LobbySystem.properties @@ -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 diff --git a/LobbySystem/src/de/steamwar/lobby/LobbySystem_de.properties b/LobbySystem/src/de/steamwar/lobby/LobbySystem_de.properties index 54bd05ec..ac11769d 100644 --- a/LobbySystem/src/de/steamwar/lobby/LobbySystem_de.properties +++ b/LobbySystem/src/de/steamwar/lobby/LobbySystem_de.properties @@ -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 diff --git a/LobbySystem/src/de/steamwar/lobby/particle/BodyFrame.java b/LobbySystem/src/de/steamwar/lobby/particle/BodyFrame.java index 68c5de0d..05ee64d3 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/BodyFrame.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/BodyFrame.java @@ -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); } diff --git a/LobbySystem/src/de/steamwar/lobby/particle/BodyYaw.java b/LobbySystem/src/de/steamwar/lobby/particle/BodyYaw.java new file mode 100644 index 00000000..f757c4c5 --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/BodyYaw.java @@ -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 . + */ + +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 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; + } +} diff --git a/LobbySystem/src/de/steamwar/lobby/particle/CompiledSet.java b/LobbySystem/src/de/steamwar/lobby/particle/CompiledSet.java index 5a668e2b..3a3b707b 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/CompiledSet.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/CompiledSet.java @@ -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()); } } diff --git a/LobbySystem/src/de/steamwar/lobby/particle/ParticleListener.java b/LobbySystem/src/de/steamwar/lobby/particle/ParticleListener.java index 9d569ebb..e6aba7a6 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/ParticleListener.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/ParticleListener.java @@ -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; diff --git a/LobbySystem/src/de/steamwar/lobby/particle/design/Designs.java b/LobbySystem/src/de/steamwar/lobby/particle/design/Designs.java index c23fe63f..2875ee8b 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/design/Designs.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/design/Designs.java @@ -66,22 +66,29 @@ public class Designs { } }; - public final ChoiceParameter 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> 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 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 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 registry() { if (registry == null) { Map 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; diff --git a/LobbySystem/src/de/steamwar/lobby/particle/design/ParticleDesign.java b/LobbySystem/src/de/steamwar/lobby/particle/design/ParticleDesign.java index ce59f23b..65f8143b 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/design/ParticleDesign.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/design/ParticleDesign.java @@ -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) { } diff --git a/LobbySystem/src/de/steamwar/lobby/particle/design/ParticleType.java b/LobbySystem/src/de/steamwar/lobby/particle/design/ParticleType.java index f80d76cf..4a4945e1 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/design/ParticleType.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/design/ParticleType.java @@ -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; } diff --git a/LobbySystem/src/de/steamwar/lobby/particle/design/WingShape.java b/LobbySystem/src/de/steamwar/lobby/particle/design/WingShape.java new file mode 100644 index 00000000..80970c42 --- /dev/null +++ b/LobbySystem/src/de/steamwar/lobby/particle/design/WingShape.java @@ -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 . + */ + +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; + } +} diff --git a/LobbySystem/src/de/steamwar/lobby/particle/elements/Wing.java b/LobbySystem/src/de/steamwar/lobby/particle/elements/Wing.java index f0778880..009bc753 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/elements/Wing.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/elements/Wing.java @@ -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); diff --git a/LobbySystem/src/de/steamwar/lobby/particle/elements/custom/HearthBeat.java b/LobbySystem/src/de/steamwar/lobby/particle/elements/custom/HearthBeat.java index b6ffd443..b8071b0d 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/elements/custom/HearthBeat.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/elements/custom/HearthBeat.java @@ -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); diff --git a/LobbySystem/src/de/steamwar/lobby/particle/gui/EntryEditor.java b/LobbySystem/src/de/steamwar/lobby/particle/gui/EntryEditor.java index b2d55a44..af539ed3 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/gui/EntryEditor.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/gui/EntryEditor.java @@ -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> 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 void pick(Context context, ChoiceParameter 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; } diff --git a/LobbySystem/src/de/steamwar/lobby/particle/gui/ParticleGuiText.java b/LobbySystem/src/de/steamwar/lobby/particle/gui/ParticleGuiText.java index ebe9f0de..f1365f77 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/gui/ParticleGuiText.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/gui/ParticleGuiText.java @@ -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; diff --git a/LobbySystem/src/de/steamwar/lobby/particle/gui/Pickers.java b/LobbySystem/src/de/steamwar/lobby/particle/gui/Pickers.java index 88ca01f0..e7f92456 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/gui/Pickers.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/gui/Pickers.java @@ -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 - void choice(Player player, ChoiceParameter parameter, E current, Function icon, Consumer onPick, Runnable onBack) { + // Lists every option; the current one glows, locked ones show their requirement and cannot be picked + void choice(Player player, ChoiceParameter parameter, E current, Function icon, Function requirement, + Consumer onPick, Runnable onBack) { + UnlockContext context = new UnlockContext(SteamwarUser.get(player.getUniqueId())); List> list = new ArrayList<>(); for (E value : parameter.getBuildableValues()) { - List lore = List.of(msg(value == current ? "PARTICLE_VALUE_SELECTED" : "PARTICLE_SELECT", player)); + boolean unlocked = requirement.apply(value).test(context); + List 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); } diff --git a/LobbySystem/src/de/steamwar/lobby/particle/gui/ValueEditors.java b/LobbySystem/src/de/steamwar/lobby/particle/gui/ValueEditors.java index 156dff9d..e1d60ed3 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/gui/ValueEditors.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/gui/ValueEditors.java @@ -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) { diff --git a/LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleEntry.java b/LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleEntry.java index fd240eae..fcb79a11 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleEntry.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/slot/ParticleEntry.java @@ -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 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); } diff --git a/LobbySystem/src/de/steamwar/lobby/particle/slot/Presets.java b/LobbySystem/src/de/steamwar/lobby/particle/slot/Presets.java index 413b9103..930bb3a5 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/slot/Presets.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/slot/Presets.java @@ -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)