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);
}
}