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; /// /// 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. /// [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? 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; /// /// Reports one frame of an active ultimate. This method is safe to call from /// projectile AI on every net mode: dedicated servers immediately return. /// 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("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 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 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 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() .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 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 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; } }