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