using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using Terraria; using Terraria.ModLoader; namespace SoulHarvest.Common; /// /// Builds a high-resolution, antialiased double-pointed mask for death-domain /// blades. Drawing a single stretched mask avoids the seams and blocky corners /// produced by assembling a slash out of dozens of MagicPixel rectangles. /// [Autoload(Side = ModSide.Client)] internal sealed class DeathDomainPrimitiveTextureSystem : ModSystem { internal static Texture2D? BladeMask { get; private set; } internal static Texture2D? RadialGradientMask { get; private set; } internal static Texture2D? SmoothLineMask { get; private set; } internal static Texture2D? SmoothStripMask { get; private set; } internal static Texture2D? SmoothDiscMask { get; private set; } internal static Texture2D? BloodScarMask { get; private set; } internal static Texture2D? VoidRiftMask { get; private set; } private static Texture2D CreateBladeMask() { 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); for (int x = 0; x < width; x++) { float horizontal = (x + 0.5f) / width; float envelope = (float)Math.Pow(Math.Max(0f, Math.Sin(horizontal * MathHelper.Pi)), 0.62f); float signedDistance = envelope <= 0.0001f ? 4f : vertical / envelope; float alpha = 1f - SmoothStep(0.955f, 1.035f, signedDistance); // Runtime textures are not processed by the content pipeline, so // store premultiplied white explicitly for clean tinted edges. byte value = (byte)Math.Clamp((int)Math.Round(alpha * 255f), 0, 255); pixels[y * width + x] = new Color(value, value, value, value); } } texture.SetData(pixels); return texture; } private static Texture2D CreateRadialGradientMask() { const int size = 512; Texture2D texture = new(Main.instance.GraphicsDevice, size, size, false, SurfaceFormat.Color); Color[] pixels = new Color[size * size]; for (int y = 0; y < size; y++) { float normalizedY = (y + 0.5f) / size * 2f - 1f; for (int x = 0; x < size; x++) { float normalizedX = (x + 0.5f) / size * 2f - 1f; float distance = (float)Math.Sqrt(normalizedX * normalizedX + normalizedY * normalizedY); float falloff = MathHelper.Clamp(1f - distance, 0f, 1f); float alpha = falloff * falloff * (3f - 2f * falloff); alpha = (float)Math.Pow(alpha, 1.18f); byte value = (byte)Math.Clamp((int)Math.Round(alpha * 255f), 0, 255); pixels[y * size + x] = new Color(value, value, value, value); } } texture.SetData(pixels); return texture; } private static Texture2D CreateSmoothDiscMask() { const int size = 512; Texture2D texture = new(Main.instance.GraphicsDevice, size, size, false, SurfaceFormat.Color); Color[] pixels = new Color[size * size]; for (int y = 0; y < size; y++) { float normalizedY = (y + 0.5f) / size * 2f - 1f; for (int x = 0; x < size; x++) { float normalizedX = (x + 0.5f) / size * 2f - 1f; float distance = (float)Math.Sqrt(normalizedX * normalizedX + normalizedY * normalizedY); float alpha = 1f - SmoothStep(0.982f, 1.006f, distance); byte value = (byte)Math.Clamp((int)Math.Round(alpha * 255f), 0, 255); pixels[y * size + x] = new Color(value, value, value, value); } } texture.SetData(pixels); return texture; } private static Texture2D CreateSmoothLineMask() { // The very wide aspect ratio keeps the antialiased end caps compact even // when a long trail segment is stretched across the screen. Unlike a // MagicPixel rectangle, both the sides and tips dissolve over several // source texels, so connected curves do not expose hard stair-steps. const int width = 1024; const int height = 96; Texture2D texture = new(Main.instance.GraphicsDevice, width, height, false, SurfaceFormat.Color); Color[] pixels = new Color[width * height]; float capRadius = height * 0.5f - 2f; float leftCenter = capRadius + 1f; float rightCenter = width - capRadius - 1f; for (int y = 0; y < height; y++) { float dy = Math.Abs(y + 0.5f - height * 0.5f); for (int x = 0; x < width; x++) { float dx = x + 0.5f < leftCenter ? leftCenter - (x + 0.5f) : x + 0.5f > rightCenter ? x + 0.5f - rightCenter : 0f; float distance = (float)Math.Sqrt(dx * dx + dy * dy); float alpha = 1f - SmoothStep(capRadius - 2.25f, capRadius + 0.75f, distance); byte value = (byte)Math.Clamp((int)Math.Round(alpha * 255f), 0, 255); pixels[y * width + x] = new Color(value, value, value, value); } } texture.SetData(pixels); return texture; } private static Texture2D CreateSmoothStripMask() { // Connected curves must not use the rounded end caps from SmoothLineMask: // dozens of overlapping caps make every sampling point visibly brighter. // This mask is fully continuous along X and only antialiases its two long // edges, allowing short rotated spans to read as one uninterrupted ribbon. const int width = 256; const int height = 96; Texture2D texture = new(Main.instance.GraphicsDevice, width, height, false, SurfaceFormat.Color); Color[] pixels = new Color[width * height]; float halfHeight = height * 0.5f - 1f; for (int y = 0; y < height; y++) { float distance = Math.Abs(y + 0.5f - height * 0.5f); float alpha = 1f - SmoothStep(halfHeight - 2.25f, halfHeight + 0.75f, distance); byte value = (byte)Math.Clamp((int)Math.Round(alpha * 255f), 0, 255); Color pixel = new(value, value, value, value); for (int x = 0; x < width; x++) pixels[y * width + x] = pixel; } texture.SetData(pixels); return texture; } private static Texture2D CreateBloodScarMask() { // One high-resolution mask owns the complete wound silhouette. Its two // edges use continuous low-frequency waves, so stretching it across the // world cannot expose joins or the saw teeth caused by per-span quads. const int width = 2048; 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 = (y + 0.5f) / height * 2f - 1f; 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.46f); float wave = (float)Math.Sin(progress * MathHelper.TwoPi * 3.1f + 0.42f) * 0.045f + (float)Math.Sin(progress * MathHelper.TwoPi * 7.3f - 1.17f) * 0.021f; float asymmetric = vertical >= 0f ? 1f + wave : 1f - wave * 0.72f; float boundary = Math.Max(0.0001f, taper * asymmetric); float distance = Math.Abs(vertical) / boundary; float alpha = 1f - SmoothStep(0.968f, 1.018f, distance); byte value = (byte)Math.Clamp((int)Math.Round(alpha * 255f), 0, 255); pixels[y * width + x] = new Color(value, value, value, value); } } texture.SetData(pixels); return texture; } private static Texture2D CreateVoidRiftMask() { // The complete giant rift is one mask. Continuous waves alter its center // and width across X, giving it a torn silhouette without assembling the // opening from independently rotated strips. const int width = 2048; const int height = 384; 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 = (y + 0.5f) / height * 2f - 1f; 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.42f); float centerWave = (float)Math.Sin(progress * MathHelper.TwoPi * 2.3f + 0.61f) * 0.050f + (float)Math.Sin(progress * MathHelper.TwoPi * 5.7f - 0.84f) * 0.018f; float widthWave = 1f + (float)Math.Sin(progress * MathHelper.TwoPi * 4.1f - 0.32f) * 0.085f + (float)Math.Sin(progress * MathHelper.TwoPi * 10.7f + 1.11f) * 0.032f; float boundary = Math.Max(0.0001f, taper * widthWave); float distance = Math.Abs(vertical - centerWave * taper) / boundary; float alpha = 1f - SmoothStep(0.966f, 1.018f, distance); byte value = (byte)Math.Clamp((int)Math.Round(alpha * 255f), 0, 255); pixels[y * width + x] = new Color(value, value, value, value); } } texture.SetData(pixels); return texture; } public override void Unload() { Texture2D? bladeTexture = BladeMask; Texture2D? gradientTexture = RadialGradientMask; Texture2D? smoothLineTexture = SmoothLineMask; Texture2D? smoothStripTexture = SmoothStripMask; Texture2D? smoothDiscTexture = SmoothDiscMask; Texture2D? bloodScarTexture = BloodScarMask; Texture2D? voidRiftTexture = VoidRiftMask; BladeMask = null; RadialGradientMask = null; SmoothLineMask = null; SmoothStripMask = null; SmoothDiscMask = null; BloodScarMask = null; VoidRiftMask = null; if (!Main.dedServ && (bladeTexture is not null || gradientTexture is not null || smoothLineTexture is not null || smoothStripTexture is not null || smoothDiscTexture is not null || bloodScarTexture is not null || voidRiftTexture is not null)) { Main.QueueMainThreadAction(() => { bladeTexture?.Dispose(); gradientTexture?.Dispose(); smoothLineTexture?.Dispose(); smoothStripTexture?.Dispose(); smoothDiscTexture?.Dispose(); bloodScarTexture?.Dispose(); voidRiftTexture?.Dispose(); }); } } internal static void DrawBlade( Vector2 start, float rotation, float length, float width, Color color, float completion = 1f) { // Mod content hooks may run on a loader worker thread. PreDraw always runs // on the main thread, so lazy creation here satisfies FNA's graphics-thread // requirement and keeps dedicated servers entirely texture-free. Texture2D? texture = BladeMask; if (texture is null && !Main.dedServ) { texture = CreateBladeMask(); BladeMask = texture; } completion = MathHelper.Clamp(completion, 0f, 1f); if (texture is null || length <= 0.05f || width <= 0.05f || completion <= 0.001f) return; float visibleLength = Math.Max(0.5f, length * completion); float widthGrowth = MathHelper.Lerp(0.22f, 1f, SmoothStep(0f, 0.55f, completion)); Main.EntitySpriteDraw( texture, start, null, color, rotation, new Vector2(0f, texture.Height * 0.5f), new Vector2(visibleLength / texture.Width, width * widthGrowth / texture.Height), SpriteEffects.None, 0f); } internal static void DrawBladeCentered( Vector2 center, float rotation, float length, float width, Color color, float completion = 1f) { completion = MathHelper.Clamp(completion, 0f, 1f); float visibleLength = Math.Max(0.5f, length * completion); Vector2 start = center - rotation.ToRotationVector2() * visibleLength * 0.5f; DrawBlade(start, rotation, length, width, color, completion); } internal static void DrawRadialGradient(SpriteBatch batch, Vector2 center, float radius, Color color) { if (Main.dedServ || radius <= 0.5f) return; Texture2D texture = RadialGradientMask ??= CreateRadialGradientMask(); batch.Draw( texture, center, null, color, 0f, texture.Size() * 0.5f, radius * 2f / texture.Width, SpriteEffects.None, 0f); } internal static void DrawSmoothDisc(SpriteBatch batch, Vector2 center, float radius, Color color) { if (Main.dedServ || radius <= 0.5f) return; Texture2D texture = SmoothDiscMask ??= CreateSmoothDiscMask(); batch.Draw(texture, center, null, color, 0f, texture.Size() * 0.5f, radius * 2f / texture.Width, SpriteEffects.None, 0f); } internal static void DrawSmoothLine( SpriteBatch batch, Vector2 start, Vector2 end, Color color, float width) { if (Main.dedServ || width <= 0.05f) return; Vector2 delta = end - start; float length = delta.Length(); if (length <= 0.1f) return; Texture2D texture = SmoothLineMask ??= CreateSmoothLineMask(); batch.Draw( texture, (start + end) * 0.5f, null, color, delta.ToRotation(), texture.Size() * 0.5f, new Vector2((length + Math.Min(width, 3f)) / texture.Width, width / texture.Height), SpriteEffects.None, 0f); } internal static void DrawSmoothWorldLine( Vector2 start, Vector2 end, Color color, float width) { DrawSmoothLine(Main.spriteBatch, start - Main.screenPosition, end - Main.screenPosition, color, width); } internal static void DrawSmoothStrip( SpriteBatch batch, Vector2 start, Vector2 end, Color color, float width) { if (Main.dedServ || width <= 0.05f) return; Vector2 delta = end - start; float length = delta.Length(); if (length <= 0.1f) return; Texture2D texture = SmoothStripMask ??= CreateSmoothStripMask(); Vector2 direction = delta / length; // A sub-pixel overlap closes rotated joins without reintroducing the // conspicuous circular stamps produced by the capped line mask. float overlap = Math.Min(0.7f, width * 0.08f); start -= direction * overlap; end += direction * overlap; delta = end - start; length = delta.Length(); batch.Draw( texture, (start + end) * 0.5f, null, color, delta.ToRotation(), texture.Size() * 0.5f, new Vector2(length / texture.Width, width / texture.Height), SpriteEffects.None, 0f); } internal static void DrawSmoothWorldStrip( Vector2 start, Vector2 end, Color color, float width) { DrawSmoothStrip(Main.spriteBatch, start - Main.screenPosition, end - Main.screenPosition, color, width); } internal static void DrawBloodScar( Vector2 center, float rotation, float length, float width, float opacity, float reveal = 1f) { if (Main.dedServ || length <= 0.5f || width <= 0.5f || opacity <= 0.001f) return; Texture2D texture = BloodScarMask ??= CreateBloodScarMask(); reveal = MathHelper.Clamp(reveal, 0f, 1f); int visibleWidth = Math.Max(1, (int)Math.Round(texture.Width * reveal)); Rectangle source = new(0, 0, visibleWidth, texture.Height); Vector2 position = center - rotation.ToRotationVector2() * length * 0.5f - Main.screenPosition; Vector2 origin = new(0f, texture.Height * 0.5f); Vector2 scale = new(length / texture.Width, width / texture.Height); Main.EntitySpriteDraw(texture, position, source, new Color(118, 0, 20, 0) * (opacity * 0.46f), rotation, origin, new Vector2(scale.X, scale.Y * 1.42f), SpriteEffects.None); Main.EntitySpriteDraw(texture, position, source, new Color(250, 20, 54, 235) * (opacity * 0.96f), rotation, origin, new Vector2(scale.X, scale.Y * 1.13f), SpriteEffects.None); Main.EntitySpriteDraw(texture, position, source, new Color(7, 0, 3, 252) * opacity, rotation, origin, scale, SpriteEffects.None); Main.EntitySpriteDraw(texture, position, source, new Color(31, 0, 9, 240) * (opacity * 0.82f), rotation, origin, new Vector2(scale.X, scale.Y * 0.56f), SpriteEffects.None); } internal static void DrawVoidRift( Vector2 center, float rotation, float length, float width, float opacity, float reveal = 1f) { if (Main.dedServ || length <= 0.5f || width <= 0.5f || opacity <= 0.001f) return; Texture2D texture = VoidRiftMask ??= CreateVoidRiftMask(); reveal = MathHelper.Clamp(reveal, 0f, 1f); int visibleWidth = Math.Max(1, (int)Math.Round(texture.Width * reveal)); Rectangle source = new(0, 0, visibleWidth, texture.Height); Vector2 position = center - rotation.ToRotationVector2() * length * 0.5f - Main.screenPosition; Vector2 origin = new(0f, texture.Height * 0.5f); Vector2 scale = new(length / texture.Width, width / texture.Height); Main.EntitySpriteDraw(texture, position, source, new Color(54, 3, 88, 0) * (opacity * 0.42f), rotation, origin, new Vector2(scale.X, scale.Y * 1.25f), SpriteEffects.None); Main.EntitySpriteDraw(texture, position, source, new Color(151, 42, 226, 235) * (opacity * 0.88f), rotation, origin, new Vector2(scale.X, scale.Y * 1.08f), SpriteEffects.None); Main.EntitySpriteDraw(texture, position, source, new Color(2, 0, 6, 252) * opacity, rotation, origin, scale, SpriteEffects.None); Main.EntitySpriteDraw(texture, position, source, new Color(10, 2, 18, 232) * (opacity * 0.78f), rotation, origin, new Vector2(scale.X, scale.Y * 0.52f), SpriteEffects.None); } internal static void DrawDeathPortalRift( Vector2 center, float rotation, float length, float width, float opacity) { if (Main.dedServ || length <= 0.5f || width <= 0.5f || opacity <= 0.001f) { return; } Texture2D texture = VoidRiftMask ??= CreateVoidRiftMask(); Vector2 position = center - rotation.ToRotationVector2() * length * 0.5f - Main.screenPosition; Vector2 origin = new(0f, texture.Height * 0.5f); Vector2 scale = new(length / texture.Width, width / texture.Height); Main.EntitySpriteDraw(texture, position, null, new Color(111, 0, 22, 0) * (opacity * 0.42f), rotation, origin, new Vector2(scale.X, scale.Y * 1.62f), SpriteEffects.None); Main.EntitySpriteDraw(texture, position, null, new Color(231, 13, 47, 0) * (opacity * 0.66f), rotation, origin, new Vector2(scale.X, scale.Y * 1.23f), SpriteEffects.None); Main.EntitySpriteDraw(texture, position, null, new Color(2, 0, 3, 252) * opacity, rotation, origin, scale, SpriteEffects.None); } private static float SmoothStep(float start, float end, float value) { if (end <= start) return value >= end ? 1f : 0f; value = MathHelper.Clamp((value - start) / (end - start), 0f, 1f); return value * value * (3f - 2f * value); } }