forked from SteamWar/SteamWar
Open wings with speed and let them flap or breathe
All wings stand 5 degrees open when idle and open up to 15 degrees the faster the player moves. The wings design gets a motion setting to flap slowly, flap quickly or breathe; picking a motion redraws the wing every 2 ticks so it looks smooth, and going back to none restores every 20. Body rotation tracking now also tracks speed, as BodyMotion.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+19
-5
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -101,13 +101,17 @@ public class Designs {
|
||||
public final ChoiceParameter<WingColoring> 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> 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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user