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)