using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using Terraria; using Terraria.ModLoader; namespace SoulHarvest.Common; /// /// 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. /// [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 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 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); }