using Microsoft.Xna.Framework; using System; using Terraria; using Terraria.ID; namespace SoulHarvest.Common; /// /// Deterministic, terrain-swept movement used by Reaper attacks. Multiplayer /// clients only consume the server's authoritative destination broadcast. /// internal static class ReaperMovementHelper { private const float SweepStep = 4f; private const float MaximumAttackMovement = 960f; /// /// Resolves a terrain-limited attack path without changing player position. /// This is retained for read-only previews; committed Reaper dashes use the /// same resolver through MoveAlong on the authoritative server. /// internal static float TraceAlong( Player player, Vector2 direction, float requestedDistance, out Vector2 startCenter, out Vector2 endCenter) { return ResolvePath(player, direction, requestedDistance, out _, out startCenter, out endCenter); } internal static float MoveAlong( Player player, Vector2 direction, float requestedDistance, out Vector2 startCenter, out Vector2 endCenter) { startCenter = player.Center; endCenter = startCenter; if (requestedDistance <= 0f || !float.IsFinite(requestedDistance) || Main.netMode == NetmodeID.MultiplayerClient) { return 0f; } float acceptedDistance = ResolvePath(player, direction, requestedDistance, out Vector2 position, out startCenter, out endCenter); if (acceptedDistance <= 0f) return 0f; // ResolvePath checks every four pixels and includes the full player // rectangle. Recheck the final rectangle before committing so a tile // mutation in the same tick cannot place an authoritative dash in solid // terrain. if (Collision.SolidCollision(position, player.width, player.height)) { endCenter = startCenter; return 0f; } player.position = position; player.velocity = Vector2.Zero; endCenter = player.Center; if (Main.netMode == NetmodeID.Server) player.GetModPlayer() .BeginAuthoritativeMovement(player.position, player.velocity); return acceptedDistance; } internal static bool ReturnToSafePosition(Player player, Vector2 safePosition) { if (!float.IsFinite(safePosition.X) || !float.IsFinite(safePosition.Y) || Collision.SolidCollision(safePosition, player.width, player.height)) { return false; } Vector2 displacement = safePosition - player.position; float distance = displacement.Length(); if (distance <= 0.5f) return true; float accepted = MoveAlong(player, displacement, distance, out _, out _); return accepted >= distance - SweepStep; } /// /// Performs a true target-point teleport. Unlike a dash it may cross solid /// terrain, but the destination rectangle itself must be safe. If the exact /// cursor point is occupied, the nearest safe point on the cursor-to-player /// line is selected so the authoritative player is never embedded in tiles. /// internal static bool TeleportToTarget(Player player, Vector2 requestedCenter, float maximumDistance, out Vector2 startCenter, out Vector2 endCenter) { startCenter = player.Center; endCenter = startCenter; if (Main.netMode == NetmodeID.MultiplayerClient || !float.IsFinite(requestedCenter.X) || !float.IsFinite(requestedCenter.Y) || !float.IsFinite(maximumDistance) || maximumDistance <= 0f) { return false; } Vector2 displacement = requestedCenter - startCenter; float requestedDistance = displacement.Length(); if (requestedDistance <= 1f) return false; Vector2 direction = displacement / requestedDistance; float acceptedDistance = Math.Min(maximumDistance, requestedDistance); Vector2 acceptedPosition = startCenter + direction * acceptedDistance - player.Size * 0.5f; while (acceptedDistance > 4f) { Point tile = (acceptedPosition + player.Size * 0.5f).ToTileCoordinates(); if (WorldGen.InWorld(tile.X, tile.Y, 2) && !Collision.SolidCollision(acceptedPosition, player.width, player.height)) { break; } acceptedDistance -= SweepStep; acceptedPosition = startCenter + direction * acceptedDistance - player.Size * 0.5f; } if (acceptedDistance <= 4f || Collision.SolidCollision(acceptedPosition, player.width, player.height)) { return false; } player.position = acceptedPosition; player.velocity = Vector2.Zero; endCenter = player.Center; if (Main.netMode == NetmodeID.Server) { player.GetModPlayer() .BeginAuthoritativeMovement(player.position, player.velocity); } return true; } private static float ResolvePath( Player player, Vector2 direction, float requestedDistance, out Vector2 endPosition, out Vector2 startCenter, out Vector2 endCenter) { startCenter = player.Center; endCenter = startCenter; endPosition = player.position; if (requestedDistance <= 0f || !float.IsFinite(requestedDistance) || !float.IsFinite(direction.X) || !float.IsFinite(direction.Y)) { return 0f; } direction = direction.SafeNormalize(Vector2.UnitX * (player.direction == 0 ? 1 : player.direction)); float acceptedDistance = 0f; float maximumDistance = Math.Min(MaximumAttackMovement, requestedDistance); while (acceptedDistance + 0.01f < maximumDistance) { float stepLength = Math.Min(SweepStep, maximumDistance - acceptedDistance); Vector2 candidate = endPosition + direction * stepLength; Point centerTile = (candidate + player.Size * 0.5f).ToTileCoordinates(); if (!WorldGen.InWorld(centerTile.X, centerTile.Y, 2) || Collision.SolidCollision(candidate, player.width, player.height)) { break; } endPosition = candidate; acceptedDistance += stepLength; } endCenter = endPosition + player.Size * 0.5f; return acceptedDistance; } }