forked from SteamWar/SteamWar
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.
166 lines
6.0 KiB
Java
166 lines
6.0 KiB
Java
/*
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
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;
|
|
|
|
// 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. It also notices jumps, landings and starting to sneak, which reward touches react to.
|
|
@UtilityClass
|
|
public class BodyMotion {
|
|
|
|
private final float MAX_HEAD_TURN = 50;
|
|
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;
|
|
private double x;
|
|
private double y;
|
|
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);
|
|
if (state == null) {
|
|
state = new State();
|
|
state.body = location.getYaw();
|
|
state.x = location.getX();
|
|
state.y = location.getY();
|
|
state.z = location.getZ();
|
|
states.put(player, state);
|
|
return;
|
|
}
|
|
|
|
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;
|
|
|
|
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;
|
|
// 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 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;
|
|
}
|
|
|
|
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;
|
|
if (wrapped < -180) wrapped += 360;
|
|
return wrapped;
|
|
}
|
|
}
|