using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using ReLogic.Content;
using System;
using System.Collections.Generic;
using Terraria;
using Terraria.GameContent;
using Terraria.ModLoader;
using Terraria.UI;
namespace SoulHarvest.Common;
/// <summary>
/// Client-only presentation for Reaper ultimates. Combat code reports the authoritative
/// visual clock each tick; this system owns only short-lived draw state and asks the
/// shared VFX director for capped camera/flash punctuation. It never creates attacks.
/// </summary>
[Autoload(Side = ModSide.Client)]
public sealed class ReaperUltimateVisualSystem : ModSystem
{
private const int TailFrames = 18;
private const int CrescentMaskSize = 512;
private const float CrescentReferenceRadius = 180f;
private static UltimateVisualState[] states = [];
private static Asset<Texture2D>? soulTexture;
private static ReaperUltimateCrescentMaskSet[] crescentMasks = [];
private static readonly float[] deathMirrorAngles =
new float[ReaperDeathUltimateGeometry.CutCount * 2];
private static readonly VertexPositionColorTexture[] deathMirrorVertices =
new VertexPositionColorTexture[ReaperDeathUltimateGeometry.CutCount * 6];
private static BasicEffect? deathMirrorEffect;
/// <summary>
/// Reports one frame of an active ultimate. This method is safe to call from
/// projectile AI on every net mode: dedicated servers immediately return.
/// </summary>
public static void ReportUltimate(Player owner, ReaperFormId form, Vector2 aim,
Vector2 focusWorld, int actionId, int timer, int duration)
{
if (Main.dedServ
|| Main.gameMenu
|| owner is null
|| !owner.active
|| owner.dead
|| owner.whoAmI < 0
|| owner.whoAmI >= Main.maxPlayers
|| form == ReaperFormId.Base
|| (byte)form > (byte)ReaperFormId.Death)
{
return;
}
EnsureStateStorage();
Vector2 fallback = Vector2.UnitX * (owner.direction == 0 ? 1 : owner.direction);
if (!float.IsFinite(aim.X) || !float.IsFinite(aim.Y) || aim.LengthSquared() < 0.0001f)
aim = fallback;
else
aim.Normalize();
if (!float.IsFinite(focusWorld.X) || !float.IsFinite(focusWorld.Y))
focusWorld = owner.MountedCenter + aim * 180f;
duration = Math.Clamp(duration, 1, 3600);
timer = Math.Clamp(timer, 0, duration);
ref UltimateVisualState state = ref states[owner.whoAmI];
bool restarted = !state.Active || state.Form != form || state.ActionId != actionId;
// A remote controller can first replicate halfway through its action.
// Floor a newly observed storyboard at that authoritative frame so all
// earlier impacts are not replayed at once. Tick-one openings still cross
// normally for actions observed from their actual start.
int previousTimer = restarted
? (timer <= 1 ? timer - 1 : timer)
: state.Timer;
if (!restarted)
{
// Extra-AI packets can arrive after the local controller has already
// advanced. Never rewind a storyboard for the same authoritative
// action, otherwise old packets replay white flashes, sounds and shake.
int monotonicFloor = state.LastReportTick < Main.GameUpdateCount
? Math.Min(duration, state.Timer + 1)
: state.Timer;
timer = Math.Max(timer, monotonicFloor);
}
state.Active = true;
state.Form = form;
state.ActionId = actionId;
state.OriginWorld = owner.MountedCenter;
state.FocusWorld = focusWorld;
state.Aim = aim;
state.Timer = timer;
state.Duration = duration;
state.FramesSinceReport = 0;
state.LastReportTick = Main.GameUpdateCount;
float normalizedTime = state.Timer / (float)Math.Max(1, state.Duration);
ReaperVfxDirector.ReportCinematicCamera(owner.whoAmI, state.ActionId,
normalizedTime, aim, form == ReaperFormId.Death ? 1f : 0.84f);
TriggerTimelineImpacts(form, aim, previousTimer, state.Timer);
}
public override void Load()
{
EnsureStateStorage();
soulTexture = ModContent.Request<Texture2D>("SoulHarvest/Items/Soul", AssetRequestMode.ImmediateLoad);
}
public override void Unload()
{
states = [];
soulTexture = null;
// Content hooks may run on a loader worker. Detach immediately so no
// future draw can observe these resources, then dispose them on FNA's
// graphics thread just like the other procedural texture systems.
ReaperUltimateCrescentMaskSet[] oldMasks = crescentMasks;
crescentMasks = [];
BasicEffect? oldMirrorEffect = deathMirrorEffect;
deathMirrorEffect = null;
if (!Main.dedServ && oldMasks.Length > 0)
Main.QueueMainThreadAction(() => DisposeCrescentMasks(oldMasks));
if (!Main.dedServ && oldMirrorEffect is not null)
Main.QueueMainThreadAction(oldMirrorEffect.Dispose);
}
public override void OnWorldUnload()
{
ClearStates();
}
public override void PostUpdateEverything()
{
if (Main.gameMenu)
{
ClearStates();
return;
}
EnsureStateStorage();
for (int index = 0; index < states.Length; index++)
{
ref UltimateVisualState state = ref states[index];
if (!state.Active)
continue;
Player owner = Main.player[index];
if (!owner.active || owner.dead || state.LastReportTick > Main.GameUpdateCount)
{
state = default;
continue;
}
if (state.LastReportTick == Main.GameUpdateCount)
{
state.FramesSinceReport = 0;
continue;
}
state.FramesSinceReport++;
if (state.FramesSinceReport > TailFrames)
state = default;
}
}
public override void ModifyInterfaceLayers(List<GameInterfaceLayer> layers)
{
int index = layers.FindIndex(layer => layer.Name == "Vanilla: Resource Bars");
if (index < 0)
index = layers.FindIndex(layer => layer.Name == "Vanilla: Mouse Text");
if (index < 0)
index = layers.Count;
layers.Insert(index, new LegacyGameInterfaceLayer(
"SoulHarvest: Reaper Ultimate Spectacle",
DrawUltimateLayer,
InterfaceScaleType.None));
}
private static bool DrawUltimateLayer()
{
if (Main.gameMenu || states.Length == 0)
return true;
Player localPlayer = Main.LocalPlayer;
if (!localPlayer.active)
return true;
// This layer renders in physical screen space. Applying UI scale here
// confined the composition to the upper-left on high-DPI displays.
const float uiScale = 1f;
Vector2 viewport = new(Main.screenWidth, Main.screenHeight);
Rectangle viewportBounds = new(0, 0,
Math.Max(1, (int)Math.Ceiling(viewport.X)),
Math.Max(1, (int)Math.Ceiling(viewport.Y)));
Texture2D pixel = TextureAssets.MagicPixel.Value;
SpriteBatch batch = Main.spriteBatch;
float strongestDarkness = 0f;
for (int index = 0; index < states.Length; index++)
{
ref UltimateVisualState state = ref states[index];
if (!IsVisible(index, in state, viewport, uiScale))
continue;
strongestDarkness = Math.Max(strongestDarkness,
GetBackdropDarkness(in state) * GetStateOpacity(in state));
}
if (strongestDarkness <= 0.001f)
return true;
// Interface drawing always runs on the graphics thread. Creating the
// procedural masks lazily here avoids allocating Texture2D instances
// from a content-loader worker and keeps dedicated servers texture-free.
EnsureCrescentMasks();
batch.Draw(pixel, viewportBounds, Color.Black * strongestDarkness);
for (int index = 0; index < states.Length; index++)
{
ref UltimateVisualState state = ref states[index];
if (!IsVisible(index, in state, viewport, uiScale))
continue;
DrawState(batch, pixel, index, in state, viewport, viewportBounds, uiScale);
}
return true;
}
private static bool IsVisible(int playerIndex, in UltimateVisualState state, Vector2 viewport, float uiScale)
{
if (!state.Active || playerIndex < 0 || playerIndex >= Main.maxPlayers)
return false;
Player owner = Main.player[playerIndex];
if (!owner.active || owner.dead)
return false;
// Every client which has the authoritative action projectile renders the full
// storyboard. Off-screen owners are re-framed in DrawState instead of receiving
// the old reduced/culled presentation.
return true;
}
private static void DrawState(
SpriteBatch batch,
Texture2D pixel,
int ownerIndex,
in UltimateVisualState state,
Vector2 viewport,
Rectangle viewportBounds,
float uiScale)
{
float progress = MathHelper.Clamp(state.Timer / (float)Math.Max(1, state.Duration), 0f, 1f);
float opacity = GetStateOpacity(in state);
if (opacity <= 0.001f)
return;
Vector2 origin = state.OriginWorld - Main.screenPosition;
const float presentationMargin = 120f;
if (origin.X < -presentationMargin || origin.X > viewport.X + presentationMargin
|| origin.Y < -presentationMargin || origin.Y > viewport.Y + presentationMargin)
{
origin = viewport * 0.5f - state.Aim * 180f;
}
Vector2 focus = state.FocusWorld - Main.screenPosition;
if (state.Form != ReaperFormId.Death
&& (focus.X < -presentationMargin
|| focus.X > viewport.X + presentationMargin
|| focus.Y < -presentationMargin
|| focus.Y > viewport.Y + presentationMargin))
{
focus = viewport * 0.5f;
}
Color primary = ReaperCombatRegistry.GetPrimaryColor(state.Form);
Color secondary = ReaperCombatRegistry.GetSecondaryColor(state.Form);
float time = Main.GlobalTimeWrappedHourly;
batch.Draw(pixel, viewportBounds, primary * (0.028f * opacity));
DrawGlow(batch, focus, Math.Min(viewport.X, viewport.Y) * 0.33f,
primary * (0.105f * opacity));
DrawCinematicFrame(batch, pixel, viewport, progress, opacity, primary, secondary);
switch (state.Form)
{
case ReaperFormId.Bone:
DrawBone(batch, pixel, focus, viewport, progress, opacity, time, primary, secondary);
break;
case ReaperFormId.Blood:
DrawBlood(batch, pixel, focus, viewport, progress, opacity, time, primary, secondary);
break;
case ReaperFormId.Infernal:
DrawInfernal(batch, pixel, focus, viewport, progress, opacity, time, primary, secondary);
break;
case ReaperFormId.Frost:
DrawFrost(batch, pixel, focus, viewport, viewportBounds, progress, opacity, time, primary, secondary);
break;
case ReaperFormId.Soul:
DrawSoul(batch, pixel, focus, viewport, progress, opacity, time, primary, secondary);
break;
case ReaperFormId.Void:
DrawVoid(batch, pixel, focus, viewport, viewportBounds, progress, opacity,
time, primary, secondary, state.Aim, state.ActionId);
break;
case ReaperFormId.Death:
DrawDeath(batch, pixel, ownerIndex, in state, focus, viewport,
viewportBounds, progress, opacity, time);
break;
}
}
private static void TriggerTimelineImpacts(
ReaperFormId form,
Vector2 direction,
int previousTimer,
int currentTimer)
{
if (currentTimer <= previousTimer)
return;
Color primary = ReaperCombatRegistry.GetPrimaryColor(form);
Color secondary = ReaperCombatRegistry.GetSecondaryColor(form);
switch (form)
{
case ReaperFormId.Bone:
TriggerImpactAt(30, previousTimer, currentTimer, direction, 3.4f, primary, 0.10f);
TriggerImpactAt(66, previousTimer, currentTimer, -direction, 4.0f, primary, 0.12f);
TriggerImpactAt(102, previousTimer, currentTimer, direction.RotatedBy(0.8f), 4.8f, secondary, 0.15f);
TriggerImpactAt(138, previousTimer, currentTimer, direction.RotatedBy(-0.8f), 5.6f, secondary, 0.18f);
TriggerImpactAt(168, previousTimer, currentTimer, Vector2.UnitY, 7.8f, secondary, 0.34f);
TriggerImpactAt(207, previousTimer, currentTimer, direction, 11.5f, Color.White, 0.68f);
break;
case ReaperFormId.Blood:
TriggerImpactAt(18, previousTimer, currentTimer, Vector2.UnitY,
4.2f, new Color(170, 0, 34), 0.16f);
foreach (int tick in new[] { 48, 78, 108, 138 })
TriggerImpactAt(tick, previousTimer, currentTimer,
direction.RotatedBy(tick * 0.017f), 4.5f,
tick % 2 == 0 ? primary : secondary, 0.13f);
TriggerImpactAt(166, previousTimer, currentTimer, direction,
12f, new Color(255, 238, 228), 0.75f);
break;
case ReaperFormId.Infernal:
for (int index = 0; index < 5; index++)
TriggerImpactAt(20 + index * 20, previousTimer, currentTimer,
direction.RotatedBy(index * MathHelper.TwoPi / 5f), 4.8f + index * 0.3f,
index % 2 == 0 ? primary : secondary, 0.16f);
TriggerImpactAt(130, previousTimer, currentTimer, direction, 8f, secondary, 0.36f);
TriggerImpactAt(190, previousTimer, currentTimer, Vector2.UnitY, 12f, Color.White, 0.72f);
break;
case ReaperFormId.Frost:
TriggerImpactAt(1, previousTimer, currentTimer, Vector2.UnitY, 2.6f, new Color(180, 230, 255), 0.12f);
for (int index = 0; index < 4; index++)
TriggerImpactAt(30 + index * 18, previousTimer, currentTimer,
direction.RotatedBy(index * MathHelper.PiOver2), 4.2f + index * 0.35f,
index % 2 == 0 ? primary : secondary, 0.17f);
TriggerImpactAt(100, previousTimer, currentTimer, direction.RotatedBy(-0.5f), 8.4f, secondary, 0.43f);
TriggerImpactAt(110, previousTimer, currentTimer, direction.RotatedBy(0.5f), 11.2f, Color.White, 0.76f);
break;
case ReaperFormId.Soul:
TriggerImpactAt(20, previousTimer, currentTimer, Vector2.UnitY, 3.3f, primary, 0.13f);
for (int index = 0; index < 8; index++)
TriggerImpactAt(32 + index * 8, previousTimer, currentTimer,
direction.RotatedBy(index * MathHelper.PiOver4), 3.4f + index * 0.22f,
index % 2 == 0 ? primary : secondary, 0.11f);
TriggerImpactAt(104, previousTimer, currentTimer, direction, 11f, Color.White, 0.66f);
break;
case ReaperFormId.Void:
TriggerImpactAt(32, previousTimer, currentTimer, direction, 11f, Color.White, 0.66f);
break;
case ReaperFormId.Death:
for (int index = 0; index < ReaperDeathUltimateGeometry.CutCount; index++)
TriggerImpactAt(ReaperDeathUltimateGeometry.GetCutTick(index),
previousTimer, currentTimer,
direction.RotatedBy(index * 2.39996f), 4.6f + index * 0.08f,
new Color(225, 25, 66), 0.14f);
TriggerImpactAt(ReaperDeathUltimateGeometry.ShatterTick,
previousTimer, currentTimer, direction,
12f, new Color(190, 14, 48), 0.14f);
break;
}
}
private static void TriggerImpactAt(
int eventTick,
int previousTimer,
int currentTimer,
Vector2 direction,
float strength,
Color color,
float opacity)
{
if (previousTimer >= eventTick || currentTimer < eventTick)
return;
int frames = strength >= 10f ? 14 : strength >= 7f ? 10 : 7;
ReaperVfxDirector.TriggerGlobalImpact(direction, strength, frames, color, opacity,
strength >= 10f ? 7 : 4, strength >= 10f ? 0.50f : 0.24f);
}
private static void DrawCinematicFrame(
SpriteBatch batch,
Texture2D pixel,
Vector2 viewport,
float progress,
float opacity,
Color primary,
Color secondary)
{
float ritual = Envelope(progress, 0f, 0.07f, 0.94f, 1f) * opacity;
int barHeight = Math.Max(8, (int)Math.Round(viewport.Y * 0.035f));
batch.Draw(pixel, new Rectangle(0, 0, (int)Math.Ceiling(viewport.X), barHeight),
Color.Black * (ritual * 0.76f));
batch.Draw(pixel, new Rectangle(0, Math.Max(0, (int)Math.Floor(viewport.Y) - barHeight),
(int)Math.Ceiling(viewport.X), barHeight), Color.Black * (ritual * 0.76f));
// Deliberately omit ornamental corner brackets. The restrained full-width
// letterbox reads as one cinematic composition and cannot resemble a
// misplaced child viewport.
// Final impacts are expressed by world geometry, sound and camera
// response. A full-viewport white heartbeat obscured those details and
// became a one-frame white screen when several attacks overlapped.
}
private static void DrawBone(
SpriteBatch batch,
Texture2D pixel,
Vector2 focus,
Vector2 viewport,
float progress,
float opacity,
float time,
Color primary,
Color secondary)
{
float graveWidth = MathHelper.Clamp(viewport.X / 11f, 48f, 76f);
for (int index = 0; index < 8; index++)
{
float reveal = Reveal(progress, 0.05f + index * 0.052f, 0.18f + index * 0.052f) * opacity;
if (reveal <= 0.001f)
continue;
float horizontal = (index - 3.5f) * graveWidth;
float restingY = focus.Y + 112f + Math.Abs(index - 3.5f) * 8f;
Vector2 position = new(focus.X + horizontal, restingY + (1f - Ease(reveal / Math.Max(opacity, 0.001f))) * 54f);
float pulse = 0.82f + (float)Math.Sin(time * 2.7f + index * 1.3f) * 0.12f;
DrawTombstone(batch, pixel, position, 36f, 56f,
new Color(7, 20, 27) * (0.90f * reveal),
Color.Lerp(primary, secondary, 0.38f) * (pulse * reveal));
}
float frameReveal = Reveal(progress, 0.58f, 0.78f) * opacity;
if (frameReveal > 0.001f)
{
float halfWidth = MathHelper.Clamp(viewport.X * 0.16f, 150f, 260f);
float height = MathHelper.Clamp(viewport.Y * 0.46f, 260f, 430f);
Vector2 topLeft = focus + new Vector2(-halfWidth, -height * 0.63f);
Vector2 topRight = focus + new Vector2(halfWidth, -height * 0.63f);
Vector2 bottomLeft = topLeft + Vector2.UnitY * height;
Vector2 bottomRight = topRight + Vector2.UnitY * height;
DrawLayeredLine(batch, pixel, topLeft, bottomLeft, primary, secondary, 10f, 2.4f, frameReveal * 0.62f);
DrawLayeredLine(batch, pixel, topRight, bottomRight, primary, secondary, 10f, 2.4f, frameReveal * 0.62f);
DrawLayeredLine(batch, pixel, topLeft, topRight, primary, secondary, 12f, 2.8f, frameReveal * 0.78f);
float bladeReveal = Reveal(progress, 0.75f, 0.94f);
Vector2 bladeStart = topRight + new Vector2(-20f, 30f);
Vector2 bladeEnd = bottomLeft + new Vector2(38f, -24f);
DrawJaggedLine(batch, pixel, bladeStart, bladeEnd, bladeReveal,
primary, Color.White, 22f, 5.2f, 3.5f, 7.8f, opacity);
Vector2 parallel = (bladeEnd - bladeStart).SafeNormalize(Vector2.UnitY).RotatedBy(MathHelper.PiOver2) * 22f;
DrawJaggedLine(batch, pixel, bladeStart + parallel, bladeEnd + parallel, bladeReveal,
new Color(25, 100, 120), secondary, 12f, 2.5f, 2.4f, 11.2f, opacity * 0.78f);
float impact = Pulse(progress, 0.965f, 0.07f) * opacity;
DrawGlow(batch, bladeEnd, 125f, secondary * (0.30f * impact));
DrawRing(batch, pixel, bladeEnd, MathHelper.Lerp(18f, 128f, Ease(impact)), 36,
secondary * (impact * 0.9f), 3f, 0f, 1f);
}
float burial = Envelope(progress, 0.18f, 0.30f, 0.72f, 0.88f) * opacity;
if (burial > 0.001f)
{
float cageRadius = MathHelper.Clamp(Math.Min(viewport.X, viewport.Y) * 0.21f, 130f, 235f);
for (int side = -1; side <= 1; side += 2)
{
for (int rib = 0; rib < 6; rib++)
{
float y = MathHelper.Lerp(-cageRadius * 0.68f, cageRadius * 0.65f, rib / 5f);
float width = cageRadius * (0.92f - Math.Abs(rib - 2.5f) * 0.09f);
Vector2 spine = focus + new Vector2(side * cageRadius * 0.88f, y);
Vector2 sternum = focus + new Vector2(side * width * 0.12f, y + 13f);
Vector2 elbow = Vector2.Lerp(spine, sternum, 0.52f) + new Vector2(side * 24f, -20f);
DrawLayeredLine(batch, pixel, spine, elbow, new Color(20, 95, 112), secondary,
9f, 2.3f, burial * 0.48f);
DrawLayeredLine(batch, pixel, elbow, sternum, primary, Color.White,
7f, 1.8f, burial * 0.58f);
}
}
}
float judge = Envelope(progress, 0.70f, 0.80f, 0.975f, 1f) * opacity;
if (judge > 0.001f)
{
Vector2 judgeCenter = focus - Vector2.UnitY * MathHelper.Clamp(viewport.Y * 0.21f, 120f, 210f);
float skullRadius = MathHelper.Clamp(viewport.Y * 0.095f, 52f, 94f);
DrawGlow(batch, judgeCenter, skullRadius * 2.4f, primary * (judge * 0.16f));
DrawPolygon(batch, pixel, judgeCenter, skullRadius, 8, new Color(8, 25, 30) * (judge * 0.94f),
skullRadius * 0.75f, -MathHelper.PiOver2, 1f);
DrawRing(batch, pixel, judgeCenter, skullRadius, 30, secondary * (judge * 0.84f), 4f, 0f, 1f);
Vector2 eyeOffset = new(skullRadius * 0.33f, -skullRadius * 0.08f);
DrawGlow(batch, judgeCenter - eyeOffset, skullRadius * 0.34f, primary * (judge * 0.72f));
DrawGlow(batch, judgeCenter + new Vector2(eyeOffset.X, eyeOffset.Y), skullRadius * 0.34f,
primary * (judge * 0.72f));
for (int tooth = -3; tooth <= 3; tooth++)
{
Vector2 root = judgeCenter + new Vector2(tooth * skullRadius * 0.16f, skullRadius * 0.62f);
DrawLayeredLine(batch, pixel, root, root + Vector2.UnitY * skullRadius * 0.34f,
primary, Color.White, 5f, 1.3f, judge * 0.72f);
}
}
}
private static void DrawBlood(
SpriteBatch batch,
Texture2D pixel,
Vector2 focus,
Vector2 viewport,
float progress,
float opacity,
float time,
Color primary,
Color secondary)
{
// The shared Blood blade projectiles draw their own exact flight paths and
// persistent wounds. This layer supplies only restrained atmosphere so it
// cannot invent a second set of fake, screen-space projectiles or scars.
float opening = Reveal(progress, 0.01f, 0.10f) * opacity;
float pulse = 0.82f + (float)Math.Sin(time * 3.4f) * 0.10f;
DrawGlow(batch, focus, MathHelper.Clamp(Math.Min(viewport.X, viewport.Y) * 0.31f,
170f, 340f), new Color(122, 0, 24) * (opening * pulse * 0.28f));
float closing = Envelope(progress, 0.76f, 0.84f, 0.97f, 1f) * opacity;
DrawGlow(batch, focus, 240f,
new Color(238, 22, 52) * (closing * 0.24f));
}
private static void DrawBloodLegacy(
SpriteBatch batch,
Texture2D pixel,
Vector2 focus,
Vector2 viewport,
float progress,
float opacity,
float time,
Color primary,
Color secondary)
{
float opening = Reveal(progress, 0.01f, 0.10f) * opacity;
DrawGlow(batch, focus, MathHelper.Clamp(Math.Min(viewport.X, viewport.Y) * 0.28f,
150f, 310f), new Color(122, 0, 24) * (opening * 0.26f));
// Six projectiles are rendered in the world. The overlay only reinforces
// their curved approach paths and preserves the wounds they cut, keeping
// the composition readable instead of replacing them with six crescents.
float flightFade = 1f - MathHelper.SmoothStep(0f, 1f,
MathHelper.Clamp((progress - 0.72f) / 0.12f, 0f, 1f));
float orbit = MathHelper.Clamp(Math.Min(viewport.X, viewport.Y) * 0.31f, 190f, 360f);
for (int index = 0; index < 6; index++)
{
float phase = progress * 5.8f + index / 6f;
float angle = index * MathHelper.TwoPi / 6f + phase * MathHelper.TwoPi;
Vector2 tangent = angle.ToRotationVector2();
Vector2 head = focus + tangent * orbit * (0.72f + (float)Math.Sin(phase * 4f) * 0.12f);
Vector2 tail = focus + (angle - 0.54f).ToRotationVector2() * orbit * 0.92f;
DrawLayeredLine(batch, pixel, tail, head, new Color(238, 20, 54),
new Color(8, 0, 3), 20f, 8f, opening * flightFade * 0.68f);
DrawTaperedCrescent(batch, pixel, head, 34f, angle - 2.5f, 4.0f,
opening * flightFade * 0.74f, new Color(58, 0, 14),
new Color(225, 18, 50), new Color(255, 214, 205), 9f);
float scarReveal = Reveal(progress, 0.16f + index * 0.035f,
0.28f + index * 0.035f) * opacity;
float scarAngle = -0.78f + index * 0.31f;
Vector2 scarAxis = scarAngle.ToRotationVector2();
Vector2 scarCenter = focus + (index - 2.5f) * 20f * Vector2.UnitY;
DrawLayeredLine(batch, pixel, scarCenter - scarAxis * orbit * 0.58f,
scarCenter + scarAxis * orbit * 0.58f,
new Color(245, 18, 52), new Color(5, 0, 2),
15f, 8f, scarReveal * flightFade * 0.56f);
}
float finalSlash = Envelope(progress, 0.76f, 0.82f, 0.96f, 1f) * opacity;
if (finalSlash > 0.001f)
{
Vector2 direction = new Vector2(1f, -0.20f).SafeNormalize(Vector2.UnitX);
float reach = (float)Math.Sqrt(viewport.X * viewport.X + viewport.Y * viewport.Y) * 0.72f;
Vector2 start = focus - direction * reach;
Vector2 end = focus + direction * reach;
DrawLayeredLine(batch, pixel, start, end,
new Color(255, 24, 58), new Color(4, 0, 2),
MathHelper.Lerp(18f, 82f, finalSlash),
MathHelper.Lerp(8f, 49f, finalSlash), finalSlash);
DrawGlow(batch, focus, 220f,
new Color(238, 22, 52) * (finalSlash * 0.22f));
}
}
private static void DrawInfernal(
SpriteBatch batch,
Texture2D pixel,
Vector2 focus,
Vector2 viewport,
float progress,
float opacity,
float time,
Color primary,
Color secondary)
{
float radius = MathHelper.Clamp(Math.Min(viewport.X, viewport.Y) * 0.23f, 145f, 265f);
float rotation = -MathHelper.PiOver2 + time * 0.055f;
float circleReveal = Reveal(progress, 0.015f, 0.22f);
DrawRing(batch, pixel, focus, radius * 1.08f, 64, primary * (0.82f * opacity), 4f,
rotation, circleReveal);
DrawRing(batch, pixel, focus, radius * 0.94f, 54, secondary * (0.48f * opacity), 1.8f,
-rotation * 0.7f, circleReveal);
Span<Vector2> vertices = stackalloc Vector2[5];
for (int index = 0; index < vertices.Length; index++)
vertices[index] = focus + (rotation + index * MathHelper.TwoPi / 5f).ToRotationVector2() * radius;
for (int edge = 0; edge < 5; edge++)
{
int startIndex = edge * 2 % 5;
int endIndex = (edge + 1) * 2 % 5;
float reveal = Reveal(progress, 0.08f + edge * 0.085f, 0.24f + edge * 0.085f);
DrawLayeredLine(batch, pixel, vertices[startIndex],
Vector2.Lerp(vertices[startIndex], vertices[endIndex], reveal),
new Color(185, 38, 5), secondary, 13f, 3f, opacity * reveal);
if (reveal > 0.02f)
{
Vector2 flame = vertices[startIndex] + Vector2.UnitY * (10f + (float)Math.Sin(time * 7f + edge) * 5f);
DrawGlow(batch, flame, 38f, primary * (0.25f * opacity * reveal));
DrawRuneTriangle(batch, pixel, vertices[startIndex], 20f, rotation + edge, secondary,
opacity * reveal);
}
}
float ignition = Reveal(progress, 0.62f, 0.82f) * opacity;
for (int index = 0; index < 5; index++)
{
Vector2 direction = (vertices[index] - focus).SafeNormalize(Vector2.UnitY);
Vector2 flameTip = vertices[index] + direction * MathHelper.Lerp(10f, 86f, ignition);
DrawJaggedLine(batch, pixel, vertices[index], flameTip, ignition,
primary, secondary, 9f, 2.2f, 5f, 130f + index * 13f, opacity);
}
float detonation = Pulse(progress, 0.94f, 0.10f) * opacity;
DrawGlow(batch, focus, radius * 1.2f, secondary * (0.25f * detonation));
DrawRing(batch, pixel, focus, MathHelper.Lerp(radius * 0.18f, radius * 1.35f, Ease(detonation)), 72,
Color.White * (0.78f * detonation), 4.5f, rotation, 1f);
float gate = Envelope(progress, 0.58f, 0.72f, 0.98f, 1f) * opacity;
if (gate > 0.001f)
{
float gateWidth = MathHelper.Clamp(viewport.X * 0.24f, 210f, 420f);
float gateHeight = MathHelper.Clamp(viewport.Y * 0.56f, 310f, 590f);
Vector2 topLeft = focus + new Vector2(-gateWidth * 0.5f, -gateHeight * 0.62f);
Vector2 topRight = focus + new Vector2(gateWidth * 0.5f, -gateHeight * 0.62f);
Vector2 bottomLeft = topLeft + Vector2.UnitY * gateHeight;
Vector2 bottomRight = topRight + Vector2.UnitY * gateHeight;
DrawLayeredLine(batch, pixel, topLeft, bottomLeft, new Color(42, 8, 3), primary, 24f, 4f, gate * 0.74f);
DrawLayeredLine(batch, pixel, topRight, bottomRight, new Color(42, 8, 3), primary, 24f, 4f, gate * 0.74f);
DrawTaperedCrescent(batch, pixel, focus - Vector2.UnitY * gateHeight * 0.50f, gateWidth * 0.52f,
MathHelper.Pi, MathHelper.Pi, gate * 0.72f, new Color(30, 4, 0),
new Color(220, 52, 4), secondary, gateWidth * 0.13f);
for (int ember = 0; ember < 9; ember++)
{
float along = ember / 8f;
Vector2 emberPosition = Vector2.Lerp(bottomLeft, bottomRight, along)
- Vector2.UnitY * (22f + (float)Math.Sin(time * 7f + ember) * 18f);
DrawGlow(batch, emberPosition, 28f + ember % 3 * 7f, primary * (gate * 0.24f));
}
}
float execution = Envelope(progress, 0.84f, 0.91f, 0.99f, 1f) * opacity;
if (execution > 0.001f)
{
DrawReaperSilhouette(batch, pixel, focus - Vector2.UnitY * radius * 0.50f,
radius * 1.45f, new Color(18, 3, 0), primary, execution * 0.78f);
DrawTaperedCrescent(batch, pixel, focus, radius * 1.78f, -2.7f, 4.55f, execution,
new Color(58, 7, 0), new Color(255, 75, 5), Color.White, radius * 0.28f);
}
}
private static void DrawFrost(
SpriteBatch batch,
Texture2D pixel,
Vector2 focus,
Vector2 viewport,
Rectangle viewportBounds,
float progress,
float opacity,
float time,
Color primary,
Color secondary)
{
// A cool gray veil mutes the world without changing Terraria's shared shader state.
batch.Draw(pixel, viewportBounds, new Color(102, 128, 148) * (0.12f * opacity));
float reach = Math.Max(viewport.X, viewport.Y) * 0.68f;
for (int index = 0; index < 4; index++)
{
float reveal = Reveal(progress, 0.16f + index * 0.085f, 0.34f + index * 0.085f);
Vector2 direction = (-MathHelper.PiOver2 + index * MathHelper.PiOver2).ToRotationVector2();
Vector2 end = focus + direction * reach;
DrawJaggedLine(batch, pixel, focus, end, reveal,
new Color(38, 105, 155), secondary, 15f, 3.1f, 17f, 208f + index * 19f, opacity);
for (int branch = 0; branch < 3; branch++)
{
float branchProgress = 0.29f + branch * 0.19f;
if (reveal <= branchProgress)
continue;
Vector2 branchOrigin = Vector2.Lerp(focus, end, branchProgress);
float side = (branch + index) % 2 == 0 ? -1f : 1f;
Vector2 branchDirection = direction.RotatedBy(side * (0.42f + branch * 0.12f));
float branchReveal = MathHelper.Clamp((reveal - branchProgress) / 0.25f, 0f, 1f);
DrawJaggedLine(batch, pixel, branchOrigin,
branchOrigin + branchDirection * (58f + branch * 24f), branchReveal,
primary, Color.White, 7f, 1.6f, 7f, 260f + index * 31f + branch * 7f, opacity * 0.82f);
}
}
float crossReveal = Reveal(progress, 0.76f, 0.93f);
float diagonalReach = Math.Max(viewport.X, viewport.Y) * 0.72f;
for (int index = 0; index < 4; index++)
{
Vector2 direction = (MathHelper.PiOver4 + index * MathHelper.PiOver2).ToRotationVector2();
DrawJaggedLine(batch, pixel, focus, focus + direction * diagonalReach, crossReveal,
primary, Color.White, 24f, 5f, 8f, 330f + index * 17f, opacity);
}
DrawGlow(batch, focus, 180f + (float)Math.Sin(time * 3f) * 12f,
secondary * (Pulse(progress, 0.90f, 0.13f) * 0.26f * opacity));
float mirrors = Envelope(progress, 0.10f, 0.24f, 0.84f, 0.96f) * opacity;
float mirrorOrbit = MathHelper.Clamp(Math.Min(viewport.X, viewport.Y) * 0.27f, 170f, 300f);
for (int index = 0; index < 4; index++)
{
float angle = MathHelper.PiOver4 + index * MathHelper.PiOver2;
Vector2 center = focus + angle.ToRotationVector2() * mirrorOrbit;
DrawPolygon(batch, pixel, center, 54f, 6, new Color(35, 100, 150) * (mirrors * 0.52f),
13f, angle, 1f);
DrawPolygon(batch, pixel, center, 47f, 6, Color.White * (mirrors * 0.72f),
2.2f, -angle, 1f);
Vector2 reflection = (focus - center).SafeNormalize(Vector2.UnitY);
DrawLayeredLine(batch, pixel, center - reflection.RotatedBy(MathHelper.PiOver2) * 25f,
center + reflection.RotatedBy(MathHelper.PiOver2) * 25f,
primary, Color.White, 8f, 1.5f, mirrors * 0.52f);
}
float shatteredScreen = Envelope(progress, 0.86f, 0.92f, 0.995f, 1f) * opacity;
if (shatteredScreen > 0.001f)
{
Vector2[] corners =
[
new Vector2(0f, 0f),
new Vector2(viewport.X, 0f),
new Vector2(viewport.X, viewport.Y),
new Vector2(0f, viewport.Y),
new Vector2(viewport.X * 0.5f, 0f),
new Vector2(viewport.X, viewport.Y * 0.5f),
new Vector2(viewport.X * 0.5f, viewport.Y),
new Vector2(0f, viewport.Y * 0.5f)
];
for (int index = 0; index < corners.Length; index++)
{
DrawJaggedLine(batch, pixel, focus, corners[index], shatteredScreen,
new Color(84, 170, 220), Color.White, 10f, 2.1f, 16f,
710f + index * 23f, opacity);
}
}
}
private static void DrawSoul(
SpriteBatch batch,
Texture2D pixel,
Vector2 focus,
Vector2 viewport,
float progress,
float opacity,
float time,
Color primary,
Color secondary)
{
float riverY = MathHelper.Clamp(focus.Y, viewport.Y * 0.26f, viewport.Y * 0.74f);
float riverReveal = Reveal(progress, 0.02f, 0.24f) * opacity;
float convergence = Reveal(progress, 0.66f, 0.93f);
for (int lane = 0; lane < 3; lane++)
{
const int segments = 34;
Vector2 previous = default;
for (int index = 0; index <= segments; index++)
{
float along = index / (float)segments;
float x = MathHelper.Lerp(-70f, viewport.X + 70f, along);
float wave = (float)Math.Sin(along * MathHelper.TwoPi * 2.2f - time * (1.15f + lane * 0.12f) + lane * 1.8f);
float y = riverY + (lane - 1) * 45f + wave * (22f + lane * 5f);
float centerWeight = 1f - MathHelper.Clamp(Math.Abs(along - 0.5f) * 2f, 0f, 1f);
y = MathHelper.Lerp(y, focus.Y, convergence * centerWeight * 0.88f);
Vector2 current = new(x, y);
if (index > 0)
{
Color laneColor = lane == 1 ? secondary : Color.Lerp(primary, secondary, lane * 0.34f);
DrawLayeredLine(batch, pixel, previous, current, primary, laneColor,
10f - lane * 1.5f, 2.1f, riverReveal * (0.56f + lane * 0.12f));
}
previous = current;
}
}
for (int index = 0; index < 12; index++)
{
float reveal = Reveal(progress, 0.07f + index * 0.022f, 0.19f + index * 0.022f) * opacity;
if (reveal <= 0.001f)
continue;
float along = PositiveModulo(time * (0.065f + index % 3 * 0.006f) + index / 12f, 1f);
int lane = index % 3;
float x = MathHelper.Lerp(-45f, viewport.X + 45f, along);
float y = riverY + (lane - 1) * 45f
+ (float)Math.Sin(along * MathHelper.TwoPi * 2.2f - time * 1.25f + lane * 1.8f) * 24f;
Vector2 riverPosition = new(x, y);
float orbitAngle = index * MathHelper.TwoPi / 12f + time * 0.75f;
Vector2 gatheredPosition = focus + orbitAngle.ToRotationVector2()
* MathHelper.Lerp(72f, 20f, Reveal(progress, 0.82f, 0.98f));
Vector2 position = Vector2.Lerp(riverPosition, gatheredPosition, convergence);
Color shadeColor = Color.Lerp(secondary, Color.White, index % 4 == 0 ? 0.34f : 0.08f);
DrawSoulShade(batch, pixel, position, shadeColor, reveal,
0.78f + (float)Math.Sin(time * 3f + index) * 0.10f, orbitAngle);
}
float burial = Reveal(progress, 0.82f, 0.98f) * opacity;
DrawRing(batch, pixel, focus, MathHelper.Lerp(210f, 26f, Ease(burial)), 56,
secondary * (0.92f * burial), 4f, -time * 0.22f, 1f);
DrawGlow(batch, focus, 150f, primary * (0.30f * burial));
float ferryman = Envelope(progress, 0.63f, 0.76f, 0.99f, 1f) * opacity;
if (ferryman > 0.001f)
{
float scale = MathHelper.Clamp(Math.Min(viewport.X, viewport.Y) * 0.42f, 260f, 480f);
DrawReaperSilhouette(batch, pixel, focus - Vector2.UnitY * scale * 0.18f,
scale, new Color(12, 5, 35), primary, ferryman * 0.72f);
DrawTaperedCrescent(batch, pixel, focus, scale * 0.92f, -2.75f, 4.72f, ferryman,
new Color(25, 10, 72), primary, new Color(170, 255, 255), scale * 0.16f);
}
float soulBloom = Pulse(progress, 0.94f, 0.08f) * opacity;
for (int petal = 0; petal < 8; petal++)
{
float angle = petal * MathHelper.PiOver4 + time * 0.08f;
Vector2 direction = angle.ToRotationVector2();
DrawLayeredLine(batch, pixel, focus + direction * 20f,
focus + direction * MathHelper.Lerp(28f, 190f, soulBloom),
primary, secondary, 11f, 2.2f, soulBloom * 0.58f);
}
}
private static void DrawVoid(
SpriteBatch batch,
Texture2D pixel,
Vector2 focus,
Vector2 viewport,
Rectangle viewportBounds,
float progress,
float opacity,
float time,
Color primary,
Color secondary,
Vector2 aim,
int actionId)
{
// The combat projectile owns the one and only world-space rift. Keeping
// this UI layer to atmosphere prevents a second crack from following the
// camera while the authoritative tear stays in the world.
batch.Draw(pixel, viewportBounds, new Color(1, 0, 5) * (0.31f * opacity));
DrawGlow(batch, focus, 210f,
secondary * (Reveal(progress, 0.48f, 1f) * 0.24f * opacity));
}
private static void DrawDeath(
SpriteBatch batch,
Texture2D pixel,
int ownerIndex,
in UltimateVisualState state,
Vector2 focus,
Vector2 viewport,
Rectangle viewportBounds,
float progress,
float opacity,
float time)
{
float shatterStart = ReaperDeathUltimateGeometry.ShatterTick
/ (float)ReaperDeathUltimateGeometry.Duration;
float isolation = Envelope(progress, 0.58f, 0.79f,
shatterStart - 0.015f, shatterStart + 0.012f) * opacity;
Texture2D? capturedWorld = Main.screenTarget;
// Each successful cut removes more of the ordinary scene. Immediately
// before the shatter, only small windows around the caster and recorded
// victims are restored from Terraria's world render target.
if (isolation > 0.001f)
{
batch.Draw(pixel, viewportBounds,
new Color(0, 0, 1) * (isolation * 0.965f));
if (capturedWorld is not null && !capturedWorld.IsDisposed)
DrawDeathUltimateActors(batch, capturedWorld, ownerIndex,
state.ActionId, viewport, isolation);
}
float shatter = Reveal(progress, shatterStart, 0.985f) * opacity;
if (shatter > 0.001f)
{
// The actual Death Necklace plates fill the void behind the broken
// game image; no substitute gradient or black fill is used here.
DeathDomainBackdropTextureSystem.DrawScreenAlignedBackdrop(batch,
viewportBounds, shatter);
if (capturedWorld is not null && !capturedWorld.IsDisposed)
DrawDeathMirrorShards(batch, capturedWorld, focus, viewport,
state.ActionId, shatter, opacity);
}
DrawDeathUltimateCutCracks(batch, pixel, in state, focus, viewport,
shatter, opacity);
DrawGlow(batch, focus, 230f,
new Color(125, 0, 34) * ((0.12f + shatter * 0.18f) * opacity));
_ = time;
}
private static void DrawDeathUltimateActors(SpriteBatch batch,
Texture2D capturedWorld, int ownerIndex, int actionId,
Vector2 viewport, float opacity)
{
if (ownerIndex < 0 || ownerIndex >= Main.maxPlayers)
return;
Player owner = Main.player[ownerIndex];
if (owner.active && !owner.dead)
DrawCapturedActorWindow(batch, capturedWorld, owner.getRect(),
viewport, opacity, 34f);
HashSet<int> drawnRoots = [];
foreach (NPC npc in Main.ActiveNPCs)
{
int root = npc.realLife >= 0 ? npc.realLife : npc.whoAmI;
if (root < 0 || root >= Main.maxNPCs || !drawnRoots.Add(root))
continue;
NPC target = Main.npc[root];
if (!target.active
|| !target.GetGlobalNPC<ReaperCombatGlobalNPC>()
.IsTrackedByDeathUltimate(ownerIndex, actionId))
{
continue;
}
DrawCapturedActorWindow(batch, capturedWorld, target.Hitbox,
viewport, opacity, 28f);
}
}
private static void DrawCapturedActorWindow(SpriteBatch batch,
Texture2D capturedWorld, Rectangle worldBounds, Vector2 viewport,
float opacity, float padding)
{
Rectangle screenBounds = new(
(int)Math.Floor(worldBounds.X - Main.screenPosition.X - padding),
(int)Math.Floor(worldBounds.Y - Main.screenPosition.Y - padding),
Math.Max(1, (int)Math.Ceiling(worldBounds.Width + padding * 2f)),
Math.Max(1, (int)Math.Ceiling(worldBounds.Height + padding * 2f)));
Rectangle clipped = Rectangle.Intersect(screenBounds,
new Rectangle(0, 0, (int)viewport.X, (int)viewport.Y));
if (clipped.Width <= 0 || clipped.Height <= 0)
return;
const int bands = 12;
for (int band = 0; band < bands; band++)
{
float y0 = band / (float)bands;
float y1 = (band + 1f) / bands;
float normalizedY = (y0 + y1) - 1f;
float halfWidth = (float)Math.Sqrt(Math.Max(0f,
1f - normalizedY * normalizedY));
int bandTop = screenBounds.Top
+ (int)Math.Floor(screenBounds.Height * y0);
int bandBottom = screenBounds.Top
+ (int)Math.Ceiling(screenBounds.Height * y1);
int bandHalfWidth = Math.Max(1,
(int)Math.Ceiling(screenBounds.Width * 0.5f * halfWidth));
Rectangle destination = new(
screenBounds.Center.X - bandHalfWidth,
bandTop,
bandHalfWidth * 2,
Math.Max(1, bandBottom - bandTop));
destination = Rectangle.Intersect(destination, clipped);
if (destination.Width <= 0 || destination.Height <= 0)
continue;
Rectangle source = new(
(int)Math.Floor(destination.X / viewport.X * capturedWorld.Width),
(int)Math.Floor(destination.Y / viewport.Y * capturedWorld.Height),
Math.Max(1, (int)Math.Ceiling(destination.Width / viewport.X
* capturedWorld.Width)),
Math.Max(1, (int)Math.Ceiling(destination.Height / viewport.Y
* capturedWorld.Height)));
source = Rectangle.Intersect(source, capturedWorld.Bounds);
if (source.Width > 0 && source.Height > 0)
batch.Draw(capturedWorld, destination, source,
Color.White * MathHelper.Clamp(opacity, 0f, 1f));
}
}
private static void DrawDeathMirrorShards(SpriteBatch batch,
Texture2D capturedWorld, Vector2 focus, Vector2 viewport,
int actionId, float shatter, float opacity)
{
if (Main.graphics?.GraphicsDevice is not GraphicsDevice graphicsDevice)
return;
int rayCount = deathMirrorAngles.Length;
for (int cut = 0; cut < ReaperDeathUltimateGeometry.CutCount; cut++)
{
float angle = ReaperDeathUltimateGeometry.GetCutAxis(actionId, cut)
.ToRotation();
if (angle < 0f)
angle += MathHelper.TwoPi;
deathMirrorAngles[cut * 2] = angle;
deathMirrorAngles[cut * 2 + 1] = PositiveModulo(
angle + MathHelper.Pi, MathHelper.TwoPi);
}
Array.Sort(deathMirrorAngles);
float eased = Ease(shatter);
float radius = GetViewportCoverRadius(focus, viewport) * 1.08f;
Color shardColor = Color.White
* (opacity * MathHelper.Lerp(1f, 0.48f, eased));
int vertex = 0;
for (int shard = 0; shard < rayCount; shard++)
{
float startAngle = deathMirrorAngles[shard];
float endAngle = shard == rayCount - 1
? deathMirrorAngles[0] + MathHelper.TwoPi
: deathMirrorAngles[shard + 1];
float middleAngle = (startAngle + endAngle) * 0.5f;
uint hash = unchecked((uint)(actionId * 16777619
+ shard * 2246822519));
hash ^= hash >> 15;
float random = (hash & 0xFFFFu) / 65535f;
float signed = ((hash >> 16) & 1u) == 0u ? -1f : 1f;
Vector2 displacement = middleAngle.ToRotationVector2()
* ((18f + random * 48f) * eased)
+ Vector2.UnitY * (eased * eased * (8f + random * 34f));
float rotation = signed * (0.012f + random * 0.032f) * eased;
Vector2 originalCenter = focus;
Vector2 originalStart = focus
+ startAngle.ToRotationVector2() * radius;
Vector2 originalEnd = focus
+ endAngle.ToRotationVector2() * radius;
WriteMirrorVertex(ref vertex,
RotatePoint(originalCenter, focus, rotation) + displacement,
originalCenter, viewport, shardColor);
WriteMirrorVertex(ref vertex,
RotatePoint(originalStart, focus, rotation) + displacement,
originalStart, viewport, shardColor);
WriteMirrorVertex(ref vertex,
RotatePoint(originalEnd, focus, rotation) + displacement,
originalEnd, viewport, shardColor);
}
batch.End();
try
{
deathMirrorEffect ??= new BasicEffect(graphicsDevice)
{
TextureEnabled = true,
VertexColorEnabled = true,
LightingEnabled = false,
FogEnabled = false
};
deathMirrorEffect.World = Matrix.Identity;
deathMirrorEffect.View = Matrix.Identity;
deathMirrorEffect.Projection = Matrix.CreateOrthographicOffCenter(
0f, viewport.X, viewport.Y, 0f, 0f, 1f);
deathMirrorEffect.Texture = capturedWorld;
graphicsDevice.BlendState = BlendState.AlphaBlend;
graphicsDevice.DepthStencilState = DepthStencilState.None;
graphicsDevice.RasterizerState = RasterizerState.CullNone;
graphicsDevice.SamplerStates[0] = SamplerState.LinearClamp;
foreach (EffectPass pass in deathMirrorEffect.CurrentTechnique.Passes)
{
pass.Apply();
graphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList,
deathMirrorVertices, 0, vertex / 3);
}
}
finally
{
batch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend,
SamplerState.LinearClamp, DepthStencilState.None,
RasterizerState.CullNone, null, Matrix.Identity);
}
}
private static void WriteMirrorVertex(ref int index, Vector2 position,
Vector2 samplePosition, Vector2 viewport, Color color)
{
Vector2 uv = new(
samplePosition.X / Math.Max(1f, viewport.X),
samplePosition.Y / Math.Max(1f, viewport.Y));
deathMirrorVertices[index++] = new VertexPositionColorTexture(
new Vector3(position, 0f), color, uv);
}
private static Vector2 RotatePoint(Vector2 point, Vector2 origin,
float rotation)
=> origin + (point - origin).RotatedBy(rotation);
private static float GetViewportCoverRadius(Vector2 focus, Vector2 viewport)
{
float radius = focus.Length();
radius = Math.Max(radius, Vector2.Distance(focus,
new Vector2(viewport.X, 0f)));
radius = Math.Max(radius, Vector2.Distance(focus,
new Vector2(0f, viewport.Y)));
radius = Math.Max(radius, Vector2.Distance(focus, viewport));
return Math.Max(1f, radius);
}
private static void DrawDeathUltimateCutCracks(SpriteBatch batch,
Texture2D pixel, in UltimateVisualState state, Vector2 focus,
Vector2 viewport, float shatter, float opacity)
{
float radius = GetViewportCoverRadius(focus, viewport) * 1.04f;
for (int cut = 0; cut < ReaperDeathUltimateGeometry.CutCount; cut++)
{
int cutTick = ReaperDeathUltimateGeometry.GetCutTick(cut);
if (state.Timer < cutTick)
continue;
float age = state.Timer - cutTick;
float fresh = (float)Math.Exp(-age / 13f);
float strength = (0.12f + fresh * 0.38f + shatter * 0.74f)
* opacity;
Vector2 axis = ReaperDeathUltimateGeometry.GetCutAxis(
state.ActionId, cut);
Vector2 start = focus - axis * radius;
Vector2 end = focus + axis * radius;
DrawLayeredLine(batch, pixel, start, end,
new Color(32, 0, 15),
Color.Lerp(new Color(235, 12, 62), Color.White,
shatter * 0.48f),
4f + shatter * 3f, 0.9f + shatter * 0.8f,
strength);
}
}
private static void DrawDeathLegacy(
SpriteBatch batch,
Texture2D pixel,
Vector2 focus,
Vector2 viewport,
float progress,
float opacity,
float time)
{
ReadOnlySpan<ReaperFormId> forms =
[
ReaperFormId.Bone,
ReaperFormId.Blood,
ReaperFormId.Infernal,
ReaperFormId.Frost,
ReaperFormId.Soul,
ReaperFormId.Void
];
float converge = Reveal(progress, 0.69f, 0.92f);
float initialOrbit = MathHelper.Clamp(Math.Min(viewport.X, viewport.Y) * 0.31f, 205f, 340f);
float orbitRadius = MathHelper.Lerp(initialOrbit, 34f, Ease(converge));
float rotation = -MathHelper.PiOver2 + time * MathHelper.Lerp(0.08f, 0.72f, converge);
for (int index = 0; index < forms.Length; index++)
{
float reveal = Reveal(progress, 0.015f + index * 0.038f, 0.105f + index * 0.038f) * opacity;
if (reveal <= 0.001f)
continue;
Color color = ReaperCombatRegistry.GetPrimaryColor(forms[index]);
Color core = ReaperCombatRegistry.GetSecondaryColor(forms[index]);
float angle = rotation + index * MathHelper.TwoPi / forms.Length;
Vector2 originalPosition = focus + angle.ToRotationVector2() * initialOrbit;
Vector2 position = focus + angle.ToRotationVector2() * orbitRadius;
if (converge > 0.001f)
DrawLayeredLine(batch, pixel, originalPosition, position, color, core, 9f, 2f,
converge * reveal * 0.58f);
DrawLayeredLine(batch, pixel, position, focus, color, core, 5f, 1.2f, reveal * 0.30f);
DrawGlow(batch, position, 54f, color * (0.24f * reveal));
float phaseCenter = 36f / 170f + index * 18f / 170f;
float activePulse = 0.74f + Pulse(progress, phaseCenter, 0.055f) * 0.36f;
DrawDeathGate(batch, pixel, position, index, angle, reveal * activePulse,
converge, time, color, core);
}
// Six executions use their branch's actual silhouette, not six recolored
// copies of one crescent. Their centers match the authoritative hit ticks.
for (int index = 0; index < forms.Length; index++)
{
float center = 36f / 170f + index * 18f / 170f;
float phase = Envelope(progress, center - 0.055f, center - 0.012f,
center + 0.052f, center + 0.105f) * opacity;
if (phase > 0.001f)
DrawDeathPhaseMotif(batch, pixel, focus, viewport, index, phase, time);
}
float unity = Reveal(progress, 0.72f, 0.96f) * opacity;
if (unity > 0.001f)
{
for (int index = 0; index < forms.Length; index++)
{
Color color = ReaperCombatRegistry.GetPrimaryColor(forms[index]);
Vector2 direction = (rotation + index * MathHelper.TwoPi / forms.Length).ToRotationVector2();
DrawLayeredLine(batch, pixel, focus, focus + direction * MathHelper.Lerp(18f, 115f, 1f - converge),
color, Color.White, 12f, 2.1f, unity * 0.72f);
}
DrawRing(batch, pixel, focus, MathHelper.Lerp(152f, 24f, Ease(unity)), 64,
Color.White * (0.88f * unity), 5f, rotation, 1f);
DrawRing(batch, pixel, focus, MathHelper.Lerp(104f, 15f, Ease(unity)), 54,
new Color(255, 58, 105) * (0.72f * unity), 2.5f, -rotation, 1f);
DrawGlow(batch, focus, 210f, Color.White * (0.20f * unity));
}
float finalCut = Reveal(progress, 0.86f, 0.985f);
float reach = Math.Max(viewport.X, viewport.Y) * 0.80f;
Vector2 cutDirection = new Vector2(1f, -0.16f).SafeNormalize(Vector2.UnitX);
DrawJaggedLine(batch, pixel, focus - cutDirection * reach, focus + cutDirection * reach,
finalCut, new Color(225, 30, 75), Color.White, 34f, 6f, 5f, 667f, opacity);
float requiem = Envelope(progress, 0.82f, 0.90f, 0.99f, 1f) * opacity;
if (requiem > 0.001f)
{
float scale = MathHelper.Clamp(Math.Min(viewport.X, viewport.Y) * 0.58f, 370f, 690f);
DrawReaperSilhouette(batch, pixel, focus - Vector2.UnitY * scale * 0.23f,
scale, new Color(3, 0, 4), new Color(210, 18, 54), requiem * 0.88f);
for (int band = 0; band < forms.Length; band++)
{
Color color = ReaperCombatRegistry.GetPrimaryColor(forms[band]);
DrawTaperedCrescent(batch, pixel, focus, scale * (0.94f - band * 0.032f),
-2.74f + band * 0.018f, 4.62f, requiem * (0.20f + band * 0.035f),
new Color(8, 0, 10), Color.Lerp(new Color(120, 0, 28), color, 0.42f),
color, scale * (0.16f - band * 0.012f));
}
DrawTaperedCrescent(batch, pixel, focus, scale, -2.76f, 4.64f, requiem,
new Color(8, 0, 10), new Color(224, 18, 56), Color.White, scale * 0.10f);
}
}
private static void DrawDeathGate(
SpriteBatch batch,
Texture2D pixel,
Vector2 center,
int gate,
float rotation,
float opacity,
float converge,
float time,
Color color,
Color core)
{
float size = MathHelper.Lerp(42f, 23f, converge);
switch (gate)
{
case 0: // Ribbed bone guillotine gate.
{
Vector2 topLeft = center + new Vector2(-size * 0.68f, -size);
Vector2 topRight = center + new Vector2(size * 0.68f, -size);
Vector2 bottomLeft = center + new Vector2(-size * 0.68f, size);
Vector2 bottomRight = center + new Vector2(size * 0.68f, size);
DrawLayeredLine(batch, pixel, topLeft, bottomLeft, new Color(6, 28, 35), color,
8f, 2f, opacity);
DrawLayeredLine(batch, pixel, topRight, bottomRight, new Color(6, 28, 35), color,
8f, 2f, opacity);
DrawJaggedLine(batch, pixel, topLeft, topRight, 1f, color, core,
9f, 2.2f, 2f, 90f, opacity);
for (int rib = 0; rib < 3; rib++)
{
float y = MathHelper.Lerp(-size * 0.52f, size * 0.58f, rib / 2f);
DrawLayeredLine(batch, pixel, center + new Vector2(-size * 0.66f, y),
center + new Vector2(-size * 0.16f, y + size * 0.13f), color, core,
4f, 1f, opacity * 0.78f);
DrawLayeredLine(batch, pixel, center + new Vector2(size * 0.66f, y),
center + new Vector2(size * 0.16f, y + size * 0.13f), color, core,
4f, 1f, opacity * 0.78f);
}
break;
}
case 1: // Blood moon gate and white-rimmed crescent.
DrawGlow(batch, center, size * 1.85f, new Color(126, 0, 24) * (opacity * 0.42f));
DrawRing(batch, pixel, center, size * 0.86f, 32, color * (opacity * 0.82f),
size * 0.18f, rotation, 1f);
DrawTaperedCrescent(batch, pixel, center, size * 0.92f, -2.55f, 4.24f,
opacity, new Color(55, 0, 16), color, Color.White, size * 0.18f);
break;
case 2: // Infernal five-point gate.
DrawRing(batch, pixel, center, size, 36, new Color(52, 8, 0) * opacity,
7f, rotation, 1f);
DrawPentagram(batch, pixel, center, size * 0.88f, rotation + time * 0.05f,
new Color(70, 7, 0), core, opacity);
break;
case 3: // Hexagonal frost mirror and X blades.
DrawPolygon(batch, pixel, center, size, 6, new Color(54, 120, 170) * (opacity * 0.72f),
10f, rotation, 1f);
DrawPolygon(batch, pixel, center, size * 0.86f, 6, Color.White * opacity,
2f, -rotation, 1f);
DrawLayeredLine(batch, pixel, center + new Vector2(-size * 0.66f, -size * 0.66f),
center + new Vector2(size * 0.66f, size * 0.66f), color, core, 5f, 1.2f, opacity);
DrawLayeredLine(batch, pixel, center + new Vector2(-size * 0.66f, size * 0.66f),
center + new Vector2(size * 0.66f, -size * 0.66f), color, Color.White, 5f, 1.2f, opacity);
break;
case 4: // Soft soul portal with a single ferryman shade.
DrawRing(batch, pixel, center, size, 36, color * (opacity * 0.72f), 5f,
rotation + time * 0.1f, 1f);
DrawRing(batch, pixel, center, size * 0.72f, 30, core * (opacity * 0.54f), 2f,
-rotation, 1f);
DrawSoulShade(batch, pixel, center, core, opacity, size / 44f, time);
break;
default: // Offset coordinate door cut out of the scene.
{
Rectangle voidCore = new(
(int)Math.Round(center.X - size * 0.72f),
(int)Math.Round(center.Y - size),
Math.Max(1, (int)Math.Round(size * 1.44f)),
Math.Max(1, (int)Math.Round(size * 2f)));
batch.Draw(pixel, voidCore, new Color(0, 0, 2) * (opacity * 0.94f));
Vector2 topLeft = new(voidCore.Left, voidCore.Top);
Vector2 topRight = new(voidCore.Right, voidCore.Top + size * 0.12f);
Vector2 bottomLeft = new(voidCore.Left, voidCore.Bottom - size * 0.12f);
Vector2 bottomRight = new(voidCore.Right, voidCore.Bottom);
DrawLayeredLine(batch, pixel, topLeft, topRight, color, Color.White, 6f, 1.3f, opacity);
DrawLayeredLine(batch, pixel, bottomLeft, bottomRight, color, core, 6f, 1.3f, opacity);
DrawLayeredLine(batch, pixel, center - Vector2.UnitX * size * 0.90f,
center + Vector2.UnitX * size * 0.90f, Color.Black, core, 10f, 2f, opacity);
break;
}
}
DrawDeathRuneGlyph(batch, pixel, center, gate, size * 0.28f, color, core, opacity * 0.74f);
}
private static void DrawDeathPhaseMotif(
SpriteBatch batch,
Texture2D pixel,
Vector2 focus,
Vector2 viewport,
int phase,
float opacity,
float time)
{
float shortSide = Math.Min(viewport.X, viewport.Y);
float longSide = Math.Max(viewport.X, viewport.Y);
switch (phase)
{
case 0: // Bone guillotine and rib frame.
{
Color bone = new(210, 244, 232);
Color corpseFire = new(58, 214, 228);
float halfWidth = MathHelper.Clamp(viewport.X * 0.17f, 145f, 270f);
float height = MathHelper.Clamp(viewport.Y * 0.52f, 300f, 520f);
Vector2 topLeft = focus + new Vector2(-halfWidth, -height * 0.56f);
Vector2 topRight = focus + new Vector2(halfWidth, -height * 0.56f);
Vector2 bottomLeft = topLeft + Vector2.UnitY * height;
Vector2 bottomRight = topRight + Vector2.UnitY * height;
DrawLayeredLine(batch, pixel, topLeft, bottomLeft, new Color(8, 28, 34), corpseFire,
18f, 3f, opacity);
DrawLayeredLine(batch, pixel, topRight, bottomRight, new Color(8, 28, 34), corpseFire,
18f, 3f, opacity);
DrawJaggedLine(batch, pixel, topLeft, topRight, 1f, corpseFire, bone,
20f, 4f, 5f, 180f, opacity);
for (int rib = 0; rib < 5; rib++)
{
float amount = (rib + 1f) / 6f;
Vector2 left = Vector2.Lerp(topLeft, bottomLeft, amount);
Vector2 right = Vector2.Lerp(topRight, bottomRight, amount);
DrawLayeredLine(batch, pixel, left, Vector2.Lerp(left, focus, 0.56f),
corpseFire, bone, 8f, 2f, opacity * 0.68f);
DrawLayeredLine(batch, pixel, right, Vector2.Lerp(right, focus, 0.56f),
corpseFire, bone, 8f, 2f, opacity * 0.68f);
}
DrawJaggedLine(batch, pixel, topRight + new Vector2(-20f, 30f),
bottomLeft + new Vector2(34f, -20f), 1f, new Color(22, 92, 110), bone,
28f, 5f, 4f, 219f, opacity);
break;
}
case 1: // Thick reference-style blood crescent.
{
float radius = MathHelper.Clamp(shortSide * 0.54f, 330f, 610f);
Vector2 center = focus + new Vector2(-radius * 0.13f, radius * 0.05f);
DrawGlow(batch, focus - Vector2.UnitY * radius * 0.25f, radius * 0.62f,
new Color(115, 0, 22) * (opacity * 0.42f));
DrawTaperedCrescent(batch, pixel, center, radius * 1.025f, -2.50f, 4.36f,
opacity * 0.24f, new Color(35, 0, 10), new Color(155, 5, 34),
new Color(255, 102, 112), radius * 0.22f);
DrawTaperedCrescent(batch, pixel, center, radius, -2.46f, 4.36f,
opacity, new Color(70, 0, 18), new Color(238, 24, 58),
new Color(255, 244, 226), radius * 0.18f);
DrawImpactStar(batch, pixel, focus + new Vector2(radius * 0.20f, radius * 0.43f),
radius * 0.09f, new Color(255, 145, 65), Color.White, opacity);
break;
}
case 2: // Pentagram and black-shell molten tracks.
{
float radius = MathHelper.Clamp(shortSide * 0.30f, 190f, 350f);
float rotation = -MathHelper.PiOver2 + time * 0.06f;
DrawPentagram(batch, pixel, focus, radius, rotation, new Color(43, 4, 0),
new Color(255, 192, 46), opacity);
for (int index = 0; index < 5; index++)
{
Vector2 direction = (rotation + index * MathHelper.TwoPi / 5f).ToRotationVector2();
Vector2 vertex = focus + direction * radius;
DrawLayeredLine(batch, pixel, focus, vertex + direction * radius * 0.55f,
new Color(12, 3, 0), new Color(255, 92, 8), 24f, 5f, opacity);
DrawGlow(batch, vertex, 52f, new Color(255, 150, 18) * (opacity * 0.30f));
}
break;
}
case 3: // Four frost mirrors and a crystalline X.
{
float orbit = MathHelper.Clamp(shortSide * 0.24f, 155f, 285f);
for (int index = 0; index < 4; index++)
{
float angle = MathHelper.PiOver4 + index * MathHelper.PiOver2;
Vector2 mirror = focus + angle.ToRotationVector2() * orbit;
DrawPolygon(batch, pixel, mirror, 58f, 6,
new Color(42, 126, 184) * (opacity * 0.66f), 15f, angle, 1f);
DrawPolygon(batch, pixel, mirror, 50f, 6, Color.White * opacity,
2.4f, -angle, 1f);
}
Vector2 diagonalA = new Vector2(1f, 1f).SafeNormalize(Vector2.One);
Vector2 diagonalB = new Vector2(1f, -1f).SafeNormalize(Vector2.UnitX);
DrawLayeredLine(batch, pixel, focus - diagonalA * longSide * 0.52f,
focus + diagonalA * longSide * 0.52f, new Color(45, 130, 190),
Color.White, 26f, 4f, opacity);
DrawLayeredLine(batch, pixel, focus - diagonalB * longSide * 0.52f,
focus + diagonalB * longSide * 0.52f, new Color(28, 91, 160),
new Color(190, 250, 255), 26f, 4f, opacity);
break;
}
case 4: // Winding soul river and ferryman shades.
{
for (int lane = 0; lane < 3; lane++)
{
Vector2 previous = default;
for (int segment = 0; segment <= 20; segment++)
{
float along = segment / 20f;
Vector2 current = new(
MathHelper.Lerp(-50f, viewport.X + 50f, along),
focus.Y + (lane - 1) * 46f
+ (float)Math.Sin(along * MathHelper.TwoPi * 2f + lane * 1.7f - time) * 24f);
if (segment > 0)
{
DrawLayeredLine(batch, pixel, previous, current, new Color(31, 8, 72),
lane == 1 ? new Color(118, 240, 242) : new Color(120, 75, 225),
13f, 2.4f, opacity * 0.78f);
}
previous = current;
}
}
for (int index = 0; index < 7; index++)
{
float angle = index * MathHelper.TwoPi / 7f + time * 0.25f;
Vector2 shade = focus + angle.ToRotationVector2() * (70f + index % 2 * 38f);
DrawSoulShade(batch, pixel, shade, new Color(154, 246, 239), opacity,
0.72f + index % 3 * 0.10f, angle);
}
break;
}
default: // Coordinate grid collapses into a negative-space world split.
{
float splitWidth = MathHelper.Clamp(shortSide * 0.10f, 68f, 128f);
batch.Draw(pixel, new Rectangle(0, (int)Math.Round(focus.Y - splitWidth * 0.5f),
(int)Math.Ceiling(viewport.X), (int)Math.Round(splitWidth)),
new Color(0, 0, 2) * (opacity * 0.96f));
batch.Draw(pixel, new Rectangle((int)Math.Round(focus.X - splitWidth * 0.38f), 0,
(int)Math.Round(splitWidth * 0.76f), (int)Math.Ceiling(viewport.Y)),
new Color(0, 0, 2) * (opacity * 0.96f));
DrawLayeredLine(batch, pixel, new Vector2(0f, focus.Y - splitWidth * 0.5f),
new Vector2(viewport.X, focus.Y - splitWidth * 0.5f), new Color(82, 9, 130),
Color.White, 9f, 2f, opacity);
DrawLayeredLine(batch, pixel, new Vector2(focus.X + splitWidth * 0.38f, 0f),
new Vector2(focus.X + splitWidth * 0.38f, viewport.Y), new Color(55, 4, 100),
new Color(220, 160, 255), 9f, 2f, opacity);
for (int panel = 0; panel < 5; panel++)
{
float y = viewport.Y * (panel + 1f) / 6f;
float offset = (panel % 2 == 0 ? 1f : -1f) * 22f * opacity;
DrawLayeredLine(batch, pixel, new Vector2(offset, y),
new Vector2(viewport.X + offset, y), Color.Black,
new Color(122, 30, 180), 7f, 1.5f, opacity * 0.72f);
}
break;
}
}
}
private static void DrawPentagram(
SpriteBatch batch,
Texture2D pixel,
Vector2 center,
float radius,
float rotation,
Color shell,
Color core,
float opacity)
{
Span<Vector2> vertices = stackalloc Vector2[5];
for (int index = 0; index < vertices.Length; index++)
vertices[index] = center + (rotation + index * MathHelper.TwoPi / 5f).ToRotationVector2() * radius;
for (int edge = 0; edge < vertices.Length; edge++)
{
int start = edge * 2 % 5;
int end = (edge + 1) * 2 % 5;
DrawLayeredLine(batch, pixel, vertices[start], vertices[end], shell, core,
15f, 3f, opacity);
}
DrawRing(batch, pixel, center, radius * 1.04f, 48, shell * (opacity * 0.80f),
7f, rotation, 1f);
}
private static void DrawTaperedCrescent(
SpriteBatch batch,
Texture2D pixel,
Vector2 center,
float radius,
float rotation,
float sweep,
float opacity,
Color shadow,
Color body,
Color edge,
float thickness)
{
opacity = MathHelper.Clamp(opacity, 0f, 1f);
if (opacity <= 0.001f || radius <= 2f || thickness <= 0.5f || Math.Abs(sweep) <= 0.01f)
return;
ReaperUltimateCrescentMaskSet? mask = SelectCrescentMask(Math.Abs(sweep), thickness / radius);
if (mask is null || mask.Body.IsDisposed || mask.Flow.IsDisposed || mask.Edge.IsDisposed)
return;
float scale = radius / CrescentReferenceRadius;
Vector2 textureOrigin = new(CrescentMaskSize * 0.5f);
SpriteEffects effects = sweep < 0f ? SpriteEffects.FlipVertically : SpriteEffects.None;
// Four cached-mask draws replace hundreds of MagicPixel segments. Body,
// longitudinal flow and white-hot rim remain independently tintable.
batch.Draw(mask.Body, center, null, shadow * (opacity * 0.48f), rotation,
textureOrigin, scale * 1.025f, effects, 0f);
batch.Draw(mask.Body, center, null, body * (opacity * 0.84f), rotation,
textureOrigin, scale, effects, 0f);
batch.Draw(mask.Flow, center, null, Color.Lerp(body, edge, 0.34f) * (opacity * 0.48f),
rotation + (float)Math.Sin(Main.GlobalTimeWrappedHourly * 2.4f) * 0.004f,
textureOrigin, scale, effects, 0f);
batch.Draw(mask.Edge, center, null, edge * opacity, rotation,
textureOrigin, scale, effects, 0f);
}
private static ReaperUltimateCrescentMaskSet? SelectCrescentMask(float sweep, float thicknessRatio)
{
ReaperUltimateCrescentMaskSet? best = null;
float bestScore = float.MaxValue;
for (int index = 0; index < crescentMasks.Length; index++)
{
ReaperUltimateCrescentMaskSet candidate = crescentMasks[index];
// Preserve the requested arc silhouette first; thickness selects the
// closer preset when two sweep families are visually comparable.
float score = Math.Abs(candidate.Sweep - sweep) * 2.4f
+ Math.Abs(candidate.ThicknessRatio - thicknessRatio) * 4f;
if (score >= bestScore)
continue;
best = candidate;
bestScore = score;
}
return best;
}
private static void EnsureCrescentMasks()
{
if (crescentMasks.Length > 0)
return;
if (Main.dedServ || Main.graphics?.GraphicsDevice is not GraphicsDevice graphicsDevice)
return;
crescentMasks =
[
CreateCrescentMask(graphicsDevice, 3.58f, 0.115f),
CreateCrescentMask(graphicsDevice, 4.24f, 0.175f),
CreateCrescentMask(graphicsDevice, 4.66f, 0.245f)
];
}
private static ReaperUltimateCrescentMaskSet CreateCrescentMask(
GraphicsDevice graphicsDevice,
float sweep,
float thicknessRatio)
{
Color[] bodyData = new Color[CrescentMaskSize * CrescentMaskSize];
Color[] flowData = new Color[bodyData.Length];
Color[] edgeData = new Color[bodyData.Length];
float center = CrescentMaskSize * 0.5f;
float maximumWidth = CrescentReferenceRadius * thicknessRatio;
for (int y = 0; y < CrescentMaskSize; y++)
{
float dy = y + 0.5f - center;
for (int x = 0; x < CrescentMaskSize; x++)
{
float dx = x + 0.5f - center;
float radial = (float)Math.Sqrt(dx * dx + dy * dy);
if (radial < CrescentReferenceRadius - maximumWidth
|| radial > CrescentReferenceRadius + maximumWidth)
{
continue;
}
float angle = (float)Math.Atan2(dy, dx);
if (angle < 0f)
angle += MathHelper.TwoPi;
if (angle > sweep)
continue;
float along = angle / sweep;
float taper = (float)Math.Pow(Math.Max(0f, Math.Sin(along * MathHelper.Pi)), 0.58f);
taper *= MathHelper.Lerp(0.72f, 1f, 1f - along * 0.42f);
float halfWidth = Math.Max(0.75f, maximumWidth * taper * 0.5f);
float distance = Math.Abs(radial - CrescentReferenceRadius);
float bodyAlpha = MathHelper.Clamp((halfWidth + 1.6f - distance) / 2.8f, 0f, 1f);
if (bodyAlpha <= 0f)
continue;
int dataIndex = y * CrescentMaskSize + x;
bodyData[dataIndex] = PremultipliedMask(bodyAlpha);
float normalizedRadial = (radial - CrescentReferenceRadius) / Math.Max(1f, halfWidth);
float firstFlow = -0.34f + (float)Math.Sin(along * MathHelper.TwoPi * 2.1f) * 0.10f;
float secondFlow = 0.18f + (float)Math.Sin(along * MathHelper.TwoPi * 3.2f + 1.3f) * 0.11f;
float flowAlpha = Math.Max(
1f - Math.Abs(normalizedRadial - firstFlow) / 0.12f,
1f - Math.Abs(normalizedRadial - secondFlow) / 0.09f);
flowAlpha = MathHelper.Clamp(flowAlpha, 0f, 1f) * bodyAlpha * (0.62f + taper * 0.38f);
flowData[dataIndex] = PremultipliedMask(flowAlpha);
float rimRadius = CrescentReferenceRadius + halfWidth * 0.88f;
float rimWidth = Math.Max(1.35f, maximumWidth * 0.055f);
float rimAlpha = MathHelper.Clamp((rimWidth + 1.2f - Math.Abs(radial - rimRadius)) / 2.2f, 0f, 1f);
edgeData[dataIndex] = PremultipliedMask(rimAlpha * bodyAlpha);
}
}
Texture2D bodyTexture = new(graphicsDevice, CrescentMaskSize, CrescentMaskSize);
Texture2D flowTexture = new(graphicsDevice, CrescentMaskSize, CrescentMaskSize);
Texture2D edgeTexture = new(graphicsDevice, CrescentMaskSize, CrescentMaskSize);
bodyTexture.SetData(bodyData);
flowTexture.SetData(flowData);
edgeTexture.SetData(edgeData);
return new ReaperUltimateCrescentMaskSet(sweep, thicknessRatio, bodyTexture, flowTexture, edgeTexture);
}
private static Color PremultipliedMask(float opacity)
{
byte value = (byte)Math.Round(MathHelper.Clamp(opacity, 0f, 1f) * byte.MaxValue);
return new Color(value, value, value, value);
}
private static void DisposeCrescentMasks(ReaperUltimateCrescentMaskSet[] masks)
{
for (int index = 0; index < masks.Length; index++)
masks[index].Dispose();
}
private static void DrawImpactStar(
SpriteBatch batch,
Texture2D pixel,
Vector2 center,
float radius,
Color outer,
Color core,
float opacity)
{
opacity = MathHelper.Clamp(opacity, 0f, 1f);
if (opacity <= 0.001f || radius <= 1f)
return;
DrawGlow(batch, center, radius * 2.4f, outer * (opacity * 0.30f));
for (int ray = 0; ray < 10; ray++)
{
float angle = ray * MathHelper.TwoPi / 10f + (ray % 2) * 0.11f;
float rayLength = radius * (ray % 2 == 0 ? 1f : 0.55f);
Vector2 direction = angle.ToRotationVector2();
DrawLayeredLine(batch, pixel, center - direction * rayLength * 0.12f,
center + direction * rayLength, outer, core, 5f, 1.4f, opacity);
}
}
private static void DrawReaperSilhouette(
SpriteBatch batch,
Texture2D pixel,
Vector2 center,
float scale,
Color shadow,
Color rim,
float opacity)
{
opacity = MathHelper.Clamp(opacity, 0f, 1f);
if (opacity <= 0.001f || scale <= 10f)
return;
float headRadius = scale * 0.11f;
Vector2 head = center - Vector2.UnitY * scale * 0.30f;
DrawGlow(batch, head, headRadius * 2.5f, rim * (opacity * 0.13f));
DrawPolygon(batch, pixel, head, headRadius, 9, shadow * (opacity * 0.96f),
headRadius * 0.82f, -MathHelper.PiOver2, 1f);
DrawRing(batch, pixel, head, headRadius * 1.04f, 28, rim * (opacity * 0.56f), 3f, 0f, 1f);
Vector2 leftShoulder = center + new Vector2(-scale * 0.25f, -scale * 0.17f);
Vector2 rightShoulder = center + new Vector2(scale * 0.25f, -scale * 0.17f);
Vector2 hem = center + Vector2.UnitY * scale * 0.42f;
DrawLayeredLine(batch, pixel, leftShoulder, hem, shadow, rim, scale * 0.22f, 3f, opacity * 0.62f);
DrawLayeredLine(batch, pixel, rightShoulder, hem, shadow, rim, scale * 0.22f, 3f, opacity * 0.62f);
DrawLayeredLine(batch, pixel, leftShoulder, rightShoulder, shadow, rim, scale * 0.18f, 3f, opacity * 0.58f);
Vector2 staffStart = center + new Vector2(-scale * 0.30f, scale * 0.36f);
Vector2 staffEnd = center + new Vector2(scale * 0.26f, -scale * 0.48f);
DrawLayeredLine(batch, pixel, staffStart, staffEnd, shadow, rim, 15f, 3.2f, opacity * 0.82f);
DrawTaperedCrescent(batch, pixel, staffEnd + new Vector2(scale * 0.10f, scale * 0.03f),
scale * 0.25f, -2.7f, 3.5f, opacity * 0.88f, shadow, rim, Color.White, scale * 0.045f);
}
private static void DrawTombstone(
SpriteBatch batch,
Texture2D pixel,
Vector2 bottomCenter,
float width,
float height,
Color fill,
Color outline)
{
Vector2 top = bottomCenter - Vector2.UnitY * height;
Rectangle body = new(
(int)Math.Round(bottomCenter.X - width * 0.5f),
(int)Math.Round(top.Y + width * 0.26f),
Math.Max(1, (int)Math.Round(width)),
Math.Max(1, (int)Math.Round(height - width * 0.26f)));
batch.Draw(pixel, body, fill);
DrawRing(batch, pixel, new Vector2(bottomCenter.X, body.Top), width * 0.5f, 16,
fill, width * 0.48f, MathHelper.Pi, 0.5f);
DrawLayeredLine(batch, pixel, new Vector2(body.Left, body.Bottom), new Vector2(body.Right, body.Bottom),
outline, Color.White, 4f, 1f, 0.78f);
DrawLayeredLine(batch, pixel, new Vector2(body.Left, body.Top), new Vector2(body.Left, body.Bottom),
outline, Color.White, 3f, 0.8f, 0.64f);
DrawLayeredLine(batch, pixel, new Vector2(body.Right, body.Top), new Vector2(body.Right, body.Bottom),
outline, Color.White, 3f, 0.8f, 0.64f);
Vector2 crossCenter = bottomCenter - Vector2.UnitY * height * 0.48f;
DrawSegment(batch, pixel, crossCenter - Vector2.UnitY * 12f, crossCenter + Vector2.UnitY * 13f,
outline, 2.2f);
DrawSegment(batch, pixel, crossCenter - Vector2.UnitX * 8f, crossCenter + Vector2.UnitX * 8f,
outline, 2.2f);
}
private static void DrawSoulShade(
SpriteBatch batch,
Texture2D pixel,
Vector2 position,
Color color,
float opacity,
float scale,
float rotation)
{
DrawGlow(batch, position, 38f * scale, color * (0.20f * opacity));
Texture2D? texture = soulTexture?.IsLoaded == true ? soulTexture.Value : null;
if (texture is not null)
{
float textureScale = 46f / Math.Max(texture.Width, texture.Height) * scale;
batch.Draw(texture, position, null, color * (0.74f * opacity), rotation * 0.04f,
new Vector2(texture.Width, texture.Height) * 0.5f, textureScale, SpriteEffects.None, 0f);
}
else
{
DrawPolygon(batch, pixel, position, 14f * scale, 4, color * opacity, 2f,
MathHelper.PiOver4, 1f);
}
Vector2 tail = position + new Vector2((float)Math.Sin(rotation) * 7f, 20f * scale);
DrawLayeredLine(batch, pixel, position, tail, color, Color.White, 5f, 1.1f, opacity * 0.56f);
}
private static void DrawDeathRuneGlyph(
SpriteBatch batch,
Texture2D pixel,
Vector2 center,
int glyph,
float size,
Color color,
Color core,
float opacity)
{
switch (glyph)
{
case 0: // Bone cross.
DrawLayeredLine(batch, pixel, center - Vector2.UnitY * size, center + Vector2.UnitY * size,
color, core, 4f, 1.2f, opacity);
DrawLayeredLine(batch, pixel, center - Vector2.UnitX * size * 0.65f, center + Vector2.UnitX * size * 0.65f,
color, core, 4f, 1.2f, opacity);
break;
case 1: // Blood fang.
DrawLayeredLine(batch, pixel, center + new Vector2(-size, -size * 0.7f), center + new Vector2(0f, size),
color, core, 4f, 1.2f, opacity);
DrawLayeredLine(batch, pixel, center + new Vector2(size, -size * 0.7f), center + new Vector2(0f, size),
color, core, 4f, 1.2f, opacity);
break;
case 2: // Infernal triangle.
DrawRuneTriangle(batch, pixel, center, size, -MathHelper.PiOver2, core, opacity);
break;
case 3: // Frost crystal.
for (int index = 0; index < 3; index++)
{
Vector2 direction = (index * MathHelper.Pi / 3f).ToRotationVector2() * size;
DrawLayeredLine(batch, pixel, center - direction, center + direction,
color, core, 3.5f, 1f, opacity);
}
break;
case 4: // Soul diamond.
DrawPolygon(batch, pixel, center, size, 4, core * opacity, 3f, MathHelper.PiOver4, 1f);
DrawSegment(batch, pixel, center - Vector2.UnitY * size * 0.55f,
center + Vector2.UnitY * size * 0.55f, color * opacity, 2f);
break;
default: // Void severance.
DrawLayeredLine(batch, pixel, center + new Vector2(-size, size), center + new Vector2(size, -size),
color, Color.White, 5f, 1.3f, opacity);
DrawLayeredLine(batch, pixel, center + new Vector2(-size * 0.55f, -size), center + new Vector2(size * 0.55f, size),
color, core, 3f, 0.9f, opacity * 0.75f);
break;
}
}
private static void DrawRuneTriangle(
SpriteBatch batch,
Texture2D pixel,
Vector2 center,
float radius,
float rotation,
Color color,
float opacity)
{
DrawPolygon(batch, pixel, center, radius, 3, color * opacity, 2.2f, rotation, 1f);
}
private static void DrawGlow(SpriteBatch batch, Vector2 center, float radius, Color color)
{
if (radius <= 0.5f || color.A == 0)
return;
DeathDomainPrimitiveTextureSystem.DrawRadialGradient(batch, center, radius, color);
}
private static void DrawRing(
SpriteBatch batch,
Texture2D pixel,
Vector2 center,
float radius,
int segments,
Color color,
float width,
float rotation,
float reveal)
{
if (radius <= 0.5f || segments < 3 || width <= 0.05f || reveal <= 0.001f || color.A == 0)
return;
reveal = MathHelper.Clamp(reveal, 0f, 1f);
segments = Math.Max(segments,
Math.Min(192, (int)Math.Ceiling(MathHelper.TwoPi * radius / 18f)));
Vector2 previous = center + rotation.ToRotationVector2() * radius;
int visibleSegments = Math.Max(1, (int)Math.Ceiling(segments * reveal));
for (int index = 1; index <= visibleSegments; index++)
{
float amount = Math.Min(reveal, index / (float)segments);
Vector2 current = center + (rotation + MathHelper.TwoPi * amount).ToRotationVector2() * radius;
DrawSegment(batch, pixel, previous, current, color, width);
previous = current;
}
}
private static void DrawPolygon(
SpriteBatch batch,
Texture2D pixel,
Vector2 center,
float radius,
int sides,
Color color,
float width,
float rotation,
float reveal)
{
if (sides < 3 || radius <= 0.5f || reveal <= 0.001f)
return;
reveal = MathHelper.Clamp(reveal, 0f, 1f);
Vector2 previous = center + rotation.ToRotationVector2() * radius;
int visibleEdges = Math.Max(1, (int)Math.Ceiling(sides * reveal));
for (int edge = 1; edge <= visibleEdges; edge++)
{
float edgeCompletion = Math.Min(1f, sides * reveal - (edge - 1));
Vector2 target = center + (rotation + MathHelper.TwoPi * edge / sides).ToRotationVector2() * radius;
Vector2 current = Vector2.Lerp(previous, target, edgeCompletion);
DrawSegment(batch, pixel, previous, current, color, width);
previous = target;
}
}
private static void DrawJaggedLine(
SpriteBatch batch,
Texture2D pixel,
Vector2 start,
Vector2 end,
float reveal,
Color outerColor,
Color coreColor,
float outerWidth,
float coreWidth,
float amplitude,
float seed,
float opacity)
{
reveal = MathHelper.Clamp(reveal, 0f, 1f);
if (reveal <= 0.001f || opacity <= 0.001f)
return;
const int segments = 18;
float totalLength = Vector2.Distance(start, end);
int segmentCount = Math.Max(3, Math.Min(segments, (int)(totalLength / 38f) + 3));
Vector2 direction = (end - start).SafeNormalize(Vector2.UnitX);
Vector2 perpendicular = new(-direction.Y, direction.X);
Vector2 previous = JaggedPoint(start, end, perpendicular, 0f, segmentCount, amplitude, seed);
int visibleSegments = Math.Max(1, (int)Math.Ceiling(segmentCount * reveal));
for (int index = 1; index <= visibleSegments; index++)
{
float amount = Math.Min(reveal, index / (float)segmentCount);
Vector2 current = JaggedPoint(start, end, perpendicular, amount, segmentCount, amplitude, seed);
DrawLayeredLine(batch, pixel, previous, current, outerColor, coreColor,
outerWidth, coreWidth, opacity);
previous = current;
}
}
private static Vector2 JaggedPoint(
Vector2 start,
Vector2 end,
Vector2 perpendicular,
float amount,
int segments,
float amplitude,
float seed)
{
float envelope = (float)Math.Sin(amount * MathHelper.Pi);
float grain = (float)Math.Sin((amount * segments + seed) * 5.137f) * 0.67f
+ (float)Math.Sin((amount * segments + seed * 1.91f) * 11.73f) * 0.33f;
return Vector2.Lerp(start, end, amount) + perpendicular * (grain * amplitude * envelope);
}
private static void DrawLayeredLine(
SpriteBatch batch,
Texture2D pixel,
Vector2 start,
Vector2 end,
Color outerColor,
Color coreColor,
float outerWidth,
float coreWidth,
float opacity)
{
if (opacity <= 0.001f)
return;
DrawSegment(batch, pixel, start, end, outerColor * (opacity * 0.72f), outerWidth);
DrawSegment(batch, pixel, start, end, coreColor * opacity, coreWidth);
}
private static void DrawSegment(
SpriteBatch batch,
Texture2D pixel,
Vector2 start,
Vector2 end,
Color color,
float width)
{
DeathDomainPrimitiveTextureSystem.DrawSmoothLine(batch, start, end, color, width);
}
private static float GetStateOpacity(in UltimateVisualState state)
{
float progress = MathHelper.Clamp(state.Timer / (float)Math.Max(1, state.Duration), 0f, 1f);
float introduction = MathHelper.Lerp(0.35f, 1f, Ease(progress / 0.06f));
float tail = 1f - MathHelper.Clamp(state.FramesSinceReport / (float)TailFrames, 0f, 1f);
return introduction * Ease(tail);
}
private static float GetBackdropDarkness(in UltimateVisualState state)
{
if (state.Form == ReaperFormId.Death)
{
float cutProgress = MathHelper.Clamp(
(state.Timer - ReaperDeathUltimateGeometry.FirstCutTick)
/ (float)(ReaperDeathUltimateGeometry.ShatterTick
- ReaperDeathUltimateGeometry.FirstCutTick), 0f, 1f);
return MathHelper.Lerp(0.30f, 0.985f, Ease(cutProgress));
}
return state.Form switch
{
ReaperFormId.Bone => 0.36f,
ReaperFormId.Blood => 0.42f,
ReaperFormId.Infernal => 0.38f,
ReaperFormId.Frost => 0.31f,
ReaperFormId.Soul => 0.37f,
ReaperFormId.Void => 0.52f,
_ => 0f
};
}
private static float Reveal(float progress, float start, float end)
{
if (end <= start)
return progress >= end ? 1f : 0f;
return Ease((progress - start) / (end - start));
}
private static float Envelope(float progress, float start, float riseEnd, float fallStart, float end)
{
if (progress < start || progress > end)
return 0f;
if (progress < riseEnd)
return Reveal(progress, start, riseEnd);
if (progress <= fallStart)
return 1f;
return 1f - Reveal(progress, fallStart, end);
}
private static float Pulse(float progress, float center, float halfWidth)
{
if (halfWidth <= 0f)
return 0f;
return Ease(1f - Math.Abs(progress - center) / halfWidth);
}
private static float Ease(float value)
{
value = MathHelper.Clamp(value, 0f, 1f);
return value * value * (3f - 2f * value);
}
private static float PositiveModulo(float value, float modulus)
{
float result = value % modulus;
return result < 0f ? result + modulus : result;
}
private static void EnsureStateStorage()
{
if (states.Length != Main.maxPlayers)
states = new UltimateVisualState[Main.maxPlayers];
}
private static void ClearStates()
{
if (states.Length > 0)
Array.Clear(states, 0, states.Length);
}
private struct UltimateVisualState
{
public bool Active;
public ReaperFormId Form;
public int ActionId;
public Vector2 OriginWorld;
public Vector2 FocusWorld;
public Vector2 Aim;
public int Timer;
public int Duration;
public int FramesSinceReport;
public ulong LastReportTick;
}
}
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using ReLogic.Content;
using System;
using System.Collections.Generic;
using Terraria;
using Terraria.GameContent;
using Terraria.ModLoader;
using Terraria.UI;
namespace SoulHarvest.Common;
/// <summary>
/// Client-only presentation for Reaper ultimates. Combat code reports the authoritative
/// visual clock each tick; this system owns only short-lived draw state and asks the
/// shared VFX director for capped camera/flash punctuation. It never creates attacks.
/// </summary>
[Autoload(Side = ModSide.Client)]
public sealed class ReaperUltimateVisualSystem : ModSystem
{
private const int TailFrames = 18;
private const int CrescentMaskSize = 512;
private const float CrescentReferenceRadius = 180f;
private static UltimateVisualState[] states = [];
private static Asset<Texture2D>? soulTexture;
private static ReaperUltimateCrescentMaskSet[] crescentMasks = [];
private static readonly float[] deathMirrorAngles =
new float[ReaperDeathUltimateGeometry.CutCount * 2];
private static readonly VertexPositionColorTexture[] deathMirrorVertices =
new VertexPositionColorTexture[ReaperDeathUltimateGeometry.CutCount * 6];
private static BasicEffect? deathMirrorEffect;
/// <summary>
/// Reports one frame of an active ultimate. This method is safe to call from
/// projectile AI on every net mode: dedicated servers immediately return.
/// </summary>
public static void ReportUltimate(Player owner, ReaperFormId form, Vector2 aim,
Vector2 focusWorld, int actionId, int timer, int duration)
{
if (Main.dedServ
|| Main.gameMenu
|| owner is null
|| !owner.active
|| owner.dead
|| owner.whoAmI < 0
|| owner.whoAmI >= Main.maxPlayers
|| form == ReaperFormId.Base
|| (byte)form > (byte)ReaperFormId.Death)
{
return;
}
EnsureStateStorage();
Vector2 fallback = Vector2.UnitX * (owner.direction == 0 ? 1 : owner.direction);
if (!float.IsFinite(aim.X) || !float.IsFinite(aim.Y) || aim.LengthSquared() < 0.0001f)
aim = fallback;
else
aim.Normalize();
if (!float.IsFinite(focusWorld.X) || !float.IsFinite(focusWorld.Y))
focusWorld = owner.MountedCenter + aim * 180f;
duration = Math.Clamp(duration, 1, 3600);
timer = Math.Clamp(timer, 0, duration);
ref UltimateVisualState state = ref states[owner.whoAmI];
bool restarted = !state.Active || state.Form != form || state.ActionId != actionId;
// A remote controller can first replicate halfway through its action.
// Floor a newly observed storyboard at that authoritative frame so all
// earlier impacts are not replayed at once. Tick-one openings still cross
// normally for actions observed from their actual start.
int previousTimer = restarted
? (timer <= 1 ? timer - 1 : timer)
: state.Timer;
if (!restarted)
{
// Extra-AI packets can arrive after the local controller has already
// advanced. Never rewind a storyboard for the same authoritative
// action, otherwise old packets replay white flashes, sounds and shake.
int monotonicFloor = state.LastReportTick < Main.GameUpdateCount
? Math.Min(duration, state.Timer + 1)
: state.Timer;
timer = Math.Max(timer, monotonicFloor);
}
state.Active = true;
state.Form = form;
state.ActionId = actionId;
state.OriginWorld = owner.MountedCenter;
state.FocusWorld = focusWorld;
state.Aim = aim;
state.Timer = timer;
state.Duration = duration;
state.FramesSinceReport = 0;
state.LastReportTick = Main.GameUpdateCount;
float normalizedTime = state.Timer / (float)Math.Max(1, state.Duration);
ReaperVfxDirector.ReportCinematicCamera(owner.whoAmI, state.ActionId,
normalizedTime, aim, form == ReaperFormId.Death ? 1f : 0.84f);
TriggerTimelineImpacts(form, aim, previousTimer, state.Timer);
}
public override void Load()
{
EnsureStateStorage();
soulTexture = ModContent.Request<Texture2D>("SoulHarvest/Items/Soul", AssetRequestMode.ImmediateLoad);
}
public override void Unload()
{
states = [];
soulTexture = null;
// Content hooks may run on a loader worker. Detach immediately so no
// future draw can observe these resources, then dispose them on FNA's
// graphics thread just like the other procedural texture systems.
ReaperUltimateCrescentMaskSet[] oldMasks = crescentMasks;
crescentMasks = [];
BasicEffect? oldMirrorEffect = deathMirrorEffect;
deathMirrorEffect = null;
if (!Main.dedServ && oldMasks.Length > 0)
Main.QueueMainThreadAction(() => DisposeCrescentMasks(oldMasks));
if (!Main.dedServ && oldMirrorEffect is not null)
Main.QueueMainThreadAction(oldMirrorEffect.Dispose);
}
public override void OnWorldUnload()
{
ClearStates();
}
public override void PostUpdateEverything()
{
if (Main.gameMenu)
{
ClearStates();
return;
}
EnsureStateStorage();
for (int index = 0; index < states.Length; index++)
{
ref UltimateVisualState state = ref states[index];
if (!state.Active)
continue;
Player owner = Main.player[index];
if (!owner.active || owner.dead || state.LastReportTick > Main.GameUpdateCount)
{
state = default;
continue;
}
if (state.LastReportTick == Main.GameUpdateCount)
{
state.FramesSinceReport = 0;
continue;
}
state.FramesSinceReport++;
if (state.FramesSinceReport > TailFrames)
state = default;
}
}
public override void ModifyInterfaceLayers(List<GameInterfaceLayer> layers)
{
int index = layers.FindIndex(layer => layer.Name == "Vanilla: Resource Bars");
if (index < 0)
index = layers.FindIndex(layer => layer.Name == "Vanilla: Mouse Text");
if (index < 0)
index = layers.Count;
layers.Insert(index, new LegacyGameInterfaceLayer(
"SoulHarvest: Reaper Ultimate Spectacle",
DrawUltimateLayer,
InterfaceScaleType.None));
}
private static bool DrawUltimateLayer()
{
if (Main.gameMenu || states.Length == 0)
return true;
Player localPlayer = Main.LocalPlayer;
if (!localPlayer.active)
return true;
// This layer renders in physical screen space. Applying UI scale here
// confined the composition to the upper-left on high-DPI displays.
const float uiScale = 1f;
Vector2 viewport = new(Main.screenWidth, Main.screenHeight);
Rectangle viewportBounds = new(0, 0,
Math.Max(1, (int)Math.Ceiling(viewport.X)),
Math.Max(1, (int)Math.Ceiling(viewport.Y)));
Texture2D pixel = TextureAssets.MagicPixel.Value;
SpriteBatch batch = Main.spriteBatch;
float strongestDarkness = 0f;
for (int index = 0; index < states.Length; index++)
{
ref UltimateVisualState state = ref states[index];
if (!IsVisible(index, in state, viewport, uiScale))
continue;
strongestDarkness = Math.Max(strongestDarkness,
GetBackdropDarkness(in state) * GetStateOpacity(in state));
}
if (strongestDarkness <= 0.001f)
return true;
// Interface drawing always runs on the graphics thread. Creating the
// procedural masks lazily here avoids allocating Texture2D instances
// from a content-loader worker and keeps dedicated servers texture-free.
EnsureCrescentMasks();
batch.Draw(pixel, viewportBounds, Color.Black * strongestDarkness);
for (int index = 0; index < states.Length; index++)
{
ref UltimateVisualState state = ref states[index];
if (!IsVisible(index, in state, viewport, uiScale))
continue;
DrawState(batch, pixel, index, in state, viewport, viewportBounds, uiScale);
}
return true;
}
private static bool IsVisible(int playerIndex, in UltimateVisualState state, Vector2 viewport, float uiScale)
{
if (!state.Active || playerIndex < 0 || playerIndex >= Main.maxPlayers)
return false;
Player owner = Main.player[playerIndex];
if (!owner.active || owner.dead)
return false;
// Every client which has the authoritative action projectile renders the full
// storyboard. Off-screen owners are re-framed in DrawState instead of receiving
// the old reduced/culled presentation.
return true;
}
private static void DrawState(
SpriteBatch batch,
Texture2D pixel,
int ownerIndex,
in UltimateVisualState state,
Vector2 viewport,
Rectangle viewportBounds,
float uiScale)
{
float progress = MathHelper.Clamp(state.Timer / (float)Math.Max(1, state.Duration), 0f, 1f);
float opacity = GetStateOpacity(in state);
if (opacity <= 0.001f)
return;
Vector2 origin = state.OriginWorld - Main.screenPosition;
const float presentationMargin = 120f;
if (origin.X < -presentationMargin || origin.X > viewport.X + presentationMargin
|| origin.Y < -presentationMargin || origin.Y > viewport.Y + presentationMargin)
{
origin = viewport * 0.5f - state.Aim * 180f;
}
Vector2 focus = state.FocusWorld - Main.screenPosition;
if (state.Form != ReaperFormId.Death
&& (focus.X < -presentationMargin
|| focus.X > viewport.X + presentationMargin
|| focus.Y < -presentationMargin
|| focus.Y > viewport.Y + presentationMargin))
{
focus = viewport * 0.5f;
}
Color primary = ReaperCombatRegistry.GetPrimaryColor(state.Form);
Color secondary = ReaperCombatRegistry.GetSecondaryColor(state.Form);
float time = Main.GlobalTimeWrappedHourly;
batch.Draw(pixel, viewportBounds, primary * (0.028f * opacity));
DrawGlow(batch, focus, Math.Min(viewport.X, viewport.Y) * 0.33f,
primary * (0.105f * opacity));
DrawCinematicFrame(batch, pixel, viewport, progress, opacity, primary, secondary);
switch (state.Form)
{
case ReaperFormId.Bone:
DrawBone(batch, pixel, focus, viewport, progress, opacity, time, primary, secondary);
break;
case ReaperFormId.Blood:
DrawBlood(batch, pixel, focus, viewport, progress, opacity, time, primary, secondary);
break;
case ReaperFormId.Infernal:
DrawInfernal(batch, pixel, focus, viewport, progress, opacity, time, primary, secondary);
break;
case ReaperFormId.Frost:
DrawFrost(batch, pixel, focus, viewport, viewportBounds, progress, opacity, time, primary, secondary);
break;
case ReaperFormId.Soul:
DrawSoul(batch, pixel, focus, viewport, progress, opacity, time, primary, secondary);
break;
case ReaperFormId.Void:
DrawVoid(batch, pixel, focus, viewport, viewportBounds, progress, opacity,
time, primary, secondary, state.Aim, state.ActionId);
break;
case ReaperFormId.Death:
DrawDeath(batch, pixel, ownerIndex, in state, focus, viewport,
viewportBounds, progress, opacity, time);
break;
}
}
private static void TriggerTimelineImpacts(
ReaperFormId form,
Vector2 direction,
int previousTimer,
int currentTimer)
{
if (currentTimer <= previousTimer)
return;
Color primary = ReaperCombatRegistry.GetPrimaryColor(form);
Color secondary = ReaperCombatRegistry.GetSecondaryColor(form);
switch (form)
{
case ReaperFormId.Bone:
TriggerImpactAt(30, previousTimer, currentTimer, direction, 3.4f, primary, 0.10f);
TriggerImpactAt(66, previousTimer, currentTimer, -direction, 4.0f, primary, 0.12f);
TriggerImpactAt(102, previousTimer, currentTimer, direction.RotatedBy(0.8f), 4.8f, secondary, 0.15f);
TriggerImpactAt(138, previousTimer, currentTimer, direction.RotatedBy(-0.8f), 5.6f, secondary, 0.18f);
TriggerImpactAt(168, previousTimer, currentTimer, Vector2.UnitY, 7.8f, secondary, 0.34f);
TriggerImpactAt(207, previousTimer, currentTimer, direction, 11.5f, Color.White, 0.68f);
break;
case ReaperFormId.Blood:
TriggerImpactAt(18, previousTimer, currentTimer, Vector2.UnitY,
4.2f, new Color(170, 0, 34), 0.16f);
foreach (int tick in new[] { 48, 78, 108, 138 })
TriggerImpactAt(tick, previousTimer, currentTimer,
direction.RotatedBy(tick * 0.017f), 4.5f,
tick % 2 == 0 ? primary : secondary, 0.13f);
TriggerImpactAt(166, previousTimer, currentTimer, direction,
12f, new Color(255, 238, 228), 0.75f);
break;
case ReaperFormId.Infernal:
for (int index = 0; index < 5; index++)
TriggerImpactAt(20 + index * 20, previousTimer, currentTimer,
direction.RotatedBy(index * MathHelper.TwoPi / 5f), 4.8f + index * 0.3f,
index % 2 == 0 ? primary : secondary, 0.16f);
TriggerImpactAt(130, previousTimer, currentTimer, direction, 8f, secondary, 0.36f);
TriggerImpactAt(190, previousTimer, currentTimer, Vector2.UnitY, 12f, Color.White, 0.72f);
break;
case ReaperFormId.Frost:
TriggerImpactAt(1, previousTimer, currentTimer, Vector2.UnitY, 2.6f, new Color(180, 230, 255), 0.12f);
for (int index = 0; index < 4; index++)
TriggerImpactAt(30 + index * 18, previousTimer, currentTimer,
direction.RotatedBy(index * MathHelper.PiOver2), 4.2f + index * 0.35f,
index % 2 == 0 ? primary : secondary, 0.17f);
TriggerImpactAt(100, previousTimer, currentTimer, direction.RotatedBy(-0.5f), 8.4f, secondary, 0.43f);
TriggerImpactAt(110, previousTimer, currentTimer, direction.RotatedBy(0.5f), 11.2f, Color.White, 0.76f);
break;
case ReaperFormId.Soul:
TriggerImpactAt(20, previousTimer, currentTimer, Vector2.UnitY, 3.3f, primary, 0.13f);
for (int index = 0; index < 8; index++)
TriggerImpactAt(32 + index * 8, previousTimer, currentTimer,
direction.RotatedBy(index * MathHelper.PiOver4), 3.4f + index * 0.22f,
index % 2 == 0 ? primary : secondary, 0.11f);
TriggerImpactAt(104, previousTimer, currentTimer, direction, 11f, Color.White, 0.66f);
break;
case ReaperFormId.Void:
TriggerImpactAt(32, previousTimer, currentTimer, direction, 11f, Color.White, 0.66f);
break;
case ReaperFormId.Death:
for (int index = 0; index < ReaperDeathUltimateGeometry.CutCount; index++)
TriggerImpactAt(ReaperDeathUltimateGeometry.GetCutTick(index),
previousTimer, currentTimer,
direction.RotatedBy(index * 2.39996f), 4.6f + index * 0.08f,
new Color(225, 25, 66), 0.14f);
TriggerImpactAt(ReaperDeathUltimateGeometry.ShatterTick,
previousTimer, currentTimer, direction,
12f, new Color(190, 14, 48), 0.14f);
break;
}
}
private static void TriggerImpactAt(
int eventTick,
int previousTimer,
int currentTimer,
Vector2 direction,
float strength,
Color color,
float opacity)
{
if (previousTimer >= eventTick || currentTimer < eventTick)
return;
int frames = strength >= 10f ? 14 : strength >= 7f ? 10 : 7;
ReaperVfxDirector.TriggerGlobalImpact(direction, strength, frames, color, opacity,
strength >= 10f ? 7 : 4, strength >= 10f ? 0.50f : 0.24f);
}
private static void DrawCinematicFrame(
SpriteBatch batch,
Texture2D pixel,
Vector2 viewport,
float progress,
float opacity,
Color primary,
Color secondary)
{
float ritual = Envelope(progress, 0f, 0.07f, 0.94f, 1f) * opacity;
int barHeight = Math.Max(8, (int)Math.Round(viewport.Y * 0.035f));
batch.Draw(pixel, new Rectangle(0, 0, (int)Math.Ceiling(viewport.X), barHeight),
Color.Black * (ritual * 0.76f));
batch.Draw(pixel, new Rectangle(0, Math.Max(0, (int)Math.Floor(viewport.Y) - barHeight),
(int)Math.Ceiling(viewport.X), barHeight), Color.Black * (ritual * 0.76f));
// Deliberately omit ornamental corner brackets. The restrained full-width
// letterbox reads as one cinematic composition and cannot resemble a
// misplaced child viewport.
// Final impacts are expressed by world geometry, sound and camera
// response. A full-viewport white heartbeat obscured those details and
// became a one-frame white screen when several attacks overlapped.
}
private static void DrawBone(
SpriteBatch batch,
Texture2D pixel,
Vector2 focus,
Vector2 viewport,
float progress,
float opacity,
float time,
Color primary,
Color secondary)
{
float graveWidth = MathHelper.Clamp(viewport.X / 11f, 48f, 76f);
for (int index = 0; index < 8; index++)
{
float reveal = Reveal(progress, 0.05f + index * 0.052f, 0.18f + index * 0.052f) * opacity;
if (reveal <= 0.001f)
continue;
float horizontal = (index - 3.5f) * graveWidth;
float restingY = focus.Y + 112f + Math.Abs(index - 3.5f) * 8f;
Vector2 position = new(focus.X + horizontal, restingY + (1f - Ease(reveal / Math.Max(opacity, 0.001f))) * 54f);
float pulse = 0.82f + (float)Math.Sin(time * 2.7f + index * 1.3f) * 0.12f;
DrawTombstone(batch, pixel, position, 36f, 56f,
new Color(7, 20, 27) * (0.90f * reveal),
Color.Lerp(primary, secondary, 0.38f) * (pulse * reveal));
}
float frameReveal = Reveal(progress, 0.58f, 0.78f) * opacity;
if (frameReveal > 0.001f)
{
float halfWidth = MathHelper.Clamp(viewport.X * 0.16f, 150f, 260f);
float height = MathHelper.Clamp(viewport.Y * 0.46f, 260f, 430f);
Vector2 topLeft = focus + new Vector2(-halfWidth, -height * 0.63f);
Vector2 topRight = focus + new Vector2(halfWidth, -height * 0.63f);
Vector2 bottomLeft = topLeft + Vector2.UnitY * height;
Vector2 bottomRight = topRight + Vector2.UnitY * height;
DrawLayeredLine(batch, pixel, topLeft, bottomLeft, primary, secondary, 10f, 2.4f, frameReveal * 0.62f);
DrawLayeredLine(batch, pixel, topRight, bottomRight, primary, secondary, 10f, 2.4f, frameReveal * 0.62f);
DrawLayeredLine(batch, pixel, topLeft, topRight, primary, secondary, 12f, 2.8f, frameReveal * 0.78f);
float bladeReveal = Reveal(progress, 0.75f, 0.94f);
Vector2 bladeStart = topRight + new Vector2(-20f, 30f);
Vector2 bladeEnd = bottomLeft + new Vector2(38f, -24f);
DrawJaggedLine(batch, pixel, bladeStart, bladeEnd, bladeReveal,
primary, Color.White, 22f, 5.2f, 3.5f, 7.8f, opacity);
Vector2 parallel = (bladeEnd - bladeStart).SafeNormalize(Vector2.UnitY).RotatedBy(MathHelper.PiOver2) * 22f;
DrawJaggedLine(batch, pixel, bladeStart + parallel, bladeEnd + parallel, bladeReveal,
new Color(25, 100, 120), secondary, 12f, 2.5f, 2.4f, 11.2f, opacity * 0.78f);
float impact = Pulse(progress, 0.965f, 0.07f) * opacity;
DrawGlow(batch, bladeEnd, 125f, secondary * (0.30f * impact));
DrawRing(batch, pixel, bladeEnd, MathHelper.Lerp(18f, 128f, Ease(impact)), 36,
secondary * (impact * 0.9f), 3f, 0f, 1f);
}
float burial = Envelope(progress, 0.18f, 0.30f, 0.72f, 0.88f) * opacity;
if (burial > 0.001f)
{
float cageRadius = MathHelper.Clamp(Math.Min(viewport.X, viewport.Y) * 0.21f, 130f, 235f);
for (int side = -1; side <= 1; side += 2)
{
for (int rib = 0; rib < 6; rib++)
{
float y = MathHelper.Lerp(-cageRadius * 0.68f, cageRadius * 0.65f, rib / 5f);
float width = cageRadius * (0.92f - Math.Abs(rib - 2.5f) * 0.09f);
Vector2 spine = focus + new Vector2(side * cageRadius * 0.88f, y);
Vector2 sternum = focus + new Vector2(side * width * 0.12f, y + 13f);
Vector2 elbow = Vector2.Lerp(spine, sternum, 0.52f) + new Vector2(side * 24f, -20f);
DrawLayeredLine(batch, pixel, spine, elbow, new Color(20, 95, 112), secondary,
9f, 2.3f, burial * 0.48f);
DrawLayeredLine(batch, pixel, elbow, sternum, primary, Color.White,
7f, 1.8f, burial * 0.58f);
}
}
}
float judge = Envelope(progress, 0.70f, 0.80f, 0.975f, 1f) * opacity;
if (judge > 0.001f)
{
Vector2 judgeCenter = focus - Vector2.UnitY * MathHelper.Clamp(viewport.Y * 0.21f, 120f, 210f);
float skullRadius = MathHelper.Clamp(viewport.Y * 0.095f, 52f, 94f);
DrawGlow(batch, judgeCenter, skullRadius * 2.4f, primary * (judge * 0.16f));
DrawPolygon(batch, pixel, judgeCenter, skullRadius, 8, new Color(8, 25, 30) * (judge * 0.94f),
skullRadius * 0.75f, -MathHelper.PiOver2, 1f);
DrawRing(batch, pixel, judgeCenter, skullRadius, 30, secondary * (judge * 0.84f), 4f, 0f, 1f);
Vector2 eyeOffset = new(skullRadius * 0.33f, -skullRadius * 0.08f);
DrawGlow(batch, judgeCenter - eyeOffset, skullRadius * 0.34f, primary * (judge * 0.72f));
DrawGlow(batch, judgeCenter + new Vector2(eyeOffset.X, eyeOffset.Y), skullRadius * 0.34f,
primary * (judge * 0.72f));
for (int tooth = -3; tooth <= 3; tooth++)
{
Vector2 root = judgeCenter + new Vector2(tooth * skullRadius * 0.16f, skullRadius * 0.62f);
DrawLayeredLine(batch, pixel, root, root + Vector2.UnitY * skullRadius * 0.34f,
primary, Color.White, 5f, 1.3f, judge * 0.72f);
}
}
}
private static void DrawBlood(
SpriteBatch batch,
Texture2D pixel,
Vector2 focus,
Vector2 viewport,
float progress,
float opacity,
float time,
Color primary,
Color secondary)
{
// The shared Blood blade projectiles draw their own exact flight paths and
// persistent wounds. This layer supplies only restrained atmosphere so it
// cannot invent a second set of fake, screen-space projectiles or scars.
float opening = Reveal(progress, 0.01f, 0.10f) * opacity;
float pulse = 0.82f + (float)Math.Sin(time * 3.4f) * 0.10f;
DrawGlow(batch, focus, MathHelper.Clamp(Math.Min(viewport.X, viewport.Y) * 0.31f,
170f, 340f), new Color(122, 0, 24) * (opening * pulse * 0.28f));
float closing = Envelope(progress, 0.76f, 0.84f, 0.97f, 1f) * opacity;
DrawGlow(batch, focus, 240f,
new Color(238, 22, 52) * (closing * 0.24f));
}
private static void DrawBloodLegacy(
SpriteBatch batch,
Texture2D pixel,
Vector2 focus,
Vector2 viewport,
float progress,
float opacity,
float time,
Color primary,
Color secondary)
{
float opening = Reveal(progress, 0.01f, 0.10f) * opacity;
DrawGlow(batch, focus, MathHelper.Clamp(Math.Min(viewport.X, viewport.Y) * 0.28f,
150f, 310f), new Color(122, 0, 24) * (opening * 0.26f));
// Six projectiles are rendered in the world. The overlay only reinforces
// their curved approach paths and preserves the wounds they cut, keeping
// the composition readable instead of replacing them with six crescents.
float flightFade = 1f - MathHelper.SmoothStep(0f, 1f,
MathHelper.Clamp((progress - 0.72f) / 0.12f, 0f, 1f));
float orbit = MathHelper.Clamp(Math.Min(viewport.X, viewport.Y) * 0.31f, 190f, 360f);
for (int index = 0; index < 6; index++)
{
float phase = progress * 5.8f + index / 6f;
float angle = index * MathHelper.TwoPi / 6f + phase * MathHelper.TwoPi;
Vector2 tangent = angle.ToRotationVector2();
Vector2 head = focus + tangent * orbit * (0.72f + (float)Math.Sin(phase * 4f) * 0.12f);
Vector2 tail = focus + (angle - 0.54f).ToRotationVector2() * orbit * 0.92f;
DrawLayeredLine(batch, pixel, tail, head, new Color(238, 20, 54),
new Color(8, 0, 3), 20f, 8f, opening * flightFade * 0.68f);
DrawTaperedCrescent(batch, pixel, head, 34f, angle - 2.5f, 4.0f,
opening * flightFade * 0.74f, new Color(58, 0, 14),
new Color(225, 18, 50), new Color(255, 214, 205), 9f);
float scarReveal = Reveal(progress, 0.16f + index * 0.035f,
0.28f + index * 0.035f) * opacity;
float scarAngle = -0.78f + index * 0.31f;
Vector2 scarAxis = scarAngle.ToRotationVector2();
Vector2 scarCenter = focus + (index - 2.5f) * 20f * Vector2.UnitY;
DrawLayeredLine(batch, pixel, scarCenter - scarAxis * orbit * 0.58f,
scarCenter + scarAxis * orbit * 0.58f,
new Color(245, 18, 52), new Color(5, 0, 2),
15f, 8f, scarReveal * flightFade * 0.56f);
}
float finalSlash = Envelope(progress, 0.76f, 0.82f, 0.96f, 1f) * opacity;
if (finalSlash > 0.001f)
{
Vector2 direction = new Vector2(1f, -0.20f).SafeNormalize(Vector2.UnitX);
float reach = (float)Math.Sqrt(viewport.X * viewport.X + viewport.Y * viewport.Y) * 0.72f;
Vector2 start = focus - direction * reach;
Vector2 end = focus + direction * reach;
DrawLayeredLine(batch, pixel, start, end,
new Color(255, 24, 58), new Color(4, 0, 2),
MathHelper.Lerp(18f, 82f, finalSlash),
MathHelper.Lerp(8f, 49f, finalSlash), finalSlash);
DrawGlow(batch, focus, 220f,
new Color(238, 22, 52) * (finalSlash * 0.22f));
}
}
private static void DrawInfernal(
SpriteBatch batch,
Texture2D pixel,
Vector2 focus,
Vector2 viewport,
float progress,
float opacity,
float time,
Color primary,
Color secondary)
{
float radius = MathHelper.Clamp(Math.Min(viewport.X, viewport.Y) * 0.23f, 145f, 265f);
float rotation = -MathHelper.PiOver2 + time * 0.055f;
float circleReveal = Reveal(progress, 0.015f, 0.22f);
DrawRing(batch, pixel, focus, radius * 1.08f, 64, primary * (0.82f * opacity), 4f,
rotation, circleReveal);
DrawRing(batch, pixel, focus, radius * 0.94f, 54, secondary * (0.48f * opacity), 1.8f,
-rotation * 0.7f, circleReveal);
Span<Vector2> vertices = stackalloc Vector2[5];
for (int index = 0; index < vertices.Length; index++)
vertices[index] = focus + (rotation + index * MathHelper.TwoPi / 5f).ToRotationVector2() * radius;
for (int edge = 0; edge < 5; edge++)
{
int startIndex = edge * 2 % 5;
int endIndex = (edge + 1) * 2 % 5;
float reveal = Reveal(progress, 0.08f + edge * 0.085f, 0.24f + edge * 0.085f);
DrawLayeredLine(batch, pixel, vertices[startIndex],
Vector2.Lerp(vertices[startIndex], vertices[endIndex], reveal),
new Color(185, 38, 5), secondary, 13f, 3f, opacity * reveal);
if (reveal > 0.02f)
{
Vector2 flame = vertices[startIndex] + Vector2.UnitY * (10f + (float)Math.Sin(time * 7f + edge) * 5f);
DrawGlow(batch, flame, 38f, primary * (0.25f * opacity * reveal));
DrawRuneTriangle(batch, pixel, vertices[startIndex], 20f, rotation + edge, secondary,
opacity * reveal);
}
}
float ignition = Reveal(progress, 0.62f, 0.82f) * opacity;
for (int index = 0; index < 5; index++)
{
Vector2 direction = (vertices[index] - focus).SafeNormalize(Vector2.UnitY);
Vector2 flameTip = vertices[index] + direction * MathHelper.Lerp(10f, 86f, ignition);
DrawJaggedLine(batch, pixel, vertices[index], flameTip, ignition,
primary, secondary, 9f, 2.2f, 5f, 130f + index * 13f, opacity);
}
float detonation = Pulse(progress, 0.94f, 0.10f) * opacity;
DrawGlow(batch, focus, radius * 1.2f, secondary * (0.25f * detonation));
DrawRing(batch, pixel, focus, MathHelper.Lerp(radius * 0.18f, radius * 1.35f, Ease(detonation)), 72,
Color.White * (0.78f * detonation), 4.5f, rotation, 1f);
float gate = Envelope(progress, 0.58f, 0.72f, 0.98f, 1f) * opacity;
if (gate > 0.001f)
{
float gateWidth = MathHelper.Clamp(viewport.X * 0.24f, 210f, 420f);
float gateHeight = MathHelper.Clamp(viewport.Y * 0.56f, 310f, 590f);
Vector2 topLeft = focus + new Vector2(-gateWidth * 0.5f, -gateHeight * 0.62f);
Vector2 topRight = focus + new Vector2(gateWidth * 0.5f, -gateHeight * 0.62f);
Vector2 bottomLeft = topLeft + Vector2.UnitY * gateHeight;
Vector2 bottomRight = topRight + Vector2.UnitY * gateHeight;
DrawLayeredLine(batch, pixel, topLeft, bottomLeft, new Color(42, 8, 3), primary, 24f, 4f, gate * 0.74f);
DrawLayeredLine(batch, pixel, topRight, bottomRight, new Color(42, 8, 3), primary, 24f, 4f, gate * 0.74f);
DrawTaperedCrescent(batch, pixel, focus - Vector2.UnitY * gateHeight * 0.50f, gateWidth * 0.52f,
MathHelper.Pi, MathHelper.Pi, gate * 0.72f, new Color(30, 4, 0),
new Color(220, 52, 4), secondary, gateWidth * 0.13f);
for (int ember = 0; ember < 9; ember++)
{
float along = ember / 8f;
Vector2 emberPosition = Vector2.Lerp(bottomLeft, bottomRight, along)
- Vector2.UnitY * (22f + (float)Math.Sin(time * 7f + ember) * 18f);
DrawGlow(batch, emberPosition, 28f + ember % 3 * 7f, primary * (gate * 0.24f));
}
}
float execution = Envelope(progress, 0.84f, 0.91f, 0.99f, 1f) * opacity;
if (execution > 0.001f)
{
DrawReaperSilhouette(batch, pixel, focus - Vector2.UnitY * radius * 0.50f,
radius * 1.45f, new Color(18, 3, 0), primary, execution * 0.78f);
DrawTaperedCrescent(batch, pixel, focus, radius * 1.78f, -2.7f, 4.55f, execution,
new Color(58, 7, 0), new Color(255, 75, 5), Color.White, radius * 0.28f);
}
}
private static void DrawFrost(
SpriteBatch batch,
Texture2D pixel,
Vector2 focus,
Vector2 viewport,
Rectangle viewportBounds,
float progress,
float opacity,
float time,
Color primary,
Color secondary)
{
// A cool gray veil mutes the world without changing Terraria's shared shader state.
batch.Draw(pixel, viewportBounds, new Color(102, 128, 148) * (0.12f * opacity));
float reach = Math.Max(viewport.X, viewport.Y) * 0.68f;
for (int index = 0; index < 4; index++)
{
float reveal = Reveal(progress, 0.16f + index * 0.085f, 0.34f + index * 0.085f);
Vector2 direction = (-MathHelper.PiOver2 + index * MathHelper.PiOver2).ToRotationVector2();
Vector2 end = focus + direction * reach;
DrawJaggedLine(batch, pixel, focus, end, reveal,
new Color(38, 105, 155), secondary, 15f, 3.1f, 17f, 208f + index * 19f, opacity);
for (int branch = 0; branch < 3; branch++)
{
float branchProgress = 0.29f + branch * 0.19f;
if (reveal <= branchProgress)
continue;
Vector2 branchOrigin = Vector2.Lerp(focus, end, branchProgress);
float side = (branch + index) % 2 == 0 ? -1f : 1f;
Vector2 branchDirection = direction.RotatedBy(side * (0.42f + branch * 0.12f));
float branchReveal = MathHelper.Clamp((reveal - branchProgress) / 0.25f, 0f, 1f);
DrawJaggedLine(batch, pixel, branchOrigin,
branchOrigin + branchDirection * (58f + branch * 24f), branchReveal,
primary, Color.White, 7f, 1.6f, 7f, 260f + index * 31f + branch * 7f, opacity * 0.82f);
}
}
float crossReveal = Reveal(progress, 0.76f, 0.93f);
float diagonalReach = Math.Max(viewport.X, viewport.Y) * 0.72f;
for (int index = 0; index < 4; index++)
{
Vector2 direction = (MathHelper.PiOver4 + index * MathHelper.PiOver2).ToRotationVector2();
DrawJaggedLine(batch, pixel, focus, focus + direction * diagonalReach, crossReveal,
primary, Color.White, 24f, 5f, 8f, 330f + index * 17f, opacity);
}
DrawGlow(batch, focus, 180f + (float)Math.Sin(time * 3f) * 12f,
secondary * (Pulse(progress, 0.90f, 0.13f) * 0.26f * opacity));
float mirrors = Envelope(progress, 0.10f, 0.24f, 0.84f, 0.96f) * opacity;
float mirrorOrbit = MathHelper.Clamp(Math.Min(viewport.X, viewport.Y) * 0.27f, 170f, 300f);
for (int index = 0; index < 4; index++)
{
float angle = MathHelper.PiOver4 + index * MathHelper.PiOver2;
Vector2 center = focus + angle.ToRotationVector2() * mirrorOrbit;
DrawPolygon(batch, pixel, center, 54f, 6, new Color(35, 100, 150) * (mirrors * 0.52f),
13f, angle, 1f);
DrawPolygon(batch, pixel, center, 47f, 6, Color.White * (mirrors * 0.72f),
2.2f, -angle, 1f);
Vector2 reflection = (focus - center).SafeNormalize(Vector2.UnitY);
DrawLayeredLine(batch, pixel, center - reflection.RotatedBy(MathHelper.PiOver2) * 25f,
center + reflection.RotatedBy(MathHelper.PiOver2) * 25f,
primary, Color.White, 8f, 1.5f, mirrors * 0.52f);
}
float shatteredScreen = Envelope(progress, 0.86f, 0.92f, 0.995f, 1f) * opacity;
if (shatteredScreen > 0.001f)
{
Vector2[] corners =
[
new Vector2(0f, 0f),
new Vector2(viewport.X, 0f),
new Vector2(viewport.X, viewport.Y),
new Vector2(0f, viewport.Y),
new Vector2(viewport.X * 0.5f, 0f),
new Vector2(viewport.X, viewport.Y * 0.5f),
new Vector2(viewport.X * 0.5f, viewport.Y),
new Vector2(0f, viewport.Y * 0.5f)
];
for (int index = 0; index < corners.Length; index++)
{
DrawJaggedLine(batch, pixel, focus, corners[index], shatteredScreen,
new Color(84, 170, 220), Color.White, 10f, 2.1f, 16f,
710f + index * 23f, opacity);
}
}
}
private static void DrawSoul(
SpriteBatch batch,
Texture2D pixel,
Vector2 focus,
Vector2 viewport,
float progress,
float opacity,
float time,
Color primary,
Color secondary)
{
float riverY = MathHelper.Clamp(focus.Y, viewport.Y * 0.26f, viewport.Y * 0.74f);
float riverReveal = Reveal(progress, 0.02f, 0.24f) * opacity;
float convergence = Reveal(progress, 0.66f, 0.93f);
for (int lane = 0; lane < 3; lane++)
{
const int segments = 34;
Vector2 previous = default;
for (int index = 0; index <= segments; index++)
{
float along = index / (float)segments;
float x = MathHelper.Lerp(-70f, viewport.X + 70f, along);
float wave = (float)Math.Sin(along * MathHelper.TwoPi * 2.2f - time * (1.15f + lane * 0.12f) + lane * 1.8f);
float y = riverY + (lane - 1) * 45f + wave * (22f + lane * 5f);
float centerWeight = 1f - MathHelper.Clamp(Math.Abs(along - 0.5f) * 2f, 0f, 1f);
y = MathHelper.Lerp(y, focus.Y, convergence * centerWeight * 0.88f);
Vector2 current = new(x, y);
if (index > 0)
{
Color laneColor = lane == 1 ? secondary : Color.Lerp(primary, secondary, lane * 0.34f);
DrawLayeredLine(batch, pixel, previous, current, primary, laneColor,
10f - lane * 1.5f, 2.1f, riverReveal * (0.56f + lane * 0.12f));
}
previous = current;
}
}
for (int index = 0; index < 12; index++)
{
float reveal = Reveal(progress, 0.07f + index * 0.022f, 0.19f + index * 0.022f) * opacity;
if (reveal <= 0.001f)
continue;
float along = PositiveModulo(time * (0.065f + index % 3 * 0.006f) + index / 12f, 1f);
int lane = index % 3;
float x = MathHelper.Lerp(-45f, viewport.X + 45f, along);
float y = riverY + (lane - 1) * 45f
+ (float)Math.Sin(along * MathHelper.TwoPi * 2.2f - time * 1.25f + lane * 1.8f) * 24f;
Vector2 riverPosition = new(x, y);
float orbitAngle = index * MathHelper.TwoPi / 12f + time * 0.75f;
Vector2 gatheredPosition = focus + orbitAngle.ToRotationVector2()
* MathHelper.Lerp(72f, 20f, Reveal(progress, 0.82f, 0.98f));
Vector2 position = Vector2.Lerp(riverPosition, gatheredPosition, convergence);
Color shadeColor = Color.Lerp(secondary, Color.White, index % 4 == 0 ? 0.34f : 0.08f);
DrawSoulShade(batch, pixel, position, shadeColor, reveal,
0.78f + (float)Math.Sin(time * 3f + index) * 0.10f, orbitAngle);
}
float burial = Reveal(progress, 0.82f, 0.98f) * opacity;
DrawRing(batch, pixel, focus, MathHelper.Lerp(210f, 26f, Ease(burial)), 56,
secondary * (0.92f * burial), 4f, -time * 0.22f, 1f);
DrawGlow(batch, focus, 150f, primary * (0.30f * burial));
float ferryman = Envelope(progress, 0.63f, 0.76f, 0.99f, 1f) * opacity;
if (ferryman > 0.001f)
{
float scale = MathHelper.Clamp(Math.Min(viewport.X, viewport.Y) * 0.42f, 260f, 480f);
DrawReaperSilhouette(batch, pixel, focus - Vector2.UnitY * scale * 0.18f,
scale, new Color(12, 5, 35), primary, ferryman * 0.72f);
DrawTaperedCrescent(batch, pixel, focus, scale * 0.92f, -2.75f, 4.72f, ferryman,
new Color(25, 10, 72), primary, new Color(170, 255, 255), scale * 0.16f);
}
float soulBloom = Pulse(progress, 0.94f, 0.08f) * opacity;
for (int petal = 0; petal < 8; petal++)
{
float angle = petal * MathHelper.PiOver4 + time * 0.08f;
Vector2 direction = angle.ToRotationVector2();
DrawLayeredLine(batch, pixel, focus + direction * 20f,
focus + direction * MathHelper.Lerp(28f, 190f, soulBloom),
primary, secondary, 11f, 2.2f, soulBloom * 0.58f);
}
}
private static void DrawVoid(
SpriteBatch batch,
Texture2D pixel,
Vector2 focus,
Vector2 viewport,
Rectangle viewportBounds,
float progress,
float opacity,
float time,
Color primary,
Color secondary,
Vector2 aim,
int actionId)
{
// The combat projectile owns the one and only world-space rift. Keeping
// this UI layer to atmosphere prevents a second crack from following the
// camera while the authoritative tear stays in the world.
batch.Draw(pixel, viewportBounds, new Color(1, 0, 5) * (0.31f * opacity));
DrawGlow(batch, focus, 210f,
secondary * (Reveal(progress, 0.48f, 1f) * 0.24f * opacity));
}
private static void DrawDeath(
SpriteBatch batch,
Texture2D pixel,
int ownerIndex,
in UltimateVisualState state,
Vector2 focus,
Vector2 viewport,
Rectangle viewportBounds,
float progress,
float opacity,
float time)
{
float shatterStart = ReaperDeathUltimateGeometry.ShatterTick
/ (float)ReaperDeathUltimateGeometry.Duration;
float isolation = Envelope(progress, 0.58f, 0.79f,
shatterStart - 0.015f, shatterStart + 0.012f) * opacity;
Texture2D? capturedWorld = Main.screenTarget;
// Each successful cut removes more of the ordinary scene. Immediately
// before the shatter, only small windows around the caster and recorded
// victims are restored from Terraria's world render target.
if (isolation > 0.001f)
{
batch.Draw(pixel, viewportBounds,
new Color(0, 0, 1) * (isolation * 0.965f));
if (capturedWorld is not null && !capturedWorld.IsDisposed)
DrawDeathUltimateActors(batch, capturedWorld, ownerIndex,
state.ActionId, viewport, isolation);
}
float shatter = Reveal(progress, shatterStart, 0.985f) * opacity;
if (shatter > 0.001f)
{
// The actual Death Necklace plates fill the void behind the broken
// game image; no substitute gradient or black fill is used here.
DeathDomainBackdropTextureSystem.DrawScreenAlignedBackdrop(batch,
viewportBounds, shatter);
if (capturedWorld is not null && !capturedWorld.IsDisposed)
DrawDeathMirrorShards(batch, capturedWorld, focus, viewport,
state.ActionId, shatter, opacity);
}
DrawDeathUltimateCutCracks(batch, pixel, in state, focus, viewport,
shatter, opacity);
DrawGlow(batch, focus, 230f,
new Color(125, 0, 34) * ((0.12f + shatter * 0.18f) * opacity));
_ = time;
}
private static void DrawDeathUltimateActors(SpriteBatch batch,
Texture2D capturedWorld, int ownerIndex, int actionId,
Vector2 viewport, float opacity)
{
if (ownerIndex < 0 || ownerIndex >= Main.maxPlayers)
return;
Player owner = Main.player[ownerIndex];
if (owner.active && !owner.dead)
DrawCapturedActorWindow(batch, capturedWorld, owner.getRect(),
viewport, opacity, 34f);
HashSet<int> drawnRoots = [];
foreach (NPC npc in Main.ActiveNPCs)
{
int root = npc.realLife >= 0 ? npc.realLife : npc.whoAmI;
if (root < 0 || root >= Main.maxNPCs || !drawnRoots.Add(root))
continue;
NPC target = Main.npc[root];
if (!target.active
|| !target.GetGlobalNPC<ReaperCombatGlobalNPC>()
.IsTrackedByDeathUltimate(ownerIndex, actionId))
{
continue;
}
DrawCapturedActorWindow(batch, capturedWorld, target.Hitbox,
viewport, opacity, 28f);
}
}
private static void DrawCapturedActorWindow(SpriteBatch batch,
Texture2D capturedWorld, Rectangle worldBounds, Vector2 viewport,
float opacity, float padding)
{
Rectangle screenBounds = new(
(int)Math.Floor(worldBounds.X - Main.screenPosition.X - padding),
(int)Math.Floor(worldBounds.Y - Main.screenPosition.Y - padding),
Math.Max(1, (int)Math.Ceiling(worldBounds.Width + padding * 2f)),
Math.Max(1, (int)Math.Ceiling(worldBounds.Height + padding * 2f)));
Rectangle clipped = Rectangle.Intersect(screenBounds,
new Rectangle(0, 0, (int)viewport.X, (int)viewport.Y));
if (clipped.Width <= 0 || clipped.Height <= 0)
return;
const int bands = 12;
for (int band = 0; band < bands; band++)
{
float y0 = band / (float)bands;
float y1 = (band + 1f) / bands;
float normalizedY = (y0 + y1) - 1f;
float halfWidth = (float)Math.Sqrt(Math.Max(0f,
1f - normalizedY * normalizedY));
int bandTop = screenBounds.Top
+ (int)Math.Floor(screenBounds.Height * y0);
int bandBottom = screenBounds.Top
+ (int)Math.Ceiling(screenBounds.Height * y1);
int bandHalfWidth = Math.Max(1,
(int)Math.Ceiling(screenBounds.Width * 0.5f * halfWidth));
Rectangle destination = new(
screenBounds.Center.X - bandHalfWidth,
bandTop,
bandHalfWidth * 2,
Math.Max(1, bandBottom - bandTop));
destination = Rectangle.Intersect(destination, clipped);
if (destination.Width <= 0 || destination.Height <= 0)
continue;
Rectangle source = new(
(int)Math.Floor(destination.X / viewport.X * capturedWorld.Width),
(int)Math.Floor(destination.Y / viewport.Y * capturedWorld.Height),
Math.Max(1, (int)Math.Ceiling(destination.Width / viewport.X
* capturedWorld.Width)),
Math.Max(1, (int)Math.Ceiling(destination.Height / viewport.Y
* capturedWorld.Height)));
source = Rectangle.Intersect(source, capturedWorld.Bounds);
if (source.Width > 0 && source.Height > 0)
batch.Draw(capturedWorld, destination, source,
Color.White * MathHelper.Clamp(opacity, 0f, 1f));
}
}
private static void DrawDeathMirrorShards(SpriteBatch batch,
Texture2D capturedWorld, Vector2 focus, Vector2 viewport,
int actionId, float shatter, float opacity)
{
if (Main.graphics?.GraphicsDevice is not GraphicsDevice graphicsDevice)
return;
int rayCount = deathMirrorAngles.Length;
for (int cut = 0; cut < ReaperDeathUltimateGeometry.CutCount; cut++)
{
float angle = ReaperDeathUltimateGeometry.GetCutAxis(actionId, cut)
.ToRotation();
if (angle < 0f)
angle += MathHelper.TwoPi;
deathMirrorAngles[cut * 2] = angle;
deathMirrorAngles[cut * 2 + 1] = PositiveModulo(
angle + MathHelper.Pi, MathHelper.TwoPi);
}
Array.Sort(deathMirrorAngles);
float eased = Ease(shatter);
float radius = GetViewportCoverRadius(focus, viewport) * 1.08f;
Color shardColor = Color.White
* (opacity * MathHelper.Lerp(1f, 0.48f, eased));
int vertex = 0;
for (int shard = 0; shard < rayCount; shard++)
{
float startAngle = deathMirrorAngles[shard];
float endAngle = shard == rayCount - 1
? deathMirrorAngles[0] + MathHelper.TwoPi
: deathMirrorAngles[shard + 1];
float middleAngle = (startAngle + endAngle) * 0.5f;
uint hash = unchecked((uint)(actionId * 16777619
+ shard * 2246822519));
hash ^= hash >> 15;
float random = (hash & 0xFFFFu) / 65535f;
float signed = ((hash >> 16) & 1u) == 0u ? -1f : 1f;
Vector2 displacement = middleAngle.ToRotationVector2()
* ((18f + random * 48f) * eased)
+ Vector2.UnitY * (eased * eased * (8f + random * 34f));
float rotation = signed * (0.012f + random * 0.032f) * eased;
Vector2 originalCenter = focus;
Vector2 originalStart = focus
+ startAngle.ToRotationVector2() * radius;
Vector2 originalEnd = focus
+ endAngle.ToRotationVector2() * radius;
WriteMirrorVertex(ref vertex,
RotatePoint(originalCenter, focus, rotation) + displacement,
originalCenter, viewport, shardColor);
WriteMirrorVertex(ref vertex,
RotatePoint(originalStart, focus, rotation) + displacement,
originalStart, viewport, shardColor);
WriteMirrorVertex(ref vertex,
RotatePoint(originalEnd, focus, rotation) + displacement,
originalEnd, viewport, shardColor);
}
batch.End();
try
{
deathMirrorEffect ??= new BasicEffect(graphicsDevice)
{
TextureEnabled = true,
VertexColorEnabled = true,
LightingEnabled = false,
FogEnabled = false
};
deathMirrorEffect.World = Matrix.Identity;
deathMirrorEffect.View = Matrix.Identity;
deathMirrorEffect.Projection = Matrix.CreateOrthographicOffCenter(
0f, viewport.X, viewport.Y, 0f, 0f, 1f);
deathMirrorEffect.Texture = capturedWorld;
graphicsDevice.BlendState = BlendState.AlphaBlend;
graphicsDevice.DepthStencilState = DepthStencilState.None;
graphicsDevice.RasterizerState = RasterizerState.CullNone;
graphicsDevice.SamplerStates[0] = SamplerState.LinearClamp;
foreach (EffectPass pass in deathMirrorEffect.CurrentTechnique.Passes)
{
pass.Apply();
graphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList,
deathMirrorVertices, 0, vertex / 3);
}
}
finally
{
batch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend,
SamplerState.LinearClamp, DepthStencilState.None,
RasterizerState.CullNone, null, Matrix.Identity);
}
}
private static void WriteMirrorVertex(ref int index, Vector2 position,
Vector2 samplePosition, Vector2 viewport, Color color)
{
Vector2 uv = new(
samplePosition.X / Math.Max(1f, viewport.X),
samplePosition.Y / Math.Max(1f, viewport.Y));
deathMirrorVertices[index++] = new VertexPositionColorTexture(
new Vector3(position, 0f), color, uv);
}
private static Vector2 RotatePoint(Vector2 point, Vector2 origin,
float rotation)
=> origin + (point - origin).RotatedBy(rotation);
private static float GetViewportCoverRadius(Vector2 focus, Vector2 viewport)
{
float radius = focus.Length();
radius = Math.Max(radius, Vector2.Distance(focus,
new Vector2(viewport.X, 0f)));
radius = Math.Max(radius, Vector2.Distance(focus,
new Vector2(0f, viewport.Y)));
radius = Math.Max(radius, Vector2.Distance(focus, viewport));
return Math.Max(1f, radius);
}
private static void DrawDeathUltimateCutCracks(SpriteBatch batch,
Texture2D pixel, in UltimateVisualState state, Vector2 focus,
Vector2 viewport, float shatter, float opacity)
{
float radius = GetViewportCoverRadius(focus, viewport) * 1.04f;
for (int cut = 0; cut < ReaperDeathUltimateGeometry.CutCount; cut++)
{
int cutTick = ReaperDeathUltimateGeometry.GetCutTick(cut);
if (state.Timer < cutTick)
continue;
float age = state.Timer - cutTick;
float fresh = (float)Math.Exp(-age / 13f);
float strength = (0.12f + fresh * 0.38f + shatter * 0.74f)
* opacity;
Vector2 axis = ReaperDeathUltimateGeometry.GetCutAxis(
state.ActionId, cut);
Vector2 start = focus - axis * radius;
Vector2 end = focus + axis * radius;
DrawLayeredLine(batch, pixel, start, end,
new Color(32, 0, 15),
Color.Lerp(new Color(235, 12, 62), Color.White,
shatter * 0.48f),
4f + shatter * 3f, 0.9f + shatter * 0.8f,
strength);
}
}
private static void DrawDeathLegacy(
SpriteBatch batch,
Texture2D pixel,
Vector2 focus,
Vector2 viewport,
float progress,
float opacity,
float time)
{
ReadOnlySpan<ReaperFormId> forms =
[
ReaperFormId.Bone,
ReaperFormId.Blood,
ReaperFormId.Infernal,
ReaperFormId.Frost,
ReaperFormId.Soul,
ReaperFormId.Void
];
float converge = Reveal(progress, 0.69f, 0.92f);
float initialOrbit = MathHelper.Clamp(Math.Min(viewport.X, viewport.Y) * 0.31f, 205f, 340f);
float orbitRadius = MathHelper.Lerp(initialOrbit, 34f, Ease(converge));
float rotation = -MathHelper.PiOver2 + time * MathHelper.Lerp(0.08f, 0.72f, converge);
for (int index = 0; index < forms.Length; index++)
{
float reveal = Reveal(progress, 0.015f + index * 0.038f, 0.105f + index * 0.038f) * opacity;
if (reveal <= 0.001f)
continue;
Color color = ReaperCombatRegistry.GetPrimaryColor(forms[index]);
Color core = ReaperCombatRegistry.GetSecondaryColor(forms[index]);
float angle = rotation + index * MathHelper.TwoPi / forms.Length;
Vector2 originalPosition = focus + angle.ToRotationVector2() * initialOrbit;
Vector2 position = focus + angle.ToRotationVector2() * orbitRadius;
if (converge > 0.001f)
DrawLayeredLine(batch, pixel, originalPosition, position, color, core, 9f, 2f,
converge * reveal * 0.58f);
DrawLayeredLine(batch, pixel, position, focus, color, core, 5f, 1.2f, reveal * 0.30f);
DrawGlow(batch, position, 54f, color * (0.24f * reveal));
float phaseCenter = 36f / 170f + index * 18f / 170f;
float activePulse = 0.74f + Pulse(progress, phaseCenter, 0.055f) * 0.36f;
DrawDeathGate(batch, pixel, position, index, angle, reveal * activePulse,
converge, time, color, core);
}
// Six executions use their branch's actual silhouette, not six recolored
// copies of one crescent. Their centers match the authoritative hit ticks.
for (int index = 0; index < forms.Length; index++)
{
float center = 36f / 170f + index * 18f / 170f;
float phase = Envelope(progress, center - 0.055f, center - 0.012f,
center + 0.052f, center + 0.105f) * opacity;
if (phase > 0.001f)
DrawDeathPhaseMotif(batch, pixel, focus, viewport, index, phase, time);
}
float unity = Reveal(progress, 0.72f, 0.96f) * opacity;
if (unity > 0.001f)
{
for (int index = 0; index < forms.Length; index++)
{
Color color = ReaperCombatRegistry.GetPrimaryColor(forms[index]);
Vector2 direction = (rotation + index * MathHelper.TwoPi / forms.Length).ToRotationVector2();
DrawLayeredLine(batch, pixel, focus, focus + direction * MathHelper.Lerp(18f, 115f, 1f - converge),
color, Color.White, 12f, 2.1f, unity * 0.72f);
}
DrawRing(batch, pixel, focus, MathHelper.Lerp(152f, 24f, Ease(unity)), 64,
Color.White * (0.88f * unity), 5f, rotation, 1f);
DrawRing(batch, pixel, focus, MathHelper.Lerp(104f, 15f, Ease(unity)), 54,
new Color(255, 58, 105) * (0.72f * unity), 2.5f, -rotation, 1f);
DrawGlow(batch, focus, 210f, Color.White * (0.20f * unity));
}
float finalCut = Reveal(progress, 0.86f, 0.985f);
float reach = Math.Max(viewport.X, viewport.Y) * 0.80f;
Vector2 cutDirection = new Vector2(1f, -0.16f).SafeNormalize(Vector2.UnitX);
DrawJaggedLine(batch, pixel, focus - cutDirection * reach, focus + cutDirection * reach,
finalCut, new Color(225, 30, 75), Color.White, 34f, 6f, 5f, 667f, opacity);
float requiem = Envelope(progress, 0.82f, 0.90f, 0.99f, 1f) * opacity;
if (requiem > 0.001f)
{
float scale = MathHelper.Clamp(Math.Min(viewport.X, viewport.Y) * 0.58f, 370f, 690f);
DrawReaperSilhouette(batch, pixel, focus - Vector2.UnitY * scale * 0.23f,
scale, new Color(3, 0, 4), new Color(210, 18, 54), requiem * 0.88f);
for (int band = 0; band < forms.Length; band++)
{
Color color = ReaperCombatRegistry.GetPrimaryColor(forms[band]);
DrawTaperedCrescent(batch, pixel, focus, scale * (0.94f - band * 0.032f),
-2.74f + band * 0.018f, 4.62f, requiem * (0.20f + band * 0.035f),
new Color(8, 0, 10), Color.Lerp(new Color(120, 0, 28), color, 0.42f),
color, scale * (0.16f - band * 0.012f));
}
DrawTaperedCrescent(batch, pixel, focus, scale, -2.76f, 4.64f, requiem,
new Color(8, 0, 10), new Color(224, 18, 56), Color.White, scale * 0.10f);
}
}
private static void DrawDeathGate(
SpriteBatch batch,
Texture2D pixel,
Vector2 center,
int gate,
float rotation,
float opacity,
float converge,
float time,
Color color,
Color core)
{
float size = MathHelper.Lerp(42f, 23f, converge);
switch (gate)
{
case 0: // Ribbed bone guillotine gate.
{
Vector2 topLeft = center + new Vector2(-size * 0.68f, -size);
Vector2 topRight = center + new Vector2(size * 0.68f, -size);
Vector2 bottomLeft = center + new Vector2(-size * 0.68f, size);
Vector2 bottomRight = center + new Vector2(size * 0.68f, size);
DrawLayeredLine(batch, pixel, topLeft, bottomLeft, new Color(6, 28, 35), color,
8f, 2f, opacity);
DrawLayeredLine(batch, pixel, topRight, bottomRight, new Color(6, 28, 35), color,
8f, 2f, opacity);
DrawJaggedLine(batch, pixel, topLeft, topRight, 1f, color, core,
9f, 2.2f, 2f, 90f, opacity);
for (int rib = 0; rib < 3; rib++)
{
float y = MathHelper.Lerp(-size * 0.52f, size * 0.58f, rib / 2f);
DrawLayeredLine(batch, pixel, center + new Vector2(-size * 0.66f, y),
center + new Vector2(-size * 0.16f, y + size * 0.13f), color, core,
4f, 1f, opacity * 0.78f);
DrawLayeredLine(batch, pixel, center + new Vector2(size * 0.66f, y),
center + new Vector2(size * 0.16f, y + size * 0.13f), color, core,
4f, 1f, opacity * 0.78f);
}
break;
}
case 1: // Blood moon gate and white-rimmed crescent.
DrawGlow(batch, center, size * 1.85f, new Color(126, 0, 24) * (opacity * 0.42f));
DrawRing(batch, pixel, center, size * 0.86f, 32, color * (opacity * 0.82f),
size * 0.18f, rotation, 1f);
DrawTaperedCrescent(batch, pixel, center, size * 0.92f, -2.55f, 4.24f,
opacity, new Color(55, 0, 16), color, Color.White, size * 0.18f);
break;
case 2: // Infernal five-point gate.
DrawRing(batch, pixel, center, size, 36, new Color(52, 8, 0) * opacity,
7f, rotation, 1f);
DrawPentagram(batch, pixel, center, size * 0.88f, rotation + time * 0.05f,
new Color(70, 7, 0), core, opacity);
break;
case 3: // Hexagonal frost mirror and X blades.
DrawPolygon(batch, pixel, center, size, 6, new Color(54, 120, 170) * (opacity * 0.72f),
10f, rotation, 1f);
DrawPolygon(batch, pixel, center, size * 0.86f, 6, Color.White * opacity,
2f, -rotation, 1f);
DrawLayeredLine(batch, pixel, center + new Vector2(-size * 0.66f, -size * 0.66f),
center + new Vector2(size * 0.66f, size * 0.66f), color, core, 5f, 1.2f, opacity);
DrawLayeredLine(batch, pixel, center + new Vector2(-size * 0.66f, size * 0.66f),
center + new Vector2(size * 0.66f, -size * 0.66f), color, Color.White, 5f, 1.2f, opacity);
break;
case 4: // Soft soul portal with a single ferryman shade.
DrawRing(batch, pixel, center, size, 36, color * (opacity * 0.72f), 5f,
rotation + time * 0.1f, 1f);
DrawRing(batch, pixel, center, size * 0.72f, 30, core * (opacity * 0.54f), 2f,
-rotation, 1f);
DrawSoulShade(batch, pixel, center, core, opacity, size / 44f, time);
break;
default: // Offset coordinate door cut out of the scene.
{
Rectangle voidCore = new(
(int)Math.Round(center.X - size * 0.72f),
(int)Math.Round(center.Y - size),
Math.Max(1, (int)Math.Round(size * 1.44f)),
Math.Max(1, (int)Math.Round(size * 2f)));
batch.Draw(pixel, voidCore, new Color(0, 0, 2) * (opacity * 0.94f));
Vector2 topLeft = new(voidCore.Left, voidCore.Top);
Vector2 topRight = new(voidCore.Right, voidCore.Top + size * 0.12f);
Vector2 bottomLeft = new(voidCore.Left, voidCore.Bottom - size * 0.12f);
Vector2 bottomRight = new(voidCore.Right, voidCore.Bottom);
DrawLayeredLine(batch, pixel, topLeft, topRight, color, Color.White, 6f, 1.3f, opacity);
DrawLayeredLine(batch, pixel, bottomLeft, bottomRight, color, core, 6f, 1.3f, opacity);
DrawLayeredLine(batch, pixel, center - Vector2.UnitX * size * 0.90f,
center + Vector2.UnitX * size * 0.90f, Color.Black, core, 10f, 2f, opacity);
break;
}
}
DrawDeathRuneGlyph(batch, pixel, center, gate, size * 0.28f, color, core, opacity * 0.74f);
}
private static void DrawDeathPhaseMotif(
SpriteBatch batch,
Texture2D pixel,
Vector2 focus,
Vector2 viewport,
int phase,
float opacity,
float time)
{
float shortSide = Math.Min(viewport.X, viewport.Y);
float longSide = Math.Max(viewport.X, viewport.Y);
switch (phase)
{
case 0: // Bone guillotine and rib frame.
{
Color bone = new(210, 244, 232);
Color corpseFire = new(58, 214, 228);
float halfWidth = MathHelper.Clamp(viewport.X * 0.17f, 145f, 270f);
float height = MathHelper.Clamp(viewport.Y * 0.52f, 300f, 520f);
Vector2 topLeft = focus + new Vector2(-halfWidth, -height * 0.56f);
Vector2 topRight = focus + new Vector2(halfWidth, -height * 0.56f);
Vector2 bottomLeft = topLeft + Vector2.UnitY * height;
Vector2 bottomRight = topRight + Vector2.UnitY * height;
DrawLayeredLine(batch, pixel, topLeft, bottomLeft, new Color(8, 28, 34), corpseFire,
18f, 3f, opacity);
DrawLayeredLine(batch, pixel, topRight, bottomRight, new Color(8, 28, 34), corpseFire,
18f, 3f, opacity);
DrawJaggedLine(batch, pixel, topLeft, topRight, 1f, corpseFire, bone,
20f, 4f, 5f, 180f, opacity);
for (int rib = 0; rib < 5; rib++)
{
float amount = (rib + 1f) / 6f;
Vector2 left = Vector2.Lerp(topLeft, bottomLeft, amount);
Vector2 right = Vector2.Lerp(topRight, bottomRight, amount);
DrawLayeredLine(batch, pixel, left, Vector2.Lerp(left, focus, 0.56f),
corpseFire, bone, 8f, 2f, opacity * 0.68f);
DrawLayeredLine(batch, pixel, right, Vector2.Lerp(right, focus, 0.56f),
corpseFire, bone, 8f, 2f, opacity * 0.68f);
}
DrawJaggedLine(batch, pixel, topRight + new Vector2(-20f, 30f),
bottomLeft + new Vector2(34f, -20f), 1f, new Color(22, 92, 110), bone,
28f, 5f, 4f, 219f, opacity);
break;
}
case 1: // Thick reference-style blood crescent.
{
float radius = MathHelper.Clamp(shortSide * 0.54f, 330f, 610f);
Vector2 center = focus + new Vector2(-radius * 0.13f, radius * 0.05f);
DrawGlow(batch, focus - Vector2.UnitY * radius * 0.25f, radius * 0.62f,
new Color(115, 0, 22) * (opacity * 0.42f));
DrawTaperedCrescent(batch, pixel, center, radius * 1.025f, -2.50f, 4.36f,
opacity * 0.24f, new Color(35, 0, 10), new Color(155, 5, 34),
new Color(255, 102, 112), radius * 0.22f);
DrawTaperedCrescent(batch, pixel, center, radius, -2.46f, 4.36f,
opacity, new Color(70, 0, 18), new Color(238, 24, 58),
new Color(255, 244, 226), radius * 0.18f);
DrawImpactStar(batch, pixel, focus + new Vector2(radius * 0.20f, radius * 0.43f),
radius * 0.09f, new Color(255, 145, 65), Color.White, opacity);
break;
}
case 2: // Pentagram and black-shell molten tracks.
{
float radius = MathHelper.Clamp(shortSide * 0.30f, 190f, 350f);
float rotation = -MathHelper.PiOver2 + time * 0.06f;
DrawPentagram(batch, pixel, focus, radius, rotation, new Color(43, 4, 0),
new Color(255, 192, 46), opacity);
for (int index = 0; index < 5; index++)
{
Vector2 direction = (rotation + index * MathHelper.TwoPi / 5f).ToRotationVector2();
Vector2 vertex = focus + direction * radius;
DrawLayeredLine(batch, pixel, focus, vertex + direction * radius * 0.55f,
new Color(12, 3, 0), new Color(255, 92, 8), 24f, 5f, opacity);
DrawGlow(batch, vertex, 52f, new Color(255, 150, 18) * (opacity * 0.30f));
}
break;
}
case 3: // Four frost mirrors and a crystalline X.
{
float orbit = MathHelper.Clamp(shortSide * 0.24f, 155f, 285f);
for (int index = 0; index < 4; index++)
{
float angle = MathHelper.PiOver4 + index * MathHelper.PiOver2;
Vector2 mirror = focus + angle.ToRotationVector2() * orbit;
DrawPolygon(batch, pixel, mirror, 58f, 6,
new Color(42, 126, 184) * (opacity * 0.66f), 15f, angle, 1f);
DrawPolygon(batch, pixel, mirror, 50f, 6, Color.White * opacity,
2.4f, -angle, 1f);
}
Vector2 diagonalA = new Vector2(1f, 1f).SafeNormalize(Vector2.One);
Vector2 diagonalB = new Vector2(1f, -1f).SafeNormalize(Vector2.UnitX);
DrawLayeredLine(batch, pixel, focus - diagonalA * longSide * 0.52f,
focus + diagonalA * longSide * 0.52f, new Color(45, 130, 190),
Color.White, 26f, 4f, opacity);
DrawLayeredLine(batch, pixel, focus - diagonalB * longSide * 0.52f,
focus + diagonalB * longSide * 0.52f, new Color(28, 91, 160),
new Color(190, 250, 255), 26f, 4f, opacity);
break;
}
case 4: // Winding soul river and ferryman shades.
{
for (int lane = 0; lane < 3; lane++)
{
Vector2 previous = default;
for (int segment = 0; segment <= 20; segment++)
{
float along = segment / 20f;
Vector2 current = new(
MathHelper.Lerp(-50f, viewport.X + 50f, along),
focus.Y + (lane - 1) * 46f
+ (float)Math.Sin(along * MathHelper.TwoPi * 2f + lane * 1.7f - time) * 24f);
if (segment > 0)
{
DrawLayeredLine(batch, pixel, previous, current, new Color(31, 8, 72),
lane == 1 ? new Color(118, 240, 242) : new Color(120, 75, 225),
13f, 2.4f, opacity * 0.78f);
}
previous = current;
}
}
for (int index = 0; index < 7; index++)
{
float angle = index * MathHelper.TwoPi / 7f + time * 0.25f;
Vector2 shade = focus + angle.ToRotationVector2() * (70f + index % 2 * 38f);
DrawSoulShade(batch, pixel, shade, new Color(154, 246, 239), opacity,
0.72f + index % 3 * 0.10f, angle);
}
break;
}
default: // Coordinate grid collapses into a negative-space world split.
{
float splitWidth = MathHelper.Clamp(shortSide * 0.10f, 68f, 128f);
batch.Draw(pixel, new Rectangle(0, (int)Math.Round(focus.Y - splitWidth * 0.5f),
(int)Math.Ceiling(viewport.X), (int)Math.Round(splitWidth)),
new Color(0, 0, 2) * (opacity * 0.96f));
batch.Draw(pixel, new Rectangle((int)Math.Round(focus.X - splitWidth * 0.38f), 0,
(int)Math.Round(splitWidth * 0.76f), (int)Math.Ceiling(viewport.Y)),
new Color(0, 0, 2) * (opacity * 0.96f));
DrawLayeredLine(batch, pixel, new Vector2(0f, focus.Y - splitWidth * 0.5f),
new Vector2(viewport.X, focus.Y - splitWidth * 0.5f), new Color(82, 9, 130),
Color.White, 9f, 2f, opacity);
DrawLayeredLine(batch, pixel, new Vector2(focus.X + splitWidth * 0.38f, 0f),
new Vector2(focus.X + splitWidth * 0.38f, viewport.Y), new Color(55, 4, 100),
new Color(220, 160, 255), 9f, 2f, opacity);
for (int panel = 0; panel < 5; panel++)
{
float y = viewport.Y * (panel + 1f) / 6f;
float offset = (panel % 2 == 0 ? 1f : -1f) * 22f * opacity;
DrawLayeredLine(batch, pixel, new Vector2(offset, y),
new Vector2(viewport.X + offset, y), Color.Black,
new Color(122, 30, 180), 7f, 1.5f, opacity * 0.72f);
}
break;
}
}
}
private static void DrawPentagram(
SpriteBatch batch,
Texture2D pixel,
Vector2 center,
float radius,
float rotation,
Color shell,
Color core,
float opacity)
{
Span<Vector2> vertices = stackalloc Vector2[5];
for (int index = 0; index < vertices.Length; index++)
vertices[index] = center + (rotation + index * MathHelper.TwoPi / 5f).ToRotationVector2() * radius;
for (int edge = 0; edge < vertices.Length; edge++)
{
int start = edge * 2 % 5;
int end = (edge + 1) * 2 % 5;
DrawLayeredLine(batch, pixel, vertices[start], vertices[end], shell, core,
15f, 3f, opacity);
}
DrawRing(batch, pixel, center, radius * 1.04f, 48, shell * (opacity * 0.80f),
7f, rotation, 1f);
}
private static void DrawTaperedCrescent(
SpriteBatch batch,
Texture2D pixel,
Vector2 center,
float radius,
float rotation,
float sweep,
float opacity,
Color shadow,
Color body,
Color edge,
float thickness)
{
opacity = MathHelper.Clamp(opacity, 0f, 1f);
if (opacity <= 0.001f || radius <= 2f || thickness <= 0.5f || Math.Abs(sweep) <= 0.01f)
return;
ReaperUltimateCrescentMaskSet? mask = SelectCrescentMask(Math.Abs(sweep), thickness / radius);
if (mask is null || mask.Body.IsDisposed || mask.Flow.IsDisposed || mask.Edge.IsDisposed)
return;
float scale = radius / CrescentReferenceRadius;
Vector2 textureOrigin = new(CrescentMaskSize * 0.5f);
SpriteEffects effects = sweep < 0f ? SpriteEffects.FlipVertically : SpriteEffects.None;
// Four cached-mask draws replace hundreds of MagicPixel segments. Body,
// longitudinal flow and white-hot rim remain independently tintable.
batch.Draw(mask.Body, center, null, shadow * (opacity * 0.48f), rotation,
textureOrigin, scale * 1.025f, effects, 0f);
batch.Draw(mask.Body, center, null, body * (opacity * 0.84f), rotation,
textureOrigin, scale, effects, 0f);
batch.Draw(mask.Flow, center, null, Color.Lerp(body, edge, 0.34f) * (opacity * 0.48f),
rotation + (float)Math.Sin(Main.GlobalTimeWrappedHourly * 2.4f) * 0.004f,
textureOrigin, scale, effects, 0f);
batch.Draw(mask.Edge, center, null, edge * opacity, rotation,
textureOrigin, scale, effects, 0f);
}
private static ReaperUltimateCrescentMaskSet? SelectCrescentMask(float sweep, float thicknessRatio)
{
ReaperUltimateCrescentMaskSet? best = null;
float bestScore = float.MaxValue;
for (int index = 0; index < crescentMasks.Length; index++)
{
ReaperUltimateCrescentMaskSet candidate = crescentMasks[index];
// Preserve the requested arc silhouette first; thickness selects the
// closer preset when two sweep families are visually comparable.
float score = Math.Abs(candidate.Sweep - sweep) * 2.4f
+ Math.Abs(candidate.ThicknessRatio - thicknessRatio) * 4f;
if (score >= bestScore)
continue;
best = candidate;
bestScore = score;
}
return best;
}
private static void EnsureCrescentMasks()
{
if (crescentMasks.Length > 0)
return;
if (Main.dedServ || Main.graphics?.GraphicsDevice is not GraphicsDevice graphicsDevice)
return;
crescentMasks =
[
CreateCrescentMask(graphicsDevice, 3.58f, 0.115f),
CreateCrescentMask(graphicsDevice, 4.24f, 0.175f),
CreateCrescentMask(graphicsDevice, 4.66f, 0.245f)
];
}
private static ReaperUltimateCrescentMaskSet CreateCrescentMask(
GraphicsDevice graphicsDevice,
float sweep,
float thicknessRatio)
{
Color[] bodyData = new Color[CrescentMaskSize * CrescentMaskSize];
Color[] flowData = new Color[bodyData.Length];
Color[] edgeData = new Color[bodyData.Length];
float center = CrescentMaskSize * 0.5f;
float maximumWidth = CrescentReferenceRadius * thicknessRatio;
for (int y = 0; y < CrescentMaskSize; y++)
{
float dy = y + 0.5f - center;
for (int x = 0; x < CrescentMaskSize; x++)
{
float dx = x + 0.5f - center;
float radial = (float)Math.Sqrt(dx * dx + dy * dy);
if (radial < CrescentReferenceRadius - maximumWidth
|| radial > CrescentReferenceRadius + maximumWidth)
{
continue;
}
float angle = (float)Math.Atan2(dy, dx);
if (angle < 0f)
angle += MathHelper.TwoPi;
if (angle > sweep)
continue;
float along = angle / sweep;
float taper = (float)Math.Pow(Math.Max(0f, Math.Sin(along * MathHelper.Pi)), 0.58f);
taper *= MathHelper.Lerp(0.72f, 1f, 1f - along * 0.42f);
float halfWidth = Math.Max(0.75f, maximumWidth * taper * 0.5f);
float distance = Math.Abs(radial - CrescentReferenceRadius);
float bodyAlpha = MathHelper.Clamp((halfWidth + 1.6f - distance) / 2.8f, 0f, 1f);
if (bodyAlpha <= 0f)
continue;
int dataIndex = y * CrescentMaskSize + x;
bodyData[dataIndex] = PremultipliedMask(bodyAlpha);
float normalizedRadial = (radial - CrescentReferenceRadius) / Math.Max(1f, halfWidth);
float firstFlow = -0.34f + (float)Math.Sin(along * MathHelper.TwoPi * 2.1f) * 0.10f;
float secondFlow = 0.18f + (float)Math.Sin(along * MathHelper.TwoPi * 3.2f + 1.3f) * 0.11f;
float flowAlpha = Math.Max(
1f - Math.Abs(normalizedRadial - firstFlow) / 0.12f,
1f - Math.Abs(normalizedRadial - secondFlow) / 0.09f);
flowAlpha = MathHelper.Clamp(flowAlpha, 0f, 1f) * bodyAlpha * (0.62f + taper * 0.38f);
flowData[dataIndex] = PremultipliedMask(flowAlpha);
float rimRadius = CrescentReferenceRadius + halfWidth * 0.88f;
float rimWidth = Math.Max(1.35f, maximumWidth * 0.055f);
float rimAlpha = MathHelper.Clamp((rimWidth + 1.2f - Math.Abs(radial - rimRadius)) / 2.2f, 0f, 1f);
edgeData[dataIndex] = PremultipliedMask(rimAlpha * bodyAlpha);
}
}
Texture2D bodyTexture = new(graphicsDevice, CrescentMaskSize, CrescentMaskSize);
Texture2D flowTexture = new(graphicsDevice, CrescentMaskSize, CrescentMaskSize);
Texture2D edgeTexture = new(graphicsDevice, CrescentMaskSize, CrescentMaskSize);
bodyTexture.SetData(bodyData);
flowTexture.SetData(flowData);
edgeTexture.SetData(edgeData);
return new ReaperUltimateCrescentMaskSet(sweep, thicknessRatio, bodyTexture, flowTexture, edgeTexture);
}
private static Color PremultipliedMask(float opacity)
{
byte value = (byte)Math.Round(MathHelper.Clamp(opacity, 0f, 1f) * byte.MaxValue);
return new Color(value, value, value, value);
}
private static void DisposeCrescentMasks(ReaperUltimateCrescentMaskSet[] masks)
{
for (int index = 0; index < masks.Length; index++)
masks[index].Dispose();
}
private static void DrawImpactStar(
SpriteBatch batch,
Texture2D pixel,
Vector2 center,
float radius,
Color outer,
Color core,
float opacity)
{
opacity = MathHelper.Clamp(opacity, 0f, 1f);
if (opacity <= 0.001f || radius <= 1f)
return;
DrawGlow(batch, center, radius * 2.4f, outer * (opacity * 0.30f));
for (int ray = 0; ray < 10; ray++)
{
float angle = ray * MathHelper.TwoPi / 10f + (ray % 2) * 0.11f;
float rayLength = radius * (ray % 2 == 0 ? 1f : 0.55f);
Vector2 direction = angle.ToRotationVector2();
DrawLayeredLine(batch, pixel, center - direction * rayLength * 0.12f,
center + direction * rayLength, outer, core, 5f, 1.4f, opacity);
}
}
private static void DrawReaperSilhouette(
SpriteBatch batch,
Texture2D pixel,
Vector2 center,
float scale,
Color shadow,
Color rim,
float opacity)
{
opacity = MathHelper.Clamp(opacity, 0f, 1f);
if (opacity <= 0.001f || scale <= 10f)
return;
float headRadius = scale * 0.11f;
Vector2 head = center - Vector2.UnitY * scale * 0.30f;
DrawGlow(batch, head, headRadius * 2.5f, rim * (opacity * 0.13f));
DrawPolygon(batch, pixel, head, headRadius, 9, shadow * (opacity * 0.96f),
headRadius * 0.82f, -MathHelper.PiOver2, 1f);
DrawRing(batch, pixel, head, headRadius * 1.04f, 28, rim * (opacity * 0.56f), 3f, 0f, 1f);
Vector2 leftShoulder = center + new Vector2(-scale * 0.25f, -scale * 0.17f);
Vector2 rightShoulder = center + new Vector2(scale * 0.25f, -scale * 0.17f);
Vector2 hem = center + Vector2.UnitY * scale * 0.42f;
DrawLayeredLine(batch, pixel, leftShoulder, hem, shadow, rim, scale * 0.22f, 3f, opacity * 0.62f);
DrawLayeredLine(batch, pixel, rightShoulder, hem, shadow, rim, scale * 0.22f, 3f, opacity * 0.62f);
DrawLayeredLine(batch, pixel, leftShoulder, rightShoulder, shadow, rim, scale * 0.18f, 3f, opacity * 0.58f);
Vector2 staffStart = center + new Vector2(-scale * 0.30f, scale * 0.36f);
Vector2 staffEnd = center + new Vector2(scale * 0.26f, -scale * 0.48f);
DrawLayeredLine(batch, pixel, staffStart, staffEnd, shadow, rim, 15f, 3.2f, opacity * 0.82f);
DrawTaperedCrescent(batch, pixel, staffEnd + new Vector2(scale * 0.10f, scale * 0.03f),
scale * 0.25f, -2.7f, 3.5f, opacity * 0.88f, shadow, rim, Color.White, scale * 0.045f);
}
private static void DrawTombstone(
SpriteBatch batch,
Texture2D pixel,
Vector2 bottomCenter,
float width,
float height,
Color fill,
Color outline)
{
Vector2 top = bottomCenter - Vector2.UnitY * height;
Rectangle body = new(
(int)Math.Round(bottomCenter.X - width * 0.5f),
(int)Math.Round(top.Y + width * 0.26f),
Math.Max(1, (int)Math.Round(width)),
Math.Max(1, (int)Math.Round(height - width * 0.26f)));
batch.Draw(pixel, body, fill);
DrawRing(batch, pixel, new Vector2(bottomCenter.X, body.Top), width * 0.5f, 16,
fill, width * 0.48f, MathHelper.Pi, 0.5f);
DrawLayeredLine(batch, pixel, new Vector2(body.Left, body.Bottom), new Vector2(body.Right, body.Bottom),
outline, Color.White, 4f, 1f, 0.78f);
DrawLayeredLine(batch, pixel, new Vector2(body.Left, body.Top), new Vector2(body.Left, body.Bottom),
outline, Color.White, 3f, 0.8f, 0.64f);
DrawLayeredLine(batch, pixel, new Vector2(body.Right, body.Top), new Vector2(body.Right, body.Bottom),
outline, Color.White, 3f, 0.8f, 0.64f);
Vector2 crossCenter = bottomCenter - Vector2.UnitY * height * 0.48f;
DrawSegment(batch, pixel, crossCenter - Vector2.UnitY * 12f, crossCenter + Vector2.UnitY * 13f,
outline, 2.2f);
DrawSegment(batch, pixel, crossCenter - Vector2.UnitX * 8f, crossCenter + Vector2.UnitX * 8f,
outline, 2.2f);
}
private static void DrawSoulShade(
SpriteBatch batch,
Texture2D pixel,
Vector2 position,
Color color,
float opacity,
float scale,
float rotation)
{
DrawGlow(batch, position, 38f * scale, color * (0.20f * opacity));
Texture2D? texture = soulTexture?.IsLoaded == true ? soulTexture.Value : null;
if (texture is not null)
{
float textureScale = 46f / Math.Max(texture.Width, texture.Height) * scale;
batch.Draw(texture, position, null, color * (0.74f * opacity), rotation * 0.04f,
new Vector2(texture.Width, texture.Height) * 0.5f, textureScale, SpriteEffects.None, 0f);
}
else
{
DrawPolygon(batch, pixel, position, 14f * scale, 4, color * opacity, 2f,
MathHelper.PiOver4, 1f);
}
Vector2 tail = position + new Vector2((float)Math.Sin(rotation) * 7f, 20f * scale);
DrawLayeredLine(batch, pixel, position, tail, color, Color.White, 5f, 1.1f, opacity * 0.56f);
}
private static void DrawDeathRuneGlyph(
SpriteBatch batch,
Texture2D pixel,
Vector2 center,
int glyph,
float size,
Color color,
Color core,
float opacity)
{
switch (glyph)
{
case 0: // Bone cross.
DrawLayeredLine(batch, pixel, center - Vector2.UnitY * size, center + Vector2.UnitY * size,
color, core, 4f, 1.2f, opacity);
DrawLayeredLine(batch, pixel, center - Vector2.UnitX * size * 0.65f, center + Vector2.UnitX * size * 0.65f,
color, core, 4f, 1.2f, opacity);
break;
case 1: // Blood fang.
DrawLayeredLine(batch, pixel, center + new Vector2(-size, -size * 0.7f), center + new Vector2(0f, size),
color, core, 4f, 1.2f, opacity);
DrawLayeredLine(batch, pixel, center + new Vector2(size, -size * 0.7f), center + new Vector2(0f, size),
color, core, 4f, 1.2f, opacity);
break;
case 2: // Infernal triangle.
DrawRuneTriangle(batch, pixel, center, size, -MathHelper.PiOver2, core, opacity);
break;
case 3: // Frost crystal.
for (int index = 0; index < 3; index++)
{
Vector2 direction = (index * MathHelper.Pi / 3f).ToRotationVector2() * size;
DrawLayeredLine(batch, pixel, center - direction, center + direction,
color, core, 3.5f, 1f, opacity);
}
break;
case 4: // Soul diamond.
DrawPolygon(batch, pixel, center, size, 4, core * opacity, 3f, MathHelper.PiOver4, 1f);
DrawSegment(batch, pixel, center - Vector2.UnitY * size * 0.55f,
center + Vector2.UnitY * size * 0.55f, color * opacity, 2f);
break;
default: // Void severance.
DrawLayeredLine(batch, pixel, center + new Vector2(-size, size), center + new Vector2(size, -size),
color, Color.White, 5f, 1.3f, opacity);
DrawLayeredLine(batch, pixel, center + new Vector2(-size * 0.55f, -size), center + new Vector2(size * 0.55f, size),
color, core, 3f, 0.9f, opacity * 0.75f);
break;
}
}
private static void DrawRuneTriangle(
SpriteBatch batch,
Texture2D pixel,
Vector2 center,
float radius,
float rotation,
Color color,
float opacity)
{
DrawPolygon(batch, pixel, center, radius, 3, color * opacity, 2.2f, rotation, 1f);
}
private static void DrawGlow(SpriteBatch batch, Vector2 center, float radius, Color color)
{
if (radius <= 0.5f || color.A == 0)
return;
DeathDomainPrimitiveTextureSystem.DrawRadialGradient(batch, center, radius, color);
}
private static void DrawRing(
SpriteBatch batch,
Texture2D pixel,
Vector2 center,
float radius,
int segments,
Color color,
float width,
float rotation,
float reveal)
{
if (radius <= 0.5f || segments < 3 || width <= 0.05f || reveal <= 0.001f || color.A == 0)
return;
reveal = MathHelper.Clamp(reveal, 0f, 1f);
segments = Math.Max(segments,
Math.Min(192, (int)Math.Ceiling(MathHelper.TwoPi * radius / 18f)));
Vector2 previous = center + rotation.ToRotationVector2() * radius;
int visibleSegments = Math.Max(1, (int)Math.Ceiling(segments * reveal));
for (int index = 1; index <= visibleSegments; index++)
{
float amount = Math.Min(reveal, index / (float)segments);
Vector2 current = center + (rotation + MathHelper.TwoPi * amount).ToRotationVector2() * radius;
DrawSegment(batch, pixel, previous, current, color, width);
previous = current;
}
}
private static void DrawPolygon(
SpriteBatch batch,
Texture2D pixel,
Vector2 center,
float radius,
int sides,
Color color,
float width,
float rotation,
float reveal)
{
if (sides < 3 || radius <= 0.5f || reveal <= 0.001f)
return;
reveal = MathHelper.Clamp(reveal, 0f, 1f);
Vector2 previous = center + rotation.ToRotationVector2() * radius;
int visibleEdges = Math.Max(1, (int)Math.Ceiling(sides * reveal));
for (int edge = 1; edge <= visibleEdges; edge++)
{
float edgeCompletion = Math.Min(1f, sides * reveal - (edge - 1));
Vector2 target = center + (rotation + MathHelper.TwoPi * edge / sides).ToRotationVector2() * radius;
Vector2 current = Vector2.Lerp(previous, target, edgeCompletion);
DrawSegment(batch, pixel, previous, current, color, width);
previous = target;
}
}
private static void DrawJaggedLine(
SpriteBatch batch,
Texture2D pixel,
Vector2 start,
Vector2 end,
float reveal,
Color outerColor,
Color coreColor,
float outerWidth,
float coreWidth,
float amplitude,
float seed,
float opacity)
{
reveal = MathHelper.Clamp(reveal, 0f, 1f);
if (reveal <= 0.001f || opacity <= 0.001f)
return;
const int segments = 18;
float totalLength = Vector2.Distance(start, end);
int segmentCount = Math.Max(3, Math.Min(segments, (int)(totalLength / 38f) + 3));
Vector2 direction = (end - start).SafeNormalize(Vector2.UnitX);
Vector2 perpendicular = new(-direction.Y, direction.X);
Vector2 previous = JaggedPoint(start, end, perpendicular, 0f, segmentCount, amplitude, seed);
int visibleSegments = Math.Max(1, (int)Math.Ceiling(segmentCount * reveal));
for (int index = 1; index <= visibleSegments; index++)
{
float amount = Math.Min(reveal, index / (float)segmentCount);
Vector2 current = JaggedPoint(start, end, perpendicular, amount, segmentCount, amplitude, seed);
DrawLayeredLine(batch, pixel, previous, current, outerColor, coreColor,
outerWidth, coreWidth, opacity);
previous = current;
}
}
private static Vector2 JaggedPoint(
Vector2 start,
Vector2 end,
Vector2 perpendicular,
float amount,
int segments,
float amplitude,
float seed)
{
float envelope = (float)Math.Sin(amount * MathHelper.Pi);
float grain = (float)Math.Sin((amount * segments + seed) * 5.137f) * 0.67f
+ (float)Math.Sin((amount * segments + seed * 1.91f) * 11.73f) * 0.33f;
return Vector2.Lerp(start, end, amount) + perpendicular * (grain * amplitude * envelope);
}
private static void DrawLayeredLine(
SpriteBatch batch,
Texture2D pixel,
Vector2 start,
Vector2 end,
Color outerColor,
Color coreColor,
float outerWidth,
float coreWidth,
float opacity)
{
if (opacity <= 0.001f)
return;
DrawSegment(batch, pixel, start, end, outerColor * (opacity * 0.72f), outerWidth);
DrawSegment(batch, pixel, start, end, coreColor * opacity, coreWidth);
}
private static void DrawSegment(
SpriteBatch batch,
Texture2D pixel,
Vector2 start,
Vector2 end,
Color color,
float width)
{
DeathDomainPrimitiveTextureSystem.DrawSmoothLine(batch, start, end, color, width);
}
private static float GetStateOpacity(in UltimateVisualState state)
{
float progress = MathHelper.Clamp(state.Timer / (float)Math.Max(1, state.Duration), 0f, 1f);
float introduction = MathHelper.Lerp(0.35f, 1f, Ease(progress / 0.06f));
float tail = 1f - MathHelper.Clamp(state.FramesSinceReport / (float)TailFrames, 0f, 1f);
return introduction * Ease(tail);
}
private static float GetBackdropDarkness(in UltimateVisualState state)
{
if (state.Form == ReaperFormId.Death)
{
float cutProgress = MathHelper.Clamp(
(state.Timer - ReaperDeathUltimateGeometry.FirstCutTick)
/ (float)(ReaperDeathUltimateGeometry.ShatterTick
- ReaperDeathUltimateGeometry.FirstCutTick), 0f, 1f);
return MathHelper.Lerp(0.30f, 0.985f, Ease(cutProgress));
}
return state.Form switch
{
ReaperFormId.Bone => 0.36f,
ReaperFormId.Blood => 0.42f,
ReaperFormId.Infernal => 0.38f,
ReaperFormId.Frost => 0.31f,
ReaperFormId.Soul => 0.37f,
ReaperFormId.Void => 0.52f,
_ => 0f
};
}
private static float Reveal(float progress, float start, float end)
{
if (end <= start)
return progress >= end ? 1f : 0f;
return Ease((progress - start) / (end - start));
}
private static float Envelope(float progress, float start, float riseEnd, float fallStart, float end)
{
if (progress < start || progress > end)
return 0f;
if (progress < riseEnd)
return Reveal(progress, start, riseEnd);
if (progress <= fallStart)
return 1f;
return 1f - Reveal(progress, fallStart, end);
}
private static float Pulse(float progress, float center, float halfWidth)
{
if (halfWidth <= 0f)
return 0f;
return Ease(1f - Math.Abs(progress - center) / halfWidth);
}
private static float Ease(float value)
{
value = MathHelper.Clamp(value, 0f, 1f);
return value * value * (3f - 2f * value);
}
private static float PositiveModulo(float value, float modulus)
{
float result = value % modulus;
return result < 0f ? result + modulus : result;
}
private static void EnsureStateStorage()
{
if (states.Length != Main.maxPlayers)
states = new UltimateVisualState[Main.maxPlayers];
}
private static void ClearStates()
{
if (states.Length > 0)
Array.Clear(states, 0, states.Length);
}
private struct UltimateVisualState
{
public bool Active;
public ReaperFormId Form;
public int ActionId;
public Vector2 OriginWorld;
public Vector2 FocusWorld;
public Vector2 Aim;
public int Timer;
public int Duration;
public int FramesSinceReport;
public ulong LastReportTick;
}
}