diff --git a/LobbySystem/src/de/steamwar/lobby/LobbySystem.properties b/LobbySystem/src/de/steamwar/lobby/LobbySystem.properties index cc61a9ff..8998c79f 100644 --- a/LobbySystem/src/de/steamwar/lobby/LobbySystem.properties +++ b/LobbySystem/src/de/steamwar/lobby/LobbySystem.properties @@ -171,6 +171,7 @@ PARTICLE_PARAM_WING_SIZE = §fWing size PARTICLE_PARAM_WING_DETAIL = §fPoints per wing PARTICLE_PARAM_WING_COLORING = §fColour mode PARTICLE_PARAM_WING_SPARKLE = §fSparkle (points shown, %) +PARTICLE_PARAM_WING_MOTION = §fMotion PARTICLE_CHOICE_TICKMODE_MOVE = §fWhile moving PARTICLE_CHOICE_TICKMODE_ALWAYS = §fAlways @@ -199,6 +200,10 @@ 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 +PARTICLE_CHOICE_MOTION_NONE = §fNone +PARTICLE_CHOICE_MOTION_FLAP_SLOW = §fFlap slowly +PARTICLE_CHOICE_MOTION_FLAP_FAST = §fFlap quickly +PARTICLE_CHOICE_MOTION_BREATHE = §fBreathe # Particle types PARTICLE_TYPE_DUST = §fDust diff --git a/LobbySystem/src/de/steamwar/lobby/LobbySystem_de.properties b/LobbySystem/src/de/steamwar/lobby/LobbySystem_de.properties index ac11769d..854c16d7 100644 --- a/LobbySystem/src/de/steamwar/lobby/LobbySystem_de.properties +++ b/LobbySystem/src/de/steamwar/lobby/LobbySystem_de.properties @@ -171,6 +171,7 @@ PARTICLE_PARAM_WING_SIZE = §fFlügelgröße PARTICLE_PARAM_WING_DETAIL = §fPunkte pro Flügel PARTICLE_PARAM_WING_COLORING = §fFarbmodus PARTICLE_PARAM_WING_SPARKLE = §fFunkeln (sichtbare Punkte, %) +PARTICLE_PARAM_WING_MOTION = §fBewegung PARTICLE_CHOICE_TICKMODE_MOVE = §fBeim Bewegen PARTICLE_CHOICE_TICKMODE_ALWAYS = §fImmer @@ -199,6 +200,10 @@ 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 +PARTICLE_CHOICE_MOTION_NONE = §fKeine +PARTICLE_CHOICE_MOTION_FLAP_SLOW = §fLangsam schlagen +PARTICLE_CHOICE_MOTION_FLAP_FAST = §fSchnell schlagen +PARTICLE_CHOICE_MOTION_BREATHE = §fAtmen # Particle types PARTICLE_TYPE_DUST = §fStaub diff --git a/LobbySystem/src/de/steamwar/lobby/particle/BodyFrame.java b/LobbySystem/src/de/steamwar/lobby/particle/BodyFrame.java index 05ee64d3..b8713aca 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 (as BodyYaw tracks it); gliding or swimming the body lies along the look direction. +// Standing it turns with the body (as BodyMotion 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(BodyYaw.of(player)).crossProduct(WORLD_UP); + right = horizontal(BodyMotion.yaw(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(BodyYaw.of(player)); + Vector forward = horizontal(BodyMotion.yaw(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/BodyMotion.java similarity index 74% rename from LobbySystem/src/de/steamwar/lobby/particle/BodyYaw.java rename to LobbySystem/src/de/steamwar/lobby/particle/BodyMotion.java index f757c4c5..75964059 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/BodyYaw.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/BodyMotion.java @@ -26,11 +26,12 @@ 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. +// How a player's body moves: the direction it 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, and how fast it moves. +// Walking turns the body towards the walking direction; standing still it only turns once the head looks more than +// 50 degrees to the side. @UtilityClass -public class BodyYaw { +public class BodyMotion { private final float MAX_HEAD_TURN = 50; private final double MOVING = 0.0025; @@ -40,7 +41,10 @@ public class BodyYaw { private static class State { private float body; private double x; + private double y; private double z; + // Blocks per tick, smoothed so single steps do not jerk the wings + private double speed; } // Called once per server tick for every online player @@ -51,6 +55,7 @@ public class BodyYaw { state = new State(); state.body = location.getYaw(); state.x = location.getX(); + state.y = location.getY(); state.z = location.getZ(); states.put(player, state); return; @@ -58,9 +63,12 @@ public class BodyYaw { float head = location.getYaw(); double dx = location.getX() - state.x; + double dy = location.getY() - state.y; double dz = location.getZ() - state.z; state.x = location.getX(); + state.y = location.getY(); state.z = location.getZ(); + state.speed = state.speed * 0.8 + Math.sqrt(dx * dx + dy * dy + dz * dz) * 0.2; float target = state.body; if (dx * dx + dz * dz > MOVING) { @@ -79,11 +87,17 @@ public class BodyYaw { } // The tracked body direction in degrees, or the head direction for players not tracked yet - public float of(Player player) { + public float yaw(Player player) { State state = states.get(player); return state == null ? player.getLocation().getYaw() : state.body; } + // Smoothed speed in blocks per tick; sprinting is about 0.28, fast elytra flight well above 1 + public double speed(Player player) { + State state = states.get(player); + return state == null ? 0 : state.speed; + } + private float wrap(float degrees) { float wrapped = degrees % 360; if (wrapped >= 180) wrapped -= 360; diff --git a/LobbySystem/src/de/steamwar/lobby/particle/ParticleListener.java b/LobbySystem/src/de/steamwar/lobby/particle/ParticleListener.java index e6aba7a6..43cbab0c 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/ParticleListener.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/ParticleListener.java @@ -60,7 +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(BodyMotion::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 2875ee8b..916ab16d 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/design/Designs.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/design/Designs.java @@ -101,13 +101,17 @@ public class Designs { public final ChoiceParameter WING_COLORING = ChoiceParameter.ofEnum("coloring", "PARTICLE_PARAM_WING_COLORING", WingColoring.OVER_TIME); // 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 ChoiceParameter WING_MOTION = ChoiceParameter.ofEnum("motion", "PARTICLE_PARAM_WING_MOTION", Wing.Motion.NONE); + // Moving wings need to be redrawn often to look smooth; still ones are fine once a second + private final int MOVING_WING_INTERVAL = 2; + private final int STILL_WING_INTERVAL = 20; - public final ParticleDesign WING = new ParticleDesign("wing", Material.ELYTRA, "PARTICLE_DESIGN_WING", WING_SHAPE, WING_DETAIL, 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, WING_MOTION) { @Override public ParticleElement build(ParameterValues values, ParticleElement particle) { 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)); + values.get(WING_COLORING) == WingColoring.ALONG_WING, values.get(WING_SPARKLE), values.get(WING_MOTION)); } @Override @@ -126,13 +130,24 @@ public class Designs { return values.get(WING_SHAPE).isDrawn(); } + @Override + public void adjust(Parameter changed, ParameterValues values) { + if (changed != WING_MOTION) return; + int interval = values.get(EntryParameters.INTERVAL); + if (values.get(WING_MOTION) != Wing.Motion.NONE && interval > MOVING_WING_INTERVAL) { + values.set(EntryParameters.INTERVAL, MOVING_WING_INTERVAL); + } else if (values.get(WING_MOTION) == Wing.Motion.NONE && interval == MOVING_WING_INTERVAL) { + values.set(EntryParameters.INTERVAL, STILL_WING_INTERVAL); + } + } + // 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) .set(EntryParameters.SPEED, 0.0) .set(EntryParameters.TICK_MODE, EntryParameters.TickMode.ALWAYS) - .set(EntryParameters.INTERVAL, 20) + .set(EntryParameters.INTERVAL, STILL_WING_INTERVAL) .set(EntryParameters.PLACEMENT, EntryParameters.Placement.BODY); } }; diff --git a/LobbySystem/src/de/steamwar/lobby/particle/design/ParameterValues.java b/LobbySystem/src/de/steamwar/lobby/particle/design/ParameterValues.java index 4f1512d4..dcc07cd7 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/design/ParameterValues.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/design/ParameterValues.java @@ -40,6 +40,12 @@ public class ParameterValues { return this; } + // Puts back values saved with copy() + public void restore(ParameterValues saved) { + values.clear(); + values.putAll(saved.values); + } + public ParameterValues copy() { ParameterValues copy = new ParameterValues(); copy.values.putAll(values); diff --git a/LobbySystem/src/de/steamwar/lobby/particle/design/ParticleDesign.java b/LobbySystem/src/de/steamwar/lobby/particle/design/ParticleDesign.java index 65f8143b..766d1f83 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/design/ParticleDesign.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/design/ParticleDesign.java @@ -72,6 +72,10 @@ public abstract class ParticleDesign { return false; } + // Called after a design setting changed, to adjust others that depend on it + public void adjust(Parameter changed, ParameterValues values) { + } + // 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/elements/Wing.java b/LobbySystem/src/de/steamwar/lobby/particle/elements/Wing.java index 009bc753..632df278 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/elements/Wing.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/elements/Wing.java @@ -20,7 +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.BodyMotion; import de.steamwar.lobby.particle.ParticleElement; import de.steamwar.lobby.particle.ParticleTickData; import de.steamwar.lobby.particle.WingDesign; @@ -29,12 +29,27 @@ import org.bukkit.util.Vector; public class Wing extends DelegatingParticleElement { + public enum Motion { + NONE, + FLAP_SLOW, + FLAP_FAST, + BREATHE, + } + + // Wings stand slightly open when idle and open further the faster the player moves + private static final double IDLE_OPEN_DEGREES = 5; + private static final double MOVING_OPEN_DEGREES = 15; + private static final double FULL_OPEN_SPEED = 0.6; + private static final double FLAP_DEGREES = 25; + private static final double BREATHE_SCALE = 0.08; + private double size; private WingDesign wingDesign; // Colour each point by its distance from the spine instead of by time private boolean colorAlongWing; // Share of the points shown each tick, in percent; below 100 the wing twinkles private int density = 100; + private Motion motion = Motion.NONE; private double widest; public Wing(ParticleElement particleElement, double size, WingDesign wingDesign) { @@ -43,10 +58,11 @@ public class Wing extends DelegatingParticleElement { this.wingDesign = wingDesign; } - public Wing(ParticleElement particleElement, double size, WingDesign wingDesign, boolean colorAlongWing, int density) { + public Wing(ParticleElement particleElement, double size, WingDesign wingDesign, boolean colorAlongWing, int density, Motion motion) { this(particleElement, size, wingDesign); this.colorAlongWing = colorAlongWing; this.density = density; + this.motion = motion; for (Vector vector : wingDesign.getVectors()) { widest = Math.max(widest, Math.abs(vector.getX())); } @@ -54,22 +70,29 @@ public class Wing extends DelegatingParticleElement { @Override public void tick(ParticleTickData particleTickData) { + double ticks = particleTickData.getDeg() * 10; + double open = Math.toRadians(openDegrees(particleTickData, ticks)); + double cos = Math.cos(open), sin = Math.sin(open); + double scale = size * (motion == Motion.BREATHE ? 1 + BREATHE_SCALE * Math.sin(2 * Math.PI * ticks / 40) : 1); + double sneak = particleTickData.getPlayer().isSneaking() ? 0.5 : 0; + BodyFrame frame = particleTickData.getFrame(); if (frame != null) { // Half a block behind the back, so the wings stay on the back even while gliding - double sneak = particleTickData.getPlayer().isSneaking() ? 0.5 : 0; Location base = particleTickData.getLocation(); for (Vector dVector : wingDesign.getVectors()) { if (hidden()) continue; - Vector local = frame.local(dVector.getX() * size, 0.6 + dVector.getY() * size - sneak, -0.5); + double out = dVector.getX() * scale; + Vector local = frame.local(out * cos, 0.6 + dVector.getY() * scale - sneak, -0.5 - Math.abs(out) * sin); particleElement.tick(shaded(particleTickData, dVector).withLocation(base.clone().add(local))); } return; } 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(BodyYaw.of(particleTickData.getPlayer()) * -1)); + double out = dVector.getX() * scale; + Vector vector = new Vector(out * cos, 0.6 + dVector.getY() * scale - sneak, 0.5 + Math.abs(out) * sin); + vector.rotateAroundY(Math.toRadians(BodyMotion.yaw(particleTickData.getPlayer()) * -1)); vector.setX(-vector.getX()); vector.setZ(-vector.getZ()); Location location = particleTickData.getPlayer().getLocation().clone().add(vector); @@ -78,6 +101,17 @@ public class Wing extends DelegatingParticleElement { } } + // How far each half is turned back around the spine; flapping swings it back and forth + private double openDegrees(ParticleTickData particleTickData, double ticks) { + double speed = Math.min(1, BodyMotion.speed(particleTickData.getPlayer()) / FULL_OPEN_SPEED); + double open = IDLE_OPEN_DEGREES + (MOVING_OPEN_DEGREES - IDLE_OPEN_DEGREES) * speed; + return switch (motion) { + case FLAP_SLOW -> open + FLAP_DEGREES * Math.sin(2 * Math.PI * ticks / 20); + case FLAP_FAST -> open + FLAP_DEGREES * Math.sin(2 * Math.PI * ticks / 10); + default -> open; + }; + } + // Full wings never draw a random number, so presets keep their exact look private boolean hidden() { return density < 100 && RANDOM.nextInt(100) >= density; 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 b8071b0d..a46b64f5 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/elements/custom/HearthBeat.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/elements/custom/HearthBeat.java @@ -19,7 +19,7 @@ package de.steamwar.lobby.particle.elements.custom; -import de.steamwar.lobby.particle.BodyYaw; +import de.steamwar.lobby.particle.BodyMotion; import de.steamwar.lobby.particle.ParticleElement; import de.steamwar.lobby.particle.ParticleTickData; import de.steamwar.lobby.particle.elements.DelegatingParticleElement; @@ -49,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(BodyYaw.of(particleTickData.getPlayer()) * -1)); + vector.rotateAroundY(Math.toRadians(BodyMotion.yaw(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 af539ed3..0e98a990 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/gui/EntryEditor.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/gui/EntryEditor.java @@ -89,7 +89,8 @@ class EntryEditor { Map.entry(Designs.WING_SIZE, Material.SCAFFOLDING), Map.entry(Designs.WING_DETAIL, Material.AMETHYST_SHARD), Map.entry(Designs.WING_COLORING, Material.CYAN_DYE), - Map.entry(Designs.WING_SPARKLE, Material.AMETHYST_CLUSTER) + Map.entry(Designs.WING_SPARKLE, Material.AMETHYST_CLUSTER), + Map.entry(Designs.WING_MOTION, Material.FEATHER) ); private record Context(Player player, SteamwarUser user, int index, int entryIndex, ParticleSlot slot, ParticleEntry entry) { @@ -261,9 +262,11 @@ class EntryEditor { return true; } double previousCost = context.slot().cost(); + ParameterValues saved = context.entry().getValues().copy(); context.entry().getValues().set(parameter, value); + context.entry().getDesign().adjust(parameter, context.entry().getValues()); if (!SlotEditor.allows(context.user(), context.slot(), previousCost)) { - context.entry().getValues().set(parameter, previous); + context.entry().getValues().restore(saved); LobbySystem.getMessage().send("PARTICLE_SLOT_OVER_BUDGET", context.player()); return false; }