using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using Terraria;
using Terraria.ModLoader;
namespace SoulHarvest.Common;
/// <summary>
/// Client-only irregular fragments cut directly from any sickle texture.
/// Each cached mask owns a disjoint Voronoi region, so the pieces reconstruct the
/// original weapon without introducing a second set of artwork.
/// </summary>
[Autoload(Side = ModSide.Client)]
internal sealed class DeathSickleAssemblyTextureSystem : ModSystem
{
private const int VariantCount = 3;
private const int FragmentCount = ReaperDefinitions.AssemblyFragmentCount;
private sealed class Fragment(Texture2D texture, Texture2D seamTexture,
Vector2 centroid)
{
internal readonly Texture2D Texture = texture;
internal readonly Texture2D SeamTexture = seamTexture;
internal readonly Vector2 Centroid = centroid;
}
private sealed class FragmentSet(Fragment[] fragments)
{
internal readonly Fragment[] Fragments = fragments;
}
private static Dictionary<Texture2D, FragmentSet[]>? fragmentSetsByTexture;
#if DEBUG
internal static int CachedSourceCount => fragmentSetsByTexture?.Count ?? 0;
internal static int LastDrawFragmentCount { get; private set; }
internal static int LastDrawPortalCount { get; private set; }
internal static int LastVisiblePortalCount { get; private set; }
internal static int MaxMovingFragmentCount { get; private set; }
internal static int MaxVisiblePortalCount { get; private set; }
internal static bool SawStaggeredPortalBatch { get; private set; }
internal static bool SawSimultaneousPortalBatch { get; private set; }
internal static bool SawFinalSeamFlash { get; private set; }
internal static bool SawPartialPortalAperture { get; private set; }
internal static bool SawSimultaneousFragmentReturn { get; private set; }
internal static void ResetDebugMetrics()
{
LastDrawFragmentCount = 0;
LastDrawPortalCount = 0;
LastVisiblePortalCount = 0;
MaxMovingFragmentCount = 0;
MaxVisiblePortalCount = 0;
SawStaggeredPortalBatch = false;
SawSimultaneousPortalBatch = false;
SawFinalSeamFlash = false;
SawPartialPortalAperture = false;
SawSimultaneousFragmentReturn = false;
}
#endif
public override void Unload()
{
Dictionary<Texture2D, FragmentSet[]>? oldCache = fragmentSetsByTexture;
fragmentSetsByTexture = null;
#if DEBUG
ResetDebugMetrics();
#endif
if (oldCache is null || Main.dedServ)
return;
Main.QueueMainThreadAction(() =>
{
foreach (FragmentSet[] sets in oldCache.Values)
{
foreach (FragmentSet set in sets)
{
foreach (Fragment fragment in set.Fragments)
{
fragment.Texture.Dispose();
fragment.SeamTexture.Dispose();
}
}
}
});
}
internal static void Draw(
Texture2D source,
Vector2 grip,
float rotation,
Vector2 anchor,
float scale,
SpriteEffects effects,
Color lightColor,
int elapsedFrames,
int totalFrames,
int actionSeed,
bool largeDeathLayout)
{
if (Main.dedServ)
return;
elapsedFrames = Math.Clamp(elapsedFrames, 0, Math.Max(1, totalFrames));
totalFrames = Math.Max(1, totalFrames);
float progress = elapsedFrames / (float)totalFrames;
Dictionary<Texture2D, FragmentSet[]> cache = fragmentSetsByTexture ??= [];
if (!cache.TryGetValue(source, out FragmentSet[]? sets))
{
sets = CreateFragmentSets(source);
cache[source] = sets;
}
if (sets.Length == 0)
{
Main.EntitySpriteDraw(source, grip - Main.screenPosition, null,
lightColor * progress, rotation, anchor, scale, effects);
return;
}
int variant = PositiveModulo(actionSeed, sets.Length);
Fragment[] fragments = sets[variant].Fragments;
Span<int> sequence = stackalloc int[fragments.Length];
BuildHeadToTailSequence(fragments, anchor, effects, sequence);
int portalCloseFrames = ReaperDefinitions
.GetAssemblyPortalCloseFrames(totalFrames);
float finalizationProgress = MathHelper.Clamp((elapsedFrames
- (totalFrames - portalCloseFrames))
/ (float)portalCloseFrames, 0f, 1f);
float seamFlash = (float)Math.Sin(finalizationProgress * MathHelper.Pi);
seamFlash *= seamFlash;
float seamBaseGlow = 1f - SmoothStep(finalizationProgress);
#if DEBUG
LastDrawFragmentCount = fragments.Length;
int movingFragmentCount = 0;
SawFinalSeamFlash |= finalizationProgress > 0.2f
&& finalizationProgress < 0.8f && seamFlash > 0.65f;
#endif
for (int sequenceIndex = 0; sequenceIndex < sequence.Length;
sequenceIndex++)
{
int index = sequence[sequenceIndex];
Fragment fragment = fragments[index];
float localProgress = GetFragmentTravelProgress(elapsedFrames,
totalFrames, sequenceIndex, fragments.Length);
#if DEBUG
if (localProgress > 0.0001f && localProgress < 0.9999f)
movingFragmentCount++;
#endif
float eased = SmoothStep(localProgress);
float remaining = 1f - eased;
float twist = (Hash01(actionSeed, index, 193) * 2f - 1f)
* MathHelper.Lerp(1.15f, 2.55f,
Hash01(actionSeed, index, 389));
float drawRotation = rotation + twist * remaining;
Vector2 localCentroid = GetLocalCentroid(fragment.Centroid,
anchor, effects);
Vector2 finalCentroid = grip
+ localCentroid.RotatedBy(rotation) * scale;
Vector2 portalCenter = GetRiftWorldCenter(grip, rotation, anchor,
scale, effects, fragments, index, actionSeed,
largeDeathLayout);
Vector2 desiredCentroid = Vector2.Lerp(portalCenter,
finalCentroid, eased);
float opacity = SmoothStep(MathHelper.Clamp(localProgress * 2.8f,
0f, 1f));
float fragmentScale = scale * MathHelper.Lerp(
0.72f + Hash01(actionSeed, index, 521) * 0.18f,
1f, eased);
Vector2 drawPosition = desiredCentroid
- localCentroid.RotatedBy(drawRotation) * fragmentScale;
Main.EntitySpriteDraw(fragment.Texture,
drawPosition - Main.screenPosition, null,
lightColor * opacity, drawRotation, anchor, fragmentScale,
effects);
// The seam mask contains only pixels bordering another fragment;
// it deliberately excludes the weapon's exterior silhouette. Keep
// those cut edges incandescent while assembling. Once the final
// fragment arrives, every seam emits one strong sealing flash and
// then extinguishes before gameplay commits the completed weapon.
float seamPulse = 0.84f + (float)Math.Sin(
Main.GlobalTimeWrappedHourly * 8.5f + actionSeed * 0.19f
+ index * 0.73f) * 0.16f;
float seamStrength = seamBaseGlow * 0.82f + seamFlash * 1.65f;
float seamOpacity = opacity * seamStrength * seamPulse;
if (seamOpacity > 0.001f)
{
float haloScale = fragmentScale * 1.035f;
Vector2 haloPosition = desiredCentroid
- localCentroid.RotatedBy(drawRotation) * haloScale;
Main.EntitySpriteDraw(fragment.SeamTexture,
haloPosition - Main.screenPosition, null,
new Color(232, 8, 45, 0) * (seamOpacity * 0.72f),
drawRotation, anchor, haloScale, effects);
Main.EntitySpriteDraw(fragment.SeamTexture,
drawPosition - Main.screenPosition, null,
new Color(255, 211, 198, 225) * (seamOpacity * 0.92f),
drawRotation, anchor, fragmentScale, effects);
}
}
#if DEBUG
MaxMovingFragmentCount = Math.Max(MaxMovingFragmentCount,
movingFragmentCount);
#endif
// Render the slit after the pieces so its void core masks the crossing
// edge. This gives every fragment a clear behind-the-rift/in-front-of-
// the-rift transition instead of looking as if it spawned beside it.
DrawRifts(grip, rotation, anchor, scale, effects, fragments, sequence,
elapsedFrames, totalFrames, actionSeed, largeDeathLayout);
}
internal static void DrawCancellation(
Texture2D source,
Vector2 grip,
float rotation,
Vector2 anchor,
float scale,
SpriteEffects effects,
Color lightColor,
int assemblyElapsedFrames,
int cancellationElapsedFrames,
int totalAssemblyFrames,
int actionSeed,
bool largeDeathLayout)
{
if (Main.dedServ)
return;
totalAssemblyFrames = Math.Max(1, totalAssemblyFrames);
assemblyElapsedFrames = Math.Clamp(assemblyElapsedFrames, 0,
totalAssemblyFrames);
cancellationElapsedFrames = Math.Clamp(cancellationElapsedFrames, 0,
ReaperDefinitions.AssemblyCancelFrames);
Dictionary<Texture2D, FragmentSet[]> cache = fragmentSetsByTexture ??= [];
if (!cache.TryGetValue(source, out FragmentSet[]? sets))
{
sets = CreateFragmentSets(source);
cache[source] = sets;
}
if (sets.Length == 0)
return;
int variant = PositiveModulo(actionSeed, sets.Length);
Fragment[] fragments = sets[variant].Fragments;
Span<int> sequence = stackalloc int[fragments.Length];
BuildHeadToTailSequence(fragments, anchor, effects, sequence);
float returnProgress = SmoothStep(MathHelper.Clamp(
cancellationElapsedFrames
/ (float)ReaperDefinitions.AssemblyFragmentReturnFrames,
0f, 1f));
#if DEBUG
LastDrawFragmentCount = fragments.Length;
int returningFragmentCount = 0;
#endif
for (int sequenceIndex = 0; sequenceIndex < sequence.Length;
sequenceIndex++)
{
int index = sequence[sequenceIndex];
Fragment fragment = fragments[index];
float assembledProgress = GetFragmentTravelProgress(
assemblyElapsedFrames, totalAssemblyFrames, sequenceIndex,
fragments.Length);
float localProgress = assembledProgress * (1f - returnProgress);
#if DEBUG
if (assembledProgress > 0.0001f && returnProgress > 0.0001f
&& returnProgress < 0.9999f)
{
returningFragmentCount++;
}
#endif
if (localProgress <= 0.0001f)
continue;
float eased = SmoothStep(localProgress);
float remaining = 1f - eased;
float twist = (Hash01(actionSeed, index, 193) * 2f - 1f)
* MathHelper.Lerp(1.15f, 2.55f,
Hash01(actionSeed, index, 389));
float drawRotation = rotation + twist * remaining;
Vector2 localCentroid = GetLocalCentroid(fragment.Centroid,
anchor, effects);
Vector2 finalCentroid = grip
+ localCentroid.RotatedBy(rotation) * scale;
Vector2 portalCenter = GetRiftWorldCenter(grip, rotation, anchor,
scale, effects, fragments, index, actionSeed,
largeDeathLayout);
Vector2 desiredCentroid = Vector2.Lerp(portalCenter,
finalCentroid, eased);
float opacity = SmoothStep(MathHelper.Clamp(localProgress * 2.8f,
0f, 1f));
float fragmentScale = scale * MathHelper.Lerp(
0.72f + Hash01(actionSeed, index, 521) * 0.18f,
1f, eased);
Vector2 drawPosition = desiredCentroid
- localCentroid.RotatedBy(drawRotation) * fragmentScale;
Main.EntitySpriteDraw(fragment.Texture,
drawPosition - Main.screenPosition, null,
lightColor * opacity, drawRotation, anchor, fragmentScale,
effects);
float seamPulse = 0.84f + (float)Math.Sin(
Main.GlobalTimeWrappedHourly * 8.5f + actionSeed * 0.19f
+ index * 0.73f) * 0.16f;
float seamOpacity = opacity * seamPulse
* (1f - returnProgress) * 0.82f;
if (seamOpacity <= 0.001f)
continue;
float haloScale = fragmentScale * 1.035f;
Vector2 haloPosition = desiredCentroid
- localCentroid.RotatedBy(drawRotation) * haloScale;
Main.EntitySpriteDraw(fragment.SeamTexture,
haloPosition - Main.screenPosition, null,
new Color(232, 8, 45, 0) * (seamOpacity * 0.72f),
drawRotation, anchor, haloScale, effects);
Main.EntitySpriteDraw(fragment.SeamTexture,
drawPosition - Main.screenPosition, null,
new Color(255, 211, 198, 225) * (seamOpacity * 0.92f),
drawRotation, anchor, fragmentScale, effects);
}
#if DEBUG
SawSimultaneousFragmentReturn |= returningFragmentCount > 1;
#endif
DrawCancellationRifts(grip, rotation, anchor, scale, effects,
fragments, sequence, assemblyElapsedFrames,
cancellationElapsedFrames, totalAssemblyFrames, actionSeed,
largeDeathLayout);
}
private static void DrawCancellationRifts(Vector2 grip, float rotation,
Vector2 anchor, float scale, SpriteEffects effects,
Fragment[] fragments, ReadOnlySpan<int> sequence,
int assemblyElapsedFrames, int cancellationElapsedFrames,
int totalAssemblyFrames, int actionSeed, bool largeDeathLayout)
{
#if DEBUG
LastDrawPortalCount = fragments.Length;
LastVisiblePortalCount = 0;
#endif
float closeProgress = MathHelper.Clamp((cancellationElapsedFrames
- ReaperDefinitions.AssemblyFragmentReturnFrames)
/ (float)ReaperDefinitions.AssemblyPortalCloseFrames, 0f, 1f);
float closing = 1f - SmoothStep(closeProgress);
for (int sequenceIndex = 0; sequenceIndex < sequence.Length;
sequenceIndex++)
{
int fragmentIndex = sequence[sequenceIndex];
float ageAtCancel = assemblyElapsedFrames - GetPortalStartFrame(
totalAssemblyFrames, sequenceIndex, fragments.Length);
float openingAtCancel = SmoothStep(MathHelper.Clamp(ageAtCancel
/ ReaperDefinitions.AssemblyPortalExpandFrames, 0f, 1f));
float aperture = openingAtCancel * closing;
if (aperture <= 0.001f)
continue;
#if DEBUG
LastVisiblePortalCount++;
SawPartialPortalAperture |= aperture > 0.12f && aperture < 0.88f;
#endif
Vector2 center = GetRiftWorldCenter(grip, rotation, anchor, scale,
effects, fragments, fragmentIndex, actionSeed,
largeDeathLayout);
Vector2 travelDirection = -GetRiftOffset(actionSeed,
fragmentIndex, largeDeathLayout);
float portalScale = MathHelper.Lerp(0.54f, 0.76f,
Hash01(actionSeed, fragmentIndex, 977))
* (largeDeathLayout ? 3.1f : 1f);
DeathDomainPortalRenderer.Draw(center, travelDirection, portalScale,
aperture, actionSeed * 31 + fragmentIndex * 977);
}
#if DEBUG
MaxVisiblePortalCount = Math.Max(MaxVisiblePortalCount,
LastVisiblePortalCount);
#endif
}
private static void DrawRifts(Vector2 grip, float rotation, Vector2 anchor,
float scale, SpriteEffects effects, Fragment[] fragments,
ReadOnlySpan<int> sequence, int elapsedFrames, int totalFrames,
int actionSeed, bool largeDeathLayout)
{
#if DEBUG
LastDrawPortalCount = fragments.Length;
LastVisiblePortalCount = 0;
#endif
// Every real fragment owns one rift. Sub-max levels preserve the readable
// serial procession. At the cap every rift shares frame zero, letting all
// fragments cross together while retaining the same per-fragment origin.
int portalCloseFrames = ReaperDefinitions
.GetAssemblyPortalCloseFrames(totalFrames);
for (int sequenceIndex = 0; sequenceIndex < sequence.Length;
sequenceIndex++)
{
int fragmentIndex = sequence[sequenceIndex];
float age = elapsedFrames - GetPortalStartFrame(totalFrames,
sequenceIndex, fragments.Length);
float opening = SmoothStep(MathHelper.Clamp(age
/ ReaperDefinitions.AssemblyPortalExpandFrames, 0f, 1f));
float closing = SmoothStep(MathHelper.Clamp((totalFrames
- elapsedFrames)
/ (float)portalCloseFrames,
0f, 1f));
float aperture = Math.Min(opening, closing);
if (aperture <= 0.001f)
continue;
#if DEBUG
LastVisiblePortalCount++;
SawPartialPortalAperture |= aperture > 0.12f && aperture < 0.88f;
#endif
Vector2 center = GetRiftWorldCenter(grip, rotation, anchor, scale,
effects, fragments, fragmentIndex, actionSeed,
largeDeathLayout);
Vector2 travelDirection = -GetRiftOffset(actionSeed,
fragmentIndex, largeDeathLayout);
float portalScale = MathHelper.Lerp(0.54f, 0.76f,
Hash01(actionSeed, fragmentIndex, 977))
* (largeDeathLayout ? 3.1f : 1f);
DeathDomainPortalRenderer.Draw(center, travelDirection, portalScale,
aperture, actionSeed * 31 + fragmentIndex * 977);
}
#if DEBUG
MaxVisiblePortalCount = Math.Max(MaxVisiblePortalCount,
LastVisiblePortalCount);
SawStaggeredPortalBatch |= LastVisiblePortalCount > 0
&& LastVisiblePortalCount < fragments.Length;
SawSimultaneousPortalBatch |= ReaperDefinitions
.UsesSimultaneousAssembly(totalFrames)
&& LastVisiblePortalCount == fragments.Length;
#endif
}
private static float GetFragmentTravelProgress(float elapsedFrames,
int totalFrames, int sequenceIndex, int fragmentCount)
{
float start = GetFragmentTravelStartFrame(totalFrames, sequenceIndex,
fragmentCount);
return MathHelper.Clamp((elapsedFrames - start)
/ ReaperDefinitions.AssemblyFragmentTravelFrames, 0f, 1f);
}
private static float GetFragmentTravelStartFrame(int totalFrames,
int sequenceIndex, int fragmentCount)
{
if (ReaperDefinitions.UsesSimultaneousAssembly(totalFrames))
return ReaperDefinitions.AssemblyPortalExpandFrames;
float interval = GetPortalInterval(totalFrames, fragmentCount);
// Portal N's fragment departs only when portal N+1 begins opening. The
// last fragment waits the same interval in lieu of a successor portal.
return (sequenceIndex + 1) * interval;
}
private static float GetPortalStartFrame(int totalFrames,
int sequenceIndex, int fragmentCount)
{
if (ReaperDefinitions.UsesSimultaneousAssembly(totalFrames))
return 0f;
return sequenceIndex * GetPortalInterval(totalFrames, fragmentCount);
}
private static float GetPortalInterval(int totalFrames, int fragmentCount)
{
return Math.Max(ReaperDefinitions.AssemblyPortalExpandFrames,
(totalFrames - ReaperDefinitions.AssemblyFragmentTravelFrames
- ReaperDefinitions.AssemblyPortalCloseFrames)
/ (float)Math.Max(1, fragmentCount));
}
private static void BuildHeadToTailSequence(Fragment[] fragments,
Vector2 anchor, SpriteEffects effects, Span<int> sequence)
{
for (int index = 0; index < sequence.Length; index++)
{
sequence[index] = index;
float distance = GetHandleDistanceSquared(fragments[index],
anchor, effects);
int position = index;
while (position > 0)
{
int previousIndex = sequence[position - 1];
float previousDistance = GetHandleDistanceSquared(
fragments[previousIndex], anchor, effects);
if (previousDistance > distance
|| previousDistance == distance
&& previousIndex < index)
{
break;
}
sequence[position] = previousIndex;
position--;
}
sequence[position] = index;
}
}
private static float GetHandleDistanceSquared(Fragment fragment,
Vector2 anchor, SpriteEffects effects)
{
return GetLocalCentroid(fragment.Centroid, anchor, effects)
.LengthSquared();
}
private static Vector2 GetRiftWorldCenter(Vector2 grip, float rotation,
Vector2 anchor, float scale, SpriteEffects effects,
Fragment[] fragments, int fragmentIndex, int actionSeed,
bool largeDeathLayout)
{
fragmentIndex = Math.Clamp(fragmentIndex, 0,
Math.Max(0, fragments.Length - 1));
Vector2 centroid = fragments[fragmentIndex].Centroid;
Vector2 localCentroid = GetLocalCentroid(centroid, anchor, effects);
return grip + localCentroid.RotatedBy(rotation) * scale
+ GetRiftOffset(actionSeed, fragmentIndex, largeDeathLayout);
}
private static Vector2 GetRiftOffset(int actionSeed, int fragmentIndex,
bool largeDeathLayout)
{
const float GoldenAngle = 2.3999632f;
float baseAngle = Hash01(actionSeed, 0, 1459) * MathHelper.TwoPi;
float jitter = (Hash01(actionSeed, fragmentIndex, 1601) - 0.5f)
* 0.24f;
float angle = baseAngle + fragmentIndex * GoldenAngle + jitter;
float radius = 132f + fragmentIndex % 4 * 47f
+ Hash01(actionSeed, fragmentIndex, 1747) * 20f;
if (largeDeathLayout)
radius *= 1.5f;
Vector2 offset = angle.ToRotationVector2() * radius;
offset.Y *= 0.72f;
return offset;
}
private static Vector2 GetLocalCentroid(Vector2 centroid, Vector2 anchor,
SpriteEffects effects)
{
Vector2 local = centroid - anchor;
if ((effects & SpriteEffects.FlipHorizontally) != 0)
local.X = -local.X;
if ((effects & SpriteEffects.FlipVertically) != 0)
local.Y = -local.Y;
return local;
}
private static FragmentSet[] CreateFragmentSets(Texture2D source)
{
Color[] sourcePixels = new Color[source.Width * source.Height];
source.GetData(sourcePixels);
List<int> opaquePixels = [];
for (int index = 0; index < sourcePixels.Length; index++)
{
if (sourcePixels[index].A >= 12)
opaquePixels.Add(index);
}
if (opaquePixels.Count == 0)
return [];
FragmentSet[] sets = new FragmentSet[VariantCount];
for (int variant = 0; variant < sets.Length; variant++)
sets[variant] = CreateFragmentSet(source, sourcePixels,
opaquePixels, variant);
return sets;
}
private static FragmentSet CreateFragmentSet(Texture2D source,
Color[] sourcePixels, List<int> opaquePixels, int variant)
{
Vector2[] seeds = new Vector2[FragmentCount];
uint random = unchecked(0xA341316Cu + (uint)variant * 0x9E3779B9u);
for (int index = 0; index < seeds.Length; index++)
{
random = XorShift(random);
int opaqueIndex = opaquePixels[(int)(random
% (uint)opaquePixels.Count)];
seeds[index] = new Vector2(opaqueIndex % source.Width,
opaqueIndex / source.Width);
}
Color[][] masks = new Color[FragmentCount][];
Color[][] seamMasks = new Color[FragmentCount][];
int[] owners = new int[sourcePixels.Length];
Array.Fill(owners, -1);
Vector2[] centroidTotals = new Vector2[FragmentCount];
int[] counts = new int[FragmentCount];
for (int index = 0; index < masks.Length; index++)
{
masks[index] = new Color[sourcePixels.Length];
seamMasks[index] = new Color[sourcePixels.Length];
}
foreach (int pixelIndex in opaquePixels)
{
int x = pixelIndex % source.Width;
int y = pixelIndex / source.Width;
int closest = 0;
float closestDistance = float.MaxValue;
for (int seed = 0; seed < seeds.Length; seed++)
{
float dx = x - seeds[seed].X;
float dy = (y - seeds[seed].Y) * 0.82f;
float noise = Hash01(variant * 7919 + x, y,
seed * 3571 + 1879) * 105f;
float weight = 0.86f + Hash01(variant, seed, 2029) * 0.28f;
float distance = (dx * dx + dy * dy) * weight + noise;
if (distance >= closestDistance)
continue;
closestDistance = distance;
closest = seed;
}
masks[closest][pixelIndex] = sourcePixels[pixelIndex];
owners[pixelIndex] = closest;
centroidTotals[closest] += new Vector2(x + 0.5f, y + 0.5f);
counts[closest]++;
}
foreach (int pixelIndex in opaquePixels)
{
int owner = owners[pixelIndex];
int x = pixelIndex % source.Width;
int y = pixelIndex / source.Width;
if (HasForeignOpaqueNeighbor(owners, source.Width,
source.Height, x, y, owner))
{
seamMasks[owner][pixelIndex] = Color.White;
}
}
List<Fragment> fragments = [];
for (int index = 0; index < masks.Length; index++)
{
if (counts[index] == 0)
continue;
Texture2D texture = new(Main.instance.GraphicsDevice, source.Width,
source.Height, false, SurfaceFormat.Color);
texture.SetData(masks[index]);
Texture2D seamTexture = new(Main.instance.GraphicsDevice,
source.Width, source.Height, false, SurfaceFormat.Color);
seamTexture.SetData(seamMasks[index]);
fragments.Add(new Fragment(texture, seamTexture,
centroidTotals[index] / counts[index]));
}
return new FragmentSet([.. fragments]);
}
private static bool HasForeignOpaqueNeighbor(int[] owners, int width,
int height, int x, int y, int owner)
{
return IsForeignOwner(owners, width, height, x - 1, y, owner)
|| IsForeignOwner(owners, width, height, x + 1, y, owner)
|| IsForeignOwner(owners, width, height, x, y - 1, owner)
|| IsForeignOwner(owners, width, height, x, y + 1, owner);
}
private static bool IsForeignOwner(int[] owners, int width, int height,
int x, int y, int owner)
{
if (x < 0 || x >= width || y < 0 || y >= height)
return false;
int neighbor = owners[y * width + x];
return neighbor >= 0 && neighbor != owner;
}
private static uint XorShift(uint value)
{
value ^= value << 13;
value ^= value >> 17;
value ^= value << 5;
return value == 0 ? 0x6D2B79F5u : value;
}
private static float Hash01(int a, int b, int c)
{
uint value = unchecked((uint)a * 0x9E3779B9u
^ (uint)b * 0x85EBCA6Bu ^ (uint)c * 0xC2B2AE35u);
value ^= value >> 16;
value *= 0x7FEB352Du;
value ^= value >> 15;
value *= 0x846CA68Bu;
value ^= value >> 16;
return (value & 0x00FFFFFFu) / 16777215f;
}
private static int PositiveModulo(int value, int modulus)
{
int result = value % modulus;
return result < 0 ? result + modulus : result;
}
private static float SmoothStep(float value)
{
value = MathHelper.Clamp(value, 0f, 1f);
return value * value * (3f - 2f * value);
}
}
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using Terraria;
using Terraria.ModLoader;
namespace SoulHarvest.Common;
/// <summary>
/// Client-only irregular fragments cut directly from any sickle texture.
/// Each cached mask owns a disjoint Voronoi region, so the pieces reconstruct the
/// original weapon without introducing a second set of artwork.
/// </summary>
[Autoload(Side = ModSide.Client)]
internal sealed class DeathSickleAssemblyTextureSystem : ModSystem
{
private const int VariantCount = 3;
private const int FragmentCount = ReaperDefinitions.AssemblyFragmentCount;
private sealed class Fragment(Texture2D texture, Texture2D seamTexture,
Vector2 centroid)
{
internal readonly Texture2D Texture = texture;
internal readonly Texture2D SeamTexture = seamTexture;
internal readonly Vector2 Centroid = centroid;
}
private sealed class FragmentSet(Fragment[] fragments)
{
internal readonly Fragment[] Fragments = fragments;
}
private static Dictionary<Texture2D, FragmentSet[]>? fragmentSetsByTexture;
#if DEBUG
internal static int CachedSourceCount => fragmentSetsByTexture?.Count ?? 0;
internal static int LastDrawFragmentCount { get; private set; }
internal static int LastDrawPortalCount { get; private set; }
internal static int LastVisiblePortalCount { get; private set; }
internal static int MaxMovingFragmentCount { get; private set; }
internal static int MaxVisiblePortalCount { get; private set; }
internal static bool SawStaggeredPortalBatch { get; private set; }
internal static bool SawSimultaneousPortalBatch { get; private set; }
internal static bool SawFinalSeamFlash { get; private set; }
internal static bool SawPartialPortalAperture { get; private set; }
internal static bool SawSimultaneousFragmentReturn { get; private set; }
internal static void ResetDebugMetrics()
{
LastDrawFragmentCount = 0;
LastDrawPortalCount = 0;
LastVisiblePortalCount = 0;
MaxMovingFragmentCount = 0;
MaxVisiblePortalCount = 0;
SawStaggeredPortalBatch = false;
SawSimultaneousPortalBatch = false;
SawFinalSeamFlash = false;
SawPartialPortalAperture = false;
SawSimultaneousFragmentReturn = false;
}
#endif
public override void Unload()
{
Dictionary<Texture2D, FragmentSet[]>? oldCache = fragmentSetsByTexture;
fragmentSetsByTexture = null;
#if DEBUG
ResetDebugMetrics();
#endif
if (oldCache is null || Main.dedServ)
return;
Main.QueueMainThreadAction(() =>
{
foreach (FragmentSet[] sets in oldCache.Values)
{
foreach (FragmentSet set in sets)
{
foreach (Fragment fragment in set.Fragments)
{
fragment.Texture.Dispose();
fragment.SeamTexture.Dispose();
}
}
}
});
}
internal static void Draw(
Texture2D source,
Vector2 grip,
float rotation,
Vector2 anchor,
float scale,
SpriteEffects effects,
Color lightColor,
int elapsedFrames,
int totalFrames,
int actionSeed,
bool largeDeathLayout)
{
if (Main.dedServ)
return;
elapsedFrames = Math.Clamp(elapsedFrames, 0, Math.Max(1, totalFrames));
totalFrames = Math.Max(1, totalFrames);
float progress = elapsedFrames / (float)totalFrames;
Dictionary<Texture2D, FragmentSet[]> cache = fragmentSetsByTexture ??= [];
if (!cache.TryGetValue(source, out FragmentSet[]? sets))
{
sets = CreateFragmentSets(source);
cache[source] = sets;
}
if (sets.Length == 0)
{
Main.EntitySpriteDraw(source, grip - Main.screenPosition, null,
lightColor * progress, rotation, anchor, scale, effects);
return;
}
int variant = PositiveModulo(actionSeed, sets.Length);
Fragment[] fragments = sets[variant].Fragments;
Span<int> sequence = stackalloc int[fragments.Length];
BuildHeadToTailSequence(fragments, anchor, effects, sequence);
int portalCloseFrames = ReaperDefinitions
.GetAssemblyPortalCloseFrames(totalFrames);
float finalizationProgress = MathHelper.Clamp((elapsedFrames
- (totalFrames - portalCloseFrames))
/ (float)portalCloseFrames, 0f, 1f);
float seamFlash = (float)Math.Sin(finalizationProgress * MathHelper.Pi);
seamFlash *= seamFlash;
float seamBaseGlow = 1f - SmoothStep(finalizationProgress);
#if DEBUG
LastDrawFragmentCount = fragments.Length;
int movingFragmentCount = 0;
SawFinalSeamFlash |= finalizationProgress > 0.2f
&& finalizationProgress < 0.8f && seamFlash > 0.65f;
#endif
for (int sequenceIndex = 0; sequenceIndex < sequence.Length;
sequenceIndex++)
{
int index = sequence[sequenceIndex];
Fragment fragment = fragments[index];
float localProgress = GetFragmentTravelProgress(elapsedFrames,
totalFrames, sequenceIndex, fragments.Length);
#if DEBUG
if (localProgress > 0.0001f && localProgress < 0.9999f)
movingFragmentCount++;
#endif
float eased = SmoothStep(localProgress);
float remaining = 1f - eased;
float twist = (Hash01(actionSeed, index, 193) * 2f - 1f)
* MathHelper.Lerp(1.15f, 2.55f,
Hash01(actionSeed, index, 389));
float drawRotation = rotation + twist * remaining;
Vector2 localCentroid = GetLocalCentroid(fragment.Centroid,
anchor, effects);
Vector2 finalCentroid = grip
+ localCentroid.RotatedBy(rotation) * scale;
Vector2 portalCenter = GetRiftWorldCenter(grip, rotation, anchor,
scale, effects, fragments, index, actionSeed,
largeDeathLayout);
Vector2 desiredCentroid = Vector2.Lerp(portalCenter,
finalCentroid, eased);
float opacity = SmoothStep(MathHelper.Clamp(localProgress * 2.8f,
0f, 1f));
float fragmentScale = scale * MathHelper.Lerp(
0.72f + Hash01(actionSeed, index, 521) * 0.18f,
1f, eased);
Vector2 drawPosition = desiredCentroid
- localCentroid.RotatedBy(drawRotation) * fragmentScale;
Main.EntitySpriteDraw(fragment.Texture,
drawPosition - Main.screenPosition, null,
lightColor * opacity, drawRotation, anchor, fragmentScale,
effects);
// The seam mask contains only pixels bordering another fragment;
// it deliberately excludes the weapon's exterior silhouette. Keep
// those cut edges incandescent while assembling. Once the final
// fragment arrives, every seam emits one strong sealing flash and
// then extinguishes before gameplay commits the completed weapon.
float seamPulse = 0.84f + (float)Math.Sin(
Main.GlobalTimeWrappedHourly * 8.5f + actionSeed * 0.19f
+ index * 0.73f) * 0.16f;
float seamStrength = seamBaseGlow * 0.82f + seamFlash * 1.65f;
float seamOpacity = opacity * seamStrength * seamPulse;
if (seamOpacity > 0.001f)
{
float haloScale = fragmentScale * 1.035f;
Vector2 haloPosition = desiredCentroid
- localCentroid.RotatedBy(drawRotation) * haloScale;
Main.EntitySpriteDraw(fragment.SeamTexture,
haloPosition - Main.screenPosition, null,
new Color(232, 8, 45, 0) * (seamOpacity * 0.72f),
drawRotation, anchor, haloScale, effects);
Main.EntitySpriteDraw(fragment.SeamTexture,
drawPosition - Main.screenPosition, null,
new Color(255, 211, 198, 225) * (seamOpacity * 0.92f),
drawRotation, anchor, fragmentScale, effects);
}
}
#if DEBUG
MaxMovingFragmentCount = Math.Max(MaxMovingFragmentCount,
movingFragmentCount);
#endif
// Render the slit after the pieces so its void core masks the crossing
// edge. This gives every fragment a clear behind-the-rift/in-front-of-
// the-rift transition instead of looking as if it spawned beside it.
DrawRifts(grip, rotation, anchor, scale, effects, fragments, sequence,
elapsedFrames, totalFrames, actionSeed, largeDeathLayout);
}
internal static void DrawCancellation(
Texture2D source,
Vector2 grip,
float rotation,
Vector2 anchor,
float scale,
SpriteEffects effects,
Color lightColor,
int assemblyElapsedFrames,
int cancellationElapsedFrames,
int totalAssemblyFrames,
int actionSeed,
bool largeDeathLayout)
{
if (Main.dedServ)
return;
totalAssemblyFrames = Math.Max(1, totalAssemblyFrames);
assemblyElapsedFrames = Math.Clamp(assemblyElapsedFrames, 0,
totalAssemblyFrames);
cancellationElapsedFrames = Math.Clamp(cancellationElapsedFrames, 0,
ReaperDefinitions.AssemblyCancelFrames);
Dictionary<Texture2D, FragmentSet[]> cache = fragmentSetsByTexture ??= [];
if (!cache.TryGetValue(source, out FragmentSet[]? sets))
{
sets = CreateFragmentSets(source);
cache[source] = sets;
}
if (sets.Length == 0)
return;
int variant = PositiveModulo(actionSeed, sets.Length);
Fragment[] fragments = sets[variant].Fragments;
Span<int> sequence = stackalloc int[fragments.Length];
BuildHeadToTailSequence(fragments, anchor, effects, sequence);
float returnProgress = SmoothStep(MathHelper.Clamp(
cancellationElapsedFrames
/ (float)ReaperDefinitions.AssemblyFragmentReturnFrames,
0f, 1f));
#if DEBUG
LastDrawFragmentCount = fragments.Length;
int returningFragmentCount = 0;
#endif
for (int sequenceIndex = 0; sequenceIndex < sequence.Length;
sequenceIndex++)
{
int index = sequence[sequenceIndex];
Fragment fragment = fragments[index];
float assembledProgress = GetFragmentTravelProgress(
assemblyElapsedFrames, totalAssemblyFrames, sequenceIndex,
fragments.Length);
float localProgress = assembledProgress * (1f - returnProgress);
#if DEBUG
if (assembledProgress > 0.0001f && returnProgress > 0.0001f
&& returnProgress < 0.9999f)
{
returningFragmentCount++;
}
#endif
if (localProgress <= 0.0001f)
continue;
float eased = SmoothStep(localProgress);
float remaining = 1f - eased;
float twist = (Hash01(actionSeed, index, 193) * 2f - 1f)
* MathHelper.Lerp(1.15f, 2.55f,
Hash01(actionSeed, index, 389));
float drawRotation = rotation + twist * remaining;
Vector2 localCentroid = GetLocalCentroid(fragment.Centroid,
anchor, effects);
Vector2 finalCentroid = grip
+ localCentroid.RotatedBy(rotation) * scale;
Vector2 portalCenter = GetRiftWorldCenter(grip, rotation, anchor,
scale, effects, fragments, index, actionSeed,
largeDeathLayout);
Vector2 desiredCentroid = Vector2.Lerp(portalCenter,
finalCentroid, eased);
float opacity = SmoothStep(MathHelper.Clamp(localProgress * 2.8f,
0f, 1f));
float fragmentScale = scale * MathHelper.Lerp(
0.72f + Hash01(actionSeed, index, 521) * 0.18f,
1f, eased);
Vector2 drawPosition = desiredCentroid
- localCentroid.RotatedBy(drawRotation) * fragmentScale;
Main.EntitySpriteDraw(fragment.Texture,
drawPosition - Main.screenPosition, null,
lightColor * opacity, drawRotation, anchor, fragmentScale,
effects);
float seamPulse = 0.84f + (float)Math.Sin(
Main.GlobalTimeWrappedHourly * 8.5f + actionSeed * 0.19f
+ index * 0.73f) * 0.16f;
float seamOpacity = opacity * seamPulse
* (1f - returnProgress) * 0.82f;
if (seamOpacity <= 0.001f)
continue;
float haloScale = fragmentScale * 1.035f;
Vector2 haloPosition = desiredCentroid
- localCentroid.RotatedBy(drawRotation) * haloScale;
Main.EntitySpriteDraw(fragment.SeamTexture,
haloPosition - Main.screenPosition, null,
new Color(232, 8, 45, 0) * (seamOpacity * 0.72f),
drawRotation, anchor, haloScale, effects);
Main.EntitySpriteDraw(fragment.SeamTexture,
drawPosition - Main.screenPosition, null,
new Color(255, 211, 198, 225) * (seamOpacity * 0.92f),
drawRotation, anchor, fragmentScale, effects);
}
#if DEBUG
SawSimultaneousFragmentReturn |= returningFragmentCount > 1;
#endif
DrawCancellationRifts(grip, rotation, anchor, scale, effects,
fragments, sequence, assemblyElapsedFrames,
cancellationElapsedFrames, totalAssemblyFrames, actionSeed,
largeDeathLayout);
}
private static void DrawCancellationRifts(Vector2 grip, float rotation,
Vector2 anchor, float scale, SpriteEffects effects,
Fragment[] fragments, ReadOnlySpan<int> sequence,
int assemblyElapsedFrames, int cancellationElapsedFrames,
int totalAssemblyFrames, int actionSeed, bool largeDeathLayout)
{
#if DEBUG
LastDrawPortalCount = fragments.Length;
LastVisiblePortalCount = 0;
#endif
float closeProgress = MathHelper.Clamp((cancellationElapsedFrames
- ReaperDefinitions.AssemblyFragmentReturnFrames)
/ (float)ReaperDefinitions.AssemblyPortalCloseFrames, 0f, 1f);
float closing = 1f - SmoothStep(closeProgress);
for (int sequenceIndex = 0; sequenceIndex < sequence.Length;
sequenceIndex++)
{
int fragmentIndex = sequence[sequenceIndex];
float ageAtCancel = assemblyElapsedFrames - GetPortalStartFrame(
totalAssemblyFrames, sequenceIndex, fragments.Length);
float openingAtCancel = SmoothStep(MathHelper.Clamp(ageAtCancel
/ ReaperDefinitions.AssemblyPortalExpandFrames, 0f, 1f));
float aperture = openingAtCancel * closing;
if (aperture <= 0.001f)
continue;
#if DEBUG
LastVisiblePortalCount++;
SawPartialPortalAperture |= aperture > 0.12f && aperture < 0.88f;
#endif
Vector2 center = GetRiftWorldCenter(grip, rotation, anchor, scale,
effects, fragments, fragmentIndex, actionSeed,
largeDeathLayout);
Vector2 travelDirection = -GetRiftOffset(actionSeed,
fragmentIndex, largeDeathLayout);
float portalScale = MathHelper.Lerp(0.54f, 0.76f,
Hash01(actionSeed, fragmentIndex, 977))
* (largeDeathLayout ? 3.1f : 1f);
DeathDomainPortalRenderer.Draw(center, travelDirection, portalScale,
aperture, actionSeed * 31 + fragmentIndex * 977);
}
#if DEBUG
MaxVisiblePortalCount = Math.Max(MaxVisiblePortalCount,
LastVisiblePortalCount);
#endif
}
private static void DrawRifts(Vector2 grip, float rotation, Vector2 anchor,
float scale, SpriteEffects effects, Fragment[] fragments,
ReadOnlySpan<int> sequence, int elapsedFrames, int totalFrames,
int actionSeed, bool largeDeathLayout)
{
#if DEBUG
LastDrawPortalCount = fragments.Length;
LastVisiblePortalCount = 0;
#endif
// Every real fragment owns one rift. Sub-max levels preserve the readable
// serial procession. At the cap every rift shares frame zero, letting all
// fragments cross together while retaining the same per-fragment origin.
int portalCloseFrames = ReaperDefinitions
.GetAssemblyPortalCloseFrames(totalFrames);
for (int sequenceIndex = 0; sequenceIndex < sequence.Length;
sequenceIndex++)
{
int fragmentIndex = sequence[sequenceIndex];
float age = elapsedFrames - GetPortalStartFrame(totalFrames,
sequenceIndex, fragments.Length);
float opening = SmoothStep(MathHelper.Clamp(age
/ ReaperDefinitions.AssemblyPortalExpandFrames, 0f, 1f));
float closing = SmoothStep(MathHelper.Clamp((totalFrames
- elapsedFrames)
/ (float)portalCloseFrames,
0f, 1f));
float aperture = Math.Min(opening, closing);
if (aperture <= 0.001f)
continue;
#if DEBUG
LastVisiblePortalCount++;
SawPartialPortalAperture |= aperture > 0.12f && aperture < 0.88f;
#endif
Vector2 center = GetRiftWorldCenter(grip, rotation, anchor, scale,
effects, fragments, fragmentIndex, actionSeed,
largeDeathLayout);
Vector2 travelDirection = -GetRiftOffset(actionSeed,
fragmentIndex, largeDeathLayout);
float portalScale = MathHelper.Lerp(0.54f, 0.76f,
Hash01(actionSeed, fragmentIndex, 977))
* (largeDeathLayout ? 3.1f : 1f);
DeathDomainPortalRenderer.Draw(center, travelDirection, portalScale,
aperture, actionSeed * 31 + fragmentIndex * 977);
}
#if DEBUG
MaxVisiblePortalCount = Math.Max(MaxVisiblePortalCount,
LastVisiblePortalCount);
SawStaggeredPortalBatch |= LastVisiblePortalCount > 0
&& LastVisiblePortalCount < fragments.Length;
SawSimultaneousPortalBatch |= ReaperDefinitions
.UsesSimultaneousAssembly(totalFrames)
&& LastVisiblePortalCount == fragments.Length;
#endif
}
private static float GetFragmentTravelProgress(float elapsedFrames,
int totalFrames, int sequenceIndex, int fragmentCount)
{
float start = GetFragmentTravelStartFrame(totalFrames, sequenceIndex,
fragmentCount);
return MathHelper.Clamp((elapsedFrames - start)
/ ReaperDefinitions.AssemblyFragmentTravelFrames, 0f, 1f);
}
private static float GetFragmentTravelStartFrame(int totalFrames,
int sequenceIndex, int fragmentCount)
{
if (ReaperDefinitions.UsesSimultaneousAssembly(totalFrames))
return ReaperDefinitions.AssemblyPortalExpandFrames;
float interval = GetPortalInterval(totalFrames, fragmentCount);
// Portal N's fragment departs only when portal N+1 begins opening. The
// last fragment waits the same interval in lieu of a successor portal.
return (sequenceIndex + 1) * interval;
}
private static float GetPortalStartFrame(int totalFrames,
int sequenceIndex, int fragmentCount)
{
if (ReaperDefinitions.UsesSimultaneousAssembly(totalFrames))
return 0f;
return sequenceIndex * GetPortalInterval(totalFrames, fragmentCount);
}
private static float GetPortalInterval(int totalFrames, int fragmentCount)
{
return Math.Max(ReaperDefinitions.AssemblyPortalExpandFrames,
(totalFrames - ReaperDefinitions.AssemblyFragmentTravelFrames
- ReaperDefinitions.AssemblyPortalCloseFrames)
/ (float)Math.Max(1, fragmentCount));
}
private static void BuildHeadToTailSequence(Fragment[] fragments,
Vector2 anchor, SpriteEffects effects, Span<int> sequence)
{
for (int index = 0; index < sequence.Length; index++)
{
sequence[index] = index;
float distance = GetHandleDistanceSquared(fragments[index],
anchor, effects);
int position = index;
while (position > 0)
{
int previousIndex = sequence[position - 1];
float previousDistance = GetHandleDistanceSquared(
fragments[previousIndex], anchor, effects);
if (previousDistance > distance
|| previousDistance == distance
&& previousIndex < index)
{
break;
}
sequence[position] = previousIndex;
position--;
}
sequence[position] = index;
}
}
private static float GetHandleDistanceSquared(Fragment fragment,
Vector2 anchor, SpriteEffects effects)
{
return GetLocalCentroid(fragment.Centroid, anchor, effects)
.LengthSquared();
}
private static Vector2 GetRiftWorldCenter(Vector2 grip, float rotation,
Vector2 anchor, float scale, SpriteEffects effects,
Fragment[] fragments, int fragmentIndex, int actionSeed,
bool largeDeathLayout)
{
fragmentIndex = Math.Clamp(fragmentIndex, 0,
Math.Max(0, fragments.Length - 1));
Vector2 centroid = fragments[fragmentIndex].Centroid;
Vector2 localCentroid = GetLocalCentroid(centroid, anchor, effects);
return grip + localCentroid.RotatedBy(rotation) * scale
+ GetRiftOffset(actionSeed, fragmentIndex, largeDeathLayout);
}
private static Vector2 GetRiftOffset(int actionSeed, int fragmentIndex,
bool largeDeathLayout)
{
const float GoldenAngle = 2.3999632f;
float baseAngle = Hash01(actionSeed, 0, 1459) * MathHelper.TwoPi;
float jitter = (Hash01(actionSeed, fragmentIndex, 1601) - 0.5f)
* 0.24f;
float angle = baseAngle + fragmentIndex * GoldenAngle + jitter;
float radius = 132f + fragmentIndex % 4 * 47f
+ Hash01(actionSeed, fragmentIndex, 1747) * 20f;
if (largeDeathLayout)
radius *= 1.5f;
Vector2 offset = angle.ToRotationVector2() * radius;
offset.Y *= 0.72f;
return offset;
}
private static Vector2 GetLocalCentroid(Vector2 centroid, Vector2 anchor,
SpriteEffects effects)
{
Vector2 local = centroid - anchor;
if ((effects & SpriteEffects.FlipHorizontally) != 0)
local.X = -local.X;
if ((effects & SpriteEffects.FlipVertically) != 0)
local.Y = -local.Y;
return local;
}
private static FragmentSet[] CreateFragmentSets(Texture2D source)
{
Color[] sourcePixels = new Color[source.Width * source.Height];
source.GetData(sourcePixels);
List<int> opaquePixels = [];
for (int index = 0; index < sourcePixels.Length; index++)
{
if (sourcePixels[index].A >= 12)
opaquePixels.Add(index);
}
if (opaquePixels.Count == 0)
return [];
FragmentSet[] sets = new FragmentSet[VariantCount];
for (int variant = 0; variant < sets.Length; variant++)
sets[variant] = CreateFragmentSet(source, sourcePixels,
opaquePixels, variant);
return sets;
}
private static FragmentSet CreateFragmentSet(Texture2D source,
Color[] sourcePixels, List<int> opaquePixels, int variant)
{
Vector2[] seeds = new Vector2[FragmentCount];
uint random = unchecked(0xA341316Cu + (uint)variant * 0x9E3779B9u);
for (int index = 0; index < seeds.Length; index++)
{
random = XorShift(random);
int opaqueIndex = opaquePixels[(int)(random
% (uint)opaquePixels.Count)];
seeds[index] = new Vector2(opaqueIndex % source.Width,
opaqueIndex / source.Width);
}
Color[][] masks = new Color[FragmentCount][];
Color[][] seamMasks = new Color[FragmentCount][];
int[] owners = new int[sourcePixels.Length];
Array.Fill(owners, -1);
Vector2[] centroidTotals = new Vector2[FragmentCount];
int[] counts = new int[FragmentCount];
for (int index = 0; index < masks.Length; index++)
{
masks[index] = new Color[sourcePixels.Length];
seamMasks[index] = new Color[sourcePixels.Length];
}
foreach (int pixelIndex in opaquePixels)
{
int x = pixelIndex % source.Width;
int y = pixelIndex / source.Width;
int closest = 0;
float closestDistance = float.MaxValue;
for (int seed = 0; seed < seeds.Length; seed++)
{
float dx = x - seeds[seed].X;
float dy = (y - seeds[seed].Y) * 0.82f;
float noise = Hash01(variant * 7919 + x, y,
seed * 3571 + 1879) * 105f;
float weight = 0.86f + Hash01(variant, seed, 2029) * 0.28f;
float distance = (dx * dx + dy * dy) * weight + noise;
if (distance >= closestDistance)
continue;
closestDistance = distance;
closest = seed;
}
masks[closest][pixelIndex] = sourcePixels[pixelIndex];
owners[pixelIndex] = closest;
centroidTotals[closest] += new Vector2(x + 0.5f, y + 0.5f);
counts[closest]++;
}
foreach (int pixelIndex in opaquePixels)
{
int owner = owners[pixelIndex];
int x = pixelIndex % source.Width;
int y = pixelIndex / source.Width;
if (HasForeignOpaqueNeighbor(owners, source.Width,
source.Height, x, y, owner))
{
seamMasks[owner][pixelIndex] = Color.White;
}
}
List<Fragment> fragments = [];
for (int index = 0; index < masks.Length; index++)
{
if (counts[index] == 0)
continue;
Texture2D texture = new(Main.instance.GraphicsDevice, source.Width,
source.Height, false, SurfaceFormat.Color);
texture.SetData(masks[index]);
Texture2D seamTexture = new(Main.instance.GraphicsDevice,
source.Width, source.Height, false, SurfaceFormat.Color);
seamTexture.SetData(seamMasks[index]);
fragments.Add(new Fragment(texture, seamTexture,
centroidTotals[index] / counts[index]));
}
return new FragmentSet([.. fragments]);
}
private static bool HasForeignOpaqueNeighbor(int[] owners, int width,
int height, int x, int y, int owner)
{
return IsForeignOwner(owners, width, height, x - 1, y, owner)
|| IsForeignOwner(owners, width, height, x + 1, y, owner)
|| IsForeignOwner(owners, width, height, x, y - 1, owner)
|| IsForeignOwner(owners, width, height, x, y + 1, owner);
}
private static bool IsForeignOwner(int[] owners, int width, int height,
int x, int y, int owner)
{
if (x < 0 || x >= width || y < 0 || y >= height)
return false;
int neighbor = owners[y * width + x];
return neighbor >= 0 && neighbor != owner;
}
private static uint XorShift(uint value)
{
value ^= value << 13;
value ^= value >> 17;
value ^= value << 5;
return value == 0 ? 0x6D2B79F5u : value;
}
private static float Hash01(int a, int b, int c)
{
uint value = unchecked((uint)a * 0x9E3779B9u
^ (uint)b * 0x85EBCA6Bu ^ (uint)c * 0xC2B2AE35u);
value ^= value >> 16;
value *= 0x7FEB352Du;
value ^= value >> 15;
value *= 0x846CA68Bu;
value ^= value >> 16;
return (value & 0x00FFFFFFu) / 16777215f;
}
private static int PositiveModulo(int value, int modulus)
{
int result = value % modulus;
return result < 0 ? result + modulus : result;
}
private static float SmoothStep(float value)
{
value = MathHelper.Clamp(value, 0f, 1f);
return value * value * (3f - 2f * value);
}
}