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