/* * 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; // 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 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; } }