diff --git a/core/src/main/java/nl/andrewl/aos_core/model/Player.java b/core/src/main/java/nl/andrewl/aos_core/model/Player.java index 23bbe99..f5cb440 100644 --- a/core/src/main/java/nl/andrewl/aos_core/model/Player.java +++ b/core/src/main/java/nl/andrewl/aos_core/model/Player.java @@ -2,6 +2,7 @@ package nl.andrewl.aos_core.model; import nl.andrewl.aos_core.Directions; import nl.andrewl.aos_core.MathUtils; +import nl.andrewl.aos_core.model.world.World; import org.joml.*; import org.joml.Math; @@ -194,6 +195,36 @@ public class Player { .translate(-0.35f, getEyeHeight() - 0.4f, 0.35f); } + /** + * Gets the list of all spaces occupied by a player's position, in the + * horizontal XZ plane. This can be between 1 and 4 spaces, depending on + * if the player's position is overlapping with a few blocks. + * @param pos The position. + * @return The list of 2d positions occupied. + */ + private List getHorizontalSpaceOccupied(Vector3f pos) { + // Get the list of 2d x,z coordinates that we overlap with. + List points = new ArrayList<>(4); // Due to the size of radius, there can only be a max of 4 blocks. + int minX = (int) Math.floor(pos.x - RADIUS); + int minZ = (int) Math.floor(pos.z - RADIUS); + int maxX = (int) Math.floor(pos.x + RADIUS); + int maxZ = (int) Math.floor(pos.z + RADIUS); + for (int x = minX; x <= maxX; x++) { + for (int z = minZ; z <= maxZ; z++) { + points.add(new Vector2i(x, z)); + } + } + return points; + } + + public boolean isGrounded(World world) { + // Player must be flat on the top of a block. + if (Math.floor(position.y) != position.y) return false; + // Check to see if there's a block under any of the spaces the player is over. + return getHorizontalSpaceOccupied(position).stream() + .anyMatch(point -> world.getBlockAt(point.x, position.y - 0.1f, point.y) != 0); + } + public List getBlockSpaceOccupied() { float playerBodyMinZ = position.z - RADIUS; float playerBodyMaxZ = position.z + RADIUS; diff --git a/server/src/main/java/nl/andrewl/aos2_server/PlayerManager.java b/server/src/main/java/nl/andrewl/aos2_server/PlayerManager.java index 662df22..f36b7b9 100644 --- a/server/src/main/java/nl/andrewl/aos2_server/PlayerManager.java +++ b/server/src/main/java/nl/andrewl/aos2_server/PlayerManager.java @@ -47,7 +47,7 @@ public class PlayerManager { joinMessage = username + " joined the game."; } player.setPosition(getBestSpawnPoint(player)); - player.setMode(PlayerMode.NORMAL); + player.setMode(PlayerMode.CREATIVE); // Tell all other players that this one has joined. broadcastTcpMessageToAllBut(new PlayerJoinMessage( player.getId(), player.getUsername(), player.getTeam() == null ? -1 : player.getTeam().getId(), diff --git a/server/src/main/java/nl/andrewl/aos2_server/logic/CreativeMovementController.java b/server/src/main/java/nl/andrewl/aos2_server/logic/CreativeMovementController.java new file mode 100644 index 0000000..b9d0254 --- /dev/null +++ b/server/src/main/java/nl/andrewl/aos2_server/logic/CreativeMovementController.java @@ -0,0 +1,58 @@ +package nl.andrewl.aos2_server.logic; + +import nl.andrewl.aos2_server.Server; +import nl.andrewl.aos2_server.config.ServerConfig; +import nl.andrewl.aos2_server.model.ServerPlayer; +import nl.andrewl.aos_core.model.world.World; +import org.joml.Vector3f; + +public class CreativeMovementController implements PlayerMovementController { + @Override + public boolean tickMovement(float dt, ServerPlayer player, PlayerInputTracker input, Server server, World world, ServerConfig.PhysicsConfig config) { + boolean updated = tickVelocity(player, input, config); + if (player.getVelocity().lengthSquared() > 0) { + Vector3f movement = new Vector3f(player.getVelocity()).mul(dt); + player.getPosition().add(movement); + updated = true; + } + return updated; + } + + private boolean tickVelocity(ServerPlayer player, PlayerInputTracker input, ServerConfig.PhysicsConfig config) { + boolean updated = false; + var velocity = player.getVelocity(); + var orientation = player.getOrientation(); + Vector3f acceleration = new Vector3f(0); + if (input.forward()) acceleration.z -= 1; + if (input.backward()) acceleration.z += 1; + if (input.left()) acceleration.x -= 1; + if (input.right()) acceleration.x += 1; + if (input.jumping()) acceleration.y += 1; + if (input.crouching()) acceleration.y -= 1; + if (acceleration.lengthSquared() > 0) { + acceleration.normalize() + .rotateY(orientation.x) + .mul(config.movementAcceleration); + velocity.add(acceleration); + + float maxSpeed = config.walkingSpeed; + if (input.sprinting()) { + maxSpeed = config.sprintingSpeed; + } + maxSpeed *= 4; + if (velocity.length() > maxSpeed) { + velocity.normalize(maxSpeed); + } + updated = true; + } else if (velocity.lengthSquared() > 0) { + float decel = Math.min(velocity.length(), config.movementDeceleration); + Vector3f deceleration = new Vector3f(velocity).negate().normalize().mul(decel); + velocity.add(deceleration); + if (Math.abs(velocity.x) < 0.1f) velocity.x = 0; + if (Math.abs(velocity.y) < 0.1f) velocity.y = 0; + if (Math.abs(velocity.z) < 0.1f) velocity.z = 0; + updated = true; + } + return updated; + } +} diff --git a/server/src/main/java/nl/andrewl/aos2_server/logic/NormalMovementController.java b/server/src/main/java/nl/andrewl/aos2_server/logic/NormalMovementController.java new file mode 100644 index 0000000..74a9dd9 --- /dev/null +++ b/server/src/main/java/nl/andrewl/aos2_server/logic/NormalMovementController.java @@ -0,0 +1,257 @@ +package nl.andrewl.aos2_server.logic; + +import nl.andrewl.aos2_server.Server; +import nl.andrewl.aos2_server.config.ServerConfig; +import nl.andrewl.aos2_server.model.ServerPlayer; +import nl.andrewl.aos_core.model.item.GunItemStack; +import nl.andrewl.aos_core.model.world.World; +import nl.andrewl.aos_core.net.client.ClientHealthMessage; +import nl.andrewl.aos_core.net.client.SoundMessage; +import org.joml.Math; +import org.joml.Vector3f; + +import java.util.concurrent.ThreadLocalRandom; + +import static nl.andrewl.aos_core.model.Player.RADIUS; + +public class NormalMovementController implements PlayerMovementController { + @Override + public boolean tickMovement(float dt, ServerPlayer player, PlayerInputTracker input, Server server, World world, ServerConfig.PhysicsConfig config) { + boolean updated; + var velocity = player.getVelocity(); + var position = player.getPosition(); + boolean grounded = player.isGrounded(world); + updated = tickHorizontalVelocity(player, input, config, grounded); + + if (grounded) { + if (input.jumping()) { + velocity.y = config.jumpVerticalSpeed * (input.sprinting() ? 1.25f : 1f); + updated = true; + } + } else { + velocity.y -= config.gravity * dt * 2; // Apply double-gravity to players to make the game feel faster. + updated = true; + } + + // Apply updated velocity to the player. + if (velocity.lengthSquared() > 0) { + Vector3f movement = new Vector3f(velocity).mul(dt); + // Check for collisions if we try to move according to what the player wants. + checkBlockCollisions(player, movement, server, world); + position.add(movement); + updated = true; + } + + // Finally, check to see if the player is outside the world, and kill them if so. + if ( + player.getPosition().x < world.getMinX() - 5 || player.getPosition().x > world.getMaxX() + 6 || + player.getPosition().z < world.getMinZ() - 5 || player.getPosition().z > world.getMaxZ() + 6 || + player.getPosition().y < world.getMinY() - 50 || player.getPosition().y > world.getMaxY() + 500 + ) { + server.getPlayerManager().playerKilled(player, null); + } + + return updated; + } + + private boolean tickHorizontalVelocity(ServerPlayer player, PlayerInputTracker input, ServerConfig.PhysicsConfig config, boolean grounded) { + boolean updated = false; + var velocity = player.getVelocity(); + var orientation = player.getOrientation(); + Vector3f horizontalVelocity = new Vector3f( + velocity.x == velocity.x ? velocity.x : 0f, + 0, + velocity.z == velocity.z ? velocity.z : 0f + ); + Vector3f acceleration = new Vector3f(0); + if (input.forward()) acceleration.z -= 1; + if (input.backward()) acceleration.z += 1; + if (input.left()) acceleration.x -= 1; + if (input.right()) acceleration.x += 1; + if (acceleration.lengthSquared() > 0) { + acceleration.normalize(); + acceleration.rotateAxis(orientation.x, 0, 1, 0); + float accelerationMagnitude = config.movementAcceleration; + if (!grounded) accelerationMagnitude *= 0.25f; + acceleration.mul(accelerationMagnitude); + horizontalVelocity.add(acceleration); + float maxSpeed; + if (input.crouching()) { + maxSpeed = config.crouchingSpeed; + } else if (input.sprinting()) { + maxSpeed = config.sprintingSpeed; + } else { + maxSpeed = config.walkingSpeed; + } + // If scoping, then force limit to crouching speed. + if (input.interacting() && player.getInventory().getSelectedItemStack() instanceof GunItemStack) { + maxSpeed = config.crouchingSpeed; + } + if (horizontalVelocity.length() > maxSpeed) { + horizontalVelocity.normalize(maxSpeed); + } + updated = true; + } else if (horizontalVelocity.lengthSquared() > 0) { + float baseDecel = config.movementDeceleration; + if (!grounded) baseDecel *= 0.25f; + float decelerationMagnitude = Math.min(horizontalVelocity.length(), baseDecel); + Vector3f deceleration = new Vector3f(horizontalVelocity).negate().normalize().mul(decelerationMagnitude); + horizontalVelocity.add(deceleration); + if (horizontalVelocity.length() < 0.1f) { + horizontalVelocity.set(0); + } + updated = true; + } + + // Update the player's velocity with what we've computed. + velocity.x = horizontalVelocity.x; + velocity.z = horizontalVelocity.z; + return updated; + } + + private void checkBlockCollisions(ServerPlayer player, Vector3f movement, Server server, World world) { + var position = player.getPosition(); + var velocity = player.getVelocity(); + final Vector3f nextTickPosition = new Vector3f(position).add(movement); + float height = player.getCurrentHeight(); + float delta = 0.00001f; + final Vector3f stepSize = new Vector3f(movement).normalize(1.0f); + // The number of steps we'll make towards the next tick position. + int stepCount = (int) Math.ceil(movement.length()); + if (stepCount == 0) return; // No movement, so exit. + final Vector3f nextPos = new Vector3f(position); + final Vector3f lastPos = new Vector3f(position); + for (int i = 0; i < stepCount; i++) { + lastPos.set(nextPos); + nextPos.add(stepSize); + // If we shot past the next tick position, clamp it to that. + if (new Vector3f(nextPos).sub(position).length() > movement.length()) { + nextPos.set(nextTickPosition); + } + + // Check if we collide with anything at this new position. + + float playerBodyPrevMinZ = lastPos.z - RADIUS; + float playerBodyPrevMaxZ = lastPos.z + RADIUS; + float playerBodyPrevMinX = lastPos.x - RADIUS; + float playerBodyPrevMaxX = lastPos.x + RADIUS; + float playerBodyPrevMinY = lastPos.y; + float playerBodyPrevMaxY = lastPos.y + height; + + float playerBodyMinZ = nextPos.z - RADIUS; + float playerBodyMaxZ = nextPos.z + RADIUS; + float playerBodyMinX = nextPos.x - RADIUS; + float playerBodyMaxX = nextPos.x + RADIUS; + float playerBodyMinY = nextPos.y; + float playerBodyMaxY = nextPos.y + height; + + // Compute the bounds of all blocks the player is intersecting with. + int minX = (int) Math.floor(playerBodyMinX); + int minZ = (int) Math.floor(playerBodyMinZ); + int minY = (int) Math.floor(playerBodyMinY); + int maxX = (int) Math.floor(playerBodyMaxX); + int maxZ = (int) Math.floor(playerBodyMaxZ); + int maxY = (int) Math.floor(playerBodyMaxY); + + for (int x = minX; x <= maxX; x++) { + for (int z = minZ; z <= maxZ; z++) { + for (int y = minY; y <= maxY; y++) { + byte block = world.getBlockAt(x, y, z); + if (block <= 0) continue; // We're not colliding with this block. + float blockMinY = (float) y; + float blockMaxY = (float) y + 1; + float blockMinX = (float) x; + float blockMaxX = (float) x + 1; + float blockMinZ = (float) z; + float blockMaxZ = (float) z + 1; + + /* + To determine if the player is moving into the -Z side of a block: + - The player's max z position went from < blockMinZ to >= blockMinZ. + - The block to the -Z direction is air. + */ + boolean collidingWithNegativeZ = playerBodyPrevMaxZ < blockMinZ && playerBodyMaxZ >= blockMinZ && world.getBlockAt(x, y, z - 1) <= 0; + if (collidingWithNegativeZ) { + position.z = blockMinZ - RADIUS - delta; + velocity.z = 0; + movement.z = 0; + } + + /* + To determine if the player is moving into the +Z side of a block: + - The player's min z position went from >= blockMaxZ to < blockMaxZ. + - The block to the +Z direction is air. + */ + boolean collidingWithPositiveZ = playerBodyPrevMinZ >= blockMaxZ && playerBodyMinZ < blockMaxZ && world.getBlockAt(x, y, z + 1) <= 0; + if (collidingWithPositiveZ) { + position.z = blockMaxZ + RADIUS + delta; + velocity.z = 0; + movement.z = 0; + } + + /* + To determine if the player is moving into the -X side of a block: + - The player's max x position went from < blockMinX to >= blockMinX + - The block to the -X direction is air. + */ + boolean collidingWithNegativeX = playerBodyPrevMaxX < blockMinX && playerBodyMaxX >= blockMinX && world.getBlockAt(x - 1, y, z) <= 0; + if (collidingWithNegativeX) { + position.x = blockMinX - RADIUS - delta; + velocity.x = 0; + movement.x = 0; + } + + /* + To determine if the player is moving into the +X side of a block: + - The player's min x position went from >= blockMaxX to < blockMaxX. + - The block to the +X direction is air. + */ + boolean collidingWithPositiveX = playerBodyPrevMinX >= blockMaxX && playerBodyMinX < blockMaxX && world.getBlockAt(x + 1, y, z) <= 0; + if (collidingWithPositiveX) { + position.x = blockMaxX + RADIUS + delta; + velocity.x = 0; + movement.x = 0; + } + + /* + To determine if the player is moving down onto a block: + - The player's min y position went from >= blockMaxY to < blockMaxY + - The block above the current one is air. + */ + boolean collidingWithFloor = playerBodyPrevMinY >= blockMaxY && playerBodyMinY < blockMaxY && world.getBlockAt(x, y + 1, z) <= 0; + if (collidingWithFloor) { + // This is a special case! We need to check for fall damage. + if (velocity.y < -20) { + float damage = velocity.y / 50f; + player.setHealth(player.getHealth() + damage); + if (player.getHealth() <= 0) { + server.getPlayerManager().playerKilled(player, player); + } else { + var handler = server.getPlayerManager().getHandler(player.getId()); + handler.sendDatagramPacket(new ClientHealthMessage(player.getHealth())); + int soundVariant = ThreadLocalRandom.current().nextInt(1, 4); + handler.sendDatagramPacket(new SoundMessage("hurt_" + soundVariant, 1, player.getPosition())); + } + } + position.y = blockMaxY; + velocity.y = 0; + movement.y = 0; + } + + /* + To determine if the player is moving up into a block: + - The player's y position went from below blockMinY to >= blockMinY + - The block below the current one is air. + */ + boolean collidingWithCeiling = playerBodyPrevMaxY < blockMinY && playerBodyMaxY >= blockMinY && world.getBlockAt(x, y - 1, z) <= 0; + if (collidingWithCeiling) { + position.y = blockMinY - height - delta; + velocity.y = 0; + movement.y = 0; + } + } + } + } + } + } +} diff --git a/server/src/main/java/nl/andrewl/aos2_server/logic/PlayerActionManager.java b/server/src/main/java/nl/andrewl/aos2_server/logic/PlayerActionManager.java index e683f3a..764818e 100644 --- a/server/src/main/java/nl/andrewl/aos2_server/logic/PlayerActionManager.java +++ b/server/src/main/java/nl/andrewl/aos2_server/logic/PlayerActionManager.java @@ -1,8 +1,8 @@ package nl.andrewl.aos2_server.logic; import nl.andrewl.aos2_server.Server; -import nl.andrewl.aos2_server.config.ServerConfig; import nl.andrewl.aos2_server.model.ServerPlayer; +import nl.andrewl.aos_core.model.PlayerMode; import nl.andrewl.aos_core.model.item.BlockItemStack; import nl.andrewl.aos_core.model.item.Gun; import nl.andrewl.aos_core.model.item.GunItemStack; @@ -14,22 +14,19 @@ import nl.andrewl.aos_core.model.world.World; import nl.andrewl.aos_core.net.client.*; import nl.andrewl.aos_core.net.world.ChunkUpdateMessage; import org.joml.Math; -import org.joml.Vector2i; import org.joml.Vector3f; import org.joml.Vector3i; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.ThreadLocalRandom; -import static nl.andrewl.aos2_server.model.ServerPlayer.RADIUS; - /** * Component that manages a server player's current actions and movement. */ public class PlayerActionManager { private final ServerPlayer player; private final PlayerInputTracker input; + private final PlayerMovementController normalMovementController = new NormalMovementController(); + private final CreativeMovementController creativeMovementController = new CreativeMovementController(); private long lastBlockRemovedAt = 0; private long lastBlockPlacedAt = 0; @@ -91,7 +88,7 @@ public class PlayerActionManager { lastResupplyAt = now; } - if (server.getConfig().actions.healthRegenPerSecond != 0 && player.getHealth() < 1) { + if (player.getMode() == PlayerMode.NORMAL && server.getConfig().actions.healthRegenPerSecond != 0 && player.getHealth() < 1) { player.setHealth(player.getHealth() + server.getConfig().actions.healthRegenPerSecond * dt); server.getPlayerManager().getHandler(player).sendDatagramPacket(new ClientHealthMessage(player.getHealth())); } @@ -101,7 +98,11 @@ public class PlayerActionManager { updated = true; } - tickMovement(dt, server, world, server.getConfig().physics); + updated = switch (player.getMode()) { + case NORMAL -> normalMovementController.tickMovement(dt, player, input, server, world, server.getConfig().physics); + case CREATIVE -> creativeMovementController.tickMovement(dt, player, input, server, world, server.getConfig().physics); + case SPECTATOR -> false; + } || updated; input.reset(); // Reset our input state after processing this tick's player input. } @@ -141,12 +142,16 @@ public class PlayerActionManager { stack.getAmount() < stack.getType().getMaxAmount() && now - lastBlockRemovedAt > server.getConfig().actions.blockBreakCooldown * 1000 ) { - var hit = world.getLookingAtPos(player.getEyePosition(), player.getViewVector(), server.getConfig().actions.blockBreakReach); + float reach = server.getConfig().actions.blockBreakReach; + if (player.getMode() == PlayerMode.CREATIVE) reach *= 10; + var hit = world.getLookingAtPos(player.getEyePosition(), player.getViewVector(), reach); if (hit != null && !server.getTeamManager().isProtected(hit.pos())) { world.setBlockAt(hit.pos().x, hit.pos().y, hit.pos().z, (byte) 0); lastBlockRemovedAt = now; - stack.incrementAmount(); - server.getPlayerManager().getHandler(player.getId()).sendDatagramPacket(new ItemStackMessage(player.getInventory())); + if (player.getMode() == PlayerMode.NORMAL) { + stack.incrementAmount(); + server.getPlayerManager().getHandler(player.getId()).sendDatagramPacket(new ItemStackMessage(player.getInventory())); + } server.getPlayerManager().broadcastUdpMessage(ChunkUpdateMessage.fromWorld(hit.pos(), world)); server.getPlayerManager().broadcastUdpMessage(new SoundMessage("block_break_1", 1, player.getPosition())); } @@ -157,7 +162,9 @@ public class PlayerActionManager { stack.getAmount() > 0 && now - lastBlockPlacedAt > server.getConfig().actions.blockPlaceCooldown * 1000 ) { - var hit = world.getLookingAtPos(player.getEyePosition(), player.getViewVector(), server.getConfig().actions.blockPlaceReach); + float reach = server.getConfig().actions.blockPlaceReach; + if (player.getMode() == PlayerMode.CREATIVE) reach *= 10; + var hit = world.getLookingAtPos(player.getEyePosition(), player.getViewVector(), reach); if (hit != null && !server.getTeamManager().isProtected(hit.pos())) { Vector3i placePos = new Vector3i(hit.pos()); placePos.add(hit.norm()); @@ -167,8 +174,10 @@ public class PlayerActionManager { if (canPlace) { // Ensure that we can't place blocks in space we're occupying. world.setBlockAt(placePos.x, placePos.y, placePos.z, stack.getSelectedValue()); lastBlockPlacedAt = now; - stack.decrementAmount(); - server.getPlayerManager().getHandler(player.getId()).sendDatagramPacket(new ItemStackMessage(player.getInventory())); + if (player.getMode() == PlayerMode.NORMAL) { + stack.decrementAmount(); + server.getPlayerManager().getHandler(player.getId()).sendDatagramPacket(new ItemStackMessage(player.getInventory())); + } server.getPlayerManager().broadcastUdpMessage(ChunkUpdateMessage.fromWorld(placePos, world)); server.getPlayerManager().broadcastUdpMessage(new SoundMessage("block_place_1", 1, player.getPosition())); } @@ -176,271 +185,6 @@ public class PlayerActionManager { } } - private void tickMovement(float dt, Server server, World world, ServerConfig.PhysicsConfig config) { - var velocity = player.getVelocity(); - var position = player.getPosition(); - boolean grounded = isGrounded(world); - tickHorizontalVelocity(config, grounded); - - if (grounded) { - if (input.jumping()) { - velocity.y = config.jumpVerticalSpeed * (input.sprinting() ? 1.25f : 1f); - updated = true; - } - } else { - velocity.y -= config.gravity * dt * 2; // Apply double-gravity to players to make the game feel faster. - updated = true; - } - - // Apply updated velocity to the player. - if (velocity.lengthSquared() > 0) { - Vector3f movement = new Vector3f(velocity).mul(dt); - // Check for collisions if we try to move according to what the player wants. - checkBlockCollisions(movement, server, world); - position.add(movement); - updated = true; - } - - // Finally, check to see if the player is outside the world, and kill them if so. - if ( - player.getPosition().x < world.getMinX() - 5 || player.getPosition().x > world.getMaxX() + 6 || - player.getPosition().z < world.getMinZ() - 5 || player.getPosition().z > world.getMaxZ() + 6 || - player.getPosition().y < world.getMinY() - 50 || player.getPosition().y > world.getMaxY() + 500 - ) { - server.getPlayerManager().playerKilled(player, null); - } - } - - private void tickHorizontalVelocity(ServerConfig.PhysicsConfig config, boolean grounded) { - var velocity = player.getVelocity(); - var orientation = player.getOrientation(); - Vector3f horizontalVelocity = new Vector3f( - velocity.x == velocity.x ? velocity.x : 0f, - 0, - velocity.z == velocity.z ? velocity.z : 0f - ); - Vector3f acceleration = new Vector3f(0); - if (input.forward()) acceleration.z -= 1; - if (input.backward()) acceleration.z += 1; - if (input.left()) acceleration.x -= 1; - if (input.right()) acceleration.x += 1; - if (acceleration.lengthSquared() > 0) { - acceleration.normalize(); - acceleration.rotateAxis(orientation.x, 0, 1, 0); - float accelerationMagnitude = config.movementAcceleration; - if (!grounded) accelerationMagnitude *= 0.25f; - acceleration.mul(accelerationMagnitude); - horizontalVelocity.add(acceleration); - float maxSpeed; - if (input.crouching()) { - maxSpeed = config.crouchingSpeed; - } else if (input.sprinting()) { - maxSpeed = config.sprintingSpeed; - } else { - maxSpeed = config.walkingSpeed; - } - // If scoping, then force limit to crouching speed. - if (isScopeEnabled()) maxSpeed = config.crouchingSpeed; - if (horizontalVelocity.length() > maxSpeed) { - horizontalVelocity.normalize(maxSpeed); - } - updated = true; - } else if (horizontalVelocity.lengthSquared() > 0) { - float baseDecel = config.movementDeceleration; - if (!grounded) baseDecel *= 0.25f; - float decelerationMagnitude = Math.min(horizontalVelocity.length(), baseDecel); - Vector3f deceleration = new Vector3f(horizontalVelocity).negate().normalize().mul(decelerationMagnitude); - horizontalVelocity.add(deceleration); - if (horizontalVelocity.length() < 0.1f) { - horizontalVelocity.set(0); - } - updated = true; - } - - // Update the player's velocity with what we've computed. - velocity.x = horizontalVelocity.x; - velocity.z = horizontalVelocity.z; - } - - private boolean isGrounded(World world) { - var position = player.getPosition(); - // Player must be flat on the top of a block. - if (Math.floor(position.y) != position.y) return false; - // Check to see if there's a block under any of the spaces the player is over. - return getHorizontalSpaceOccupied(position).stream() - .anyMatch(point -> world.getBlockAt(point.x, position.y - 0.1f, point.y) != 0); - } - - /** - * Gets the list of all spaces occupied by a player's position, in the - * horizontal XZ plane. This can be between 1 and 4 spaces, depending on - * if the player's position is overlapping with a few blocks. - * @param pos The position. - * @return The list of 2d positions occupied. - */ - private List getHorizontalSpaceOccupied(Vector3f pos) { - // Get the list of 2d x,z coordinates that we overlap with. - List points = new ArrayList<>(4); // Due to the size of radius, there can only be a max of 4 blocks. - int minX = (int) Math.floor(pos.x - RADIUS); - int minZ = (int) Math.floor(pos.z - RADIUS); - int maxX = (int) Math.floor(pos.x + RADIUS); - int maxZ = (int) Math.floor(pos.z + RADIUS); - for (int x = minX; x <= maxX; x++) { - for (int z = minZ; z <= maxZ; z++) { - points.add(new Vector2i(x, z)); - } - } - return points; - } - - private void checkBlockCollisions(Vector3f movement, Server server, World world) { - var position = player.getPosition(); - var velocity = player.getVelocity(); - final Vector3f nextTickPosition = new Vector3f(position).add(movement); - float height = player.getCurrentHeight(); - float delta = 0.00001f; - final Vector3f stepSize = new Vector3f(movement).normalize(1.0f); - // The number of steps we'll make towards the next tick position. - int stepCount = (int) Math.ceil(movement.length()); - if (stepCount == 0) return; // No movement, so exit. - final Vector3f nextPos = new Vector3f(position); - final Vector3f lastPos = new Vector3f(position); - for (int i = 0; i < stepCount; i++) { - lastPos.set(nextPos); - nextPos.add(stepSize); - // If we shot past the next tick position, clamp it to that. - if (new Vector3f(nextPos).sub(position).length() > movement.length()) { - nextPos.set(nextTickPosition); - } - - // Check if we collide with anything at this new position. - - float playerBodyPrevMinZ = lastPos.z - RADIUS; - float playerBodyPrevMaxZ = lastPos.z + RADIUS; - float playerBodyPrevMinX = lastPos.x - RADIUS; - float playerBodyPrevMaxX = lastPos.x + RADIUS; - float playerBodyPrevMinY = lastPos.y; - float playerBodyPrevMaxY = lastPos.y + height; - - float playerBodyMinZ = nextPos.z - RADIUS; - float playerBodyMaxZ = nextPos.z + RADIUS; - float playerBodyMinX = nextPos.x - RADIUS; - float playerBodyMaxX = nextPos.x + RADIUS; - float playerBodyMinY = nextPos.y; - float playerBodyMaxY = nextPos.y + height; - - // Compute the bounds of all blocks the player is intersecting with. - int minX = (int) Math.floor(playerBodyMinX); - int minZ = (int) Math.floor(playerBodyMinZ); - int minY = (int) Math.floor(playerBodyMinY); - int maxX = (int) Math.floor(playerBodyMaxX); - int maxZ = (int) Math.floor(playerBodyMaxZ); - int maxY = (int) Math.floor(playerBodyMaxY); - - for (int x = minX; x <= maxX; x++) { - for (int z = minZ; z <= maxZ; z++) { - for (int y = minY; y <= maxY; y++) { - byte block = world.getBlockAt(x, y, z); - if (block <= 0) continue; // We're not colliding with this block. - float blockMinY = (float) y; - float blockMaxY = (float) y + 1; - float blockMinX = (float) x; - float blockMaxX = (float) x + 1; - float blockMinZ = (float) z; - float blockMaxZ = (float) z + 1; - - /* - To determine if the player is moving into the -Z side of a block: - - The player's max z position went from < blockMinZ to >= blockMinZ. - - The block to the -Z direction is air. - */ - boolean collidingWithNegativeZ = playerBodyPrevMaxZ < blockMinZ && playerBodyMaxZ >= blockMinZ && world.getBlockAt(x, y, z - 1) <= 0; - if (collidingWithNegativeZ) { - position.z = blockMinZ - RADIUS - delta; - velocity.z = 0; - movement.z = 0; - } - - /* - To determine if the player is moving into the +Z side of a block: - - The player's min z position went from >= blockMaxZ to < blockMaxZ. - - The block to the +Z direction is air. - */ - boolean collidingWithPositiveZ = playerBodyPrevMinZ >= blockMaxZ && playerBodyMinZ < blockMaxZ && world.getBlockAt(x, y, z + 1) <= 0; - if (collidingWithPositiveZ) { - position.z = blockMaxZ + RADIUS + delta; - velocity.z = 0; - movement.z = 0; - } - - /* - To determine if the player is moving into the -X side of a block: - - The player's max x position went from < blockMinX to >= blockMinX - - The block to the -X direction is air. - */ - boolean collidingWithNegativeX = playerBodyPrevMaxX < blockMinX && playerBodyMaxX >= blockMinX && world.getBlockAt(x - 1, y, z) <= 0; - if (collidingWithNegativeX) { - position.x = blockMinX - RADIUS - delta; - velocity.x = 0; - movement.x = 0; - } - - /* - To determine if the player is moving into the +X side of a block: - - The player's min x position went from >= blockMaxX to < blockMaxX. - - The block to the +X direction is air. - */ - boolean collidingWithPositiveX = playerBodyPrevMinX >= blockMaxX && playerBodyMinX < blockMaxX && world.getBlockAt(x + 1, y, z) <= 0; - if (collidingWithPositiveX) { - position.x = blockMaxX + RADIUS + delta; - velocity.x = 0; - movement.x = 0; - } - - /* - To determine if the player is moving down onto a block: - - The player's min y position went from >= blockMaxY to < blockMaxY - - The block above the current one is air. - */ - boolean collidingWithFloor = playerBodyPrevMinY >= blockMaxY && playerBodyMinY < blockMaxY && world.getBlockAt(x, y + 1, z) <= 0; - if (collidingWithFloor) { - // This is a special case! We need to check for fall damage. - if (velocity.y < -20) { - float damage = velocity.y / 50f; - player.setHealth(player.getHealth() + damage); - if (player.getHealth() <= 0) { - server.getPlayerManager().playerKilled(player, player); - } else { - var handler = server.getPlayerManager().getHandler(player.getId()); - handler.sendDatagramPacket(new ClientHealthMessage(player.getHealth())); - int soundVariant = ThreadLocalRandom.current().nextInt(1, 4); - handler.sendDatagramPacket(new SoundMessage("hurt_" + soundVariant, 1, player.getPosition())); - } - } - position.y = blockMaxY; - velocity.y = 0; - movement.y = 0; - } - - /* - To determine if the player is moving up into a block: - - The player's y position went from below blockMinY to >= blockMinY - - The block below the current one is air. - */ - boolean collidingWithCeiling = playerBodyPrevMaxY < blockMinY && playerBodyMaxY >= blockMinY && world.getBlockAt(x, y - 1, z) <= 0; - if (collidingWithCeiling) { - position.y = blockMinY - height - delta; - velocity.y = 0; - movement.y = 0; - } - - updated = true; - } - } - } - } - } - private void shootGun(long now, Server server, Gun gun, GunItemStack g) { server.getProjectileManager().spawnBullets(player, gun); g.setBulletCount(g.getBulletCount() - 1); diff --git a/server/src/main/java/nl/andrewl/aos2_server/logic/PlayerMovementController.java b/server/src/main/java/nl/andrewl/aos2_server/logic/PlayerMovementController.java new file mode 100644 index 0000000..96fa86c --- /dev/null +++ b/server/src/main/java/nl/andrewl/aos2_server/logic/PlayerMovementController.java @@ -0,0 +1,10 @@ +package nl.andrewl.aos2_server.logic; + +import nl.andrewl.aos2_server.Server; +import nl.andrewl.aos2_server.config.ServerConfig; +import nl.andrewl.aos2_server.model.ServerPlayer; +import nl.andrewl.aos_core.model.world.World; + +public interface PlayerMovementController { + boolean tickMovement(float dt, ServerPlayer player, PlayerInputTracker input, Server server, World world, ServerConfig.PhysicsConfig config); +} diff --git a/server/src/main/java/nl/andrewl/aos2_server/model/ServerPlayer.java b/server/src/main/java/nl/andrewl/aos2_server/model/ServerPlayer.java index 88cc82a..02126d8 100644 --- a/server/src/main/java/nl/andrewl/aos2_server/model/ServerPlayer.java +++ b/server/src/main/java/nl/andrewl/aos2_server/model/ServerPlayer.java @@ -2,7 +2,6 @@ package nl.andrewl.aos2_server.model; import nl.andrewl.aos2_server.logic.PlayerActionManager; import nl.andrewl.aos_core.model.Player; -import nl.andrewl.aos_core.model.PlayerMode; import nl.andrewl.aos_core.model.item.BlockItemStack; import nl.andrewl.aos_core.model.item.GunItemStack; import nl.andrewl.aos_core.model.item.Inventory; diff --git a/server/src/main/resources/redfort.wld b/server/src/main/resources/redfort.wld index 073b6b1..0a72483 100644 Binary files a/server/src/main/resources/redfort.wld and b/server/src/main/resources/redfort.wld differ