using SoulHarvest.Items;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using Terraria;
using Terraria.GameContent;
using Terraria.ModLoader;
namespace SoulHarvest.Common;
[Autoload(Side = ModSide.Client)]
internal class DeathDomainVisualSystem : ModSystem
{
private const float OpenAnimationSpeed = 1f / 42f;
private const float CloseAnimationSpeed = 1f / 42f;
private const float DescentAnimationSpeed = 1f / 52f;
private const float DepartureAnimationSpeed = 1f / 42f;
private readonly float[] domainAnimation = new float[Main.maxPlayers];
private readonly float[] descentAnimation = new float[Main.maxPlayers];
private readonly float[] cachedRadius = new float[Main.maxPlayers];
private readonly int[] cachedMastery = new int[Main.maxPlayers];
private readonly bool[] cachedFullScreen = new bool[Main.maxPlayers];
public override void Load()
{
On_Main.DoDraw_WallsTilesNPCs += DrawDomainBackdropsBeforeWallsAndTiles;
}
public override void Unload()
{
On_Main.DoDraw_WallsTilesNPCs -= DrawDomainBackdropsBeforeWallsAndTiles;
}
public override void PostUpdateEverything()
{
if (Main.gameMenu)
{
ClearAnimationState();
return;
}
for (int index = 0; index < Main.maxPlayers; index++)
{
Player player = Main.player[index];
if (!player.active)
{
domainAnimation[index] = 0f;
descentAnimation[index] = 0f;
cachedRadius[index] = 0f;
cachedMastery[index] = 0;
cachedFullScreen[index] = false;
continue;
}
MyPlayer data = player.GetModPlayer<MyPlayer>();
DeathNecklace? necklace = data.ActiveDeathNecklace;
if (necklace is not null)
{
cachedRadius[index] = necklace.DomainRadius;
cachedMastery[index] = 5 + (int)MathF.Round(data.DeathDomainProgression.VisualMastery * 70f);
}
bool enabled = !player.dead && necklace is not null && data.DeathDomainEnabled;
bool descended = enabled && data.IsDeathDomainDescended(necklace);
cachedFullScreen[index] = descended;
float speed = enabled ? OpenAnimationSpeed : CloseAnimationSpeed;
domainAnimation[index] = MathHelper.Clamp(
domainAnimation[index] + (enabled ? speed : -speed),
0f,
1f);
float descentSpeed = descended
? DescentAnimationSpeed
: DepartureAnimationSpeed;
descentAnimation[index] = MathHelper.Clamp(
descentAnimation[index] + (descended ? descentSpeed : -descentSpeed),
0f,
1f);
}
}
public override void OnWorldUnload()
{
ClearAnimationState();
}
public override void PostDrawTiles()
{
Player localPlayer = Main.LocalPlayer;
if (Main.gameMenu || !localPlayer.active)
return;
bool anyVisibleDomain = false;
for (int index = 0; index < Main.maxPlayers; index++)
{
if (domainAnimation[index] > 0.001f)
{
anyVisibleDomain = true;
break;
}
}
if (!anyVisibleDomain)
return;
SpriteBatch batch = Main.spriteBatch;
batch.Begin(
SpriteSortMode.Deferred,
BlendState.AlphaBlend,
SamplerState.LinearClamp,
DepthStencilState.None,
RasterizerState.CullNone,
null,
Main.GameViewMatrix.TransformationMatrix);
for (int index = 0; index < Main.maxPlayers; index++)
{
Player player = Main.player[index];
float animation = domainAnimation[index];
if (!player.active || animation <= 0.001f)
continue;
float radius = cachedRadius[index];
int mastery = cachedMastery[index];
bool fullScreen = cachedFullScreen[index];
if (radius <= 0f)
radius = 160f;
Vector2 screenCenter = player.Center - Main.screenPosition;
if (!fullScreen
&& (screenCenter.X < -radius
|| screenCenter.X > Main.screenWidth + radius
|| screenCenter.Y < -radius
|| screenCenter.Y > Main.screenHeight + radius))
{
continue;
}
if (!fullScreen)
DrawDomainRing(batch, screenCenter, radius, mastery, animation, drawBackdrop: false);
else
DrawFullScreenDomain(batch, screenCenter, radius, mastery, animation);
float descent = descentAnimation[index];
if (descent > 0.001f && descent < 0.999f)
{
DrawManifestationWave(
batch,
screenCenter,
radius,
GetMasteryProgress(mastery),
GetDomainApertureReveal(animation),
descent,
drawBackdrop: false,
drawBoundary: true);
}
}
batch.End();
}
private static void DrawDomainRing(
SpriteBatch batch,
Vector2 center,
float targetRadius,
int mastery,
float animation,
bool drawBackdrop = true)
{
float time = Main.GlobalTimeWrappedHourly;
float progress = MathHelper.SmoothStep(0f, 1f, animation);
float reveal = GetDomainApertureReveal(animation);
float rangeProgress = MathHelper.Clamp(
(targetRadius - 160f) / (DeathNecklace.RadiusGrowthPerLevel * (DeathNecklace.MaxCoreLevel - 1)),
0f,
1f);
float manifestation = MathHelper.Lerp(0.38f, 1f, rangeProgress);
float manifestedReveal = reveal * manifestation;
float transition = (float)Math.Sqrt(Math.Max(0f,
(float)Math.Sin(animation * MathHelper.Pi)));
float overshoot = 1f + (float)Math.Sin(progress * MathHelper.Pi) * 0.028f;
float radius = MathHelper.Lerp(8f, targetRadius, progress) * overshoot;
float pulse = (0.82f + (float)Math.Sin(time * 3.2f) * 0.12f) * manifestedReveal;
float masteryProgress = GetMasteryProgress(mastery);
DrawBlackHoleField(batch, center, radius, masteryProgress, pulse, time, manifestedReveal, drawBackdrop);
DrawNoiseBoundary(batch, center, radius + 3f, time, 0.4f,
12f + transition * 28f, 9f + transition * 3f,
(0.18f + transition * 0.16f) * pulse, masteryProgress, gaps: true);
DrawNoiseBoundary(batch, center, radius, time, 1.7f,
9f + transition * 20f, 5f + transition * 1.8f,
(0.42f + transition * 0.34f) * pulse, masteryProgress, gaps: true);
DrawNoiseBoundary(batch, center, radius - 2f, time, 3.3f,
7f + transition * 14f, 2.7f + masteryProgress + transition * 1.6f,
(0.94f + transition * 0.22f) * pulse, masteryProgress, gaps: false);
DrawNoiseBoundary(batch, center, radius - 7f, -time, 5.1f, 5f, 1.4f, 0.58f * pulse, masteryProgress, gaps: true);
DrawDomainRippleWaves(batch, center, radius, time, masteryProgress, pulse);
}
private static void DrawBlackHoleField(
SpriteBatch batch,
Vector2 center,
float radius,
float mastery,
float pulse,
float time,
float reveal,
bool drawBackdrop)
{
float coreGap = MathHelper.Lerp(22f, 14f, mastery);
float coreRadius = radius > coreGap + 6f ? radius - coreGap : radius * 0.58f;
for (int arm = 0; arm < 5; arm++)
DrawAccretionSpiral(batch, center, coreRadius, Math.Max(coreRadius + 2f, radius - 3f), time, arm, mastery, pulse);
float gap = Math.Max(2f, radius - coreRadius);
DrawNoiseBoundary(batch, center, coreRadius + gap * 0.72f, -time * 1.35f, 9.4f, 4.5f, 7f, 0.18f * pulse, mastery, gaps: true);
DrawNoiseBoundary(batch, center, coreRadius + gap * 0.34f, time * 1.8f, 12.8f, 2.8f, 3.8f, 0.78f * pulse, mastery, gaps: true);
float coreNoiseTime = time * 0.8f;
const float coreNoiseSeed = 16.2f;
float coreNoiseAmplitude = Math.Min(coreRadius * 0.18f, MathHelper.Lerp(5f, 10f, mastery) * MathHelper.Lerp(0.35f, 1f, reveal));
if (drawBackdrop)
{
DrawClippedDeathDomainBackdrop(
batch,
center,
coreRadius,
coreNoiseTime,
coreNoiseSeed,
coreNoiseAmplitude,
mastery,
reveal);
}
// The old scan-band fill stamped hundreds of horizontal rounded strips
// over the centre and produced the visible television-line pattern. A
// single antialiased disc supplies the same restrained shadow without any
// row seams; the animated noisy boundary still defines its organic edge.
DeathDomainPrimitiveTextureSystem.DrawSmoothDisc(
batch,
center,
coreRadius,
new Color(3, 0, 10) * (MathHelper.Lerp(0.08f, 0.26f, mastery) * reveal));
DeathDomainPrimitiveTextureSystem.DrawRadialGradient(
batch,
center,
coreRadius * MathHelper.Lerp(0.48f, 0.58f, mastery),
new Color(255, 58, 84) * (MathHelper.Lerp(0.16f, 0.24f, mastery) * pulse));
DrawOutwardPressureWaves(batch, center, coreRadius, time, mastery, pulse);
DrawNoiseBoundary(batch, center, coreRadius, coreNoiseTime, coreNoiseSeed, coreNoiseAmplitude, 2.2f, 0.76f * pulse, mastery, gaps: false);
DrawInwardSuctionWaves(batch, center, radius, coreRadius, time, mastery, pulse);
}
private static void DrawClippedDeathDomainBackdrop(
SpriteBatch batch,
Vector2 center,
float baseRadius,
float noiseTime,
float noiseSeed,
float noiseAmplitude,
float mastery,
float reveal)
{
const int boundarySegments = 192;
float preferredBandHeight = MathHelper.Clamp(baseRadius / 280f, 2f, 8f);
Span<Vector2> boundary = stackalloc Vector2[boundarySegments];
float extent = 0f;
for (int index = 0; index < boundarySegments; index++)
{
float angle = MathHelper.TwoPi * index / boundarySegments;
float radius = Math.Max(1f, baseRadius + BoundaryNoise(angle, noiseTime, noiseSeed) * noiseAmplitude);
boundary[index] = angle.ToRotationVector2() * radius;
extent = Math.Max(extent, Math.Abs(boundary[index].Y));
}
int bands = Math.Max(1, (int)Math.Ceiling(extent * 2f / preferredBandHeight));
float bandHeight = extent * 2f / bands;
DeathDomainBackdropTextureSystem.GetExpandedDomainDrawBounds(
out Vector2 expandedDestination,
out float expandedDestinationWidth,
out float expandedDestinationHeight);
float expandedDestinationRight = expandedDestination.X
+ expandedDestinationWidth;
float expandedDestinationBottom = expandedDestination.Y
+ expandedDestinationHeight;
// Draw one complete layer at a time. This retains proper alpha compositing
// while allowing SpriteBatch to group hundreds of clipped scan bands into
// only three texture batches instead of switching textures for every row.
for (int layer = 0; layer < 3; layer++)
{
float opacity = layer switch
{
0 => MathHelper.Lerp(0.88f, 1f, mastery) * reveal,
1 => MathHelper.Lerp(0.72f, 0.9f, mastery) * reveal,
_ => MathHelper.Lerp(0.78f, 0.96f, mastery) * reveal
};
Texture2D texture = DeathDomainBackdropTextureSystem.GetLayer(layer);
DeathDomainBackdropTextureSystem.GetExpandedDomainSourceFrame(layer,
out float sourceOffsetX, out float sourceOffsetY,
out float viewWidth, out float viewHeight);
for (int index = 0; index < bands; index++)
{
float scanY = -extent + (index + 0.5f) * bandHeight;
float left = float.MaxValue;
float right = float.MinValue;
for (int edge = 0; edge < boundarySegments; edge++)
{
Vector2 start = boundary[edge];
Vector2 end = boundary[(edge + 1) % boundarySegments];
if ((start.Y <= scanY && end.Y > scanY) || (end.Y <= scanY && start.Y > scanY))
{
float amount = (scanY - start.Y) / (end.Y - start.Y);
float intersectionX = MathHelper.Lerp(start.X, end.X, amount);
left = Math.Min(left, intersectionX);
right = Math.Max(right, intersectionX);
}
}
if (left >= right)
continue;
float drawLeft = Math.Max(expandedDestination.X,
center.X + left);
float drawRight = Math.Min(expandedDestinationRight,
center.X + right);
float drawTop = Math.Max(expandedDestination.Y,
center.Y + scanY - bandHeight * 0.5f);
float drawBottom = Math.Min(expandedDestinationBottom,
center.Y + scanY + bandHeight * 0.5f);
if (drawLeft >= drawRight || drawTop >= drawBottom)
continue;
float normalizedLeft = (drawLeft - expandedDestination.X)
/ expandedDestinationWidth;
float normalizedRight = (drawRight - expandedDestination.X)
/ expandedDestinationWidth;
float normalizedTop = (drawTop - expandedDestination.Y)
/ expandedDestinationHeight;
float normalizedBottom = (drawBottom - expandedDestination.Y)
/ expandedDestinationHeight;
DrawBackdropBand(
batch,
texture,
new Vector2(drawLeft, drawTop),
drawRight - drawLeft,
drawBottom - drawTop,
sourceOffsetX + normalizedLeft * viewWidth,
sourceOffsetY + normalizedTop * viewHeight,
Math.Max(0.5f, (normalizedRight - normalizedLeft) * viewWidth),
Math.Max(0.5f, (normalizedBottom - normalizedTop) * viewHeight),
opacity);
}
}
}
private static void DrawBackdropBand(
SpriteBatch batch,
Texture2D texture,
Vector2 destination,
float destinationWidth,
float destinationHeight,
float sourceX,
float sourceY,
float sourceWidth,
float sourceHeight,
float opacity)
{
if (destinationWidth <= 0.05f || destinationHeight <= 0.05f || opacity <= 0.001f)
return;
float clampedSourceY = MathHelper.Clamp(sourceY, 0f, texture.Height - 0.001f);
float clampedSourceHeight = Math.Min(sourceHeight, texture.Height - clampedSourceY);
if (clampedSourceHeight <= 0.001f)
return;
float remainingSourceWidth = sourceWidth;
float consumedSourceWidth = 0f;
float wrappedSourceX = PositiveModulo(sourceX, texture.Width);
while (remainingSourceWidth > 0.001f)
{
float sourceWidthPart = Math.Min(remainingSourceWidth, texture.Width - wrappedSourceX);
float destinationWidthPart = destinationWidth * sourceWidthPart / sourceWidth;
float destinationX = destination.X + destinationWidth * consumedSourceWidth / sourceWidth;
DrawBackdropPart(
batch,
texture,
new Vector2(destinationX, destination.Y),
destinationWidthPart,
destinationHeight,
wrappedSourceX,
clampedSourceY,
sourceWidthPart,
clampedSourceHeight,
opacity);
remainingSourceWidth -= sourceWidthPart;
consumedSourceWidth += sourceWidthPart;
wrappedSourceX = 0f;
}
}
private static void DrawBackdropPart(
SpriteBatch batch,
Texture2D texture,
Vector2 destination,
float destinationWidth,
float destinationHeight,
float sourceX,
float sourceY,
float sourceWidth,
float sourceHeight,
float opacity)
{
int sourceLeft = Math.Clamp((int)Math.Floor(sourceX), 0, texture.Width - 1);
int sourceTop = Math.Clamp((int)Math.Floor(sourceY), 0, texture.Height - 1);
int sourceRight = Math.Clamp((int)Math.Ceiling(sourceX + sourceWidth), sourceLeft + 1, texture.Width);
int sourceBottom = Math.Clamp((int)Math.Ceiling(sourceY + sourceHeight), sourceTop + 1, texture.Height);
Rectangle source = new(sourceLeft, sourceTop, sourceRight - sourceLeft, sourceBottom - sourceTop);
batch.Draw(
texture,
destination,
source,
Color.White * opacity,
0f,
Vector2.Zero,
new Vector2(destinationWidth / source.Width, destinationHeight / source.Height),
SpriteEffects.None,
0f);
}
private static void DrawFullScreenDomain(
SpriteBatch batch,
Vector2 center,
float playerDomainRadius,
int mastery,
float animation)
{
// Full-screen mastery removes the circular crop from the domain scene. The
// player's event horizon is still the same max-level field, rather than a
// circle enlarged until it reaches the viewport corners.
DrawDomainRing(batch, center, playerDomainRadius, mastery, animation, drawBackdrop: false);
}
private void DrawDomainBackdropsBeforeWallsAndTiles(On_Main.orig_DoDraw_WallsTilesNPCs orig, Main self)
{
DrawDomainBackdrops();
orig(self);
}
private void DrawDomainBackdrops()
{
if (Main.gameMenu || !Main.LocalPlayer.active)
return;
bool anyCompletedFullScreen = false;
bool anyClipped = false;
float fullScreenMastery = 0f;
float fullScreenReveal = 0f;
for (int index = 0; index < Main.maxPlayers; index++)
{
if (domainAnimation[index] <= 0.001f || !Main.player[index].active)
continue;
float descent = descentAnimation[index];
if (descent >= 0.999f)
{
anyCompletedFullScreen = true;
fullScreenMastery = Math.Max(fullScreenMastery, GetMasteryProgress(cachedMastery[index]));
fullScreenReveal = Math.Max(
fullScreenReveal,
MathHelper.SmoothStep(0f, 1f, domainAnimation[index]));
}
if (descent < 0.999f)
{
anyClipped = true;
}
}
if (!anyCompletedFullScreen && !anyClipped)
return;
// This hook runs during final world composition, after Terraria has drawn
// (or composited) its background and before it starts walls and tiles.
// Reuse the active world SpriteBatch so the domain is never captured in
// the scrolling background render target.
SpriteBatch batch = Main.spriteBatch;
if (anyClipped)
{
for (int index = 0; index < Main.maxPlayers; index++)
{
Player player = Main.player[index];
float animation = domainAnimation[index];
if (descentAnimation[index] >= 0.999f
|| animation <= 0.001f
|| !player.active)
continue;
float targetRadius = Math.Max(160f, cachedRadius[index]);
Vector2 center = player.Center - Main.screenPosition;
if (center.X < -targetRadius || center.X > Main.screenWidth + targetRadius
|| center.Y < -targetRadius || center.Y > Main.screenHeight + targetRadius)
{
continue;
}
GetAnimatedBackdropGeometry(targetRadius, cachedMastery[index], animation,
out float coreRadius, out float noiseTime, out float noiseAmplitude,
out float masteryProgress, out float reveal);
DrawClippedDeathDomainBackdrop(
batch,
center,
coreRadius,
noiseTime,
16.2f,
noiseAmplitude,
masteryProgress,
reveal);
DrawTransientBackdropBlackHoles(
batch,
center,
new Vector2(coreRadius),
masteryProgress,
reveal,
coreRadius,
Main.worldID + index * 101);
}
}
if (anyCompletedFullScreen)
{
// The backdrop is camera-relative, so multiple manifested domains share
// one strongest presentation instead of darkening the scene repeatedly.
DrawFullScreenBackdrop(batch, fullScreenMastery, fullScreenReveal);
}
for (int index = 0; index < Main.maxPlayers; index++)
{
Player player = Main.player[index];
float descent = descentAnimation[index];
if (!player.active || domainAnimation[index] <= 0.001f
|| descent <= 0.001f || descent >= 0.999f)
{
continue;
}
if (!anyCompletedFullScreen)
{
DrawManifestationWave(
batch,
player.Center - Main.screenPosition,
Math.Max(160f, cachedRadius[index]),
GetMasteryProgress(cachedMastery[index]),
GetDomainApertureReveal(domainAnimation[index]),
descent,
drawBackdrop: true,
drawBoundary: false);
}
}
}
private static void GetAnimatedBackdropGeometry(
float targetRadius,
int mastery,
float animation,
out float coreRadius,
out float noiseTime,
out float noiseAmplitude,
out float masteryProgress,
out float reveal)
{
float progress = MathHelper.SmoothStep(0f, 1f, animation);
reveal = GetDomainApertureReveal(animation);
float rangeProgress = MathHelper.Clamp(
(targetRadius - 160f) / (DeathNecklace.RadiusGrowthPerLevel * (DeathNecklace.MaxCoreLevel - 1)),
0f,
1f);
float manifestedReveal = reveal * MathHelper.Lerp(0.38f, 1f, rangeProgress);
float overshoot = 1f + (float)Math.Sin(progress * MathHelper.Pi) * 0.028f;
float radius = MathHelper.Lerp(8f, targetRadius, progress) * overshoot;
masteryProgress = GetMasteryProgress(mastery);
float coreGap = MathHelper.Lerp(22f, 14f, masteryProgress);
coreRadius = radius > coreGap + 6f ? radius - coreGap : radius * 0.58f;
noiseTime = Main.GlobalTimeWrappedHourly * 0.8f;
noiseAmplitude = Math.Min(
coreRadius * 0.18f,
MathHelper.Lerp(5f, 10f, masteryProgress) * MathHelper.Lerp(0.35f, 1f, manifestedReveal));
reveal = manifestedReveal;
}
private static float GetDomainApertureReveal(float animation)
{
// The domain material becomes opaque almost immediately behind the moving
// noisy edge. Opacity only falls once the contracting aperture is nearly
// closed, so opening and closing read as spatial sweeps instead of fades.
return MathHelper.SmoothStep(0f, 1f,
MathHelper.Clamp(animation / 0.1f, 0f, 1f));
}
private static void DrawManifestationWave(
SpriteBatch batch,
Vector2 center,
float finiteRadius,
float mastery,
float domainReveal,
float progress,
bool drawBackdrop,
bool drawBoundary)
{
DeathDomainBackdropTextureSystem.GetExpandedDomainDrawBounds(
out Vector2 destination,
out float destinationWidth,
out float destinationHeight);
Vector2 topRight = destination + Vector2.UnitX * destinationWidth;
Vector2 bottomLeft = destination + Vector2.UnitY * destinationHeight;
Vector2 bottomRight = destination + new Vector2(destinationWidth, destinationHeight);
float screenRadius = Math.Max(
Math.Max(Vector2.Distance(center, destination), Vector2.Distance(center, topRight)),
Math.Max(Vector2.Distance(center, bottomLeft), Vector2.Distance(center, bottomRight)));
float expansion = MathHelper.SmoothStep(0f, 1f, progress);
float radius = MathHelper.Lerp(Math.Max(24f, finiteRadius), screenRadius + 72f, expansion);
float time = Main.GlobalTimeWrappedHourly;
float noiseAmplitude = MathHelper.Lerp(46f, 24f, expansion)
* (0.88f + (float)Math.Sin(time * 4.1f) * 0.12f);
float backdropReveal = MathHelper.SmoothStep(0f, 1f,
MathHelper.Clamp(progress / 0.14f, 0f, 1f)) * domainReveal;
if (drawBackdrop)
{
DrawClippedFullScreenBackdrop(
batch,
center,
radius,
time * 1.15f,
31.7f,
noiseAmplitude,
mastery,
backdropReveal,
destination,
destinationWidth,
destinationHeight);
}
if (drawBoundary)
{
float edgeFade = (float)Math.Sin(progress * MathHelper.Pi);
edgeFade = (float)Math.Sqrt(Math.Max(0f, edgeFade)) * domainReveal;
DrawNoiseBoundary(batch, center, radius + 8f, -time * 1.38f,
47.3f, noiseAmplitude * 1.16f, 10f, edgeFade * 0.22f,
mastery, gaps: true);
DrawNoiseBoundary(batch, center, radius, time * 1.15f,
31.7f, noiseAmplitude, 5.5f, edgeFade * 0.92f,
mastery, gaps: false);
DrawNoiseBoundary(batch, center, radius - 10f, time * 1.72f,
63.1f, noiseAmplitude * 0.58f, 2.4f, edgeFade * 0.48f,
mastery, gaps: true);
}
}
private static void DrawClippedFullScreenBackdrop(
SpriteBatch batch,
Vector2 center,
float baseRadius,
float noiseTime,
float noiseSeed,
float noiseAmplitude,
float mastery,
float reveal,
Vector2 destination,
float destinationWidth,
float destinationHeight)
{
const int boundarySegments = 192;
const int maximumBands = 1024;
Span<Vector2> boundary = stackalloc Vector2[boundarySegments];
float extent = 0f;
for (int index = 0; index < boundarySegments; index++)
{
float angle = MathHelper.TwoPi * index / boundarySegments;
float radius = Math.Max(1f,
baseRadius + BoundaryNoise(angle, noiseTime, noiseSeed) * noiseAmplitude);
boundary[index] = angle.ToRotationVector2() * radius;
extent = Math.Max(extent, Math.Abs(boundary[index].Y));
}
int bands = Math.Clamp((int)Math.Ceiling(extent * 2f / 8f), 1, maximumBands);
float bandHeight = extent * 2f / bands;
Span<float> leftEdges = stackalloc float[bands];
Span<float> rightEdges = stackalloc float[bands];
for (int band = 0; band < bands; band++)
{
float scanY = -extent + (band + 0.5f) * bandHeight;
float left = float.MaxValue;
float right = float.MinValue;
for (int edge = 0; edge < boundarySegments; edge++)
{
Vector2 start = boundary[edge];
Vector2 end = boundary[(edge + 1) % boundarySegments];
if ((start.Y <= scanY && end.Y > scanY)
|| (end.Y <= scanY && start.Y > scanY))
{
float amount = (scanY - start.Y) / (end.Y - start.Y);
float intersectionX = MathHelper.Lerp(start.X, end.X, amount);
left = Math.Min(left, intersectionX);
right = Math.Max(right, intersectionX);
}
}
leftEdges[band] = left;
rightEdges[band] = right;
}
float destinationRight = destination.X + destinationWidth;
float destinationBottom = destination.Y + destinationHeight;
for (int layer = 0; layer < 3; layer++)
{
float opacity = layer switch
{
0 => MathHelper.Lerp(0.88f, 1f, mastery) * reveal,
1 => MathHelper.Lerp(0.72f, 0.9f, mastery) * reveal,
_ => MathHelper.Lerp(0.78f, 0.96f, mastery) * reveal
};
Texture2D texture = DeathDomainBackdropTextureSystem.GetLayer(layer);
DeathDomainBackdropTextureSystem.GetExpandedDomainSourceFrame(layer,
out float sourceOffsetX, out float sourceOffsetY,
out float viewWidth, out float viewHeight);
for (int band = 0; band < bands; band++)
{
float left = leftEdges[band];
float right = rightEdges[band];
if (left >= right)
continue;
float scanY = -extent + (band + 0.5f) * bandHeight;
float drawLeft = Math.Max(destination.X, center.X + left);
float drawRight = Math.Min(destinationRight, center.X + right);
float drawTop = Math.Max(destination.Y,
center.Y + scanY - bandHeight * 0.5f);
float drawBottom = Math.Min(destinationBottom,
center.Y + scanY + bandHeight * 0.5f);
if (drawLeft >= drawRight || drawTop >= drawBottom)
continue;
float normalizedLeft = (drawLeft - destination.X) / destinationWidth;
float normalizedRight = (drawRight - destination.X) / destinationWidth;
float normalizedTop = (drawTop - destination.Y) / destinationHeight;
float normalizedBottom = (drawBottom - destination.Y) / destinationHeight;
DrawBackdropBand(
batch,
texture,
new Vector2(drawLeft, drawTop),
drawRight - drawLeft,
drawBottom - drawTop,
sourceOffsetX + normalizedLeft * viewWidth,
sourceOffsetY + normalizedTop * viewHeight,
Math.Max(0.5f, (normalizedRight - normalizedLeft) * viewWidth),
Math.Max(0.5f, (normalizedBottom - normalizedTop) * viewHeight),
opacity);
}
}
}
private static void DrawFullScreenBackdrop(SpriteBatch batch, float mastery, float reveal)
{
DeathDomainBackdropTextureSystem.GetExpandedDomainDrawBounds(
out Vector2 destination, out float destinationWidth,
out float destinationHeight);
for (int layer = 0; layer < 3; layer++)
{
float opacity = layer switch
{
0 => MathHelper.Lerp(0.88f, 1f, mastery) * reveal,
1 => MathHelper.Lerp(0.72f, 0.9f, mastery) * reveal,
_ => MathHelper.Lerp(0.78f, 0.96f, mastery) * reveal
};
Texture2D texture = DeathDomainBackdropTextureSystem.GetLayer(layer);
DeathDomainBackdropTextureSystem.GetExpandedDomainSourceFrame(layer,
out float sourceOffsetX, out float sourceOffsetY, out float viewWidth, out float viewHeight);
DrawBackdropBand(
batch,
texture,
destination,
destinationWidth,
destinationHeight,
sourceOffsetX,
sourceOffsetY,
viewWidth,
viewHeight,
opacity);
}
DrawTransientBackdropBlackHoles(
batch,
destination + new Vector2(destinationWidth, destinationHeight) * 0.5f,
new Vector2(destinationWidth, destinationHeight) * 0.5f,
mastery,
reveal,
0f,
Main.worldID);
}
private static void DrawTransientBackdropBlackHoles(
SpriteBatch batch,
Vector2 center,
Vector2 halfExtent,
float mastery,
float domainReveal,
float clipRadius,
int seedGroup)
{
if (domainReveal <= 0.01f || halfExtent.X <= 8f || halfExtent.Y <= 8f)
return;
bool clipped = clipRadius > 0f;
int largeCount = clipped ? 1 : 2;
int smallCount = clipped ? 3 : 6;
float minimumExtent = Math.Min(halfExtent.X, halfExtent.Y);
float largeRadius = clipped
? MathHelper.Clamp(clipRadius * 0.13f, 13f, 52f)
: MathHelper.Clamp(minimumExtent * 0.115f, 58f, 138f);
float smallRadius = clipped
? MathHelper.Clamp(clipRadius * 0.058f, 7f, 25f)
: MathHelper.Clamp(minimumExtent * 0.043f, 20f, 52f);
for (int slot = 0; slot < largeCount + smallCount; slot++)
{
bool large = slot < largeCount;
int seed = seedGroup * 37 + slot * 193 + (large ? 701 : 1301);
if (!TryGetBackdropHoleAnimation(seed, large,
out float opacity, out float scale, out float collapse))
{
continue;
}
Vector2 position;
if (clipped)
{
float angle = Hash01(seed + 11) * MathHelper.TwoPi;
float distance = clipRadius * MathHelper.Lerp(
large ? 0.19f : 0.27f,
large ? 0.42f : 0.62f,
Hash01(seed + 23));
position = center + angle.ToRotationVector2() * distance;
}
else
{
position = center + new Vector2(
MathHelper.Lerp(-0.76f, 0.76f, Hash01(seed + 11)) * halfExtent.X,
MathHelper.Lerp(-0.68f, 0.68f, Hash01(seed + 23)) * halfExtent.Y);
}
float finalOpacity = opacity * domainReveal * MathHelper.Lerp(0.72f, 1f, mastery);
float rotation = Hash01(seed + 41) * MathHelper.TwoPi
+ Main.GlobalTimeWrappedHourly * (large ? 0.055f : -0.16f);
if (large)
{
DrawLargeBackdropBlackHole(batch, position,
largeRadius * scale, rotation, finalOpacity, collapse, mastery, seed);
}
else
{
DrawSmallBackdropBlackHole(batch, position,
smallRadius * scale, rotation, finalOpacity, collapse, seed);
}
}
}
private static bool TryGetBackdropHoleAnimation(
int seed,
bool large,
out float opacity,
out float scale,
out float collapse)
{
float cycle = large ? 780f : 570f;
float visibleDuration = large ? 470f : 330f;
float introDuration = large ? 58f : 40f;
float outroDuration = large ? 152f : 112f;
float offset = Hash01(seed + 67) * cycle;
float age = PositiveModulo((float)Main.GameUpdateCount + offset, cycle);
if (age >= visibleDuration)
{
opacity = 0f;
scale = 0f;
collapse = 0f;
return false;
}
float intro = MathHelper.SmoothStep(0f, 1f, MathHelper.Clamp(age / introDuration, 0f, 1f));
float outro = MathHelper.SmoothStep(0f, 1f,
MathHelper.Clamp((visibleDuration - age) / outroDuration, 0f, 1f));
opacity = Math.Min(intro, outro);
collapse = 1f - outro;
float openingScale = MathHelper.Lerp(0.08f, 1f, 1f - (float)Math.Pow(1f - intro, 3f));
openingScale *= 1f + (float)Math.Sin(intro * MathHelper.Pi) * 0.12f;
scale = openingScale * MathHelper.Lerp(0.18f, 1f, outro);
return opacity > 0.005f && scale > 0.02f;
}
private static void DrawSmallBackdropBlackHole(
SpriteBatch batch,
Vector2 center,
float radius,
float rotation,
float opacity,
float collapse,
int seed)
{
if (radius <= 1f || opacity <= 0.001f)
return;
DrawBackdropDomainBlackHole(batch, center, radius, rotation, opacity,
collapse, 0.34f, seed, large: false);
}
private static void DrawLargeBackdropBlackHole(
SpriteBatch batch,
Vector2 center,
float radius,
float rotation,
float opacity,
float collapse,
float mastery,
int seed)
{
if (radius <= 2f || opacity <= 0.001f)
return;
DrawBackdropDomainBlackHole(batch, center, radius, rotation, opacity,
collapse, mastery, seed, large: true);
}
private static void DrawBackdropDomainBlackHole(
SpriteBatch batch,
Vector2 center,
float radius,
float rotation,
float opacity,
float collapse,
float mastery,
int seed,
bool large)
{
float time = Main.GlobalTimeWrappedHourly;
float coreRadius = radius * (large ? 0.68f : 0.62f);
float pulse = opacity * (0.88f + (float)Math.Sin(time * 2.7f + seed) * 0.08f);
DeathDomainPrimitiveTextureSystem.DrawRadialGradient(batch, center,
radius * (large ? 1.95f : 1.65f), new Color(42, 0, 16) * (opacity * 0.42f));
DrawBlackHoleSuctionStreams(batch, center, radius, 1f, rotation,
opacity * (1f - collapse * 0.38f), seed, large);
// Five inward-curving arms deliberately mirror the player's event
// horizon instead of the old unrelated crossed-orbit atom symbol.
for (int arm = 0; arm < 5; arm++)
DrawAccretionSpiral(batch, center, coreRadius * 0.96f, radius * 1.06f,
time + rotation * 0.72f, arm, mastery, pulse);
DeathDomainPrimitiveTextureSystem.DrawSmoothDisc(batch, center, coreRadius,
new Color(0, 0, 2) * (opacity * 0.98f));
DrawNoiseBoundary(batch, center, coreRadius + radius * 0.07f,
time * 0.76f + rotation, seed * 0.013f, radius * 0.07f,
large ? 3.2f : 2.1f, opacity * 0.34f, mastery, gaps: true);
DrawNoiseBoundary(batch, center, coreRadius,
-time * 0.94f + rotation, seed * 0.021f, radius * 0.045f,
large ? 2.4f : 1.55f, opacity * 0.92f, mastery, gaps: false);
}
private static void DrawFilledEllipse(
SpriteBatch batch,
Vector2 center,
float radiusX,
float radiusY,
Color color)
{
int bands = Math.Clamp((int)Math.Ceiling(radiusY), 6, 72);
for (int band = 0; band < bands; band++)
{
float normalizedY = (band + 0.5f) / bands * 2f - 1f;
float halfWidth = radiusX * (float)Math.Sqrt(Math.Max(0f, 1f - normalizedY * normalizedY));
float y = center.Y + normalizedY * radiusY;
DrawSegment(batch, new Vector2(center.X - halfWidth, y),
new Vector2(halfWidth * 2f, 0f), color, Math.Max(1f, radiusY * 2f / bands + 0.6f));
}
}
private static void DrawEllipseArc(
SpriteBatch batch,
Vector2 center,
float radiusX,
float radiusY,
float rotation,
float startAngle,
float endAngle,
int segments,
Color color,
float width)
{
Vector2 AxisPoint(float angle)
{
Vector2 local = new((float)Math.Cos(angle) * radiusX, (float)Math.Sin(angle) * radiusY);
return center + local.RotatedBy(rotation);
}
Vector2 previous = AxisPoint(startAngle);
for (int index = 1; index <= segments; index++)
{
float angle = MathHelper.Lerp(startAngle, endAngle, index / (float)segments);
Vector2 current = AxisPoint(angle);
DrawSegment(batch, previous, current - previous, color, width);
previous = current;
}
}
private static float Hash01(int seed)
{
double value = Math.Sin(seed * 12.9898 + 78.233) * 43758.5453;
return (float)(value - Math.Floor(value));
}
private static void DrawDomainRippleWaves(SpriteBatch batch, Vector2 center,
float radius, float time, float mastery, float pulse)
{
for (int wave = 0; wave < 3; wave++)
{
float travel = PositiveModulo(time * (0.22f + mastery * 0.08f)
+ wave / 3f, 1f);
float waveRadius = radius + MathHelper.Lerp(8f, 64f, travel);
float opacity = (1f - travel) * pulse * (0.14f + mastery * 0.08f);
const int segments = 128;
float firstRadius = waveRadius + (float)Math.Sin(-time * 2.1f + wave) * 2.6f;
Vector2 previous = center + Vector2.UnitX * firstRadius;
for (int segment = 1; segment <= segments; segment++)
{
float angle = MathHelper.TwoPi * segment / segments;
float ripple = (float)Math.Sin(angle * 9f - time * 2.1f + wave * 1.7f) * 2.6f
+ (float)Math.Sin(angle * 17f + time * 1.3f) * 0.9f;
Vector2 current = center + angle.ToRotationVector2() * (waveRadius + ripple);
DeathDomainPrimitiveTextureSystem.DrawSmoothStrip(batch, previous, current,
Color.Lerp(new Color(122, 4, 34), new Color(255, 54, 92), mastery)
* opacity,
MathHelper.Lerp(1.1f, 2.4f, 1f - travel));
previous = current;
}
}
}
private static void DrawBlackHoleSuctionStreams(SpriteBatch batch, Vector2 center,
float radius, float verticalScale, float rotation, float opacity,
int seed, bool large)
{
int streams = large ? 10 : 6;
int points = large ? 30 : 22;
float time = Main.GlobalTimeWrappedHourly;
for (int stream = 0; stream < streams; stream++)
{
float streamOffset = stream * MathHelper.TwoPi / streams + seed * 0.013f;
float movingHead = 1f - PositiveModulo(time * (large ? 0.42f : 0.58f)
+ stream / (float)streams, 1f);
Vector2 previous = Vector2.Zero;
for (int point = 0; point < points; point++)
{
float inward = point / (points - 1f);
float curved = inward * inward * (3f - 2f * inward);
float localAngle = streamOffset - time * (large ? 0.18f : 0.27f)
+ inward * (large ? 1.72f : 1.38f);
float localRadius = MathHelper.Lerp(radius * (large ? 2.75f : 2.05f),
radius * 0.56f, curved);
localRadius += (float)Math.Sin(localAngle * 5f + seed * 0.07f)
* radius * 0.025f * (1f - inward);
Vector2 local = new((float)Math.Cos(localAngle) * localRadius,
(float)Math.Sin(localAngle) * localRadius * verticalScale);
Vector2 current = center + local.RotatedBy(rotation);
if (point > 0)
{
float body = (float)Math.Sin(inward * MathHelper.Pi);
float glow = 1f - MathHelper.SmoothStep(0f, 1f,
MathHelper.Clamp(Math.Abs(inward - movingHead) / 0.20f, 0f, 1f));
// The near half of the tilted disc is brighter and thicker;
// the far half remains visible behind the black core, giving
// the accretion flow genuine front/back depth.
float nearSide = 0.55f + 0.45f * Math.Max(0f, (float)Math.Sin(localAngle));
Color color = Color.Lerp(new Color(58, 0, 12),
new Color(255, 42, 78), inward * 0.82f + glow * 0.18f);
float streamOpacity = opacity * body * nearSide * (0.18f + glow * 0.62f);
float width = MathHelper.Lerp(0.65f, large ? 3.6f : 2.2f, inward)
* (0.72f + glow * 0.38f);
DeathDomainPrimitiveTextureSystem.DrawSmoothStrip(batch, previous,
current, color * streamOpacity, width);
}
previous = current;
}
}
}
private static void DrawFilledNoiseDisc(
SpriteBatch batch,
Vector2 center,
float baseRadius,
float time,
float seed,
float noiseAmplitude,
Color color)
{
const int boundarySegments = 192;
float preferredBandHeight = MathHelper.Clamp(baseRadius / 240f, 3f, 9f);
Span<Vector2> boundary = stackalloc Vector2[boundarySegments];
float extent = 0f;
for (int index = 0; index < boundarySegments; index++)
{
float angle = MathHelper.TwoPi * index / boundarySegments;
float radius = Math.Max(1f, baseRadius + BoundaryNoise(angle, time, seed) * noiseAmplitude);
boundary[index] = angle.ToRotationVector2() * radius;
extent = Math.Max(extent, Math.Abs(boundary[index].Y));
}
int bands = Math.Max(1, (int)Math.Ceiling(extent * 2f / preferredBandHeight));
float bandHeight = extent * 2f / bands;
for (int index = 0; index < bands; index++)
{
float scanY = -extent + (index + 0.5f) * bandHeight;
float left = float.MaxValue;
float right = float.MinValue;
for (int edge = 0; edge < boundarySegments; edge++)
{
Vector2 start = boundary[edge];
Vector2 end = boundary[(edge + 1) % boundarySegments];
if ((start.Y <= scanY && end.Y > scanY) || (end.Y <= scanY && start.Y > scanY))
{
float amount = (scanY - start.Y) / (end.Y - start.Y);
float intersectionX = MathHelper.Lerp(start.X, end.X, amount);
left = Math.Min(left, intersectionX);
right = Math.Max(right, intersectionX);
}
}
if (left >= right)
continue;
Vector2 fillStart = new(center.X + left, center.Y + scanY);
Vector2 fillEnd = new(center.X + right, center.Y + scanY);
DeathDomainPrimitiveTextureSystem.DrawSmoothLine(batch, fillStart, fillEnd,
color, bandHeight + 0.8f);
}
}
private static void DrawNoiseBoundary(
SpriteBatch batch,
Vector2 center,
float baseRadius,
float time,
float seed,
float noiseAmplitude,
float width,
float opacity,
float mastery,
bool gaps)
{
const int segments = 256;
float previousAngle = 0f;
float previousRadius = baseRadius + BoundaryNoise(previousAngle, time, seed) * noiseAmplitude;
Vector2 previous = center + Vector2.UnitX * previousRadius;
Color darkRed = Color.Lerp(new Color(58, 0, 12), new Color(118, 0, 25), mastery);
Color brightRed = Color.Lerp(new Color(225, 12, 42), new Color(255, 72, 105), mastery);
for (int index = 1; index <= segments; index++)
{
float angle = MathHelper.TwoPi * index / segments;
float noisyRadius = baseRadius + BoundaryNoise(angle, time, seed) * noiseAmplitude;
Vector2 current = center + angle.ToRotationVector2() * noisyRadius;
Vector2 segment = current - previous;
float middleAngle = (previousAngle + angle) * 0.5f;
float flow = 0.5f + 0.5f * (float)Math.Sin(middleAngle * 3f - time * 1.9f + seed);
float arcMask = 0.58f + 0.42f * (float)Math.Sin(middleAngle * 2f + time * 0.47f + seed * 1.31f);
arcMask *= 0.72f + 0.28f * (float)Math.Sin(middleAngle * 7f - time * 0.83f + seed * 0.73f);
float visibility = gaps ? MathHelper.SmoothStep(0.08f, 0.62f, arcMask) : 1f;
if (visibility > 0.025f)
{
Color color = Color.Lerp(darkRed, brightRed, flow);
DrawSegment(batch, previous, segment, color * (opacity * visibility), width * (0.72f + flow * 0.42f));
}
previousAngle = angle;
previous = current;
}
}
private static float BoundaryNoise(float angle, float time, float seed)
{
float low = (float)Math.Sin(angle * 3f + time * 0.72f + seed) * 0.48f;
float middle = (float)Math.Sin(angle * 7f - time * 1.11f + seed * 1.73f) * 0.29f;
float high = (float)Math.Sin(angle * 13f + time * 1.67f + seed * 0.37f) * 0.16f;
float grain = (float)Math.Sin(angle * 29f - time * 2.23f + seed * 2.17f) * 0.07f;
return low + middle + high + grain;
}
private static void DrawInwardSuctionWaves(
SpriteBatch batch,
Vector2 center,
float edgeRadius,
float coreRadius,
float time,
float mastery,
float pulse)
{
// Preserve the original long black-hole accretion lines. Only the animated
// highlight direction is reversed so they now read as being swallowed by
// the event horizon instead of being emitted from it.
int streamCount = 7 + (int)Math.Round(mastery * 5f);
float outerRadius = edgeRadius + MathHelper.Lerp(72f, 126f, mastery);
for (int stream = 0; stream < streamCount; stream++)
{
float seed = stream * 4.731f + 2.4f;
float movingHead = 1f - PositiveModulo(time * MathHelper.Lerp(0.48f, 0.78f, mastery)
+ stream / (float)streamCount, 1f);
float baseAngle = stream * MathHelper.TwoPi / streamCount
- time * (0.13f + mastery * 0.08f)
+ BoundaryNoise(stream, time * 0.18f, seed) * 0.06f;
const int points = 46;
Vector2 previous = Vector2.Zero;
for (int point = 0; point < points; point++)
{
float inward = point / (points - 1f);
float curved = inward * inward * (3f - 2f * inward);
float angle = baseAngle + inward * MathHelper.Lerp(1.08f, 1.54f, mastery);
float radius = MathHelper.Lerp(outerRadius, edgeRadius + 2f, curved)
+ BoundaryNoise(angle, -time * 0.82f, seed) * MathHelper.Lerp(5.2f, 1.2f, inward);
Vector2 current = center + angle.ToRotationVector2() * radius;
if (point > 0)
{
float bodyFade = (float)Math.Sin(inward * MathHelper.Pi);
float distanceFromHead = Math.Abs(inward - movingHead);
float movingGlow = 1f - MathHelper.SmoothStep(0.02f, 0.25f, distanceFromHead);
float opacity = bodyFade * pulse * (0.16f + movingGlow * 0.64f);
Color color = Color.Lerp(
new Color(18, 0, 8),
Color.Lerp(new Color(174, 3, 40), new Color(255, 34, 76), mastery),
inward * (0.45f + movingGlow * 0.55f));
float width = MathHelper.Lerp(0.55f, 3.4f + mastery * 1.7f, inward)
* (0.68f + movingGlow * 0.48f);
DrawSegment(batch, previous, current - previous, color * opacity, width);
}
previous = current;
}
}
}
private static void DrawOutwardPressureWaves(
SpriteBatch batch,
Vector2 center,
float coreRadius,
float time,
float mastery,
float pulse)
{
// This is the same continuous accretion-line construction used outside,
// with radius progression, spiral turn and highlight travel all inverted.
int streamCount = 7 + (int)Math.Round(mastery * 5f);
float innerRadius = Math.Max(5f, coreRadius * 0.06f);
float outerRadius = coreRadius * 0.90f;
for (int stream = 0; stream < streamCount; stream++)
{
float seed = 42.7f + stream * 3.117f;
float movingHead = PositiveModulo(time * MathHelper.Lerp(0.48f, 0.78f, mastery)
+ stream / (float)streamCount, 1f);
float baseAngle = stream * MathHelper.TwoPi / streamCount
+ time * (0.13f + mastery * 0.08f)
- BoundaryNoise(stream, time * 0.18f, seed) * 0.06f;
const int points = 40;
Vector2 previous = Vector2.Zero;
for (int point = 0; point < points; point++)
{
float outward = point / (points - 1f);
float curved = outward * outward * (3f - 2f * outward);
float angle = baseAngle - outward * MathHelper.Lerp(1.08f, 1.54f, mastery);
float radius = MathHelper.Lerp(innerRadius, outerRadius, curved)
+ BoundaryNoise(angle, time * 0.82f, seed) * MathHelper.Lerp(1.2f, 4.6f, outward);
Vector2 current = center + angle.ToRotationVector2() * radius;
if (point > 0)
{
float bodyFade = (float)Math.Sin(outward * MathHelper.Pi);
float distanceFromHead = Math.Abs(outward - movingHead);
float movingGlow = 1f - MathHelper.SmoothStep(0.02f, 0.25f, distanceFromHead);
float opacity = bodyFade * pulse * (0.13f + movingGlow * 0.58f);
Color color = Color.Lerp(
new Color(255, 116, 136),
Color.Lerp(new Color(112, 2, 28), new Color(220, 18, 58), mastery),
outward * 0.78f);
float width = MathHelper.Lerp(3.5f + mastery * 1.4f, 0.65f, outward)
* (0.66f + movingGlow * 0.46f);
DrawSegment(batch, previous, current - previous, color * opacity, width);
}
previous = current;
}
}
}
private static void DrawAccretionSpiral(SpriteBatch batch, Vector2 center, float coreRadius, float outerRadius, float time, int arm, float mastery, float pulse)
{
const int points = 34;
float armOffset = arm * MathHelper.TwoPi / 5f;
Vector2 previous = Vector2.Zero;
for (int index = 0; index < points; index++)
{
float progress = index / (points - 1f);
float angle = armOffset - time * (0.72f + arm * 0.035f) + progress * MathHelper.Pi * 1.12f;
float radius = MathHelper.Lerp(coreRadius * 0.88f, outerRadius, progress);
radius += BoundaryNoise(angle, time, arm * 2.7f) * 2.4f;
Vector2 current = center + angle.ToRotationVector2() * radius;
if (index > 0)
{
Vector2 segment = current - previous;
Color color = Color.Lerp(new Color(255, 70, 92), new Color(82, 0, 22), progress);
float fade = (float)Math.Sin(progress * MathHelper.Pi);
DrawSegment(batch, previous, segment, color * (fade * pulse * 0.54f), MathHelper.Lerp(2.6f + mastery, 0.7f, progress));
}
previous = current;
}
}
private static void DrawCircle(SpriteBatch batch, Vector2 center, float radius, int segments, Color color, float width, float rotation)
{
segments = Math.Max(segments,
Math.Min(192, (int)Math.Ceiling(MathHelper.TwoPi * radius / 18f)));
Vector2 previous = center + rotation.ToRotationVector2() * radius;
for (int index = 1; index <= segments; index++)
{
float angle = rotation + MathHelper.TwoPi * index / segments;
Vector2 current = center + angle.ToRotationVector2() * radius;
DeathDomainPrimitiveTextureSystem.DrawSmoothStrip(batch, previous, current, color, width);
previous = current;
}
}
private static void DrawSegment(SpriteBatch batch, Vector2 start, Vector2 segment, Color color, float width)
{
DeathDomainPrimitiveTextureSystem.DrawSmoothStrip(batch,
start, start + segment, color, width);
}
private static float PositiveModulo(float value, float modulus)
{
float result = value % modulus;
return result < 0f ? result + modulus : result;
}
private static float GetMasteryProgress(int mastery)
{
// The character-owned Death Domain tree is packed into the legacy 5..75
// visual range so existing shaders keep the same progression curve.
return MathHelper.Clamp((mastery - 5f) / 70f, 0f, 1f);
}
private void ClearAnimationState()
{
Array.Clear(domainAnimation, 0, domainAnimation.Length);
Array.Clear(descentAnimation, 0, descentAnimation.Length);
Array.Clear(cachedRadius, 0, cachedRadius.Length);
Array.Clear(cachedMastery, 0, cachedMastery.Length);
Array.Clear(cachedFullScreen, 0, cachedFullScreen.Length);
}
}
using SoulHarvest.Items;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using Terraria;
using Terraria.GameContent;
using Terraria.ModLoader;
namespace SoulHarvest.Common;
[Autoload(Side = ModSide.Client)]
internal class DeathDomainVisualSystem : ModSystem
{
private const float OpenAnimationSpeed = 1f / 42f;
private const float CloseAnimationSpeed = 1f / 42f;
private const float DescentAnimationSpeed = 1f / 52f;
private const float DepartureAnimationSpeed = 1f / 42f;
private readonly float[] domainAnimation = new float[Main.maxPlayers];
private readonly float[] descentAnimation = new float[Main.maxPlayers];
private readonly float[] cachedRadius = new float[Main.maxPlayers];
private readonly int[] cachedMastery = new int[Main.maxPlayers];
private readonly bool[] cachedFullScreen = new bool[Main.maxPlayers];
public override void Load()
{
On_Main.DoDraw_WallsTilesNPCs += DrawDomainBackdropsBeforeWallsAndTiles;
}
public override void Unload()
{
On_Main.DoDraw_WallsTilesNPCs -= DrawDomainBackdropsBeforeWallsAndTiles;
}
public override void PostUpdateEverything()
{
if (Main.gameMenu)
{
ClearAnimationState();
return;
}
for (int index = 0; index < Main.maxPlayers; index++)
{
Player player = Main.player[index];
if (!player.active)
{
domainAnimation[index] = 0f;
descentAnimation[index] = 0f;
cachedRadius[index] = 0f;
cachedMastery[index] = 0;
cachedFullScreen[index] = false;
continue;
}
MyPlayer data = player.GetModPlayer<MyPlayer>();
DeathNecklace? necklace = data.ActiveDeathNecklace;
if (necklace is not null)
{
cachedRadius[index] = necklace.DomainRadius;
cachedMastery[index] = 5 + (int)MathF.Round(data.DeathDomainProgression.VisualMastery * 70f);
}
bool enabled = !player.dead && necklace is not null && data.DeathDomainEnabled;
bool descended = enabled && data.IsDeathDomainDescended(necklace);
cachedFullScreen[index] = descended;
float speed = enabled ? OpenAnimationSpeed : CloseAnimationSpeed;
domainAnimation[index] = MathHelper.Clamp(
domainAnimation[index] + (enabled ? speed : -speed),
0f,
1f);
float descentSpeed = descended
? DescentAnimationSpeed
: DepartureAnimationSpeed;
descentAnimation[index] = MathHelper.Clamp(
descentAnimation[index] + (descended ? descentSpeed : -descentSpeed),
0f,
1f);
}
}
public override void OnWorldUnload()
{
ClearAnimationState();
}
public override void PostDrawTiles()
{
Player localPlayer = Main.LocalPlayer;
if (Main.gameMenu || !localPlayer.active)
return;
bool anyVisibleDomain = false;
for (int index = 0; index < Main.maxPlayers; index++)
{
if (domainAnimation[index] > 0.001f)
{
anyVisibleDomain = true;
break;
}
}
if (!anyVisibleDomain)
return;
SpriteBatch batch = Main.spriteBatch;
batch.Begin(
SpriteSortMode.Deferred,
BlendState.AlphaBlend,
SamplerState.LinearClamp,
DepthStencilState.None,
RasterizerState.CullNone,
null,
Main.GameViewMatrix.TransformationMatrix);
for (int index = 0; index < Main.maxPlayers; index++)
{
Player player = Main.player[index];
float animation = domainAnimation[index];
if (!player.active || animation <= 0.001f)
continue;
float radius = cachedRadius[index];
int mastery = cachedMastery[index];
bool fullScreen = cachedFullScreen[index];
if (radius <= 0f)
radius = 160f;
Vector2 screenCenter = player.Center - Main.screenPosition;
if (!fullScreen
&& (screenCenter.X < -radius
|| screenCenter.X > Main.screenWidth + radius
|| screenCenter.Y < -radius
|| screenCenter.Y > Main.screenHeight + radius))
{
continue;
}
if (!fullScreen)
DrawDomainRing(batch, screenCenter, radius, mastery, animation, drawBackdrop: false);
else
DrawFullScreenDomain(batch, screenCenter, radius, mastery, animation);
float descent = descentAnimation[index];
if (descent > 0.001f && descent < 0.999f)
{
DrawManifestationWave(
batch,
screenCenter,
radius,
GetMasteryProgress(mastery),
GetDomainApertureReveal(animation),
descent,
drawBackdrop: false,
drawBoundary: true);
}
}
batch.End();
}
private static void DrawDomainRing(
SpriteBatch batch,
Vector2 center,
float targetRadius,
int mastery,
float animation,
bool drawBackdrop = true)
{
float time = Main.GlobalTimeWrappedHourly;
float progress = MathHelper.SmoothStep(0f, 1f, animation);
float reveal = GetDomainApertureReveal(animation);
float rangeProgress = MathHelper.Clamp(
(targetRadius - 160f) / (DeathNecklace.RadiusGrowthPerLevel * (DeathNecklace.MaxCoreLevel - 1)),
0f,
1f);
float manifestation = MathHelper.Lerp(0.38f, 1f, rangeProgress);
float manifestedReveal = reveal * manifestation;
float transition = (float)Math.Sqrt(Math.Max(0f,
(float)Math.Sin(animation * MathHelper.Pi)));
float overshoot = 1f + (float)Math.Sin(progress * MathHelper.Pi) * 0.028f;
float radius = MathHelper.Lerp(8f, targetRadius, progress) * overshoot;
float pulse = (0.82f + (float)Math.Sin(time * 3.2f) * 0.12f) * manifestedReveal;
float masteryProgress = GetMasteryProgress(mastery);
DrawBlackHoleField(batch, center, radius, masteryProgress, pulse, time, manifestedReveal, drawBackdrop);
DrawNoiseBoundary(batch, center, radius + 3f, time, 0.4f,
12f + transition * 28f, 9f + transition * 3f,
(0.18f + transition * 0.16f) * pulse, masteryProgress, gaps: true);
DrawNoiseBoundary(batch, center, radius, time, 1.7f,
9f + transition * 20f, 5f + transition * 1.8f,
(0.42f + transition * 0.34f) * pulse, masteryProgress, gaps: true);
DrawNoiseBoundary(batch, center, radius - 2f, time, 3.3f,
7f + transition * 14f, 2.7f + masteryProgress + transition * 1.6f,
(0.94f + transition * 0.22f) * pulse, masteryProgress, gaps: false);
DrawNoiseBoundary(batch, center, radius - 7f, -time, 5.1f, 5f, 1.4f, 0.58f * pulse, masteryProgress, gaps: true);
DrawDomainRippleWaves(batch, center, radius, time, masteryProgress, pulse);
}
private static void DrawBlackHoleField(
SpriteBatch batch,
Vector2 center,
float radius,
float mastery,
float pulse,
float time,
float reveal,
bool drawBackdrop)
{
float coreGap = MathHelper.Lerp(22f, 14f, mastery);
float coreRadius = radius > coreGap + 6f ? radius - coreGap : radius * 0.58f;
for (int arm = 0; arm < 5; arm++)
DrawAccretionSpiral(batch, center, coreRadius, Math.Max(coreRadius + 2f, radius - 3f), time, arm, mastery, pulse);
float gap = Math.Max(2f, radius - coreRadius);
DrawNoiseBoundary(batch, center, coreRadius + gap * 0.72f, -time * 1.35f, 9.4f, 4.5f, 7f, 0.18f * pulse, mastery, gaps: true);
DrawNoiseBoundary(batch, center, coreRadius + gap * 0.34f, time * 1.8f, 12.8f, 2.8f, 3.8f, 0.78f * pulse, mastery, gaps: true);
float coreNoiseTime = time * 0.8f;
const float coreNoiseSeed = 16.2f;
float coreNoiseAmplitude = Math.Min(coreRadius * 0.18f, MathHelper.Lerp(5f, 10f, mastery) * MathHelper.Lerp(0.35f, 1f, reveal));
if (drawBackdrop)
{
DrawClippedDeathDomainBackdrop(
batch,
center,
coreRadius,
coreNoiseTime,
coreNoiseSeed,
coreNoiseAmplitude,
mastery,
reveal);
}
// The old scan-band fill stamped hundreds of horizontal rounded strips
// over the centre and produced the visible television-line pattern. A
// single antialiased disc supplies the same restrained shadow without any
// row seams; the animated noisy boundary still defines its organic edge.
DeathDomainPrimitiveTextureSystem.DrawSmoothDisc(
batch,
center,
coreRadius,
new Color(3, 0, 10) * (MathHelper.Lerp(0.08f, 0.26f, mastery) * reveal));
DeathDomainPrimitiveTextureSystem.DrawRadialGradient(
batch,
center,
coreRadius * MathHelper.Lerp(0.48f, 0.58f, mastery),
new Color(255, 58, 84) * (MathHelper.Lerp(0.16f, 0.24f, mastery) * pulse));
DrawOutwardPressureWaves(batch, center, coreRadius, time, mastery, pulse);
DrawNoiseBoundary(batch, center, coreRadius, coreNoiseTime, coreNoiseSeed, coreNoiseAmplitude, 2.2f, 0.76f * pulse, mastery, gaps: false);
DrawInwardSuctionWaves(batch, center, radius, coreRadius, time, mastery, pulse);
}
private static void DrawClippedDeathDomainBackdrop(
SpriteBatch batch,
Vector2 center,
float baseRadius,
float noiseTime,
float noiseSeed,
float noiseAmplitude,
float mastery,
float reveal)
{
const int boundarySegments = 192;
float preferredBandHeight = MathHelper.Clamp(baseRadius / 280f, 2f, 8f);
Span<Vector2> boundary = stackalloc Vector2[boundarySegments];
float extent = 0f;
for (int index = 0; index < boundarySegments; index++)
{
float angle = MathHelper.TwoPi * index / boundarySegments;
float radius = Math.Max(1f, baseRadius + BoundaryNoise(angle, noiseTime, noiseSeed) * noiseAmplitude);
boundary[index] = angle.ToRotationVector2() * radius;
extent = Math.Max(extent, Math.Abs(boundary[index].Y));
}
int bands = Math.Max(1, (int)Math.Ceiling(extent * 2f / preferredBandHeight));
float bandHeight = extent * 2f / bands;
DeathDomainBackdropTextureSystem.GetExpandedDomainDrawBounds(
out Vector2 expandedDestination,
out float expandedDestinationWidth,
out float expandedDestinationHeight);
float expandedDestinationRight = expandedDestination.X
+ expandedDestinationWidth;
float expandedDestinationBottom = expandedDestination.Y
+ expandedDestinationHeight;
// Draw one complete layer at a time. This retains proper alpha compositing
// while allowing SpriteBatch to group hundreds of clipped scan bands into
// only three texture batches instead of switching textures for every row.
for (int layer = 0; layer < 3; layer++)
{
float opacity = layer switch
{
0 => MathHelper.Lerp(0.88f, 1f, mastery) * reveal,
1 => MathHelper.Lerp(0.72f, 0.9f, mastery) * reveal,
_ => MathHelper.Lerp(0.78f, 0.96f, mastery) * reveal
};
Texture2D texture = DeathDomainBackdropTextureSystem.GetLayer(layer);
DeathDomainBackdropTextureSystem.GetExpandedDomainSourceFrame(layer,
out float sourceOffsetX, out float sourceOffsetY,
out float viewWidth, out float viewHeight);
for (int index = 0; index < bands; index++)
{
float scanY = -extent + (index + 0.5f) * bandHeight;
float left = float.MaxValue;
float right = float.MinValue;
for (int edge = 0; edge < boundarySegments; edge++)
{
Vector2 start = boundary[edge];
Vector2 end = boundary[(edge + 1) % boundarySegments];
if ((start.Y <= scanY && end.Y > scanY) || (end.Y <= scanY && start.Y > scanY))
{
float amount = (scanY - start.Y) / (end.Y - start.Y);
float intersectionX = MathHelper.Lerp(start.X, end.X, amount);
left = Math.Min(left, intersectionX);
right = Math.Max(right, intersectionX);
}
}
if (left >= right)
continue;
float drawLeft = Math.Max(expandedDestination.X,
center.X + left);
float drawRight = Math.Min(expandedDestinationRight,
center.X + right);
float drawTop = Math.Max(expandedDestination.Y,
center.Y + scanY - bandHeight * 0.5f);
float drawBottom = Math.Min(expandedDestinationBottom,
center.Y + scanY + bandHeight * 0.5f);
if (drawLeft >= drawRight || drawTop >= drawBottom)
continue;
float normalizedLeft = (drawLeft - expandedDestination.X)
/ expandedDestinationWidth;
float normalizedRight = (drawRight - expandedDestination.X)
/ expandedDestinationWidth;
float normalizedTop = (drawTop - expandedDestination.Y)
/ expandedDestinationHeight;
float normalizedBottom = (drawBottom - expandedDestination.Y)
/ expandedDestinationHeight;
DrawBackdropBand(
batch,
texture,
new Vector2(drawLeft, drawTop),
drawRight - drawLeft,
drawBottom - drawTop,
sourceOffsetX + normalizedLeft * viewWidth,
sourceOffsetY + normalizedTop * viewHeight,
Math.Max(0.5f, (normalizedRight - normalizedLeft) * viewWidth),
Math.Max(0.5f, (normalizedBottom - normalizedTop) * viewHeight),
opacity);
}
}
}
private static void DrawBackdropBand(
SpriteBatch batch,
Texture2D texture,
Vector2 destination,
float destinationWidth,
float destinationHeight,
float sourceX,
float sourceY,
float sourceWidth,
float sourceHeight,
float opacity)
{
if (destinationWidth <= 0.05f || destinationHeight <= 0.05f || opacity <= 0.001f)
return;
float clampedSourceY = MathHelper.Clamp(sourceY, 0f, texture.Height - 0.001f);
float clampedSourceHeight = Math.Min(sourceHeight, texture.Height - clampedSourceY);
if (clampedSourceHeight <= 0.001f)
return;
float remainingSourceWidth = sourceWidth;
float consumedSourceWidth = 0f;
float wrappedSourceX = PositiveModulo(sourceX, texture.Width);
while (remainingSourceWidth > 0.001f)
{
float sourceWidthPart = Math.Min(remainingSourceWidth, texture.Width - wrappedSourceX);
float destinationWidthPart = destinationWidth * sourceWidthPart / sourceWidth;
float destinationX = destination.X + destinationWidth * consumedSourceWidth / sourceWidth;
DrawBackdropPart(
batch,
texture,
new Vector2(destinationX, destination.Y),
destinationWidthPart,
destinationHeight,
wrappedSourceX,
clampedSourceY,
sourceWidthPart,
clampedSourceHeight,
opacity);
remainingSourceWidth -= sourceWidthPart;
consumedSourceWidth += sourceWidthPart;
wrappedSourceX = 0f;
}
}
private static void DrawBackdropPart(
SpriteBatch batch,
Texture2D texture,
Vector2 destination,
float destinationWidth,
float destinationHeight,
float sourceX,
float sourceY,
float sourceWidth,
float sourceHeight,
float opacity)
{
int sourceLeft = Math.Clamp((int)Math.Floor(sourceX), 0, texture.Width - 1);
int sourceTop = Math.Clamp((int)Math.Floor(sourceY), 0, texture.Height - 1);
int sourceRight = Math.Clamp((int)Math.Ceiling(sourceX + sourceWidth), sourceLeft + 1, texture.Width);
int sourceBottom = Math.Clamp((int)Math.Ceiling(sourceY + sourceHeight), sourceTop + 1, texture.Height);
Rectangle source = new(sourceLeft, sourceTop, sourceRight - sourceLeft, sourceBottom - sourceTop);
batch.Draw(
texture,
destination,
source,
Color.White * opacity,
0f,
Vector2.Zero,
new Vector2(destinationWidth / source.Width, destinationHeight / source.Height),
SpriteEffects.None,
0f);
}
private static void DrawFullScreenDomain(
SpriteBatch batch,
Vector2 center,
float playerDomainRadius,
int mastery,
float animation)
{
// Full-screen mastery removes the circular crop from the domain scene. The
// player's event horizon is still the same max-level field, rather than a
// circle enlarged until it reaches the viewport corners.
DrawDomainRing(batch, center, playerDomainRadius, mastery, animation, drawBackdrop: false);
}
private void DrawDomainBackdropsBeforeWallsAndTiles(On_Main.orig_DoDraw_WallsTilesNPCs orig, Main self)
{
DrawDomainBackdrops();
orig(self);
}
private void DrawDomainBackdrops()
{
if (Main.gameMenu || !Main.LocalPlayer.active)
return;
bool anyCompletedFullScreen = false;
bool anyClipped = false;
float fullScreenMastery = 0f;
float fullScreenReveal = 0f;
for (int index = 0; index < Main.maxPlayers; index++)
{
if (domainAnimation[index] <= 0.001f || !Main.player[index].active)
continue;
float descent = descentAnimation[index];
if (descent >= 0.999f)
{
anyCompletedFullScreen = true;
fullScreenMastery = Math.Max(fullScreenMastery, GetMasteryProgress(cachedMastery[index]));
fullScreenReveal = Math.Max(
fullScreenReveal,
MathHelper.SmoothStep(0f, 1f, domainAnimation[index]));
}
if (descent < 0.999f)
{
anyClipped = true;
}
}
if (!anyCompletedFullScreen && !anyClipped)
return;
// This hook runs during final world composition, after Terraria has drawn
// (or composited) its background and before it starts walls and tiles.
// Reuse the active world SpriteBatch so the domain is never captured in
// the scrolling background render target.
SpriteBatch batch = Main.spriteBatch;
if (anyClipped)
{
for (int index = 0; index < Main.maxPlayers; index++)
{
Player player = Main.player[index];
float animation = domainAnimation[index];
if (descentAnimation[index] >= 0.999f
|| animation <= 0.001f
|| !player.active)
continue;
float targetRadius = Math.Max(160f, cachedRadius[index]);
Vector2 center = player.Center - Main.screenPosition;
if (center.X < -targetRadius || center.X > Main.screenWidth + targetRadius
|| center.Y < -targetRadius || center.Y > Main.screenHeight + targetRadius)
{
continue;
}
GetAnimatedBackdropGeometry(targetRadius, cachedMastery[index], animation,
out float coreRadius, out float noiseTime, out float noiseAmplitude,
out float masteryProgress, out float reveal);
DrawClippedDeathDomainBackdrop(
batch,
center,
coreRadius,
noiseTime,
16.2f,
noiseAmplitude,
masteryProgress,
reveal);
DrawTransientBackdropBlackHoles(
batch,
center,
new Vector2(coreRadius),
masteryProgress,
reveal,
coreRadius,
Main.worldID + index * 101);
}
}
if (anyCompletedFullScreen)
{
// The backdrop is camera-relative, so multiple manifested domains share
// one strongest presentation instead of darkening the scene repeatedly.
DrawFullScreenBackdrop(batch, fullScreenMastery, fullScreenReveal);
}
for (int index = 0; index < Main.maxPlayers; index++)
{
Player player = Main.player[index];
float descent = descentAnimation[index];
if (!player.active || domainAnimation[index] <= 0.001f
|| descent <= 0.001f || descent >= 0.999f)
{
continue;
}
if (!anyCompletedFullScreen)
{
DrawManifestationWave(
batch,
player.Center - Main.screenPosition,
Math.Max(160f, cachedRadius[index]),
GetMasteryProgress(cachedMastery[index]),
GetDomainApertureReveal(domainAnimation[index]),
descent,
drawBackdrop: true,
drawBoundary: false);
}
}
}
private static void GetAnimatedBackdropGeometry(
float targetRadius,
int mastery,
float animation,
out float coreRadius,
out float noiseTime,
out float noiseAmplitude,
out float masteryProgress,
out float reveal)
{
float progress = MathHelper.SmoothStep(0f, 1f, animation);
reveal = GetDomainApertureReveal(animation);
float rangeProgress = MathHelper.Clamp(
(targetRadius - 160f) / (DeathNecklace.RadiusGrowthPerLevel * (DeathNecklace.MaxCoreLevel - 1)),
0f,
1f);
float manifestedReveal = reveal * MathHelper.Lerp(0.38f, 1f, rangeProgress);
float overshoot = 1f + (float)Math.Sin(progress * MathHelper.Pi) * 0.028f;
float radius = MathHelper.Lerp(8f, targetRadius, progress) * overshoot;
masteryProgress = GetMasteryProgress(mastery);
float coreGap = MathHelper.Lerp(22f, 14f, masteryProgress);
coreRadius = radius > coreGap + 6f ? radius - coreGap : radius * 0.58f;
noiseTime = Main.GlobalTimeWrappedHourly * 0.8f;
noiseAmplitude = Math.Min(
coreRadius * 0.18f,
MathHelper.Lerp(5f, 10f, masteryProgress) * MathHelper.Lerp(0.35f, 1f, manifestedReveal));
reveal = manifestedReveal;
}
private static float GetDomainApertureReveal(float animation)
{
// The domain material becomes opaque almost immediately behind the moving
// noisy edge. Opacity only falls once the contracting aperture is nearly
// closed, so opening and closing read as spatial sweeps instead of fades.
return MathHelper.SmoothStep(0f, 1f,
MathHelper.Clamp(animation / 0.1f, 0f, 1f));
}
private static void DrawManifestationWave(
SpriteBatch batch,
Vector2 center,
float finiteRadius,
float mastery,
float domainReveal,
float progress,
bool drawBackdrop,
bool drawBoundary)
{
DeathDomainBackdropTextureSystem.GetExpandedDomainDrawBounds(
out Vector2 destination,
out float destinationWidth,
out float destinationHeight);
Vector2 topRight = destination + Vector2.UnitX * destinationWidth;
Vector2 bottomLeft = destination + Vector2.UnitY * destinationHeight;
Vector2 bottomRight = destination + new Vector2(destinationWidth, destinationHeight);
float screenRadius = Math.Max(
Math.Max(Vector2.Distance(center, destination), Vector2.Distance(center, topRight)),
Math.Max(Vector2.Distance(center, bottomLeft), Vector2.Distance(center, bottomRight)));
float expansion = MathHelper.SmoothStep(0f, 1f, progress);
float radius = MathHelper.Lerp(Math.Max(24f, finiteRadius), screenRadius + 72f, expansion);
float time = Main.GlobalTimeWrappedHourly;
float noiseAmplitude = MathHelper.Lerp(46f, 24f, expansion)
* (0.88f + (float)Math.Sin(time * 4.1f) * 0.12f);
float backdropReveal = MathHelper.SmoothStep(0f, 1f,
MathHelper.Clamp(progress / 0.14f, 0f, 1f)) * domainReveal;
if (drawBackdrop)
{
DrawClippedFullScreenBackdrop(
batch,
center,
radius,
time * 1.15f,
31.7f,
noiseAmplitude,
mastery,
backdropReveal,
destination,
destinationWidth,
destinationHeight);
}
if (drawBoundary)
{
float edgeFade = (float)Math.Sin(progress * MathHelper.Pi);
edgeFade = (float)Math.Sqrt(Math.Max(0f, edgeFade)) * domainReveal;
DrawNoiseBoundary(batch, center, radius + 8f, -time * 1.38f,
47.3f, noiseAmplitude * 1.16f, 10f, edgeFade * 0.22f,
mastery, gaps: true);
DrawNoiseBoundary(batch, center, radius, time * 1.15f,
31.7f, noiseAmplitude, 5.5f, edgeFade * 0.92f,
mastery, gaps: false);
DrawNoiseBoundary(batch, center, radius - 10f, time * 1.72f,
63.1f, noiseAmplitude * 0.58f, 2.4f, edgeFade * 0.48f,
mastery, gaps: true);
}
}
private static void DrawClippedFullScreenBackdrop(
SpriteBatch batch,
Vector2 center,
float baseRadius,
float noiseTime,
float noiseSeed,
float noiseAmplitude,
float mastery,
float reveal,
Vector2 destination,
float destinationWidth,
float destinationHeight)
{
const int boundarySegments = 192;
const int maximumBands = 1024;
Span<Vector2> boundary = stackalloc Vector2[boundarySegments];
float extent = 0f;
for (int index = 0; index < boundarySegments; index++)
{
float angle = MathHelper.TwoPi * index / boundarySegments;
float radius = Math.Max(1f,
baseRadius + BoundaryNoise(angle, noiseTime, noiseSeed) * noiseAmplitude);
boundary[index] = angle.ToRotationVector2() * radius;
extent = Math.Max(extent, Math.Abs(boundary[index].Y));
}
int bands = Math.Clamp((int)Math.Ceiling(extent * 2f / 8f), 1, maximumBands);
float bandHeight = extent * 2f / bands;
Span<float> leftEdges = stackalloc float[bands];
Span<float> rightEdges = stackalloc float[bands];
for (int band = 0; band < bands; band++)
{
float scanY = -extent + (band + 0.5f) * bandHeight;
float left = float.MaxValue;
float right = float.MinValue;
for (int edge = 0; edge < boundarySegments; edge++)
{
Vector2 start = boundary[edge];
Vector2 end = boundary[(edge + 1) % boundarySegments];
if ((start.Y <= scanY && end.Y > scanY)
|| (end.Y <= scanY && start.Y > scanY))
{
float amount = (scanY - start.Y) / (end.Y - start.Y);
float intersectionX = MathHelper.Lerp(start.X, end.X, amount);
left = Math.Min(left, intersectionX);
right = Math.Max(right, intersectionX);
}
}
leftEdges[band] = left;
rightEdges[band] = right;
}
float destinationRight = destination.X + destinationWidth;
float destinationBottom = destination.Y + destinationHeight;
for (int layer = 0; layer < 3; layer++)
{
float opacity = layer switch
{
0 => MathHelper.Lerp(0.88f, 1f, mastery) * reveal,
1 => MathHelper.Lerp(0.72f, 0.9f, mastery) * reveal,
_ => MathHelper.Lerp(0.78f, 0.96f, mastery) * reveal
};
Texture2D texture = DeathDomainBackdropTextureSystem.GetLayer(layer);
DeathDomainBackdropTextureSystem.GetExpandedDomainSourceFrame(layer,
out float sourceOffsetX, out float sourceOffsetY,
out float viewWidth, out float viewHeight);
for (int band = 0; band < bands; band++)
{
float left = leftEdges[band];
float right = rightEdges[band];
if (left >= right)
continue;
float scanY = -extent + (band + 0.5f) * bandHeight;
float drawLeft = Math.Max(destination.X, center.X + left);
float drawRight = Math.Min(destinationRight, center.X + right);
float drawTop = Math.Max(destination.Y,
center.Y + scanY - bandHeight * 0.5f);
float drawBottom = Math.Min(destinationBottom,
center.Y + scanY + bandHeight * 0.5f);
if (drawLeft >= drawRight || drawTop >= drawBottom)
continue;
float normalizedLeft = (drawLeft - destination.X) / destinationWidth;
float normalizedRight = (drawRight - destination.X) / destinationWidth;
float normalizedTop = (drawTop - destination.Y) / destinationHeight;
float normalizedBottom = (drawBottom - destination.Y) / destinationHeight;
DrawBackdropBand(
batch,
texture,
new Vector2(drawLeft, drawTop),
drawRight - drawLeft,
drawBottom - drawTop,
sourceOffsetX + normalizedLeft * viewWidth,
sourceOffsetY + normalizedTop * viewHeight,
Math.Max(0.5f, (normalizedRight - normalizedLeft) * viewWidth),
Math.Max(0.5f, (normalizedBottom - normalizedTop) * viewHeight),
opacity);
}
}
}
private static void DrawFullScreenBackdrop(SpriteBatch batch, float mastery, float reveal)
{
DeathDomainBackdropTextureSystem.GetExpandedDomainDrawBounds(
out Vector2 destination, out float destinationWidth,
out float destinationHeight);
for (int layer = 0; layer < 3; layer++)
{
float opacity = layer switch
{
0 => MathHelper.Lerp(0.88f, 1f, mastery) * reveal,
1 => MathHelper.Lerp(0.72f, 0.9f, mastery) * reveal,
_ => MathHelper.Lerp(0.78f, 0.96f, mastery) * reveal
};
Texture2D texture = DeathDomainBackdropTextureSystem.GetLayer(layer);
DeathDomainBackdropTextureSystem.GetExpandedDomainSourceFrame(layer,
out float sourceOffsetX, out float sourceOffsetY, out float viewWidth, out float viewHeight);
DrawBackdropBand(
batch,
texture,
destination,
destinationWidth,
destinationHeight,
sourceOffsetX,
sourceOffsetY,
viewWidth,
viewHeight,
opacity);
}
DrawTransientBackdropBlackHoles(
batch,
destination + new Vector2(destinationWidth, destinationHeight) * 0.5f,
new Vector2(destinationWidth, destinationHeight) * 0.5f,
mastery,
reveal,
0f,
Main.worldID);
}
private static void DrawTransientBackdropBlackHoles(
SpriteBatch batch,
Vector2 center,
Vector2 halfExtent,
float mastery,
float domainReveal,
float clipRadius,
int seedGroup)
{
if (domainReveal <= 0.01f || halfExtent.X <= 8f || halfExtent.Y <= 8f)
return;
bool clipped = clipRadius > 0f;
int largeCount = clipped ? 1 : 2;
int smallCount = clipped ? 3 : 6;
float minimumExtent = Math.Min(halfExtent.X, halfExtent.Y);
float largeRadius = clipped
? MathHelper.Clamp(clipRadius * 0.13f, 13f, 52f)
: MathHelper.Clamp(minimumExtent * 0.115f, 58f, 138f);
float smallRadius = clipped
? MathHelper.Clamp(clipRadius * 0.058f, 7f, 25f)
: MathHelper.Clamp(minimumExtent * 0.043f, 20f, 52f);
for (int slot = 0; slot < largeCount + smallCount; slot++)
{
bool large = slot < largeCount;
int seed = seedGroup * 37 + slot * 193 + (large ? 701 : 1301);
if (!TryGetBackdropHoleAnimation(seed, large,
out float opacity, out float scale, out float collapse))
{
continue;
}
Vector2 position;
if (clipped)
{
float angle = Hash01(seed + 11) * MathHelper.TwoPi;
float distance = clipRadius * MathHelper.Lerp(
large ? 0.19f : 0.27f,
large ? 0.42f : 0.62f,
Hash01(seed + 23));
position = center + angle.ToRotationVector2() * distance;
}
else
{
position = center + new Vector2(
MathHelper.Lerp(-0.76f, 0.76f, Hash01(seed + 11)) * halfExtent.X,
MathHelper.Lerp(-0.68f, 0.68f, Hash01(seed + 23)) * halfExtent.Y);
}
float finalOpacity = opacity * domainReveal * MathHelper.Lerp(0.72f, 1f, mastery);
float rotation = Hash01(seed + 41) * MathHelper.TwoPi
+ Main.GlobalTimeWrappedHourly * (large ? 0.055f : -0.16f);
if (large)
{
DrawLargeBackdropBlackHole(batch, position,
largeRadius * scale, rotation, finalOpacity, collapse, mastery, seed);
}
else
{
DrawSmallBackdropBlackHole(batch, position,
smallRadius * scale, rotation, finalOpacity, collapse, seed);
}
}
}
private static bool TryGetBackdropHoleAnimation(
int seed,
bool large,
out float opacity,
out float scale,
out float collapse)
{
float cycle = large ? 780f : 570f;
float visibleDuration = large ? 470f : 330f;
float introDuration = large ? 58f : 40f;
float outroDuration = large ? 152f : 112f;
float offset = Hash01(seed + 67) * cycle;
float age = PositiveModulo((float)Main.GameUpdateCount + offset, cycle);
if (age >= visibleDuration)
{
opacity = 0f;
scale = 0f;
collapse = 0f;
return false;
}
float intro = MathHelper.SmoothStep(0f, 1f, MathHelper.Clamp(age / introDuration, 0f, 1f));
float outro = MathHelper.SmoothStep(0f, 1f,
MathHelper.Clamp((visibleDuration - age) / outroDuration, 0f, 1f));
opacity = Math.Min(intro, outro);
collapse = 1f - outro;
float openingScale = MathHelper.Lerp(0.08f, 1f, 1f - (float)Math.Pow(1f - intro, 3f));
openingScale *= 1f + (float)Math.Sin(intro * MathHelper.Pi) * 0.12f;
scale = openingScale * MathHelper.Lerp(0.18f, 1f, outro);
return opacity > 0.005f && scale > 0.02f;
}
private static void DrawSmallBackdropBlackHole(
SpriteBatch batch,
Vector2 center,
float radius,
float rotation,
float opacity,
float collapse,
int seed)
{
if (radius <= 1f || opacity <= 0.001f)
return;
DrawBackdropDomainBlackHole(batch, center, radius, rotation, opacity,
collapse, 0.34f, seed, large: false);
}
private static void DrawLargeBackdropBlackHole(
SpriteBatch batch,
Vector2 center,
float radius,
float rotation,
float opacity,
float collapse,
float mastery,
int seed)
{
if (radius <= 2f || opacity <= 0.001f)
return;
DrawBackdropDomainBlackHole(batch, center, radius, rotation, opacity,
collapse, mastery, seed, large: true);
}
private static void DrawBackdropDomainBlackHole(
SpriteBatch batch,
Vector2 center,
float radius,
float rotation,
float opacity,
float collapse,
float mastery,
int seed,
bool large)
{
float time = Main.GlobalTimeWrappedHourly;
float coreRadius = radius * (large ? 0.68f : 0.62f);
float pulse = opacity * (0.88f + (float)Math.Sin(time * 2.7f + seed) * 0.08f);
DeathDomainPrimitiveTextureSystem.DrawRadialGradient(batch, center,
radius * (large ? 1.95f : 1.65f), new Color(42, 0, 16) * (opacity * 0.42f));
DrawBlackHoleSuctionStreams(batch, center, radius, 1f, rotation,
opacity * (1f - collapse * 0.38f), seed, large);
// Five inward-curving arms deliberately mirror the player's event
// horizon instead of the old unrelated crossed-orbit atom symbol.
for (int arm = 0; arm < 5; arm++)
DrawAccretionSpiral(batch, center, coreRadius * 0.96f, radius * 1.06f,
time + rotation * 0.72f, arm, mastery, pulse);
DeathDomainPrimitiveTextureSystem.DrawSmoothDisc(batch, center, coreRadius,
new Color(0, 0, 2) * (opacity * 0.98f));
DrawNoiseBoundary(batch, center, coreRadius + radius * 0.07f,
time * 0.76f + rotation, seed * 0.013f, radius * 0.07f,
large ? 3.2f : 2.1f, opacity * 0.34f, mastery, gaps: true);
DrawNoiseBoundary(batch, center, coreRadius,
-time * 0.94f + rotation, seed * 0.021f, radius * 0.045f,
large ? 2.4f : 1.55f, opacity * 0.92f, mastery, gaps: false);
}
private static void DrawFilledEllipse(
SpriteBatch batch,
Vector2 center,
float radiusX,
float radiusY,
Color color)
{
int bands = Math.Clamp((int)Math.Ceiling(radiusY), 6, 72);
for (int band = 0; band < bands; band++)
{
float normalizedY = (band + 0.5f) / bands * 2f - 1f;
float halfWidth = radiusX * (float)Math.Sqrt(Math.Max(0f, 1f - normalizedY * normalizedY));
float y = center.Y + normalizedY * radiusY;
DrawSegment(batch, new Vector2(center.X - halfWidth, y),
new Vector2(halfWidth * 2f, 0f), color, Math.Max(1f, radiusY * 2f / bands + 0.6f));
}
}
private static void DrawEllipseArc(
SpriteBatch batch,
Vector2 center,
float radiusX,
float radiusY,
float rotation,
float startAngle,
float endAngle,
int segments,
Color color,
float width)
{
Vector2 AxisPoint(float angle)
{
Vector2 local = new((float)Math.Cos(angle) * radiusX, (float)Math.Sin(angle) * radiusY);
return center + local.RotatedBy(rotation);
}
Vector2 previous = AxisPoint(startAngle);
for (int index = 1; index <= segments; index++)
{
float angle = MathHelper.Lerp(startAngle, endAngle, index / (float)segments);
Vector2 current = AxisPoint(angle);
DrawSegment(batch, previous, current - previous, color, width);
previous = current;
}
}
private static float Hash01(int seed)
{
double value = Math.Sin(seed * 12.9898 + 78.233) * 43758.5453;
return (float)(value - Math.Floor(value));
}
private static void DrawDomainRippleWaves(SpriteBatch batch, Vector2 center,
float radius, float time, float mastery, float pulse)
{
for (int wave = 0; wave < 3; wave++)
{
float travel = PositiveModulo(time * (0.22f + mastery * 0.08f)
+ wave / 3f, 1f);
float waveRadius = radius + MathHelper.Lerp(8f, 64f, travel);
float opacity = (1f - travel) * pulse * (0.14f + mastery * 0.08f);
const int segments = 128;
float firstRadius = waveRadius + (float)Math.Sin(-time * 2.1f + wave) * 2.6f;
Vector2 previous = center + Vector2.UnitX * firstRadius;
for (int segment = 1; segment <= segments; segment++)
{
float angle = MathHelper.TwoPi * segment / segments;
float ripple = (float)Math.Sin(angle * 9f - time * 2.1f + wave * 1.7f) * 2.6f
+ (float)Math.Sin(angle * 17f + time * 1.3f) * 0.9f;
Vector2 current = center + angle.ToRotationVector2() * (waveRadius + ripple);
DeathDomainPrimitiveTextureSystem.DrawSmoothStrip(batch, previous, current,
Color.Lerp(new Color(122, 4, 34), new Color(255, 54, 92), mastery)
* opacity,
MathHelper.Lerp(1.1f, 2.4f, 1f - travel));
previous = current;
}
}
}
private static void DrawBlackHoleSuctionStreams(SpriteBatch batch, Vector2 center,
float radius, float verticalScale, float rotation, float opacity,
int seed, bool large)
{
int streams = large ? 10 : 6;
int points = large ? 30 : 22;
float time = Main.GlobalTimeWrappedHourly;
for (int stream = 0; stream < streams; stream++)
{
float streamOffset = stream * MathHelper.TwoPi / streams + seed * 0.013f;
float movingHead = 1f - PositiveModulo(time * (large ? 0.42f : 0.58f)
+ stream / (float)streams, 1f);
Vector2 previous = Vector2.Zero;
for (int point = 0; point < points; point++)
{
float inward = point / (points - 1f);
float curved = inward * inward * (3f - 2f * inward);
float localAngle = streamOffset - time * (large ? 0.18f : 0.27f)
+ inward * (large ? 1.72f : 1.38f);
float localRadius = MathHelper.Lerp(radius * (large ? 2.75f : 2.05f),
radius * 0.56f, curved);
localRadius += (float)Math.Sin(localAngle * 5f + seed * 0.07f)
* radius * 0.025f * (1f - inward);
Vector2 local = new((float)Math.Cos(localAngle) * localRadius,
(float)Math.Sin(localAngle) * localRadius * verticalScale);
Vector2 current = center + local.RotatedBy(rotation);
if (point > 0)
{
float body = (float)Math.Sin(inward * MathHelper.Pi);
float glow = 1f - MathHelper.SmoothStep(0f, 1f,
MathHelper.Clamp(Math.Abs(inward - movingHead) / 0.20f, 0f, 1f));
// The near half of the tilted disc is brighter and thicker;
// the far half remains visible behind the black core, giving
// the accretion flow genuine front/back depth.
float nearSide = 0.55f + 0.45f * Math.Max(0f, (float)Math.Sin(localAngle));
Color color = Color.Lerp(new Color(58, 0, 12),
new Color(255, 42, 78), inward * 0.82f + glow * 0.18f);
float streamOpacity = opacity * body * nearSide * (0.18f + glow * 0.62f);
float width = MathHelper.Lerp(0.65f, large ? 3.6f : 2.2f, inward)
* (0.72f + glow * 0.38f);
DeathDomainPrimitiveTextureSystem.DrawSmoothStrip(batch, previous,
current, color * streamOpacity, width);
}
previous = current;
}
}
}
private static void DrawFilledNoiseDisc(
SpriteBatch batch,
Vector2 center,
float baseRadius,
float time,
float seed,
float noiseAmplitude,
Color color)
{
const int boundarySegments = 192;
float preferredBandHeight = MathHelper.Clamp(baseRadius / 240f, 3f, 9f);
Span<Vector2> boundary = stackalloc Vector2[boundarySegments];
float extent = 0f;
for (int index = 0; index < boundarySegments; index++)
{
float angle = MathHelper.TwoPi * index / boundarySegments;
float radius = Math.Max(1f, baseRadius + BoundaryNoise(angle, time, seed) * noiseAmplitude);
boundary[index] = angle.ToRotationVector2() * radius;
extent = Math.Max(extent, Math.Abs(boundary[index].Y));
}
int bands = Math.Max(1, (int)Math.Ceiling(extent * 2f / preferredBandHeight));
float bandHeight = extent * 2f / bands;
for (int index = 0; index < bands; index++)
{
float scanY = -extent + (index + 0.5f) * bandHeight;
float left = float.MaxValue;
float right = float.MinValue;
for (int edge = 0; edge < boundarySegments; edge++)
{
Vector2 start = boundary[edge];
Vector2 end = boundary[(edge + 1) % boundarySegments];
if ((start.Y <= scanY && end.Y > scanY) || (end.Y <= scanY && start.Y > scanY))
{
float amount = (scanY - start.Y) / (end.Y - start.Y);
float intersectionX = MathHelper.Lerp(start.X, end.X, amount);
left = Math.Min(left, intersectionX);
right = Math.Max(right, intersectionX);
}
}
if (left >= right)
continue;
Vector2 fillStart = new(center.X + left, center.Y + scanY);
Vector2 fillEnd = new(center.X + right, center.Y + scanY);
DeathDomainPrimitiveTextureSystem.DrawSmoothLine(batch, fillStart, fillEnd,
color, bandHeight + 0.8f);
}
}
private static void DrawNoiseBoundary(
SpriteBatch batch,
Vector2 center,
float baseRadius,
float time,
float seed,
float noiseAmplitude,
float width,
float opacity,
float mastery,
bool gaps)
{
const int segments = 256;
float previousAngle = 0f;
float previousRadius = baseRadius + BoundaryNoise(previousAngle, time, seed) * noiseAmplitude;
Vector2 previous = center + Vector2.UnitX * previousRadius;
Color darkRed = Color.Lerp(new Color(58, 0, 12), new Color(118, 0, 25), mastery);
Color brightRed = Color.Lerp(new Color(225, 12, 42), new Color(255, 72, 105), mastery);
for (int index = 1; index <= segments; index++)
{
float angle = MathHelper.TwoPi * index / segments;
float noisyRadius = baseRadius + BoundaryNoise(angle, time, seed) * noiseAmplitude;
Vector2 current = center + angle.ToRotationVector2() * noisyRadius;
Vector2 segment = current - previous;
float middleAngle = (previousAngle + angle) * 0.5f;
float flow = 0.5f + 0.5f * (float)Math.Sin(middleAngle * 3f - time * 1.9f + seed);
float arcMask = 0.58f + 0.42f * (float)Math.Sin(middleAngle * 2f + time * 0.47f + seed * 1.31f);
arcMask *= 0.72f + 0.28f * (float)Math.Sin(middleAngle * 7f - time * 0.83f + seed * 0.73f);
float visibility = gaps ? MathHelper.SmoothStep(0.08f, 0.62f, arcMask) : 1f;
if (visibility > 0.025f)
{
Color color = Color.Lerp(darkRed, brightRed, flow);
DrawSegment(batch, previous, segment, color * (opacity * visibility), width * (0.72f + flow * 0.42f));
}
previousAngle = angle;
previous = current;
}
}
private static float BoundaryNoise(float angle, float time, float seed)
{
float low = (float)Math.Sin(angle * 3f + time * 0.72f + seed) * 0.48f;
float middle = (float)Math.Sin(angle * 7f - time * 1.11f + seed * 1.73f) * 0.29f;
float high = (float)Math.Sin(angle * 13f + time * 1.67f + seed * 0.37f) * 0.16f;
float grain = (float)Math.Sin(angle * 29f - time * 2.23f + seed * 2.17f) * 0.07f;
return low + middle + high + grain;
}
private static void DrawInwardSuctionWaves(
SpriteBatch batch,
Vector2 center,
float edgeRadius,
float coreRadius,
float time,
float mastery,
float pulse)
{
// Preserve the original long black-hole accretion lines. Only the animated
// highlight direction is reversed so they now read as being swallowed by
// the event horizon instead of being emitted from it.
int streamCount = 7 + (int)Math.Round(mastery * 5f);
float outerRadius = edgeRadius + MathHelper.Lerp(72f, 126f, mastery);
for (int stream = 0; stream < streamCount; stream++)
{
float seed = stream * 4.731f + 2.4f;
float movingHead = 1f - PositiveModulo(time * MathHelper.Lerp(0.48f, 0.78f, mastery)
+ stream / (float)streamCount, 1f);
float baseAngle = stream * MathHelper.TwoPi / streamCount
- time * (0.13f + mastery * 0.08f)
+ BoundaryNoise(stream, time * 0.18f, seed) * 0.06f;
const int points = 46;
Vector2 previous = Vector2.Zero;
for (int point = 0; point < points; point++)
{
float inward = point / (points - 1f);
float curved = inward * inward * (3f - 2f * inward);
float angle = baseAngle + inward * MathHelper.Lerp(1.08f, 1.54f, mastery);
float radius = MathHelper.Lerp(outerRadius, edgeRadius + 2f, curved)
+ BoundaryNoise(angle, -time * 0.82f, seed) * MathHelper.Lerp(5.2f, 1.2f, inward);
Vector2 current = center + angle.ToRotationVector2() * radius;
if (point > 0)
{
float bodyFade = (float)Math.Sin(inward * MathHelper.Pi);
float distanceFromHead = Math.Abs(inward - movingHead);
float movingGlow = 1f - MathHelper.SmoothStep(0.02f, 0.25f, distanceFromHead);
float opacity = bodyFade * pulse * (0.16f + movingGlow * 0.64f);
Color color = Color.Lerp(
new Color(18, 0, 8),
Color.Lerp(new Color(174, 3, 40), new Color(255, 34, 76), mastery),
inward * (0.45f + movingGlow * 0.55f));
float width = MathHelper.Lerp(0.55f, 3.4f + mastery * 1.7f, inward)
* (0.68f + movingGlow * 0.48f);
DrawSegment(batch, previous, current - previous, color * opacity, width);
}
previous = current;
}
}
}
private static void DrawOutwardPressureWaves(
SpriteBatch batch,
Vector2 center,
float coreRadius,
float time,
float mastery,
float pulse)
{
// This is the same continuous accretion-line construction used outside,
// with radius progression, spiral turn and highlight travel all inverted.
int streamCount = 7 + (int)Math.Round(mastery * 5f);
float innerRadius = Math.Max(5f, coreRadius * 0.06f);
float outerRadius = coreRadius * 0.90f;
for (int stream = 0; stream < streamCount; stream++)
{
float seed = 42.7f + stream * 3.117f;
float movingHead = PositiveModulo(time * MathHelper.Lerp(0.48f, 0.78f, mastery)
+ stream / (float)streamCount, 1f);
float baseAngle = stream * MathHelper.TwoPi / streamCount
+ time * (0.13f + mastery * 0.08f)
- BoundaryNoise(stream, time * 0.18f, seed) * 0.06f;
const int points = 40;
Vector2 previous = Vector2.Zero;
for (int point = 0; point < points; point++)
{
float outward = point / (points - 1f);
float curved = outward * outward * (3f - 2f * outward);
float angle = baseAngle - outward * MathHelper.Lerp(1.08f, 1.54f, mastery);
float radius = MathHelper.Lerp(innerRadius, outerRadius, curved)
+ BoundaryNoise(angle, time * 0.82f, seed) * MathHelper.Lerp(1.2f, 4.6f, outward);
Vector2 current = center + angle.ToRotationVector2() * radius;
if (point > 0)
{
float bodyFade = (float)Math.Sin(outward * MathHelper.Pi);
float distanceFromHead = Math.Abs(outward - movingHead);
float movingGlow = 1f - MathHelper.SmoothStep(0.02f, 0.25f, distanceFromHead);
float opacity = bodyFade * pulse * (0.13f + movingGlow * 0.58f);
Color color = Color.Lerp(
new Color(255, 116, 136),
Color.Lerp(new Color(112, 2, 28), new Color(220, 18, 58), mastery),
outward * 0.78f);
float width = MathHelper.Lerp(3.5f + mastery * 1.4f, 0.65f, outward)
* (0.66f + movingGlow * 0.46f);
DrawSegment(batch, previous, current - previous, color * opacity, width);
}
previous = current;
}
}
}
private static void DrawAccretionSpiral(SpriteBatch batch, Vector2 center, float coreRadius, float outerRadius, float time, int arm, float mastery, float pulse)
{
const int points = 34;
float armOffset = arm * MathHelper.TwoPi / 5f;
Vector2 previous = Vector2.Zero;
for (int index = 0; index < points; index++)
{
float progress = index / (points - 1f);
float angle = armOffset - time * (0.72f + arm * 0.035f) + progress * MathHelper.Pi * 1.12f;
float radius = MathHelper.Lerp(coreRadius * 0.88f, outerRadius, progress);
radius += BoundaryNoise(angle, time, arm * 2.7f) * 2.4f;
Vector2 current = center + angle.ToRotationVector2() * radius;
if (index > 0)
{
Vector2 segment = current - previous;
Color color = Color.Lerp(new Color(255, 70, 92), new Color(82, 0, 22), progress);
float fade = (float)Math.Sin(progress * MathHelper.Pi);
DrawSegment(batch, previous, segment, color * (fade * pulse * 0.54f), MathHelper.Lerp(2.6f + mastery, 0.7f, progress));
}
previous = current;
}
}
private static void DrawCircle(SpriteBatch batch, Vector2 center, float radius, int segments, Color color, float width, float rotation)
{
segments = Math.Max(segments,
Math.Min(192, (int)Math.Ceiling(MathHelper.TwoPi * radius / 18f)));
Vector2 previous = center + rotation.ToRotationVector2() * radius;
for (int index = 1; index <= segments; index++)
{
float angle = rotation + MathHelper.TwoPi * index / segments;
Vector2 current = center + angle.ToRotationVector2() * radius;
DeathDomainPrimitiveTextureSystem.DrawSmoothStrip(batch, previous, current, color, width);
previous = current;
}
}
private static void DrawSegment(SpriteBatch batch, Vector2 start, Vector2 segment, Color color, float width)
{
DeathDomainPrimitiveTextureSystem.DrawSmoothStrip(batch,
start, start + segment, color, width);
}
private static float PositiveModulo(float value, float modulus)
{
float result = value % modulus;
return result < 0f ? result + modulus : result;
}
private static float GetMasteryProgress(int mastery)
{
// The character-owned Death Domain tree is packed into the legacy 5..75
// visual range so existing shaders keep the same progression curve.
return MathHelper.Clamp((mastery - 5f) / 70f, 0f, 1f);
}
private void ClearAnimationState()
{
Array.Clear(domainAnimation, 0, domainAnimation.Length);
Array.Clear(descentAnimation, 0, descentAnimation.Length);
Array.Clear(cachedRadius, 0, cachedRadius.Length);
Array.Clear(cachedMastery, 0, cachedMastery.Length);
Array.Clear(cachedFullScreen, 0, cachedFullScreen.Length);
}
}