using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using Terraria;
using Terraria.ModLoader;
namespace SoulHarvest.Common;
/// <summary>
/// High-resolution, seamless crescent surfaces shared by every non-Void reaper.
/// Each form keeps the Blood crescent's continuous silhouette while baking its
/// own material into the face. Textures are created lazily on the draw thread:
/// dedicated servers never own graphics resources.
/// </summary>
[Autoload(Side = ModSide.Client)]
internal sealed class ReaperCrescentPrimitiveTextureSystem : ModSystem
{
private const int TextureSize = 768;
private const int MaterialTextureSize = 512;
private const float OuterRadius = 0.91f;
private const int BloodLifecycleFrames = 16;
private const int MaterialLifecycleFrames = 10;
private static readonly Texture2D?[] bloodCrescents = new Texture2D?[BloodLifecycleFrames];
private static readonly Texture2D?[] deathDomainCrescents = new Texture2D?[BloodLifecycleFrames];
private static readonly Texture2D?[] deathDomainRimCrescents = new Texture2D?[BloodLifecycleFrames];
private static readonly Dictionary<ReaperFormId, Texture2D?[]> formCrescents = [];
private static Color[]? deathDomainSurface;
private static Texture2D? deathDomainBlade;
private static Texture2D? deathDomainRiftCrescent;
public override void PostSetupContent()
{
if (Main.dedServ)
return;
// These procedural atlases used to be built synchronously by the first
// Blood/Death swing. Prewarm them on the graphics thread after content
// setup so combat never pays several million samples and GPU uploads in
// a single frame. Dedicated servers remain completely texture-free.
Main.QueueMainThreadAction(() =>
{
DeathDomainBackdropTextureSystem.Prewarm();
for (int frame = 0; frame < BloodLifecycleFrames; frame++)
{
_ = GetBloodCrescent(frame);
_ = GetDeathDomainRimCrescent(frame);
}
});
}
internal static void DrawFormCrescent(
ReaperFormId form,
Vector2 center,
float rotation,
float radius,
float opacity,
int motionLayers,
int swingDirection,
float time,
float lifeProgress)
{
if (form == ReaperFormId.Blood)
{
DrawBloodCrescent(center, rotation, radius, opacity, motionLayers,
swingDirection, time, lifeProgress);
return;
}
if (form == ReaperFormId.Death)
{
DrawDeathDomainCrescent(center, rotation, radius, opacity, motionLayers,
swingDirection, time, lifeProgress);
return;
}
if (Main.dedServ || radius <= 1f || opacity <= 0.001f
|| form == ReaperFormId.Void)
{
return;
}
lifeProgress = MathHelper.Clamp(lifeProgress, 0f, 1f);
float lifecycleFrame = lifeProgress * (MaterialLifecycleFrames - 1);
int firstFrame = Math.Clamp((int)Math.Floor(lifecycleFrame), 0,
MaterialLifecycleFrames - 1);
int secondFrame = Math.Min(firstFrame + 1, MaterialLifecycleFrames - 1);
float frameBlend = lifecycleFrame - firstFrame;
Texture2D firstTexture = GetFormCrescent(form, firstFrame);
Texture2D secondTexture = secondFrame == firstFrame
? firstTexture : GetFormCrescent(form, secondFrame);
Vector2 screenCenter = center - Main.screenPosition;
float scale = GetMaterialScale(radius);
motionLayers = Math.Clamp(motionLayers, 0, 4);
SpriteEffects effects = swingDirection < 0
? SpriteEffects.FlipVertically : SpriteEffects.None;
Color aura = ReaperCombatRegistry.GetPrimaryColor(form) with { A = 0 };
for (int layer = motionLayers; layer >= 1; layer--)
{
float lag = swingDirection * (0.015f + layer * 0.017f);
DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
screenCenter, rotation - lag, scale * (1f - layer * 0.014f),
aura * (opacity * (0.030f + layer * 0.012f)), effects);
}
float pulse = 0.982f + (float)Math.Sin(time * 6.4f + (int)form) * 0.012f;
DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
screenCenter, rotation, scale * 1.018f,
aura * (opacity * 0.16f), effects);
DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
screenCenter, rotation, scale * pulse, Color.White * opacity, effects);
}
internal static void DrawBloodCrescent(
Vector2 center,
float rotation,
float radius,
float opacity,
int motionLayers,
int swingDirection,
float time,
float lifeProgress)
{
if (Main.dedServ || radius <= 1f || opacity <= 0.001f)
return;
lifeProgress = MathHelper.Clamp(lifeProgress, 0f, 1f);
float lifecycleFrame = lifeProgress * (BloodLifecycleFrames - 1);
int firstFrame = Math.Clamp((int)Math.Floor(lifecycleFrame), 0, BloodLifecycleFrames - 1);
int secondFrame = Math.Min(firstFrame + 1, BloodLifecycleFrames - 1);
float frameBlend = lifecycleFrame - firstFrame;
Texture2D firstTexture = GetBloodCrescent(firstFrame);
Texture2D secondTexture = secondFrame == firstFrame
? firstTexture
: GetBloodCrescent(secondFrame);
float scale = GetScale(radius);
Vector2 screenCenter = center - Main.screenPosition;
motionLayers = Math.Clamp(motionLayers, 0, 4);
SpriteEffects effects = swingDirection < 0
? SpriteEffects.FlipVertically
: SpriteEffects.None;
// Reference-style motion exposure: translucent copies sit behind one
// continuous surface rather than breaking the blade into radial blocks.
for (int layer = motionLayers; layer >= 1; layer--)
{
float layerOpacity = opacity * (0.055f + layer * 0.018f);
float rotationLag = swingDirection * (0.018f + layer * 0.018f);
float breathing = 1f - layer * 0.018f;
DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
screenCenter, rotation - rotationLag, scale * breathing,
Color.White * layerOpacity, effects);
}
float pulse = 0.96f + (float)Math.Sin(time * 8.2f) * 0.015f;
DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
screenCenter, rotation, scale * 1.018f,
new Color(255, 34, 42, 0) * (opacity * 0.18f), effects);
DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
screenCenter, rotation, scale * pulse, Color.White * opacity, effects);
}
internal static void DrawDeathDomainCrescent(
Vector2 center,
float rotation,
float radius,
float opacity,
int motionLayers,
int swingDirection,
float time,
float lifeProgress)
{
if (Main.dedServ || radius <= 1f || opacity <= 0.001f)
return;
lifeProgress = MathHelper.Clamp(lifeProgress, 0f, 1f);
float lifecycleFrame = lifeProgress * (BloodLifecycleFrames - 1);
int firstFrame = Math.Clamp((int)Math.Floor(lifecycleFrame), 0,
BloodLifecycleFrames - 1);
int secondFrame = Math.Min(firstFrame + 1, BloodLifecycleFrames - 1);
float frameBlend = lifecycleFrame - firstFrame;
Texture2D firstTexture = GetDeathDomainCrescent(firstFrame);
Texture2D secondTexture = secondFrame == firstFrame
? firstTexture
: GetDeathDomainCrescent(secondFrame);
float scale = GetScale(radius);
Vector2 screenCenter = center - Main.screenPosition;
motionLayers = Math.Clamp(motionLayers, 0, 4);
SpriteEffects effects = swingDirection < 0
? SpriteEffects.FlipVertically
: SpriteEffects.None;
for (int layer = motionLayers; layer >= 1; layer--)
{
float layerOpacity = opacity * (0.042f + layer * 0.014f);
float lag = swingDirection * (0.016f + layer * 0.017f);
DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
screenCenter, rotation - lag, scale * (1f - layer * 0.014f),
new Color(194, 16, 61, 0) * layerOpacity, effects);
}
DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
screenCenter, rotation, scale,
new Color(235, 20, 68, 0) * (opacity * 0.16f), effects);
DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
screenCenter, rotation, scale, Color.White * opacity, effects);
}
internal static void DrawDeathDomainCrescentRim(
Vector2 center,
float rotation,
float radius,
float opacity,
int motionLayers,
int swingDirection,
float time,
float lifeProgress)
{
if (Main.dedServ || radius <= 1f || opacity <= 0.001f)
return;
lifeProgress = MathHelper.Clamp(lifeProgress, 0f, 1f);
float lifecycleFrame = lifeProgress * (BloodLifecycleFrames - 1);
int firstFrame = Math.Clamp((int)Math.Floor(lifecycleFrame), 0,
BloodLifecycleFrames - 1);
int secondFrame = Math.Min(firstFrame + 1, BloodLifecycleFrames - 1);
float frameBlend = lifecycleFrame - firstFrame;
Texture2D firstTexture = GetDeathDomainRimCrescent(firstFrame);
Texture2D secondTexture = secondFrame == firstFrame
? firstTexture
: GetDeathDomainRimCrescent(secondFrame);
float scale = GetScale(radius);
Vector2 screenCenter = center - Main.screenPosition;
motionLayers = Math.Clamp(motionLayers, 0, 4);
SpriteEffects effects = swingDirection < 0
? SpriteEffects.FlipVertically
: SpriteEffects.None;
for (int layer = motionLayers; layer >= 1; layer--)
{
float lag = swingDirection * (0.016f + layer * 0.017f);
DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
screenCenter, rotation - lag, scale,
new Color(208, 12, 54, 0)
* (opacity * (0.034f + layer * 0.012f)), effects);
}
DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
screenCenter, rotation, scale,
new Color(235, 20, 68, 0) * (opacity * 0.18f), effects);
DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
screenCenter, rotation, scale, Color.White * opacity,
effects);
}
internal static void DrawDeathDomainBlade(Vector2 screenCenter, float rotation,
float length, float width, float opacity, float completion)
{
if (Main.dedServ || length <= 0.5f || width <= 0.5f
|| opacity <= 0.001f || completion <= 0.001f)
{
return;
}
Texture2D texture = deathDomainBlade ??= CreateDeathDomainBlade();
completion = MathHelper.Clamp(completion, 0f, 1f);
float visibleLength = Math.Max(0.5f, length * completion);
float widthGrowth = MathHelper.Lerp(0.22f, 1f,
SmoothStep(0f, 0.55f, completion));
Vector2 start = screenCenter
- rotation.ToRotationVector2() * visibleLength * 0.5f;
Main.EntitySpriteDraw(texture, start, null, Color.White * opacity,
rotation, new Vector2(0f, texture.Height * 0.5f),
new Vector2(visibleLength / texture.Width,
width * widthGrowth / texture.Height),
SpriteEffects.None, 0f);
}
internal static void DrawDeathDomainRiftCrescent(Vector2 center, float rotation,
float radius, float opacity, int swingDirection)
{
if (Main.dedServ || radius <= 1f || opacity <= 0.001f)
return;
Texture2D texture = deathDomainRiftCrescent ??= CreateDeathDomainRiftCrescent();
SpriteEffects effects = swingDirection < 0
? SpriteEffects.FlipVertically : SpriteEffects.None;
Vector2 screenCenter = center - Main.screenPosition;
float scale = GetScale(radius);
DrawTexture(texture, screenCenter, rotation, scale * 1.035f,
new Color(188, 9, 54, 0) * (opacity * 0.28f), effects);
DrawTexture(texture, screenCenter, rotation, scale, Color.White * opacity,
effects);
}
public override void Unload()
{
Texture2D?[] oldBlood = new Texture2D?[BloodLifecycleFrames];
Array.Copy(bloodCrescents, oldBlood, BloodLifecycleFrames);
Texture2D?[] oldDeath = new Texture2D?[BloodLifecycleFrames];
Array.Copy(deathDomainCrescents, oldDeath, BloodLifecycleFrames);
Texture2D?[] oldDeathRims = new Texture2D?[BloodLifecycleFrames];
Array.Copy(deathDomainRimCrescents, oldDeathRims, BloodLifecycleFrames);
Texture2D? oldDeathBlade = deathDomainBlade;
Texture2D? oldDeathRift = deathDomainRiftCrescent;
List<Texture2D?> oldForms = [];
foreach (Texture2D?[] textures in formCrescents.Values)
oldForms.AddRange(textures);
Array.Clear(bloodCrescents, 0, bloodCrescents.Length);
Array.Clear(deathDomainCrescents, 0, deathDomainCrescents.Length);
Array.Clear(deathDomainRimCrescents, 0, deathDomainRimCrescents.Length);
formCrescents.Clear();
deathDomainSurface = null;
deathDomainBlade = null;
deathDomainRiftCrescent = null;
if (Main.dedServ)
return;
Main.QueueMainThreadAction(() =>
{
foreach (Texture2D? texture in oldBlood)
texture?.Dispose();
foreach (Texture2D? texture in oldDeath)
texture?.Dispose();
foreach (Texture2D? texture in oldDeathRims)
texture?.Dispose();
foreach (Texture2D? texture in oldForms)
texture?.Dispose();
oldDeathBlade?.Dispose();
oldDeathRift?.Dispose();
});
}
private static Texture2D GetBloodCrescent(int frame)
{
return bloodCrescents[frame] ??= CreateBloodCrescent(
frame / (float)(BloodLifecycleFrames - 1));
}
private static Texture2D GetDeathDomainCrescent(int frame)
{
return deathDomainCrescents[frame] ??= CreateDeathDomainCrescent(
frame / (float)(BloodLifecycleFrames - 1));
}
private static Texture2D GetDeathDomainRimCrescent(int frame)
{
return deathDomainRimCrescents[frame] ??= CreateDeathDomainRimCrescent(
frame / (float)(BloodLifecycleFrames - 1));
}
private static Texture2D GetFormCrescent(ReaperFormId form, int frame)
{
if (!formCrescents.TryGetValue(form, out Texture2D?[]? textures))
{
textures = new Texture2D?[MaterialLifecycleFrames];
formCrescents[form] = textures;
}
return textures[frame] ??= CreateFormCrescent(form,
frame / (float)(MaterialLifecycleFrames - 1));
}
private static Texture2D CreateFormCrescent(ReaperFormId form, float lifeProgress)
{
Texture2D texture = new(Main.instance.GraphicsDevice, MaterialTextureSize,
MaterialTextureSize,
false, SurfaceFormat.Color);
Color[] pixels = new Color[MaterialTextureSize * MaterialTextureSize];
float antialias = 3f / MaterialTextureSize;
float reveal = Smooth01(lifeProgress);
for (int y = 0; y < MaterialTextureSize; y++)
{
float normalizedY = (y + 0.5f) / MaterialTextureSize * 2f - 1f;
for (int x = 0; x < MaterialTextureSize; x++)
{
float normalizedX = (x + 0.5f) / MaterialTextureSize * 2f - 1f;
CrescentSample sample = SampleCrescent(normalizedX, normalizedY,
halfSweep: 2.43f, maximumThickness:
ReaperCombatRegistry.StandardCrescentThicknessRatio,
innerWarp: 0.015f, asymmetry: 0.22f, antialias);
if (sample.Body <= 0.001f)
continue;
float revealMask = 1f - SmoothStep(reveal - 0.018f,
reveal + 0.012f, sample.Progress);
float body = sample.Body * revealMask;
if (body <= 0.001f)
continue;
Vector3 color;
float materialAlpha;
SampleFormMaterial(form, sample.Progress, sample.Depth,
out color, out materialAlpha);
float outerGlow = sample.OuterGlow * revealMask;
float outerHot = sample.OuterHot * revealMask;
Vector3 rim = form switch
{
ReaperFormId.Bone => new Vector3(0.94f, 1f, 0.89f),
ReaperFormId.Infernal => new Vector3(1f, 0.94f, 0.55f),
ReaperFormId.Frost => new Vector3(0.91f, 1f, 1f),
ReaperFormId.Soul => new Vector3(0.75f, 1f, 1f),
_ => new Vector3(0.84f, 1f, 1f)
};
Vector3 glow = ReaperCombatRegistry.GetPrimaryColor(form).ToVector3();
color = Vector3.Lerp(color, glow, outerGlow * 0.68f);
color = Vector3.Lerp(color, rim, outerHot * 0.94f);
float alpha = Math.Max(body * materialAlpha,
Math.Max(outerGlow * 0.88f, outerHot));
pixels[y * MaterialTextureSize + x] = Premultiplied(color, alpha);
}
}
texture.SetData(pixels);
return texture;
}
private static void SampleFormMaterial(ReaperFormId form, float progress,
float depth, out Vector3 color, out float alpha)
{
switch (form)
{
case ReaperFormId.Bone:
{
float joint = Ridge((float)Math.Sin(progress * 45f + 0.4f), 13f);
float marrow = Ridge((float)Math.Sin(progress * 22f
- depth * 13f + 1.1f), 8f);
color = Vector3.Lerp(new Vector3(0.025f, 0.20f, 0.25f),
new Vector3(0.91f, 0.91f, 0.72f),
MathHelper.Clamp(0.20f + joint * 0.62f
+ marrow * (1f - depth) * 0.30f, 0f, 1f));
alpha = MathHelper.Lerp(0.82f, 0.58f, depth);
break;
}
case ReaperFormId.Infernal:
{
float lava = Ridge((float)Math.Sin(progress * 30f
+ depth * 19f + (float)Math.Sin(progress * 9f)), 8f);
float heat = MathHelper.Clamp(lava * 0.82f
+ (1f - depth) * 0.25f, 0f, 1f);
color = Vector3.Lerp(new Vector3(0.035f, 0.006f, 0.002f),
new Vector3(1f, 0.42f, 0.025f), heat);
color = Vector3.Lerp(color, new Vector3(1f, 0.96f, 0.56f),
lava * lava * 0.58f);
alpha = MathHelper.Lerp(0.92f, 0.68f, depth);
break;
}
case ReaperFormId.Frost:
{
float facetA = Math.Abs((float)Math.Sin(progress * 27f + depth * 8f));
float facetB = Math.Abs((float)Math.Sin(progress * 13f - depth * 17f));
float facet = MathHelper.Clamp(facetA * 0.52f + facetB * 0.38f, 0f, 1f);
color = Vector3.Lerp(new Vector3(0.025f, 0.17f, 0.38f),
new Vector3(0.62f, 0.94f, 1f), facet);
color = Vector3.Lerp(color, Vector3.One,
Ridge((float)Math.Sin(progress * 34f - depth * 24f), 14f) * 0.62f);
alpha = MathHelper.Lerp(0.72f, 0.43f, depth);
break;
}
case ReaperFormId.Soul:
{
float stream = Ridge((float)Math.Sin(progress * 21f
- depth * 26f + (float)Math.Sin(progress * 8f) * 1.4f), 6f);
float echo = Ridge((float)Math.Sin(progress * 38f
+ depth * 15f + 2.2f), 12f);
color = Vector3.Lerp(new Vector3(0.10f, 0.018f, 0.28f),
new Vector3(0.34f, 0.28f, 1f), stream * 0.72f);
color = Vector3.Lerp(color, new Vector3(0.22f, 1f, 1f),
echo * 0.62f);
alpha = MathHelper.Lerp(0.76f, 0.40f, depth);
break;
}
default:
{
float spirit = Ridge((float)Math.Sin(progress * 25f
- depth * 18f), 8f);
color = Vector3.Lerp(new Vector3(0.025f, 0.18f, 0.21f),
new Vector3(0.50f, 0.96f, 1f), spirit * 0.68f);
alpha = MathHelper.Lerp(0.68f, 0.38f, depth);
break;
}
}
}
private static Texture2D CreateDeathDomainCrescent(float lifeProgress)
{
Texture2D texture = new(Main.instance.GraphicsDevice, TextureSize, TextureSize,
false, SurfaceFormat.Color);
Color[] pixels = new Color[TextureSize * TextureSize];
Color[] surface = deathDomainSurface ??= CreateDeathDomainSurface();
float antialias = 3f / TextureSize;
float reveal = Smooth01(lifeProgress);
float erosion = Smooth01((lifeProgress - 0.30f) / 0.66f);
float edgeFade = 1f - Smooth01((lifeProgress - 0.96f) / 0.04f);
for (int y = 0; y < TextureSize; y++)
{
float normalizedY = (y + 0.5f) / TextureSize * 2f - 1f;
for (int x = 0; x < TextureSize; x++)
{
float normalizedX = (x + 0.5f) / TextureSize * 2f - 1f;
CrescentSample sample = SampleCrescent(normalizedX, normalizedY,
halfSweep: 2.43f, maximumThickness:
ReaperCombatRegistry.StandardCrescentThicknessRatio,
innerWarp: 0.018f, asymmetry: 0.22f, antialias);
if (sample.Body <= 0.001f)
continue;
float revealMask = 1f - SmoothStep(reveal - 0.018f,
reveal + 0.012f, sample.Progress);
float integrity = SampleDeathInteriorIntegrity(sample.Progress,
sample.Depth, erosion);
float body = sample.Body * revealMask * integrity;
Vector4 scene = surface[y * TextureSize + x].ToVector4();
Vector3 color = new(scene.X, scene.Y, scene.Z);
float alpha = body;
float crimsonVein = Ridge((float)Math.Sin(sample.Progress * 21f
- sample.Depth * 18f + 0.9f), 9f);
color = Vector3.Lerp(color, new Vector3(0.66f, 0.008f, 0.09f),
crimsonVein * 0.18f * body);
// The outer cutting edge is never eroded. Only the domain-filled
// face tears away, keeping one continuous hot blade silhouette.
float redRim = sample.OuterGlow * revealMask * edgeFade;
float whiteRim = sample.OuterHot * revealMask * edgeFade;
color = Vector3.Lerp(color, new Vector3(0.92f, 0.025f, 0.12f),
redRim * 0.78f);
color = Vector3.Lerp(color, new Vector3(1f, 0.88f, 0.84f),
whiteRim * 0.90f);
alpha = Math.Max(alpha, Math.Max(redRim * 0.90f,
whiteRim * 0.98f));
if (alpha <= 0.001f)
continue;
pixels[y * TextureSize + x] = Premultiplied(color, alpha);
}
}
texture.SetData(pixels);
return texture;
}
private static Texture2D CreateDeathDomainRimCrescent(float lifeProgress)
{
Texture2D texture = new(Main.instance.GraphicsDevice, TextureSize,
TextureSize, false, SurfaceFormat.Color);
Color[] pixels = new Color[TextureSize * TextureSize];
float antialias = 3f / TextureSize;
float reveal = Smooth01(lifeProgress);
float edgeFade = 1f - Smooth01((lifeProgress - 0.96f) / 0.04f);
for (int y = 0; y < TextureSize; y++)
{
float normalizedY = (y + 0.5f) / TextureSize * 2f - 1f;
for (int x = 0; x < TextureSize; x++)
{
float normalizedX = (x + 0.5f) / TextureSize * 2f - 1f;
CrescentSample sample = SampleCrescent(normalizedX, normalizedY,
halfSweep: 2.43f, maximumThickness:
ReaperCombatRegistry.StandardCrescentThicknessRatio,
innerWarp: 0.018f, asymmetry: 0.22f, antialias);
if (sample.Body <= 0.001f)
continue;
float revealMask = 1f - SmoothStep(reveal - 0.018f,
reveal + 0.012f, sample.Progress);
float redRim = sample.OuterGlow * revealMask * edgeFade;
float whiteRim = sample.OuterHot * revealMask * edgeFade;
float alpha = Math.Max(redRim * 0.90f, whiteRim * 0.98f);
if (alpha <= 0.001f)
continue;
Vector3 color = Vector3.Lerp(new Vector3(0.92f, 0.025f, 0.12f),
new Vector3(1f, 0.88f, 0.84f), whiteRim * 0.94f);
pixels[y * TextureSize + x] = Premultiplied(color, alpha);
}
}
texture.SetData(pixels);
return texture;
}
private static Color[] CreateDeathDomainSurface()
{
Color[] surface = new Color[TextureSize * TextureSize];
for (int y = 0; y < TextureSize; y++)
{
float v = (y + 0.5f) / TextureSize;
for (int x = 0; x < TextureSize; x++)
{
float u = (x + 0.5f) / TextureSize;
surface[y * TextureSize + x] =
DeathDomainBackdropTextureSystem.SampleCompositePixel(
x * 2, y * 2, u, v);
}
}
return surface;
}
private static Texture2D CreateDeathDomainBlade()
{
const int width = 1024;
const int height = 256;
Texture2D texture = new(Main.instance.GraphicsDevice, width, height,
false, SurfaceFormat.Color);
Color[] pixels = new Color[width * height];
for (int y = 0; y < height; y++)
{
float vertical = Math.Abs((y + 0.5f) / height * 2f - 1f);
float v = (y + 0.5f) / height;
for (int x = 0; x < width; x++)
{
float progress = (x + 0.5f) / width;
float taper = (float)Math.Pow(Math.Max(0f,
Math.Sin(progress * MathHelper.Pi)), 0.56f);
taper *= 1f
+ (float)Math.Sin(progress * MathHelper.TwoPi * 5.1f) * 0.035f
+ (float)Math.Sin(progress * MathHelper.TwoPi * 13.7f) * 0.014f;
float normalizedDistance = vertical / Math.Max(0.0001f, taper);
float body = 1f - SmoothStep(0.965f, 1.018f,
normalizedDistance);
if (body <= 0.001f)
continue;
float u = progress;
Vector4 sampled = DeathDomainBackdropTextureSystem
.SampleCompositePixel(x * 2, y * 3, u, v).ToVector4();
Vector3 color = new(sampled.X, sampled.Y, sampled.Z);
float redRim = SmoothStep(0.70f, 0.92f, normalizedDistance)
* (1f - SmoothStep(0.97f, 1.02f, normalizedDistance));
float whiteRim = SmoothStep(0.86f, 0.965f, normalizedDistance)
* (1f - SmoothStep(0.985f, 1.02f, normalizedDistance));
color = Vector3.Lerp(color, new Vector3(0.94f, 0.018f, 0.12f),
redRim * 0.86f);
color = Vector3.Lerp(color, new Vector3(1f, 0.90f, 0.86f),
whiteRim * 0.88f);
float alpha = Math.Max(body * 0.96f,
Math.Max(redRim * 0.92f, whiteRim));
pixels[y * width + x] = Premultiplied(color, alpha);
}
}
texture.SetData(pixels);
return texture;
}
private static Texture2D CreateDeathDomainRiftCrescent()
{
Texture2D texture = new(Main.instance.GraphicsDevice, TextureSize, TextureSize,
false, SurfaceFormat.Color);
Color[] pixels = new Color[TextureSize * TextureSize];
float antialias = 3f / TextureSize;
for (int y = 0; y < TextureSize; y++)
{
float normalizedY = (y + 0.5f) / TextureSize * 2f - 1f;
for (int x = 0; x < TextureSize; x++)
{
float normalizedX = (x + 0.5f) / TextureSize * 2f - 1f;
CrescentSample sample = SampleCrescent(normalizedX, normalizedY,
halfSweep: 2.43f, maximumThickness: 0.15f,
innerWarp: 0.026f, asymmetry: 0.20f, antialias);
if (sample.Body <= 0.001f)
continue;
float integrity = SampleInteriorIntegrity(sample.Progress,
sample.Depth, 1f, 1.28f);
Vector4 sampled = DeathDomainBackdropTextureSystem
.SampleCompositePixel(x * 2, y * 2,
(x + 0.5f) / TextureSize, (y + 0.5f) / TextureSize)
.ToVector4();
Vector3 color = new(sampled.X, sampled.Y, sampled.Z);
float redRim = sample.OuterGlow;
float whiteRim = sample.OuterHot;
color = Vector3.Lerp(color, new Vector3(0.94f, 0.012f, 0.12f),
redRim * 0.88f);
color = Vector3.Lerp(color, new Vector3(1f, 0.88f, 0.84f),
whiteRim * 0.92f);
float alpha = Math.Max(sample.Body * integrity * 0.94f,
Math.Max(redRim * 0.94f, whiteRim));
pixels[y * TextureSize + x] = Premultiplied(color, alpha);
}
}
texture.SetData(pixels);
return texture;
}
private static Texture2D CreateBloodCrescent(float lifeProgress)
{
Texture2D texture = new(Main.instance.GraphicsDevice, TextureSize, TextureSize,
false, SurfaceFormat.Color);
Color[] pixels = new Color[TextureSize * TextureSize];
float antialias = 3f / TextureSize;
// The weapon pose uses the same smooth-step clock. Keeping the mask on
// that clock makes the bright leading edge sit at the live blade tip
// instead of revealing most of the crescent before the hand gets there.
float reveal = Smooth01(lifeProgress);
float erosion = Smooth01((lifeProgress - 0.26f) / 0.68f);
for (int y = 0; y < TextureSize; y++)
{
float normalizedY = ((y + 0.5f) / TextureSize * 2f - 1f);
for (int x = 0; x < TextureSize; x++)
{
float normalizedX = (x + 0.5f) / TextureSize * 2f - 1f;
CrescentSample sample = SampleCrescent(normalizedX, normalizedY,
halfSweep: 2.43f, maximumThickness:
ReaperCombatRegistry.StandardCrescentThicknessRatio,
innerWarp: 0.018f, asymmetry: 0.22f, antialias);
if (sample.Body <= 0.001f)
continue;
// Only the arc already crossed by the moving blade is visible.
// FlipVertically reverses this reveal for the opposite swing.
float revealMask = 1f - SmoothStep(reveal - 0.018f, reveal + 0.012f, sample.Progress);
float integrity = SampleInteriorIntegrity(sample.Progress,
sample.Depth, erosion, 1.12f) * revealMask;
if (integrity <= 0.001f)
continue;
float liquidA = Ridge((float)Math.Sin(
sample.Progress * 37f + sample.Depth * 15f
+ (float)Math.Sin(sample.Progress * 12f) * 1.8f), 7f);
float liquidB = Ridge((float)Math.Sin(
sample.Progress * 19f - sample.Depth * 24f + 1.7f), 11f);
float flow = MathHelper.Clamp(liquidA * 0.52f + liquidB * 0.30f, 0f, 1f);
Vector3 deepRed = new(0.19f, 0.002f, 0.018f);
Vector3 outerRed = new(1f, 0.025f, 0.055f);
Vector3 warmFlow = new(1f, 0.22f, 0.105f);
Vector3 color = Vector3.Lerp(outerRed, deepRed,
(float)Math.Pow(sample.Depth, 0.70f));
color = Vector3.Lerp(color, warmFlow, flow * 0.46f);
float alpha = sample.Body * integrity * MathHelper.Lerp(0.78f, 0.52f, sample.Depth);
float innerHeat = sample.InnerEdge * integrity * (0.18f + flow * 0.24f);
color = Vector3.Lerp(color, new Vector3(1f, 0.09f, 0.08f), innerHeat);
alpha = Math.Max(alpha, innerHeat * 0.72f);
// Fractures consume only the liquid energy face. The hot cutting
// edge remains continuous from tip to tip at every lifetime frame.
float whiteRim = sample.OuterHot * revealMask;
float redRim = sample.OuterGlow * revealMask;
color = Vector3.Lerp(color, new Vector3(1f, 0.18f, 0.10f), redRim * 0.62f);
color = Vector3.Lerp(color, new Vector3(1f, 0.94f, 0.79f), whiteRim);
alpha = Math.Max(alpha, Math.Max(redRim * 0.82f, whiteRim * 0.98f));
pixels[y * TextureSize + x] = Premultiplied(color, alpha);
}
}
texture.SetData(pixels);
return texture;
}
private static float SampleInteriorIntegrity(float progress, float depth,
float erosion, float intensity)
{
if (erosion <= 0.015f)
return 1f;
float integrity = 1f;
// Persistent bites grow from the inner edge. Their deterministic layout
// keeps the crescent coherent from frame to frame instead of shimmering.
for (int index = 0; index < 18; index++)
{
float hashA = Hash01((uint)index, 0xB10D51C1u);
float hashB = Hash01((uint)index, 0xA63E218Fu);
float center = 0.055f + hashA * 0.89f;
float activation = 0.035f + index / 17f * 0.62f;
float growth = Smooth01((erosion - activation) / 0.28f) * intensity;
if (growth <= 0f)
continue;
float halfWidth = MathHelper.Lerp(0.018f, 0.052f, hashB) * (0.35f + growth * 0.65f);
float horizontal = Math.Abs(progress - center) / Math.Max(0.001f, halfWidth);
if (horizontal >= 1f)
continue;
float arch = (float)Math.Sqrt(Math.Max(0f, 1f - horizontal * horizontal));
float biteDepth = MathHelper.Lerp(0.16f, 0.52f, hashA)
* Math.Min(1f, growth) * arch;
float boundary = 1f - biteDepth;
integrity = Math.Min(integrity, 1f - SmoothStep(boundary - 0.018f, boundary + 0.018f, depth));
}
// Late frames tear several isolated wounds through the energy face. This
// is what creates the irregular missing islands visible in the reference.
for (int index = 0; index < 13; index++)
{
float hashA = Hash01((uint)index, 0xC04A711Du);
float hashB = Hash01((uint)index, 0xF731A2E9u);
float activation = 0.18f + index / 12f * 0.50f;
float growth = Smooth01((erosion - activation) / 0.27f) * intensity;
if (growth <= 0f)
continue;
float centerX = 0.08f + hashA * 0.84f;
// Keep every isolated wound away from depth zero: the outer blade
// rim may glow over a missing interior but is never itself severed.
float centerY = 0.30f + hashB * 0.54f;
float radiusX = MathHelper.Lerp(0.018f, 0.058f, hashB)
* Math.Min(1f, growth);
float radiusY = MathHelper.Lerp(0.055f, 0.15f, hashA)
* Math.Min(1f, growth);
float nx = (progress - centerX) / Math.Max(0.001f, radiusX);
float ny = (depth - centerY) / Math.Max(0.001f, radiusY);
float distance = (float)Math.Sqrt(nx * nx + ny * ny);
integrity = Math.Min(integrity, SmoothStep(0.82f, 1.08f, distance));
}
return MathHelper.Clamp(integrity, 0f, 1f);
}
internal static float SampleDeathInteriorIntegrity(float progress,
float depth, float erosion)
{
float bites = SampleInteriorIntegrity(progress, depth, erosion, 1.18f);
if (erosion <= 0.24f)
return bites;
// A continuous low-frequency field turns the early isolated wounds into
// one spreading tear. At the end every point of the energy face has
// crossed the threshold; the separately drawn hot cutting edge is not
// sampled here and therefore never breaks into teeth or blocks.
float waveA = (float)Math.Sin(progress * 19.7f + depth * 11.3f + 0.6f);
float waveB = (float)Math.Sin(progress * 37.1f - depth * 16.9f + 2.2f);
float waveC = (float)Math.Sin(progress * 8.3f + depth * 31.7f - 1.1f);
float field = MathHelper.Clamp(0.50f + waveA * 0.19f
+ waveB * 0.11f + waveC * 0.07f, 0.12f, 0.88f);
float dissolve = Smooth01((erosion - 0.24f) / 0.76f);
float continuousIntegrity = 1f - SmoothStep(field - 0.09f,
field + 0.09f, dissolve);
return MathHelper.Clamp(bites * continuousIntegrity, 0f, 1f);
}
private static void DrawBloodFramePair(
Texture2D first,
Texture2D second,
float blend,
Vector2 center,
float rotation,
float scale,
Color color,
SpriteEffects effects)
{
if (blend < 0.999f)
DrawTexture(first, center, rotation, scale, color * (1f - blend), effects);
if (blend > 0.001f)
DrawTexture(second, center, rotation, scale, color * blend, effects);
}
private static CrescentSample SampleCrescent(
float x,
float y,
float halfSweep,
float maximumThickness,
float innerWarp,
float asymmetry,
float antialias)
{
float angle = (float)Math.Atan2(y, x);
if (angle <= -halfSweep - 0.02f || angle >= halfSweep + 0.02f)
return default;
float progress = MathHelper.Clamp((angle + halfSweep) / (halfSweep * 2f), 0f, 1f);
float taper = (float)Math.Pow(Math.Max(0f, Math.Sin(progress * MathHelper.Pi)), 0.67f);
taper *= 1f + asymmetry * (progress * 2f - 1f);
float thickness = maximumThickness * taper;
if (thickness <= antialias * 0.35f)
return default;
float radius = (float)Math.Sqrt(x * x + y * y);
float outerRadius = OuterRadius
+ (float)Math.Sin(progress * 29f + (float)Math.Sin(progress * 8f) * 1.3f)
* innerWarp * 0.18f * taper;
float innerDistortion = ((float)Math.Sin(progress * 23f + 0.7f)
+ (float)Math.Sin(progress * 47f - 1.4f) * 0.42f)
* innerWarp * taper;
float innerRadius = outerRadius - thickness + innerDistortion;
float outerMask = 1f - SmoothStep(outerRadius - antialias, outerRadius + antialias, radius);
float innerMask = SmoothStep(innerRadius - antialias, innerRadius + antialias, radius);
float capMask = SmoothStep(0f, 0.018f, progress)
* SmoothStep(0f, 0.018f, 1f - progress);
float body = outerMask * innerMask * capMask;
float depth = MathHelper.Clamp((outerRadius - radius) / Math.Max(antialias, thickness), 0f, 1f);
float outerDistance = Math.Abs(radius - outerRadius);
float innerDistance = Math.Abs(radius - innerRadius);
float outerGlow = (1f - SmoothStep(0.020f, 0.052f, outerDistance)) * capMask;
float outerHot = (1f - SmoothStep(0.004f, 0.014f, outerDistance)) * capMask;
float innerEdge = (1f - SmoothStep(0.006f, 0.020f, innerDistance)) * capMask;
return new CrescentSample(body, progress, depth, outerGlow, outerHot, innerEdge);
}
private static void DrawTexture(Texture2D texture, Vector2 center, float rotation,
float scale, Color color, SpriteEffects effects = SpriteEffects.None)
{
Main.EntitySpriteDraw(texture, center, null, color, rotation,
texture.Size() * 0.5f, scale, effects);
}
private static float GetScale(float radius)
=> radius / (TextureSize * 0.5f * OuterRadius);
private static float GetMaterialScale(float radius)
=> radius / (MaterialTextureSize * 0.5f * OuterRadius);
private static float Ridge(float sine, float sharpness)
=> (float)Math.Pow(MathHelper.Clamp((sine + 1f) * 0.5f, 0f, 1f), sharpness);
private static Color Premultiplied(Vector3 color, float alpha)
{
alpha = MathHelper.Clamp(alpha, 0f, 1f);
color *= alpha;
return new Color(
MathHelper.Clamp(color.X, 0f, 1f),
MathHelper.Clamp(color.Y, 0f, 1f),
MathHelper.Clamp(color.Z, 0f, 1f),
alpha);
}
private static float SmoothStep(float start, float end, float value)
{
if (end <= start)
return value >= end ? 1f : 0f;
return Smooth01((value - start) / (end - start));
}
private static float Smooth01(float value)
{
value = MathHelper.Clamp(value, 0f, 1f);
return value * value * (3f - 2f * value);
}
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);
}
private static float Hash01(uint index, uint seed)
=> (Hash(index, index * 17u + 5u, seed) & 0x00FFFFFFu) / 16777215f;
private readonly record struct CrescentSample(
float Body,
float Progress,
float Depth,
float OuterGlow,
float OuterHot,
float InnerEdge);
}
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using Terraria;
using Terraria.ModLoader;
namespace SoulHarvest.Common;
/// <summary>
/// High-resolution, seamless crescent surfaces shared by every non-Void reaper.
/// Each form keeps the Blood crescent's continuous silhouette while baking its
/// own material into the face. Textures are created lazily on the draw thread:
/// dedicated servers never own graphics resources.
/// </summary>
[Autoload(Side = ModSide.Client)]
internal sealed class ReaperCrescentPrimitiveTextureSystem : ModSystem
{
private const int TextureSize = 768;
private const int MaterialTextureSize = 512;
private const float OuterRadius = 0.91f;
private const int BloodLifecycleFrames = 16;
private const int MaterialLifecycleFrames = 10;
private static readonly Texture2D?[] bloodCrescents = new Texture2D?[BloodLifecycleFrames];
private static readonly Texture2D?[] deathDomainCrescents = new Texture2D?[BloodLifecycleFrames];
private static readonly Texture2D?[] deathDomainRimCrescents = new Texture2D?[BloodLifecycleFrames];
private static readonly Dictionary<ReaperFormId, Texture2D?[]> formCrescents = [];
private static Color[]? deathDomainSurface;
private static Texture2D? deathDomainBlade;
private static Texture2D? deathDomainRiftCrescent;
public override void PostSetupContent()
{
if (Main.dedServ)
return;
// These procedural atlases used to be built synchronously by the first
// Blood/Death swing. Prewarm them on the graphics thread after content
// setup so combat never pays several million samples and GPU uploads in
// a single frame. Dedicated servers remain completely texture-free.
Main.QueueMainThreadAction(() =>
{
DeathDomainBackdropTextureSystem.Prewarm();
for (int frame = 0; frame < BloodLifecycleFrames; frame++)
{
_ = GetBloodCrescent(frame);
_ = GetDeathDomainRimCrescent(frame);
}
});
}
internal static void DrawFormCrescent(
ReaperFormId form,
Vector2 center,
float rotation,
float radius,
float opacity,
int motionLayers,
int swingDirection,
float time,
float lifeProgress)
{
if (form == ReaperFormId.Blood)
{
DrawBloodCrescent(center, rotation, radius, opacity, motionLayers,
swingDirection, time, lifeProgress);
return;
}
if (form == ReaperFormId.Death)
{
DrawDeathDomainCrescent(center, rotation, radius, opacity, motionLayers,
swingDirection, time, lifeProgress);
return;
}
if (Main.dedServ || radius <= 1f || opacity <= 0.001f
|| form == ReaperFormId.Void)
{
return;
}
lifeProgress = MathHelper.Clamp(lifeProgress, 0f, 1f);
float lifecycleFrame = lifeProgress * (MaterialLifecycleFrames - 1);
int firstFrame = Math.Clamp((int)Math.Floor(lifecycleFrame), 0,
MaterialLifecycleFrames - 1);
int secondFrame = Math.Min(firstFrame + 1, MaterialLifecycleFrames - 1);
float frameBlend = lifecycleFrame - firstFrame;
Texture2D firstTexture = GetFormCrescent(form, firstFrame);
Texture2D secondTexture = secondFrame == firstFrame
? firstTexture : GetFormCrescent(form, secondFrame);
Vector2 screenCenter = center - Main.screenPosition;
float scale = GetMaterialScale(radius);
motionLayers = Math.Clamp(motionLayers, 0, 4);
SpriteEffects effects = swingDirection < 0
? SpriteEffects.FlipVertically : SpriteEffects.None;
Color aura = ReaperCombatRegistry.GetPrimaryColor(form) with { A = 0 };
for (int layer = motionLayers; layer >= 1; layer--)
{
float lag = swingDirection * (0.015f + layer * 0.017f);
DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
screenCenter, rotation - lag, scale * (1f - layer * 0.014f),
aura * (opacity * (0.030f + layer * 0.012f)), effects);
}
float pulse = 0.982f + (float)Math.Sin(time * 6.4f + (int)form) * 0.012f;
DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
screenCenter, rotation, scale * 1.018f,
aura * (opacity * 0.16f), effects);
DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
screenCenter, rotation, scale * pulse, Color.White * opacity, effects);
}
internal static void DrawBloodCrescent(
Vector2 center,
float rotation,
float radius,
float opacity,
int motionLayers,
int swingDirection,
float time,
float lifeProgress)
{
if (Main.dedServ || radius <= 1f || opacity <= 0.001f)
return;
lifeProgress = MathHelper.Clamp(lifeProgress, 0f, 1f);
float lifecycleFrame = lifeProgress * (BloodLifecycleFrames - 1);
int firstFrame = Math.Clamp((int)Math.Floor(lifecycleFrame), 0, BloodLifecycleFrames - 1);
int secondFrame = Math.Min(firstFrame + 1, BloodLifecycleFrames - 1);
float frameBlend = lifecycleFrame - firstFrame;
Texture2D firstTexture = GetBloodCrescent(firstFrame);
Texture2D secondTexture = secondFrame == firstFrame
? firstTexture
: GetBloodCrescent(secondFrame);
float scale = GetScale(radius);
Vector2 screenCenter = center - Main.screenPosition;
motionLayers = Math.Clamp(motionLayers, 0, 4);
SpriteEffects effects = swingDirection < 0
? SpriteEffects.FlipVertically
: SpriteEffects.None;
// Reference-style motion exposure: translucent copies sit behind one
// continuous surface rather than breaking the blade into radial blocks.
for (int layer = motionLayers; layer >= 1; layer--)
{
float layerOpacity = opacity * (0.055f + layer * 0.018f);
float rotationLag = swingDirection * (0.018f + layer * 0.018f);
float breathing = 1f - layer * 0.018f;
DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
screenCenter, rotation - rotationLag, scale * breathing,
Color.White * layerOpacity, effects);
}
float pulse = 0.96f + (float)Math.Sin(time * 8.2f) * 0.015f;
DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
screenCenter, rotation, scale * 1.018f,
new Color(255, 34, 42, 0) * (opacity * 0.18f), effects);
DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
screenCenter, rotation, scale * pulse, Color.White * opacity, effects);
}
internal static void DrawDeathDomainCrescent(
Vector2 center,
float rotation,
float radius,
float opacity,
int motionLayers,
int swingDirection,
float time,
float lifeProgress)
{
if (Main.dedServ || radius <= 1f || opacity <= 0.001f)
return;
lifeProgress = MathHelper.Clamp(lifeProgress, 0f, 1f);
float lifecycleFrame = lifeProgress * (BloodLifecycleFrames - 1);
int firstFrame = Math.Clamp((int)Math.Floor(lifecycleFrame), 0,
BloodLifecycleFrames - 1);
int secondFrame = Math.Min(firstFrame + 1, BloodLifecycleFrames - 1);
float frameBlend = lifecycleFrame - firstFrame;
Texture2D firstTexture = GetDeathDomainCrescent(firstFrame);
Texture2D secondTexture = secondFrame == firstFrame
? firstTexture
: GetDeathDomainCrescent(secondFrame);
float scale = GetScale(radius);
Vector2 screenCenter = center - Main.screenPosition;
motionLayers = Math.Clamp(motionLayers, 0, 4);
SpriteEffects effects = swingDirection < 0
? SpriteEffects.FlipVertically
: SpriteEffects.None;
for (int layer = motionLayers; layer >= 1; layer--)
{
float layerOpacity = opacity * (0.042f + layer * 0.014f);
float lag = swingDirection * (0.016f + layer * 0.017f);
DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
screenCenter, rotation - lag, scale * (1f - layer * 0.014f),
new Color(194, 16, 61, 0) * layerOpacity, effects);
}
DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
screenCenter, rotation, scale,
new Color(235, 20, 68, 0) * (opacity * 0.16f), effects);
DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
screenCenter, rotation, scale, Color.White * opacity, effects);
}
internal static void DrawDeathDomainCrescentRim(
Vector2 center,
float rotation,
float radius,
float opacity,
int motionLayers,
int swingDirection,
float time,
float lifeProgress)
{
if (Main.dedServ || radius <= 1f || opacity <= 0.001f)
return;
lifeProgress = MathHelper.Clamp(lifeProgress, 0f, 1f);
float lifecycleFrame = lifeProgress * (BloodLifecycleFrames - 1);
int firstFrame = Math.Clamp((int)Math.Floor(lifecycleFrame), 0,
BloodLifecycleFrames - 1);
int secondFrame = Math.Min(firstFrame + 1, BloodLifecycleFrames - 1);
float frameBlend = lifecycleFrame - firstFrame;
Texture2D firstTexture = GetDeathDomainRimCrescent(firstFrame);
Texture2D secondTexture = secondFrame == firstFrame
? firstTexture
: GetDeathDomainRimCrescent(secondFrame);
float scale = GetScale(radius);
Vector2 screenCenter = center - Main.screenPosition;
motionLayers = Math.Clamp(motionLayers, 0, 4);
SpriteEffects effects = swingDirection < 0
? SpriteEffects.FlipVertically
: SpriteEffects.None;
for (int layer = motionLayers; layer >= 1; layer--)
{
float lag = swingDirection * (0.016f + layer * 0.017f);
DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
screenCenter, rotation - lag, scale,
new Color(208, 12, 54, 0)
* (opacity * (0.034f + layer * 0.012f)), effects);
}
DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
screenCenter, rotation, scale,
new Color(235, 20, 68, 0) * (opacity * 0.18f), effects);
DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
screenCenter, rotation, scale, Color.White * opacity,
effects);
}
internal static void DrawDeathDomainBlade(Vector2 screenCenter, float rotation,
float length, float width, float opacity, float completion)
{
if (Main.dedServ || length <= 0.5f || width <= 0.5f
|| opacity <= 0.001f || completion <= 0.001f)
{
return;
}
Texture2D texture = deathDomainBlade ??= CreateDeathDomainBlade();
completion = MathHelper.Clamp(completion, 0f, 1f);
float visibleLength = Math.Max(0.5f, length * completion);
float widthGrowth = MathHelper.Lerp(0.22f, 1f,
SmoothStep(0f, 0.55f, completion));
Vector2 start = screenCenter
- rotation.ToRotationVector2() * visibleLength * 0.5f;
Main.EntitySpriteDraw(texture, start, null, Color.White * opacity,
rotation, new Vector2(0f, texture.Height * 0.5f),
new Vector2(visibleLength / texture.Width,
width * widthGrowth / texture.Height),
SpriteEffects.None, 0f);
}
internal static void DrawDeathDomainRiftCrescent(Vector2 center, float rotation,
float radius, float opacity, int swingDirection)
{
if (Main.dedServ || radius <= 1f || opacity <= 0.001f)
return;
Texture2D texture = deathDomainRiftCrescent ??= CreateDeathDomainRiftCrescent();
SpriteEffects effects = swingDirection < 0
? SpriteEffects.FlipVertically : SpriteEffects.None;
Vector2 screenCenter = center - Main.screenPosition;
float scale = GetScale(radius);
DrawTexture(texture, screenCenter, rotation, scale * 1.035f,
new Color(188, 9, 54, 0) * (opacity * 0.28f), effects);
DrawTexture(texture, screenCenter, rotation, scale, Color.White * opacity,
effects);
}
public override void Unload()
{
Texture2D?[] oldBlood = new Texture2D?[BloodLifecycleFrames];
Array.Copy(bloodCrescents, oldBlood, BloodLifecycleFrames);
Texture2D?[] oldDeath = new Texture2D?[BloodLifecycleFrames];
Array.Copy(deathDomainCrescents, oldDeath, BloodLifecycleFrames);
Texture2D?[] oldDeathRims = new Texture2D?[BloodLifecycleFrames];
Array.Copy(deathDomainRimCrescents, oldDeathRims, BloodLifecycleFrames);
Texture2D? oldDeathBlade = deathDomainBlade;
Texture2D? oldDeathRift = deathDomainRiftCrescent;
List<Texture2D?> oldForms = [];
foreach (Texture2D?[] textures in formCrescents.Values)
oldForms.AddRange(textures);
Array.Clear(bloodCrescents, 0, bloodCrescents.Length);
Array.Clear(deathDomainCrescents, 0, deathDomainCrescents.Length);
Array.Clear(deathDomainRimCrescents, 0, deathDomainRimCrescents.Length);
formCrescents.Clear();
deathDomainSurface = null;
deathDomainBlade = null;
deathDomainRiftCrescent = null;
if (Main.dedServ)
return;
Main.QueueMainThreadAction(() =>
{
foreach (Texture2D? texture in oldBlood)
texture?.Dispose();
foreach (Texture2D? texture in oldDeath)
texture?.Dispose();
foreach (Texture2D? texture in oldDeathRims)
texture?.Dispose();
foreach (Texture2D? texture in oldForms)
texture?.Dispose();
oldDeathBlade?.Dispose();
oldDeathRift?.Dispose();
});
}
private static Texture2D GetBloodCrescent(int frame)
{
return bloodCrescents[frame] ??= CreateBloodCrescent(
frame / (float)(BloodLifecycleFrames - 1));
}
private static Texture2D GetDeathDomainCrescent(int frame)
{
return deathDomainCrescents[frame] ??= CreateDeathDomainCrescent(
frame / (float)(BloodLifecycleFrames - 1));
}
private static Texture2D GetDeathDomainRimCrescent(int frame)
{
return deathDomainRimCrescents[frame] ??= CreateDeathDomainRimCrescent(
frame / (float)(BloodLifecycleFrames - 1));
}
private static Texture2D GetFormCrescent(ReaperFormId form, int frame)
{
if (!formCrescents.TryGetValue(form, out Texture2D?[]? textures))
{
textures = new Texture2D?[MaterialLifecycleFrames];
formCrescents[form] = textures;
}
return textures[frame] ??= CreateFormCrescent(form,
frame / (float)(MaterialLifecycleFrames - 1));
}
private static Texture2D CreateFormCrescent(ReaperFormId form, float lifeProgress)
{
Texture2D texture = new(Main.instance.GraphicsDevice, MaterialTextureSize,
MaterialTextureSize,
false, SurfaceFormat.Color);
Color[] pixels = new Color[MaterialTextureSize * MaterialTextureSize];
float antialias = 3f / MaterialTextureSize;
float reveal = Smooth01(lifeProgress);
for (int y = 0; y < MaterialTextureSize; y++)
{
float normalizedY = (y + 0.5f) / MaterialTextureSize * 2f - 1f;
for (int x = 0; x < MaterialTextureSize; x++)
{
float normalizedX = (x + 0.5f) / MaterialTextureSize * 2f - 1f;
CrescentSample sample = SampleCrescent(normalizedX, normalizedY,
halfSweep: 2.43f, maximumThickness:
ReaperCombatRegistry.StandardCrescentThicknessRatio,
innerWarp: 0.015f, asymmetry: 0.22f, antialias);
if (sample.Body <= 0.001f)
continue;
float revealMask = 1f - SmoothStep(reveal - 0.018f,
reveal + 0.012f, sample.Progress);
float body = sample.Body * revealMask;
if (body <= 0.001f)
continue;
Vector3 color;
float materialAlpha;
SampleFormMaterial(form, sample.Progress, sample.Depth,
out color, out materialAlpha);
float outerGlow = sample.OuterGlow * revealMask;
float outerHot = sample.OuterHot * revealMask;
Vector3 rim = form switch
{
ReaperFormId.Bone => new Vector3(0.94f, 1f, 0.89f),
ReaperFormId.Infernal => new Vector3(1f, 0.94f, 0.55f),
ReaperFormId.Frost => new Vector3(0.91f, 1f, 1f),
ReaperFormId.Soul => new Vector3(0.75f, 1f, 1f),
_ => new Vector3(0.84f, 1f, 1f)
};
Vector3 glow = ReaperCombatRegistry.GetPrimaryColor(form).ToVector3();
color = Vector3.Lerp(color, glow, outerGlow * 0.68f);
color = Vector3.Lerp(color, rim, outerHot * 0.94f);
float alpha = Math.Max(body * materialAlpha,
Math.Max(outerGlow * 0.88f, outerHot));
pixels[y * MaterialTextureSize + x] = Premultiplied(color, alpha);
}
}
texture.SetData(pixels);
return texture;
}
private static void SampleFormMaterial(ReaperFormId form, float progress,
float depth, out Vector3 color, out float alpha)
{
switch (form)
{
case ReaperFormId.Bone:
{
float joint = Ridge((float)Math.Sin(progress * 45f + 0.4f), 13f);
float marrow = Ridge((float)Math.Sin(progress * 22f
- depth * 13f + 1.1f), 8f);
color = Vector3.Lerp(new Vector3(0.025f, 0.20f, 0.25f),
new Vector3(0.91f, 0.91f, 0.72f),
MathHelper.Clamp(0.20f + joint * 0.62f
+ marrow * (1f - depth) * 0.30f, 0f, 1f));
alpha = MathHelper.Lerp(0.82f, 0.58f, depth);
break;
}
case ReaperFormId.Infernal:
{
float lava = Ridge((float)Math.Sin(progress * 30f
+ depth * 19f + (float)Math.Sin(progress * 9f)), 8f);
float heat = MathHelper.Clamp(lava * 0.82f
+ (1f - depth) * 0.25f, 0f, 1f);
color = Vector3.Lerp(new Vector3(0.035f, 0.006f, 0.002f),
new Vector3(1f, 0.42f, 0.025f), heat);
color = Vector3.Lerp(color, new Vector3(1f, 0.96f, 0.56f),
lava * lava * 0.58f);
alpha = MathHelper.Lerp(0.92f, 0.68f, depth);
break;
}
case ReaperFormId.Frost:
{
float facetA = Math.Abs((float)Math.Sin(progress * 27f + depth * 8f));
float facetB = Math.Abs((float)Math.Sin(progress * 13f - depth * 17f));
float facet = MathHelper.Clamp(facetA * 0.52f + facetB * 0.38f, 0f, 1f);
color = Vector3.Lerp(new Vector3(0.025f, 0.17f, 0.38f),
new Vector3(0.62f, 0.94f, 1f), facet);
color = Vector3.Lerp(color, Vector3.One,
Ridge((float)Math.Sin(progress * 34f - depth * 24f), 14f) * 0.62f);
alpha = MathHelper.Lerp(0.72f, 0.43f, depth);
break;
}
case ReaperFormId.Soul:
{
float stream = Ridge((float)Math.Sin(progress * 21f
- depth * 26f + (float)Math.Sin(progress * 8f) * 1.4f), 6f);
float echo = Ridge((float)Math.Sin(progress * 38f
+ depth * 15f + 2.2f), 12f);
color = Vector3.Lerp(new Vector3(0.10f, 0.018f, 0.28f),
new Vector3(0.34f, 0.28f, 1f), stream * 0.72f);
color = Vector3.Lerp(color, new Vector3(0.22f, 1f, 1f),
echo * 0.62f);
alpha = MathHelper.Lerp(0.76f, 0.40f, depth);
break;
}
default:
{
float spirit = Ridge((float)Math.Sin(progress * 25f
- depth * 18f), 8f);
color = Vector3.Lerp(new Vector3(0.025f, 0.18f, 0.21f),
new Vector3(0.50f, 0.96f, 1f), spirit * 0.68f);
alpha = MathHelper.Lerp(0.68f, 0.38f, depth);
break;
}
}
}
private static Texture2D CreateDeathDomainCrescent(float lifeProgress)
{
Texture2D texture = new(Main.instance.GraphicsDevice, TextureSize, TextureSize,
false, SurfaceFormat.Color);
Color[] pixels = new Color[TextureSize * TextureSize];
Color[] surface = deathDomainSurface ??= CreateDeathDomainSurface();
float antialias = 3f / TextureSize;
float reveal = Smooth01(lifeProgress);
float erosion = Smooth01((lifeProgress - 0.30f) / 0.66f);
float edgeFade = 1f - Smooth01((lifeProgress - 0.96f) / 0.04f);
for (int y = 0; y < TextureSize; y++)
{
float normalizedY = (y + 0.5f) / TextureSize * 2f - 1f;
for (int x = 0; x < TextureSize; x++)
{
float normalizedX = (x + 0.5f) / TextureSize * 2f - 1f;
CrescentSample sample = SampleCrescent(normalizedX, normalizedY,
halfSweep: 2.43f, maximumThickness:
ReaperCombatRegistry.StandardCrescentThicknessRatio,
innerWarp: 0.018f, asymmetry: 0.22f, antialias);
if (sample.Body <= 0.001f)
continue;
float revealMask = 1f - SmoothStep(reveal - 0.018f,
reveal + 0.012f, sample.Progress);
float integrity = SampleDeathInteriorIntegrity(sample.Progress,
sample.Depth, erosion);
float body = sample.Body * revealMask * integrity;
Vector4 scene = surface[y * TextureSize + x].ToVector4();
Vector3 color = new(scene.X, scene.Y, scene.Z);
float alpha = body;
float crimsonVein = Ridge((float)Math.Sin(sample.Progress * 21f
- sample.Depth * 18f + 0.9f), 9f);
color = Vector3.Lerp(color, new Vector3(0.66f, 0.008f, 0.09f),
crimsonVein * 0.18f * body);
// The outer cutting edge is never eroded. Only the domain-filled
// face tears away, keeping one continuous hot blade silhouette.
float redRim = sample.OuterGlow * revealMask * edgeFade;
float whiteRim = sample.OuterHot * revealMask * edgeFade;
color = Vector3.Lerp(color, new Vector3(0.92f, 0.025f, 0.12f),
redRim * 0.78f);
color = Vector3.Lerp(color, new Vector3(1f, 0.88f, 0.84f),
whiteRim * 0.90f);
alpha = Math.Max(alpha, Math.Max(redRim * 0.90f,
whiteRim * 0.98f));
if (alpha <= 0.001f)
continue;
pixels[y * TextureSize + x] = Premultiplied(color, alpha);
}
}
texture.SetData(pixels);
return texture;
}
private static Texture2D CreateDeathDomainRimCrescent(float lifeProgress)
{
Texture2D texture = new(Main.instance.GraphicsDevice, TextureSize,
TextureSize, false, SurfaceFormat.Color);
Color[] pixels = new Color[TextureSize * TextureSize];
float antialias = 3f / TextureSize;
float reveal = Smooth01(lifeProgress);
float edgeFade = 1f - Smooth01((lifeProgress - 0.96f) / 0.04f);
for (int y = 0; y < TextureSize; y++)
{
float normalizedY = (y + 0.5f) / TextureSize * 2f - 1f;
for (int x = 0; x < TextureSize; x++)
{
float normalizedX = (x + 0.5f) / TextureSize * 2f - 1f;
CrescentSample sample = SampleCrescent(normalizedX, normalizedY,
halfSweep: 2.43f, maximumThickness:
ReaperCombatRegistry.StandardCrescentThicknessRatio,
innerWarp: 0.018f, asymmetry: 0.22f, antialias);
if (sample.Body <= 0.001f)
continue;
float revealMask = 1f - SmoothStep(reveal - 0.018f,
reveal + 0.012f, sample.Progress);
float redRim = sample.OuterGlow * revealMask * edgeFade;
float whiteRim = sample.OuterHot * revealMask * edgeFade;
float alpha = Math.Max(redRim * 0.90f, whiteRim * 0.98f);
if (alpha <= 0.001f)
continue;
Vector3 color = Vector3.Lerp(new Vector3(0.92f, 0.025f, 0.12f),
new Vector3(1f, 0.88f, 0.84f), whiteRim * 0.94f);
pixels[y * TextureSize + x] = Premultiplied(color, alpha);
}
}
texture.SetData(pixels);
return texture;
}
private static Color[] CreateDeathDomainSurface()
{
Color[] surface = new Color[TextureSize * TextureSize];
for (int y = 0; y < TextureSize; y++)
{
float v = (y + 0.5f) / TextureSize;
for (int x = 0; x < TextureSize; x++)
{
float u = (x + 0.5f) / TextureSize;
surface[y * TextureSize + x] =
DeathDomainBackdropTextureSystem.SampleCompositePixel(
x * 2, y * 2, u, v);
}
}
return surface;
}
private static Texture2D CreateDeathDomainBlade()
{
const int width = 1024;
const int height = 256;
Texture2D texture = new(Main.instance.GraphicsDevice, width, height,
false, SurfaceFormat.Color);
Color[] pixels = new Color[width * height];
for (int y = 0; y < height; y++)
{
float vertical = Math.Abs((y + 0.5f) / height * 2f - 1f);
float v = (y + 0.5f) / height;
for (int x = 0; x < width; x++)
{
float progress = (x + 0.5f) / width;
float taper = (float)Math.Pow(Math.Max(0f,
Math.Sin(progress * MathHelper.Pi)), 0.56f);
taper *= 1f
+ (float)Math.Sin(progress * MathHelper.TwoPi * 5.1f) * 0.035f
+ (float)Math.Sin(progress * MathHelper.TwoPi * 13.7f) * 0.014f;
float normalizedDistance = vertical / Math.Max(0.0001f, taper);
float body = 1f - SmoothStep(0.965f, 1.018f,
normalizedDistance);
if (body <= 0.001f)
continue;
float u = progress;
Vector4 sampled = DeathDomainBackdropTextureSystem
.SampleCompositePixel(x * 2, y * 3, u, v).ToVector4();
Vector3 color = new(sampled.X, sampled.Y, sampled.Z);
float redRim = SmoothStep(0.70f, 0.92f, normalizedDistance)
* (1f - SmoothStep(0.97f, 1.02f, normalizedDistance));
float whiteRim = SmoothStep(0.86f, 0.965f, normalizedDistance)
* (1f - SmoothStep(0.985f, 1.02f, normalizedDistance));
color = Vector3.Lerp(color, new Vector3(0.94f, 0.018f, 0.12f),
redRim * 0.86f);
color = Vector3.Lerp(color, new Vector3(1f, 0.90f, 0.86f),
whiteRim * 0.88f);
float alpha = Math.Max(body * 0.96f,
Math.Max(redRim * 0.92f, whiteRim));
pixels[y * width + x] = Premultiplied(color, alpha);
}
}
texture.SetData(pixels);
return texture;
}
private static Texture2D CreateDeathDomainRiftCrescent()
{
Texture2D texture = new(Main.instance.GraphicsDevice, TextureSize, TextureSize,
false, SurfaceFormat.Color);
Color[] pixels = new Color[TextureSize * TextureSize];
float antialias = 3f / TextureSize;
for (int y = 0; y < TextureSize; y++)
{
float normalizedY = (y + 0.5f) / TextureSize * 2f - 1f;
for (int x = 0; x < TextureSize; x++)
{
float normalizedX = (x + 0.5f) / TextureSize * 2f - 1f;
CrescentSample sample = SampleCrescent(normalizedX, normalizedY,
halfSweep: 2.43f, maximumThickness: 0.15f,
innerWarp: 0.026f, asymmetry: 0.20f, antialias);
if (sample.Body <= 0.001f)
continue;
float integrity = SampleInteriorIntegrity(sample.Progress,
sample.Depth, 1f, 1.28f);
Vector4 sampled = DeathDomainBackdropTextureSystem
.SampleCompositePixel(x * 2, y * 2,
(x + 0.5f) / TextureSize, (y + 0.5f) / TextureSize)
.ToVector4();
Vector3 color = new(sampled.X, sampled.Y, sampled.Z);
float redRim = sample.OuterGlow;
float whiteRim = sample.OuterHot;
color = Vector3.Lerp(color, new Vector3(0.94f, 0.012f, 0.12f),
redRim * 0.88f);
color = Vector3.Lerp(color, new Vector3(1f, 0.88f, 0.84f),
whiteRim * 0.92f);
float alpha = Math.Max(sample.Body * integrity * 0.94f,
Math.Max(redRim * 0.94f, whiteRim));
pixels[y * TextureSize + x] = Premultiplied(color, alpha);
}
}
texture.SetData(pixels);
return texture;
}
private static Texture2D CreateBloodCrescent(float lifeProgress)
{
Texture2D texture = new(Main.instance.GraphicsDevice, TextureSize, TextureSize,
false, SurfaceFormat.Color);
Color[] pixels = new Color[TextureSize * TextureSize];
float antialias = 3f / TextureSize;
// The weapon pose uses the same smooth-step clock. Keeping the mask on
// that clock makes the bright leading edge sit at the live blade tip
// instead of revealing most of the crescent before the hand gets there.
float reveal = Smooth01(lifeProgress);
float erosion = Smooth01((lifeProgress - 0.26f) / 0.68f);
for (int y = 0; y < TextureSize; y++)
{
float normalizedY = ((y + 0.5f) / TextureSize * 2f - 1f);
for (int x = 0; x < TextureSize; x++)
{
float normalizedX = (x + 0.5f) / TextureSize * 2f - 1f;
CrescentSample sample = SampleCrescent(normalizedX, normalizedY,
halfSweep: 2.43f, maximumThickness:
ReaperCombatRegistry.StandardCrescentThicknessRatio,
innerWarp: 0.018f, asymmetry: 0.22f, antialias);
if (sample.Body <= 0.001f)
continue;
// Only the arc already crossed by the moving blade is visible.
// FlipVertically reverses this reveal for the opposite swing.
float revealMask = 1f - SmoothStep(reveal - 0.018f, reveal + 0.012f, sample.Progress);
float integrity = SampleInteriorIntegrity(sample.Progress,
sample.Depth, erosion, 1.12f) * revealMask;
if (integrity <= 0.001f)
continue;
float liquidA = Ridge((float)Math.Sin(
sample.Progress * 37f + sample.Depth * 15f
+ (float)Math.Sin(sample.Progress * 12f) * 1.8f), 7f);
float liquidB = Ridge((float)Math.Sin(
sample.Progress * 19f - sample.Depth * 24f + 1.7f), 11f);
float flow = MathHelper.Clamp(liquidA * 0.52f + liquidB * 0.30f, 0f, 1f);
Vector3 deepRed = new(0.19f, 0.002f, 0.018f);
Vector3 outerRed = new(1f, 0.025f, 0.055f);
Vector3 warmFlow = new(1f, 0.22f, 0.105f);
Vector3 color = Vector3.Lerp(outerRed, deepRed,
(float)Math.Pow(sample.Depth, 0.70f));
color = Vector3.Lerp(color, warmFlow, flow * 0.46f);
float alpha = sample.Body * integrity * MathHelper.Lerp(0.78f, 0.52f, sample.Depth);
float innerHeat = sample.InnerEdge * integrity * (0.18f + flow * 0.24f);
color = Vector3.Lerp(color, new Vector3(1f, 0.09f, 0.08f), innerHeat);
alpha = Math.Max(alpha, innerHeat * 0.72f);
// Fractures consume only the liquid energy face. The hot cutting
// edge remains continuous from tip to tip at every lifetime frame.
float whiteRim = sample.OuterHot * revealMask;
float redRim = sample.OuterGlow * revealMask;
color = Vector3.Lerp(color, new Vector3(1f, 0.18f, 0.10f), redRim * 0.62f);
color = Vector3.Lerp(color, new Vector3(1f, 0.94f, 0.79f), whiteRim);
alpha = Math.Max(alpha, Math.Max(redRim * 0.82f, whiteRim * 0.98f));
pixels[y * TextureSize + x] = Premultiplied(color, alpha);
}
}
texture.SetData(pixels);
return texture;
}
private static float SampleInteriorIntegrity(float progress, float depth,
float erosion, float intensity)
{
if (erosion <= 0.015f)
return 1f;
float integrity = 1f;
// Persistent bites grow from the inner edge. Their deterministic layout
// keeps the crescent coherent from frame to frame instead of shimmering.
for (int index = 0; index < 18; index++)
{
float hashA = Hash01((uint)index, 0xB10D51C1u);
float hashB = Hash01((uint)index, 0xA63E218Fu);
float center = 0.055f + hashA * 0.89f;
float activation = 0.035f + index / 17f * 0.62f;
float growth = Smooth01((erosion - activation) / 0.28f) * intensity;
if (growth <= 0f)
continue;
float halfWidth = MathHelper.Lerp(0.018f, 0.052f, hashB) * (0.35f + growth * 0.65f);
float horizontal = Math.Abs(progress - center) / Math.Max(0.001f, halfWidth);
if (horizontal >= 1f)
continue;
float arch = (float)Math.Sqrt(Math.Max(0f, 1f - horizontal * horizontal));
float biteDepth = MathHelper.Lerp(0.16f, 0.52f, hashA)
* Math.Min(1f, growth) * arch;
float boundary = 1f - biteDepth;
integrity = Math.Min(integrity, 1f - SmoothStep(boundary - 0.018f, boundary + 0.018f, depth));
}
// Late frames tear several isolated wounds through the energy face. This
// is what creates the irregular missing islands visible in the reference.
for (int index = 0; index < 13; index++)
{
float hashA = Hash01((uint)index, 0xC04A711Du);
float hashB = Hash01((uint)index, 0xF731A2E9u);
float activation = 0.18f + index / 12f * 0.50f;
float growth = Smooth01((erosion - activation) / 0.27f) * intensity;
if (growth <= 0f)
continue;
float centerX = 0.08f + hashA * 0.84f;
// Keep every isolated wound away from depth zero: the outer blade
// rim may glow over a missing interior but is never itself severed.
float centerY = 0.30f + hashB * 0.54f;
float radiusX = MathHelper.Lerp(0.018f, 0.058f, hashB)
* Math.Min(1f, growth);
float radiusY = MathHelper.Lerp(0.055f, 0.15f, hashA)
* Math.Min(1f, growth);
float nx = (progress - centerX) / Math.Max(0.001f, radiusX);
float ny = (depth - centerY) / Math.Max(0.001f, radiusY);
float distance = (float)Math.Sqrt(nx * nx + ny * ny);
integrity = Math.Min(integrity, SmoothStep(0.82f, 1.08f, distance));
}
return MathHelper.Clamp(integrity, 0f, 1f);
}
internal static float SampleDeathInteriorIntegrity(float progress,
float depth, float erosion)
{
float bites = SampleInteriorIntegrity(progress, depth, erosion, 1.18f);
if (erosion <= 0.24f)
return bites;
// A continuous low-frequency field turns the early isolated wounds into
// one spreading tear. At the end every point of the energy face has
// crossed the threshold; the separately drawn hot cutting edge is not
// sampled here and therefore never breaks into teeth or blocks.
float waveA = (float)Math.Sin(progress * 19.7f + depth * 11.3f + 0.6f);
float waveB = (float)Math.Sin(progress * 37.1f - depth * 16.9f + 2.2f);
float waveC = (float)Math.Sin(progress * 8.3f + depth * 31.7f - 1.1f);
float field = MathHelper.Clamp(0.50f + waveA * 0.19f
+ waveB * 0.11f + waveC * 0.07f, 0.12f, 0.88f);
float dissolve = Smooth01((erosion - 0.24f) / 0.76f);
float continuousIntegrity = 1f - SmoothStep(field - 0.09f,
field + 0.09f, dissolve);
return MathHelper.Clamp(bites * continuousIntegrity, 0f, 1f);
}
private static void DrawBloodFramePair(
Texture2D first,
Texture2D second,
float blend,
Vector2 center,
float rotation,
float scale,
Color color,
SpriteEffects effects)
{
if (blend < 0.999f)
DrawTexture(first, center, rotation, scale, color * (1f - blend), effects);
if (blend > 0.001f)
DrawTexture(second, center, rotation, scale, color * blend, effects);
}
private static CrescentSample SampleCrescent(
float x,
float y,
float halfSweep,
float maximumThickness,
float innerWarp,
float asymmetry,
float antialias)
{
float angle = (float)Math.Atan2(y, x);
if (angle <= -halfSweep - 0.02f || angle >= halfSweep + 0.02f)
return default;
float progress = MathHelper.Clamp((angle + halfSweep) / (halfSweep * 2f), 0f, 1f);
float taper = (float)Math.Pow(Math.Max(0f, Math.Sin(progress * MathHelper.Pi)), 0.67f);
taper *= 1f + asymmetry * (progress * 2f - 1f);
float thickness = maximumThickness * taper;
if (thickness <= antialias * 0.35f)
return default;
float radius = (float)Math.Sqrt(x * x + y * y);
float outerRadius = OuterRadius
+ (float)Math.Sin(progress * 29f + (float)Math.Sin(progress * 8f) * 1.3f)
* innerWarp * 0.18f * taper;
float innerDistortion = ((float)Math.Sin(progress * 23f + 0.7f)
+ (float)Math.Sin(progress * 47f - 1.4f) * 0.42f)
* innerWarp * taper;
float innerRadius = outerRadius - thickness + innerDistortion;
float outerMask = 1f - SmoothStep(outerRadius - antialias, outerRadius + antialias, radius);
float innerMask = SmoothStep(innerRadius - antialias, innerRadius + antialias, radius);
float capMask = SmoothStep(0f, 0.018f, progress)
* SmoothStep(0f, 0.018f, 1f - progress);
float body = outerMask * innerMask * capMask;
float depth = MathHelper.Clamp((outerRadius - radius) / Math.Max(antialias, thickness), 0f, 1f);
float outerDistance = Math.Abs(radius - outerRadius);
float innerDistance = Math.Abs(radius - innerRadius);
float outerGlow = (1f - SmoothStep(0.020f, 0.052f, outerDistance)) * capMask;
float outerHot = (1f - SmoothStep(0.004f, 0.014f, outerDistance)) * capMask;
float innerEdge = (1f - SmoothStep(0.006f, 0.020f, innerDistance)) * capMask;
return new CrescentSample(body, progress, depth, outerGlow, outerHot, innerEdge);
}
private static void DrawTexture(Texture2D texture, Vector2 center, float rotation,
float scale, Color color, SpriteEffects effects = SpriteEffects.None)
{
Main.EntitySpriteDraw(texture, center, null, color, rotation,
texture.Size() * 0.5f, scale, effects);
}
private static float GetScale(float radius)
=> radius / (TextureSize * 0.5f * OuterRadius);
private static float GetMaterialScale(float radius)
=> radius / (MaterialTextureSize * 0.5f * OuterRadius);
private static float Ridge(float sine, float sharpness)
=> (float)Math.Pow(MathHelper.Clamp((sine + 1f) * 0.5f, 0f, 1f), sharpness);
private static Color Premultiplied(Vector3 color, float alpha)
{
alpha = MathHelper.Clamp(alpha, 0f, 1f);
color *= alpha;
return new Color(
MathHelper.Clamp(color.X, 0f, 1f),
MathHelper.Clamp(color.Y, 0f, 1f),
MathHelper.Clamp(color.Z, 0f, 1f),
alpha);
}
private static float SmoothStep(float start, float end, float value)
{
if (end <= start)
return value >= end ? 1f : 0f;
return Smooth01((value - start) / (end - start));
}
private static float Smooth01(float value)
{
value = MathHelper.Clamp(value, 0f, 1f);
return value * value * (3f - 2f * value);
}
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);
}
private static float Hash01(uint index, uint seed)
=> (Hash(index, index * 17u + 5u, seed) & 0x00FFFFFFu) / 16777215f;
private readonly record struct CrescentSample(
float Body,
float Progress,
float Depth,
float OuterGlow,
float OuterHot,
float InnerEdge);
}