Give the rebuildable reward designs unique touches

Rewards whose look the basic designs can build now carry a touch the builder
cannot make: bursts on jumping, landing or sneaking, footprints, a line to
nearby players with the same reward, a December extra, aimed enchanting
glyphs, a heartbeat, a jump-beat wing or an orbiting shape. BodyMotion tracks
the jump, land and sneak events the touches react to.
This commit is contained in:
2026-10-26 14:34:50 +01:00
parent ad9087c038
commit 7d0f5aceae
15 changed files with 521 additions and 22 deletions
@@ -29,7 +29,7 @@ import java.util.WeakHashMap;
// 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.
// 50 degrees to the side. It also notices jumps, landings and starting to sneak, which reward touches react to.
@UtilityClass
public class BodyMotion {
@@ -37,6 +37,8 @@ public class BodyMotion {
private final double MOVING = 0.0025;
private final Map<Player, State> states = new WeakHashMap<>();
// Counts server ticks, so events can be matched to the tick they happened in
private long tick;
private static class State {
private float body;
@@ -45,9 +47,23 @@ public class BodyMotion {
private double z;
// Blocks per tick, smoothed so single steps do not jerk the wings
private double speed;
private boolean onGround = true;
private boolean sneaking;
// Highest point of the current time in the air
private double peakY;
private long jumpTick = Long.MIN_VALUE;
private long landTick = Long.MIN_VALUE;
private double landFall;
private long sneakTick = Long.MIN_VALUE;
}
// Called once per server tick before the players are updated
public void nextTick() {
tick++;
}
// Called once per server tick for every online player
@SuppressWarnings("deprecation")
public void tick(Player player) {
Location location = player.getLocation();
State state = states.get(player);
@@ -70,6 +86,22 @@ public class BodyMotion {
state.z = location.getZ();
state.speed = state.speed * 0.8 + Math.sqrt(dx * dx + dy * dy + dz * dz) * 0.2;
boolean onGround = player.isOnGround();
if (state.onGround && !onGround && dy > 0) {
state.jumpTick = tick;
}
if (!onGround) {
state.peakY = state.onGround ? location.getY() : Math.max(state.peakY, location.getY());
} else if (!state.onGround) {
state.landTick = tick;
state.landFall = state.peakY - location.getY();
}
state.onGround = onGround;
if (player.isSneaking() && !state.sneaking) {
state.sneakTick = tick;
}
state.sneaking = player.isSneaking();
float target = state.body;
if (dx * dx + dz * dz > MOVING) {
target = (float) Math.toDegrees(Math.atan2(dz, dx)) - 90;
@@ -98,6 +130,32 @@ public class BodyMotion {
return state == null ? 0 : state.speed;
}
public long currentTick() {
return tick;
}
public boolean jumped(Player player) {
State state = states.get(player);
return state != null && state.jumpTick == tick;
}
// Ticks since the last jump, or a large number if there was none
public long ticksSinceJump(Player player) {
State state = states.get(player);
return state == null || state.jumpTick == Long.MIN_VALUE ? Long.MAX_VALUE : tick - state.jumpTick;
}
// How far the player fell if they landed this tick, otherwise -1
public double landed(Player player) {
State state = states.get(player);
return state != null && state.landTick == tick ? state.landFall : -1;
}
public boolean startedSneaking(Player player) {
State state = states.get(player);
return state != null && state.sneakTick == tick;
}
private float wrap(float degrees) {
float wrapped = degrees % 360;
if (wrapped >= 180) wrapped -= 360;