using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using Terraria; namespace SoulHarvest.Common; /// /// One restrained Void surface shared by blade-tip tears and hit rifts. The star /// field is sampled in world-sized cells, so both effects keep identical scale and /// parallax instead of each inventing a differently zoomed background. /// internal static class ReaperVoidBackdropRenderer { private const float StarCellSize = 42f; internal static void DrawTrailStrip(SpriteBatch batch, Vector2 start, Vector2 end, float width, float opacity, bool drawStars = true, int branchSeed = 0, int branchIndex = -1) { if (opacity <= 0.001f || width <= 0.5f) return; DrawCoreStrip(batch, start, end, width, opacity); if (branchIndex >= 0 && branchIndex % 6 == 3) DrawBranchCrack(batch, start, end, width, opacity, branchSeed, branchIndex); if (drawStars) DrawStarsInStrip(batch, start, end, width, opacity); } private static void DrawCoreStrip(SpriteBatch batch, Vector2 start, Vector2 end, float width, float opacity) { Vector2 screenStart = start - Main.screenPosition; Vector2 screenEnd = end - Main.screenPosition; DeathDomainPrimitiveTextureSystem.DrawSmoothStrip(batch, screenStart, screenEnd, new Color(58, 4, 92, 0) * (opacity * 0.26f), width + 4f); DeathDomainPrimitiveTextureSystem.DrawSmoothStrip(batch, screenStart, screenEnd, new Color(112, 26, 181, 220) * (opacity * 0.68f), width + 1.7f); DeathDomainPrimitiveTextureSystem.DrawSmoothStrip(batch, screenStart, screenEnd, new Color(2, 0, 6, 252) * opacity, width); DeathDomainPrimitiveTextureSystem.DrawSmoothStrip(batch, screenStart, screenEnd, new Color(9, 2, 16, 220) * (opacity * 0.72f), Math.Max(1f, width * 0.46f)); } internal static void DrawHitRift(SpriteBatch batch, Vector2 center, Vector2 axis, float length, float width, int seed, float reveal, float opacity) { axis = axis.SafeNormalize(Vector2.UnitX); Vector2 normal = axis.RotatedBy(MathHelper.PiOver2); reveal = MathHelper.Clamp(reveal, 0f, 1f); float visibleEnd = MathHelper.Lerp(-1f, 1f, reveal); // The entire opening, including its continuous noisy silhouette and rim, // is one high-resolution mask just like the Blood ultimate's wound. DrawContinuousRiftBody(center, axis, length, width, reveal, opacity); DrawRiftBranches(batch, center, axis, normal, length, width, seed, visibleEnd, opacity); DrawStarsInRift(batch, center, axis, normal, length, width, seed, visibleEnd, opacity); } private static void DrawContinuousRiftBody(Vector2 center, Vector2 axis, float length, float width, float reveal, float opacity) { DeathDomainPrimitiveTextureSystem.DrawVoidRift(center, axis.ToRotation(), length, width, opacity, reveal); } internal static void DrawPersistentRift(SpriteBatch batch, Vector2 center, Vector2 axis, float length, float width, int seed, float reveal, float opacity) { DrawHitRift(batch, center, axis, length, width, seed, reveal, opacity); } private static Vector2 GetRiftCenter(Vector2 center, Vector2 axis, Vector2 normal, float u, float width, int seed) { // A stable multi-frequency wave keeps both sides of the opening // continuous. No per-segment random offsets are used, so even a giant // rift reads as one torn surface rather than disconnected polygons. float bend = ((float)Math.Sin(u * 11.7f + seed * 0.031f) + (float)Math.Sin(u * 23.3f - seed * 0.017f) * 0.44f + (float)Math.Sin(u * 47.1f + seed * 0.009f) * 0.17f) * width * 0.052f; return center + normal * bend; } private static float GetRiftHalfWidth(float u, float width, int seed) { float envelope = (float)Math.Pow(Math.Max(0f, Math.Sin((u * 0.5f + 0.5f) * MathHelper.Pi)), 0.58f); float noise = 1f + (float)Math.Sin(u * 27f + seed * 0.071f) * 0.09f + (float)Math.Sin(u * 61f - seed * 0.037f) * 0.042f + (float)Math.Sin(u * 113f + seed * 0.013f) * 0.016f; return width * 0.5f * envelope * MathHelper.Clamp(noise, 0.80f, 1.16f); } private static void DrawRiftBranches(SpriteBatch batch, Vector2 center, Vector2 axis, Vector2 normal, float length, float width, int seed, float visibleEnd, float opacity) { int branchCount = Math.Clamp((int)Math.Ceiling(length / 150f), 3, 11); for (int branch = 0; branch < branchCount; branch++) { float jitter = Hash01(seed + branch * 97 + 17) * 0.08f - 0.04f; float u = MathHelper.Lerp(-0.82f, 0.82f, (branch + 0.5f) / branchCount) + jitter; if (u > visibleEnd) continue; float side = ((branch + seed) & 1) == 0 ? 1f : -1f; float halfWidth = GetRiftHalfWidth(u, width, seed); Vector2 root = GetRiftCenter(center, axis, normal, u, width, seed) + axis * (u * length * 0.5f) + normal * side * halfWidth * 0.82f; float branchLength = MathHelper.Clamp(width * (0.30f + Hash01(seed + branch * 31) * 0.20f), 5f, 28f); Vector2 direction = (normal * side + axis * (Hash01(seed + branch * 43) - 0.5f) * 0.85f) .SafeNormalize(normal * side); DrawBranchPath(batch, root, direction, branchLength, MathHelper.Clamp(width * 0.045f, 0.7f, 3.2f), opacity * 0.58f, seed + branch * 173); } } private static void DrawBranchCrack(SpriteBatch batch, Vector2 start, Vector2 end, float width, float opacity, int seed, int index) { Vector2 delta = end - start; if (delta.LengthSquared() < 9f) return; Vector2 axis = delta.SafeNormalize(Vector2.UnitX); Vector2 normal = axis.RotatedBy(MathHelper.PiOver2); float side = ((seed + index) & 1) == 0 ? 1f : -1f; Vector2 root = Vector2.Lerp(start, end, 0.58f) + normal * side * width * 0.47f; Vector2 direction = axis.RotatedBy(side * MathHelper.Lerp(0.55f, 0.92f, Hash01(seed + index * 79))); float length = MathHelper.Clamp(width * 1.7f, 7f, 29f); DrawBranchPath(batch, root, direction, length, Math.Max(0.65f, width * 0.13f), opacity * 0.50f, seed + index * 211); } private static void DrawBranchPath(SpriteBatch batch, Vector2 root, Vector2 direction, float length, float width, float opacity, int seed) { direction = direction.SafeNormalize(Vector2.UnitY); Vector2 normal = direction.RotatedBy(MathHelper.PiOver2); int samples = Math.Clamp((int)Math.Ceiling(length / 5f), 5, 15); float wavePhase = Hash01(seed + 19) * MathHelper.TwoPi; Vector2 previous = root; for (int sample = 1; sample <= samples; sample++) { float progress = sample / (float)samples; float envelope = (float)Math.Sin(progress * MathHelper.Pi); float wave = ((float)Math.Sin(progress * 5.4f + wavePhase) + (float)Math.Sin(progress * 11.1f - wavePhase * 0.7f) * 0.26f) * length * 0.055f * envelope; Vector2 current = root + direction * (length * progress) + normal * wave; float localWidth = Math.Max(0.45f, width * (float)Math.Pow(1f - progress * 0.86f, 0.72f)); DrawCoreStrip(batch, previous, current, localWidth, opacity * (1f - progress * 0.32f)); DrawStarsInStrip(batch, previous, current, localWidth, opacity * (1f - progress * 0.42f)); previous = current; } } private static float Hash01(int seed) { uint value = (uint)seed; value ^= value >> 16; value *= 0x7FEB352Du; value ^= value >> 15; value *= 0x846CA68Bu; value ^= value >> 16; return (value & 0x00FFFFFFu) / 16777215f; } private static void DrawStarsInStrip(SpriteBatch batch, Vector2 start, Vector2 end, float width, float opacity) { Vector2 delta = end - start; float lengthSquared = delta.LengthSquared(); if (lengthSquared <= 0.1f) return; float margin = width + StarCellSize; EnumerateStars(Vector2.Min(start, end) - new Vector2(margin), Vector2.Max(start, end) + new Vector2(margin), (point, size) => { float amount = MathHelper.Clamp(Vector2.Dot(point - start, delta) / lengthSquared, 0f, 1f); Vector2 closest = Vector2.Lerp(start, end, amount); if (Vector2.DistanceSquared(point, closest) > width * width * 0.12f) return; DrawStar(batch, point, size, opacity); }); } private static void DrawStarsInRift(SpriteBatch batch, Vector2 center, Vector2 axis, Vector2 normal, float length, float width, int seed, float visibleEnd, float opacity) { float margin = length * 0.55f + width + StarCellSize; EnumerateStars(center - new Vector2(margin), center + new Vector2(margin), (point, size) => { Vector2 local = point - center; float u = Vector2.Dot(local, axis) / Math.Max(1f, length * 0.5f); if (u is < -1f or > 1f || u > visibleEnd) return; float centerOffset = Vector2.Dot( GetRiftCenter(center, axis, normal, u, width, seed) - center, normal); float v = Vector2.Dot(local, normal) - centerOffset; if (Math.Abs(v) > GetRiftHalfWidth(u, width, seed) * 0.62f) return; DrawStar(batch, point, size, opacity); }); } private static void EnumerateStars(Vector2 minimum, Vector2 maximum, Action draw) { Vector2 drift = Main.screenPosition * 0.018f + new Vector2( (float)Math.Sin(Main.GlobalTimeWrappedHourly * 0.31f) * 3.5f, (float)Math.Cos(Main.GlobalTimeWrappedHourly * 0.27f) * 2.4f); int minX = (int)Math.Floor((minimum.X - drift.X) / StarCellSize); int maxX = (int)Math.Ceiling((maximum.X - drift.X) / StarCellSize); int minY = (int)Math.Floor((minimum.Y - drift.Y) / StarCellSize); int maxY = (int)Math.Ceiling((maximum.Y - drift.Y) / StarCellSize); for (int cellY = minY; cellY <= maxY; cellY++) for (int cellX = minX; cellX <= maxX; cellX++) { // One global world-space star field is clipped by every Void surface. // A hit opening over an existing trail therefore reveals the same // points at the same scale instead of a second randomized texture. uint hash = Hash((uint)cellX, (uint)cellY, 0x564F4944u); if ((hash & 0xFFFFu) < 0xD900u) continue; float offsetX = ((hash >> 8) & 0xFFu) / 255f; float offsetY = ((hash >> 16) & 0xFFu) / 255f; Vector2 point = new Vector2((cellX + offsetX) * StarCellSize, (cellY + offsetY) * StarCellSize) + drift; draw(point, 0.75f + ((hash >> 24) & 0xFFu) / 255f * 0.9f); } } private static void DrawStar(SpriteBatch batch, Vector2 worldPoint, float size, float opacity) { Vector2 point = worldPoint - Main.screenPosition; Color color = new Color(232, 222, 255, 0) * (opacity * 0.82f); DeathDomainPrimitiveTextureSystem.DrawSmoothLine(batch, point - Vector2.UnitX * size, point + Vector2.UnitX * size, color, Math.Max(0.7f, size * 0.62f)); if (size > 1.15f) DeathDomainPrimitiveTextureSystem.DrawSmoothLine(batch, point - Vector2.UnitY * size, point + Vector2.UnitY * size, color * 0.65f, 0.7f); } private static uint Hash(uint x, uint y, uint seed) { uint value = x * 374761393u + y * 668265263u + seed; value = (value ^ (value >> 13)) * 1274126177u; return value ^ (value >> 16); } }