using SoulHarvest.Projectiles; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using Terraria; using Terraria.Audio; using Terraria.GameContent; using Terraria.ID; using Terraria.ModLoader; namespace SoulHarvest.Common; /// /// Client-only storyboards for the eighteen branch skills. The authoritative /// cue is synchronized by ; everything kept /// here is disposable presentation state and can never deal damage or move an NPC. /// [Autoload(Side = ModSide.Client)] internal sealed class ReaperSkillVisualSystem : ModSystem { private const int MaximumVisuals = 40; private static readonly List Visuals = []; public static void ReportSkill( int owner, ReaperFormId form, ReaperStage stage, int skill, int level, int variant, Vector2 center, Vector2 direction, int actionId, int seed) { if (Main.dedServ || Main.gameMenu || owner < 0 || owner >= Main.maxPlayers || !ReaperDefinitions.IsBranchForm(form) || skill is < 0 or > 2 || level is < 1 or > 3) { return; } direction = direction.SafeNormalize(Vector2.UnitY * -1f); if (form == ReaperFormId.Blood && skill is 1 or 2 && variant == 1) { for (int index = Visuals.Count - 1; index >= 0; index--) { ReaperSkillVisual shield = Visuals[index]; if (shield.Owner == owner && shield.Form == form && shield.Skill == skill && shield.Variant == 0) Visuals.RemoveAt(index); } } for (int index = Visuals.Count - 1; index >= 0; index--) { ReaperSkillVisual existing = Visuals[index]; bool persistent = IsPersistentOwnerVisual(form, skill, variant); if (existing.Owner != owner || existing.Form != form || existing.Skill != skill || existing.Variant != variant) { continue; } // A wide attack hitting several targets in the same update becomes one // centered execution instead of several opaque copies of the same VFX. bool sameAction = existing.ActionId == actionId; bool nearbyBurst = (existing.Age <= 7 || persistent) && Vector2.DistanceSquared(existing.Center, center) <= 240f * 240f; if (sameAction || nearbyBurst) { int samples = Math.Clamp(existing.CenterSamples, 1, 64); float sampleWeight = 1f / (samples + 1f); existing.Center = Vector2.Lerp(existing.Center, center, sampleWeight); existing.Direction = Vector2.Lerp(existing.Direction, direction, sampleWeight) .SafeNormalize(existing.Direction); existing.CenterSamples = Math.Min(64, samples + 1); existing.Level = Math.Max(existing.Level, level); existing.Duration = Math.Max(existing.Duration, GetDuration(form, skill, existing.Level, variant)); existing.Age = persistent ? 0 : Math.Min(existing.Age, 3); return; } } if (Visuals.Count >= MaximumVisuals) Visuals.RemoveAt(0); Visuals.Add(new ReaperSkillVisual { Owner = owner, Form = form, Stage = stage, Skill = skill, Level = level, Variant = variant, ActionId = actionId, Seed = seed, Center = center, Direction = direction, CenterSamples = 1, Duration = GetDuration(form, skill, level, variant) }); if (IsNearScreen(center, 620f)) PlaySkillSound(form, skill, level, variant, center); // Unique skills keep their restrained camera punctuation, but never tint // the entire viewport. A three-frame pale impact used to present as a // one-frame white screen when several automatic triggers were merged. if (level >= 3 && IsNearScreen(center)) ReaperVfxDirector.TriggerGlobalImpact(direction, 2.8f, 6, Color.Transparent, 0f, 0, 0.06f); } public override void OnWorldUnload() => Visuals.Clear(); public override void Unload() => Visuals.Clear(); public override void PostUpdateEverything() { if (Main.gameMenu) { Visuals.Clear(); return; } for (int index = Visuals.Count - 1; index >= 0; index--) { if (++Visuals[index].Age >= Visuals[index].Duration) Visuals.RemoveAt(index); } } public override void PostDrawTiles() { if (Main.dedServ || Main.gameMenu || Visuals.Count == 0) return; SpriteBatch batch = Main.spriteBatch; batch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, DepthStencilState.None, RasterizerState.CullNone, null, Main.GameViewMatrix.TransformationMatrix); Texture2D pixel = TextureAssets.MagicPixel.Value; foreach (ReaperSkillVisual visual in Visuals) { Vector2 center = ResolveCenter(visual); if (!IsNearScreen(center, 440f)) continue; DrawVisual(batch, pixel, visual, center); } batch.End(); } private static int GetDuration(ReaperFormId form, int skill, int level, int variant) { if (form == ReaperFormId.Blood && skill == 2 && variant == 0) return 300; if (form == ReaperFormId.Blood && skill == 1 && variant == 0) return 120; if (form == ReaperFormId.Infernal && skill == 0 && variant == 0) return 130; return variant > 0 ? 28 + level * 3 : level switch { 1 => 30, 2 => 37, _ => 46 }; } private static bool IsPersistentOwnerVisual(ReaperFormId form, int skill, int variant) => variant == 0 && (form == ReaperFormId.Blood && skill is 1 or 2 || form == ReaperFormId.Infernal && skill == 0); private static Vector2 ResolveCenter(ReaperSkillVisual visual) { bool followsOwner = visual.Form switch { ReaperFormId.Blood => visual.Skill is 1 or 2 && visual.Variant == 0, ReaperFormId.Infernal => visual.Skill is 0 or 2, ReaperFormId.Soul => visual.Skill == 1, _ => false }; if (!followsOwner || visual.Owner < 0 || visual.Owner >= Main.maxPlayers) return visual.Center; Player owner = Main.player[visual.Owner]; return owner.active ? owner.MountedCenter : visual.Center; } private static bool IsNearScreen(Vector2 center, float margin = 180f) { Rectangle area = new( (int)(Main.screenPosition.X - margin), (int)(Main.screenPosition.Y - margin), Main.screenWidth + (int)(margin * 2f), Main.screenHeight + (int)(margin * 2f)); return area.Contains(center.ToPoint()); } private static void PlaySkillSound( ReaperFormId form, int skill, int level, int variant, Vector2 center) { SoundStyle primary = form switch { ReaperFormId.Bone => skill == 2 ? SoundID.NPCDeath6 : SoundID.Item71, ReaperFormId.Blood => skill == 0 ? SoundID.Item29 : SoundID.NPCDeath6, ReaperFormId.Infernal => skill == 0 ? SoundID.Item74 : SoundID.Item14, ReaperFormId.Frost => SoundID.Shatter, ReaperFormId.Soul => skill == 1 ? SoundID.NPCDeath6 : SoundID.Item8, ReaperFormId.Void => skill == 1 ? SoundID.Item122 : SoundID.Item8, _ => SoundID.Item1 }; SoundEngine.PlaySound(primary with { Volume = MathHelper.Clamp(0.42f + level * 0.09f, 0f, 0.74f), Pitch = MathHelper.Clamp(-0.22f + level * 0.08f + variant * 0.035f, -1f, 1f), PitchVariance = 0.045f, MaxInstances = 2, SoundLimitBehavior = SoundLimitBehavior.IgnoreNew }, center); // Lv.3 executions get one restrained low accent. The shared instance cap // prevents simultaneous multi-target triggers from producing audio peaks. if (level >= 3 && variant == 0) { SoundEngine.PlaySound(SoundID.Item71 with { Volume = 0.36f, Pitch = -0.52f, PitchVariance = 0.03f, MaxInstances = 2, SoundLimitBehavior = SoundLimitBehavior.IgnoreNew }, center); } } private static void DrawVisual(SpriteBatch batch, Texture2D pixel, ReaperSkillVisual visual, Vector2 center) { float progress = visual.Age / (float)Math.Max(1, visual.Duration); float reveal = MathHelper.SmoothStep(0f, 1f, MathHelper.Clamp(progress / 0.22f, 0f, 1f)); float fade = 1f - MathHelper.SmoothStep(0f, 1f, MathHelper.Clamp((progress - 0.66f) / 0.34f, 0f, 1f)); float opacity = reveal * fade; float pulse = 1f + (float)Math.Sin(progress * MathHelper.TwoPi * 2f + visual.Seed * 0.013f) * 0.055f; Color primary = ReaperCombatRegistry.GetPrimaryColor(visual.Form) with { A = 0 }; Color secondary = ReaperCombatRegistry.GetSecondaryColor(visual.Form) with { A = 0 }; switch (visual.Form) { case ReaperFormId.Bone: if (visual.Skill == 0) DrawBoneShatter(batch, pixel, visual, center, progress, opacity, pulse, primary, secondary); else if (visual.Skill == 1) DrawBoneRestraint(batch, pixel, visual, center, progress, opacity, primary, secondary); else DrawBoneChorus(batch, pixel, visual, center, progress, opacity, primary, secondary); break; case ReaperFormId.Blood: if (visual.Skill == 0) DrawBloodDrink(batch, pixel, visual, center, progress, opacity, primary, secondary); else if (visual.Skill == 1) DrawBloodFrenzy(batch, pixel, visual, center, progress, opacity, primary, secondary); else DrawBloodShield(batch, pixel, visual, center, progress, opacity, primary, secondary); break; case ReaperFormId.Infernal: if (visual.Skill == 0) DrawInfernalMomentum(batch, pixel, visual, center, progress, opacity, primary, secondary); else if (visual.Skill == 1) DrawInfernalScorch(batch, pixel, visual, center, progress, opacity, primary, secondary); else DrawInfernalCircuit(batch, pixel, visual, center, progress, opacity, primary, secondary); break; case ReaperFormId.Frost: if (visual.Skill == 0) DrawFrostMark(batch, pixel, visual, center, progress, opacity, primary, secondary); else if (visual.Skill == 1) DrawFrostRefraction(batch, pixel, visual, center, progress, opacity, primary, secondary); else DrawFrostExecution(batch, pixel, visual, center, progress, opacity, primary, secondary); break; case ReaperFormId.Soul: if (visual.Skill == 0) DrawSoulEcho(batch, pixel, visual, center, progress, opacity, primary, secondary); else if (visual.Skill == 1) DrawSoulFamiliar(batch, pixel, visual, center, progress, opacity, primary, secondary); else DrawSoulFate(batch, pixel, visual, center, progress, opacity, primary, secondary); break; case ReaperFormId.Void: if (visual.Skill == 0) DrawVoidIntersection(batch, pixel, visual, center, progress, opacity, primary, secondary); else if (visual.Skill == 1) DrawVoidResidual(batch, pixel, visual, center, progress, opacity, primary, secondary); else DrawVoidCoordinate(batch, pixel, visual, center, progress, opacity, primary, secondary); break; } } private static void DrawBoneShatter(SpriteBatch batch, Texture2D pixel, ReaperSkillVisual v, Vector2 center, float p, float opacity, float pulse, Color primary, Color secondary) { int spikes = v.Level switch { 1 => 3, 2 => 6, _ => 10 }; float radius = (40f + v.Level * 15f) * pulse; float collapse = 1f - MathHelper.SmoothStep(0f, 1f, MathHelper.Clamp((p - 0.08f) / 0.48f, 0f, 1f)); for (int i = 0; i < spikes; i++) { Vector2 direction = (i * MathHelper.TwoPi / spikes - MathHelper.PiOver2).ToRotationVector2(); Vector2 tangent = direction.RotatedBy(MathHelper.PiOver2); Vector2 tip = center + direction * (10f + radius * collapse); Vector2 root = center + direction * (radius + 22f) + tangent * ((i & 1) == 0 ? 5f : -5f); DrawSegment(batch, pixel, root, tip, secondary * opacity, 3.4f + v.Level); DrawSegment(batch, pixel, root + tangent * 3f, tip, primary * (opacity * 0.72f), 1.5f); } DrawRing(batch, pixel, center, radius * (0.56f + 0.2f * collapse), spikes * 2, primary * (opacity * 0.58f), 2.5f, p * 0.8f); if (v.Level >= 2) { for (int i = 0; i < 6; i++) { float x = (i - 2.5f) * 14f; float bow = 18f - Math.Abs(i - 2.5f) * 3f; DrawSegment(batch, pixel, center + new Vector2(x, 38f), center + new Vector2(x * 0.7f, -bow), secondary * (opacity * 0.48f), 2.8f); } } if (v.Level >= 3) { Vector2 skull = center + new Vector2(0f, -44f); DrawArc(batch, pixel, skull, 34f, MathHelper.Pi, MathHelper.Pi, 16, secondary * (opacity * 0.7f), 4f); DrawDiamond(batch, pixel, skull + new Vector2(-12f, 2f), 5f, primary * opacity, 2f, 0f); DrawDiamond(batch, pixel, skull + new Vector2(12f, 2f), 5f, primary * opacity, 2f, 0f); if (p > 0.42f) DrawCross(batch, pixel, center, 72f, secondary * (opacity * 0.88f), 4.5f, v.Direction.ToRotation()); } } private static void DrawBoneRestraint(SpriteBatch batch, Texture2D pixel, ReaperSkillVisual v, Vector2 center, float p, float opacity, Color primary, Color secondary) { float radius = 43f + v.Level * 13f; DrawChain(batch, pixel, center + new Vector2(-radius, -radius * 0.45f), center + new Vector2(radius, radius * 0.45f), primary * opacity, secondary * opacity, 8 + v.Level * 2); DrawChain(batch, pixel, center + new Vector2(radius, -radius * 0.45f), center + new Vector2(-radius, radius * 0.45f), primary * opacity, secondary * opacity, 8 + v.Level * 2); if (v.Level >= 2) { for (int i = 0; i < 6; i++) { float angle = i * MathHelper.TwoPi / 6f; Vector2 direction = angle.ToRotationVector2(); Vector2 basePoint = center + direction * radius; DrawSegment(batch, pixel, basePoint - Vector2.UnitY * 42f, basePoint + Vector2.UnitY * 42f, secondary * (opacity * 0.72f), 4f); DrawSegment(batch, pixel, basePoint - Vector2.UnitY * 42f, basePoint - Vector2.UnitY * 51f + direction * 8f, primary * opacity, 2.2f); } } if (v.Level >= 3) { Vector2 palm = center + new Vector2(0f, 58f); for (int finger = -2; finger <= 2; finger++) DrawSegment(batch, pixel, palm + new Vector2(finger * 8f, 0f), center + new Vector2(finger * 14f, -42f), secondary * (opacity * 0.58f), 6f - Math.Abs(finger) * 0.6f); float fall = MathHelper.SmoothStep(0f, 1f, MathHelper.Clamp((p - 0.42f) / 0.25f, 0f, 1f)); float bladeY = MathHelper.Lerp(-120f, 20f, fall); DrawSegment(batch, pixel, center + new Vector2(-82f, bladeY), center + new Vector2(82f, bladeY), secondary * opacity, 8f); DrawSegment(batch, pixel, center + new Vector2(-80f, bladeY - 4f), center + new Vector2(80f, bladeY - 4f), primary * opacity, 2f); } } private static void DrawBoneChorus(SpriteBatch batch, Texture2D pixel, ReaperSkillVisual v, Vector2 center, float p, float opacity, Color primary, Color secondary) { int ghosts = v.Level switch { 1 => 2, 2 => 4, _ => 6 }; float radius = 54f + v.Level * 9f; for (int i = 0; i < ghosts; i++) { float angle = i * MathHelper.TwoPi / ghosts + 0.35f; Vector2 position = center + angle.ToRotationVector2() * radius; DrawGhost(batch, pixel, position, center - position, primary * (opacity * 0.62f), secondary * (opacity * 0.82f), 0.8f); Vector2 inward = (center - position).SafeNormalize(Vector2.UnitX); float sweep = MathHelper.Lerp(-0.7f, 0.65f, MathHelper.SmoothStep(0f, 1f, MathHelper.Clamp(p * 1.7f, 0f, 1f))); DrawSegment(batch, pixel, position - inward.RotatedBy(sweep) * 10f, position + inward.RotatedBy(sweep) * 52f, secondary * opacity, 3f); } DrawRing(batch, pixel, center, radius * 0.72f, 30, primary * (opacity * 0.42f), 3f, -p * 2f); if (v.Level >= 3 && p > 0.43f) { Vector2 reaper = center - Vector2.UnitY * 82f; DrawGhost(batch, pixel, reaper, Vector2.UnitY, primary * (opacity * 0.54f), secondary * opacity, 1.8f); DrawSegment(batch, pixel, reaper + new Vector2(-78f, 27f), center + new Vector2(78f, 25f), secondary * opacity, 7f); } } private static void DrawBloodDrink(SpriteBatch batch, Texture2D pixel, ReaperSkillVisual v, Vector2 center, float p, float opacity, Color primary, Color secondary) { Vector2 owner = v.Owner >= 0 && v.Owner < Main.maxPlayers && Main.player[v.Owner].active ? Main.player[v.Owner].MountedCenter : center + v.Direction * 110f; Vector2 delta = owner - center; Vector2 tangent = delta.SafeNormalize(Vector2.UnitX).RotatedBy(MathHelper.PiOver2); int strands = v.Level == 1 ? 1 : 3; for (int strand = 0; strand < strands; strand++) { Vector2 previous = center; for (int point = 1; point <= 14; point++) { float t = point / 14f; float wave = (float)Math.Sin(t * MathHelper.TwoPi * (1f + v.Level * 0.3f) + strand * 2.1f + p * 5f) * (strand == 0 ? 5f : 10f) * (float)Math.Sin(t * MathHelper.Pi); Vector2 next = center + delta * t + tangent * wave; DrawSegment(batch, pixel, previous, next, (strand == 0 ? secondary : primary) * opacity, strand == 0 ? 3.4f : 1.8f); previous = next; } } if (v.Level >= 2) { Vector2 rose = Vector2.Lerp(center, owner, 0.46f); for (int petal = 0; petal < (v.Level >= 3 ? 8 : 5); petal++) DrawArc(batch, pixel, rose, 17f + petal % 2 * 4f, petal * MathHelper.TwoPi / (v.Level >= 3 ? 8f : 5f), 1.7f, 7, primary * (opacity * 0.65f), 2.5f); } if (v.Level >= 3) { for (int petal = 0; petal < 6; petal++) { float angle = petal * MathHelper.TwoPi / 6f; DrawDiamond(batch, pixel, center + angle.ToRotationVector2() * 29f, 18f, secondary * (opacity * 0.72f), 3f, angle); } DrawArc(batch, pixel, center, 60f, -2.4f, 4.7f, 28, secondary * opacity, 7f); } } private static void DrawBloodFrenzy(SpriteBatch batch, Texture2D pixel, ReaperSkillVisual v, Vector2 center, float p, float opacity, Color primary, Color secondary) { if (v.Variant == 1) { float burst = MathHelper.SmoothStep(0f, 1f, MathHelper.Clamp(p * 1.7f, 0f, 1f)); DrawRing(batch, pixel, center, MathHelper.Lerp(28f, 112f, burst), 36, secondary * opacity, 7f, -p); for (int bead = 0; bead < 14; bead++) { Vector2 direction = (bead * MathHelper.TwoPi / 14f + 0.18f).ToRotationVector2(); Vector2 start = center + direction * MathHelper.Lerp(22f, 62f, burst); DrawDiamond(batch, pixel, start + direction * burst * 58f, 7f, (bead % 2 == 0 ? secondary : primary) * opacity, 2.4f, direction.ToRotation()); } return; } if (v.Variant >= 2) { float facing = v.Direction.ToRotation(); int crescents = v.Level == 2 ? 2 : 1; float radius = v.Level >= 3 ? 132f : v.Level == 2 ? 110f : 92f; float reveal = MathHelper.SmoothStep(0f, 1f, MathHelper.Clamp(p * 2.2f, 0f, 1f)); for (int slash = 0; slash < crescents; slash++) { float side = crescents == 1 ? 0f : slash == 0 ? -0.22f : 0.22f; Vector2 slashCenter = center + v.Direction * (radius * 0.18f) + v.Direction.RotatedBy(MathHelper.PiOver2) * side * 34f; float start = facing - 1.9f + side; DrawArc(batch, pixel, slashCenter, radius, start, 3.8f, 42, new Color(65, 0, 13, 0) * (opacity * 0.82f), v.Level >= 3 ? 31f : 25f); DrawArc(batch, pixel, slashCenter, radius, start, 3.8f, 42, primary * (opacity * (0.62f + reveal * 0.2f)), v.Level >= 3 ? 20f : 15f); DrawArc(batch, pixel, slashCenter, radius + 2f, start, 3.8f, 42, secondary * opacity, v.Level >= 3 ? 4.5f : 3.5f); } return; } float beat = 1f + Math.Max(0f, (float)Math.Sin(p * MathHelper.TwoPi * 3f)) * 0.22f; DrawRing(batch, pixel, center, 42f * beat, 28, primary * opacity, 5f, 0f); DrawSegment(batch, pixel, center + new Vector2(-33f, 0f), center + new Vector2(-12f, 0f), secondary * opacity, 2.5f); DrawSegment(batch, pixel, center + new Vector2(-12f, 0f), center + new Vector2(-5f, -11f), secondary * opacity, 2.5f); DrawSegment(batch, pixel, center + new Vector2(-5f, -11f), center + new Vector2(4f, 13f), secondary * opacity, 2.5f); DrawSegment(batch, pixel, center + new Vector2(4f, 13f), center + new Vector2(13f, 0f), secondary * opacity, 2.5f); DrawSegment(batch, pixel, center + new Vector2(13f, 0f), center + new Vector2(34f, 0f), secondary * opacity, 2.5f); if (v.Level >= 2) { for (int side = -1; side <= 1; side += 2) { Vector2 root = center + new Vector2(side * 10f, -3f); Vector2 tip = center + new Vector2(side * 82f, -45f); DrawArc(batch, pixel, root, 78f, side > 0 ? -1.05f : MathHelper.Pi + 0.05f, side * 1.15f, 14, primary * (opacity * 0.62f), 7f); DrawSegment(batch, pixel, root, tip, secondary * (opacity * 0.72f), 3f); } } if (v.Level >= 3) { Vector2 moon = center + new Vector2(0f, -68f); for (int layer = 0; layer < 5; layer++) DrawRing(batch, pixel, moon, 26f - layer * 4f, 28, primary * (opacity * (0.13f + layer * 0.035f)), 5f, 0f); DrawArc(batch, pixel, moon, 31f, -1.85f, 3.7f, 26, secondary * opacity, 4.5f); } } private static void DrawBloodShield(SpriteBatch batch, Texture2D pixel, ReaperSkillVisual v, Vector2 center, float p, float opacity, Color primary, Color secondary) { if (v.Variant == 1) { float radius = MathHelper.Lerp(24f, 88f, MathHelper.SmoothStep(0f, 1f, MathHelper.Clamp(p * 1.7f, 0f, 1f))); DrawRing(batch, pixel, center, radius, 34, secondary * opacity, 8f, p); for (int i = 0; i < 12; i++) { Vector2 direction = (i * MathHelper.TwoPi / 12f).ToRotationVector2(); DrawSegment(batch, pixel, center + direction * radius * 0.7f, center + direction * (radius + 25f), primary * opacity, 4f); } return; } if (v.Variant >= 2) { float angle = v.Direction.ToRotation(); Vector2 strikeCenter = center + v.Direction * 30f; DrawArc(batch, pixel, strikeCenter, v.Level >= 3 ? 101f : 86f, angle - 1.7f, 3.4f, 34, primary * (opacity * 0.78f), v.Level >= 3 ? 20f : 16f); DrawArc(batch, pixel, strikeCenter, v.Level >= 3 ? 104f : 89f, angle - 1.7f, 3.4f, 34, secondary * opacity, 4f); return; } int shards = v.Level switch { 1 => 4, 2 => 8, _ => 12 }; float radiusOrbit = 45f + v.Level * 5f; for (int i = 0; i < shards; i++) { float angle = i * MathHelper.TwoPi / shards + p * 2.4f; Vector2 position = center + angle.ToRotationVector2() * radiusOrbit; DrawDiamond(batch, pixel, position, v.Level >= 2 ? 10f : 8f, (i % 2 == 0 ? secondary : primary) * opacity, 3f, angle); } if (v.Level >= 2) DrawRing(batch, pixel, center, radiusOrbit - 9f, 32, primary * (opacity * 0.34f), 5f, -p); if (v.Level >= 3) { DrawArc(batch, pixel, center, 65f, -2.25f, 4.5f, 28, primary * (opacity * 0.54f), 9f); Vector2 crown = center - Vector2.UnitY * 69f; DrawSegment(batch, pixel, crown + new Vector2(-23f, 10f), crown + new Vector2(-15f, -8f), secondary * opacity, 3f); DrawSegment(batch, pixel, crown + new Vector2(-15f, -8f), crown, secondary * opacity, 3f); DrawSegment(batch, pixel, crown, crown + new Vector2(15f, -8f), secondary * opacity, 3f); DrawSegment(batch, pixel, crown + new Vector2(15f, -8f), crown + new Vector2(23f, 10f), secondary * opacity, 3f); } } private static void DrawInfernalMomentum(SpriteBatch batch, Texture2D pixel, ReaperSkillVisual v, Vector2 center, float p, float opacity, Color primary, Color secondary) { Vector2 direction = v.Direction.SafeNormalize(Vector2.UnitX); Vector2 tangent = direction.RotatedBy(MathHelper.PiOver2); for (int foot = 0; foot < 4 + v.Level; foot++) { Vector2 position = center - direction * (18f + foot * 17f) + tangent * ((foot & 1) == 0 ? 7f : -7f); DrawDiamond(batch, pixel, position, 7f + v.Level, primary * (opacity * (1f - foot * 0.1f)), 3f, direction.ToRotation()); } if (v.Level >= 2) { Vector2 ghost = center - direction * 66f; DrawGhost(batch, pixel, ghost, direction, new Color(35, 20, 12, 0) * opacity, secondary * (opacity * 0.66f), 1.25f); DrawSegment(batch, pixel, ghost - tangent * 42f, ghost + direction * 90f + tangent * 38f, secondary * opacity, 5f); } if (v.Level >= 3) DrawStar(batch, pixel, center + direction * 25f, 67f, 27f, 5, primary * (opacity * 0.76f), 4f, p * 1.8f); } private static void DrawInfernalScorch(SpriteBatch batch, Texture2D pixel, ReaperSkillVisual v, Vector2 center, float p, float opacity, Color primary, Color secondary) { if (v.Variant > 0) { float burst = MathHelper.SmoothStep(0f, 1f, MathHelper.Clamp(p * 1.8f, 0f, 1f)); DrawRing(batch, pixel, center, MathHelper.Lerp(34f, 102f, burst), 36, secondary * opacity, 11f, p); DrawStar(batch, pixel, center, 86f, 37f, 5, primary * (opacity * 0.86f), 7f, -p * 1.4f); for (int rib = -1; rib <= 1; rib += 2) { DrawArc(batch, pixel, center + new Vector2(rib * 20f, 0f), 68f, rib < 0 ? -1.35f : MathHelper.Pi - 1.35f, rib * 2.7f, 22, secondary * opacity, 6f); } return; } float size = 45f + v.Level * 8f; Vector2 head = center - Vector2.UnitY * size * 0.58f; DrawRing(batch, pixel, head, 12f, 18, secondary * opacity, 3f, 0f); DrawSegment(batch, pixel, head + Vector2.UnitY * 12f, center + Vector2.UnitY * 25f, secondary * opacity, 4f); DrawSegment(batch, pixel, center - Vector2.UnitY * 9f, center + new Vector2(-size * 0.48f, 8f), primary * opacity, 3f); DrawSegment(batch, pixel, center - Vector2.UnitY * 9f, center + new Vector2(size * 0.48f, 8f), primary * opacity, 3f); DrawSegment(batch, pixel, center + Vector2.UnitY * 24f, center + new Vector2(-size * 0.33f, size * 0.68f), secondary * opacity, 3f); DrawSegment(batch, pixel, center + Vector2.UnitY * 24f, center + new Vector2(size * 0.33f, size * 0.68f), secondary * opacity, 3f); int ribs = 2 + v.Level; for (int i = 0; i < ribs; i++) { float y = -2f + i * 9f; DrawArc(batch, pixel, center + new Vector2(0f, y), 19f + i * 3f, MathHelper.Pi, MathHelper.Pi, 9, primary * (opacity * 0.7f), 3f); } if (v.Level >= 2) { for (int i = 0; i < 3; i++) { Vector2 hot = center + (i * MathHelper.TwoPi / 3f - MathHelper.PiOver2).ToRotationVector2() * (24f + i * 6f); DrawRing(batch, pixel, hot, 7f + p * 9f, 14, secondary * opacity, 4f, 0f); } } if (v.Level >= 3) DrawRing(batch, pixel, center, MathHelper.Lerp(35f, 88f, p), 32, secondary * (opacity * 0.8f), 8f, 0f); } private static void DrawInfernalCircuit(SpriteBatch batch, Texture2D pixel, ReaperSkillVisual v, Vector2 center, float p, float opacity, Color primary, Color secondary) { float radius = 51f + v.Level * 17f; DrawRing(batch, pixel, center, radius * (0.75f + p * 0.25f), 36, secondary * opacity, 6f, p * 2f); if (v.Level >= 2) DrawStar(batch, pixel, center, radius, radius * 0.42f, 5, primary * opacity, 4f, -p * 1.5f); if (v.Level >= 3) { Vector2 portal = center + v.Direction * 54f; DrawArc(batch, pixel, portal, radius * 0.78f, 0f, MathHelper.TwoPi, 38, primary * (opacity * 0.72f), 12f); DrawArc(batch, pixel, portal, radius * 0.62f, 0f, MathHelper.TwoPi, 34, secondary * opacity, 4f); Vector2 tangent = v.Direction.RotatedBy(MathHelper.PiOver2); float sweep = MathHelper.Lerp(-0.8f, 0.8f, MathHelper.SmoothStep(0f, 1f, p)); Vector2 slashDirection = v.Direction.RotatedBy(sweep); DrawSegment(batch, pixel, portal - slashDirection * 112f - tangent * 18f, portal + slashDirection * 112f + tangent * 18f, secondary * opacity, 11f); } } private static void DrawFrostMark(SpriteBatch batch, Texture2D pixel, ReaperSkillVisual v, Vector2 center, float p, float opacity, Color primary, Color secondary) { float radius = 44f + v.Level * 13f; for (int arm = 0; arm < 6; arm++) { Vector2 direction = (arm * MathHelper.TwoPi / 6f).ToRotationVector2(); Vector2 tangent = direction.RotatedBy(MathHelper.PiOver2); DrawSegment(batch, pixel, center, center + direction * radius, secondary * opacity, 3.5f); DrawSegment(batch, pixel, center + direction * radius * 0.62f, center + direction * radius * 0.78f + tangent * 10f, primary * opacity, 2f); DrawSegment(batch, pixel, center + direction * radius * 0.62f, center + direction * radius * 0.78f - tangent * 10f, primary * opacity, 2f); } if (v.Level >= 2) { for (int mirror = 0; mirror < 6; mirror++) { float angle = mirror * MathHelper.TwoPi / 6f + p * 0.4f; DrawDiamond(batch, pixel, center + angle.ToRotationVector2() * (radius + 25f), 16f, primary * (opacity * 0.66f), 3f, angle); } } if (v.Level >= 3) { DrawPolygon(batch, pixel, center, radius * 1.28f, 8, secondary * (opacity * 0.8f), 6f, p * 0.2f); DrawCross(batch, pixel, center, radius * 1.18f, Color.White with { A = 0 } * opacity, 4f, MathHelper.PiOver4); } } private static void DrawFrostRefraction(SpriteBatch batch, Texture2D pixel, ReaperSkillVisual v, Vector2 center, float p, float opacity, Color primary, Color secondary) { int mirrors = v.Level switch { 1 => 1, 2 => 3, _ => 6 }; for (int i = 0; i < mirrors; i++) { float angle = i * MathHelper.TwoPi / mirrors + p * 0.65f; Vector2 mirror = center + angle.ToRotationVector2() * (42f + v.Level * 10f); DrawPolygon(batch, pixel, mirror, 17f + v.Level * 2f, 6, (i % 2 == 0 ? secondary : primary) * (opacity * 0.72f), 3f, angle); DrawSegment(batch, pixel, mirror - v.Direction.RotatedBy(angle * 0.08f) * 28f, mirror + v.Direction.RotatedBy(angle * 0.08f) * 28f, secondary * opacity, 3f); } Vector2 cometEnd = center + v.Direction * (70f + v.Level * 28f); DrawSegment(batch, pixel, center - v.Direction * 45f, cometEnd, secondary * opacity, 7f + v.Level * 2f); DrawSegment(batch, pixel, center - v.Direction * 58f, cometEnd, Color.White with { A = 0 } * opacity, 2f); if (v.Level >= 3) DrawRing(batch, pixel, cometEnd, 38f + p * 18f, 24, primary * opacity, 6f, p); } private static void DrawFrostExecution(SpriteBatch batch, Texture2D pixel, ReaperSkillVisual v, Vector2 center, float p, float opacity, Color primary, Color secondary) { float length = 70f + v.Level * 22f; DrawCross(batch, pixel, center, length, secondary * opacity, 7f + v.Level, MathHelper.PiOver4); DrawCross(batch, pixel, center, length, Color.White with { A = 0 } * opacity, 2f, MathHelper.PiOver4); if (v.Level >= 2) { float frame = 65f + v.Level * 14f; DrawPolygon(batch, pixel, center, frame, 4, primary * (opacity * 0.72f), 5f, MathHelper.PiOver4); DrawSegment(batch, pixel, center - Vector2.UnitX * frame, center + Vector2.UnitX * frame, primary * (opacity * 0.46f), 3f); DrawSegment(batch, pixel, center - Vector2.UnitY * frame, center + Vector2.UnitY * frame, primary * (opacity * 0.46f), 3f); } if (v.Level >= 3) { DrawPolygon(batch, pixel, center, 82f, 6, secondary * (opacity * 0.64f), 8f, MathHelper.PiOver2); for (int crack = 0; crack < 6; crack++) { Vector2 direction = (crack * MathHelper.TwoPi / 6f + 0.2f).ToRotationVector2(); DrawSegment(batch, pixel, center + direction * 62f, center + direction * (108f + crack % 2 * 16f), Color.White with { A = 0 } * (opacity * 0.72f), 1.7f); } } } private static void DrawSoulEcho(SpriteBatch batch, Texture2D pixel, ReaperSkillVisual v, Vector2 center, float p, float opacity, Color primary, Color secondary) { int ghosts = v.Level; for (int i = 0; i < ghosts; i++) { Vector2 offset = -v.Direction * (28f + i * 29f) + v.Direction.RotatedBy(MathHelper.PiOver2) * (i - (ghosts - 1) * 0.5f) * 15f; Vector2 ghost = center + offset; DrawGhost(batch, pixel, ghost, v.Direction, primary * (opacity * (0.52f + i * 0.09f)), secondary * (opacity * 0.82f), 1f + i * 0.12f); DrawArc(batch, pixel, ghost, 45f + i * 6f, v.Direction.ToRotation() - 1.7f, 2.8f, 14, secondary * opacity, 4f); } if (v.Level >= 3 && p > 0.36f) { Vector2 giant = center - Vector2.UnitY * 64f; DrawGhost(batch, pixel, giant, v.Direction, primary * (opacity * 0.45f), secondary * opacity, 1.7f); DrawArc(batch, pixel, giant, 88f, v.Direction.ToRotation() - 1.75f, 2.9f, 24, secondary * opacity, 7f); } } private static void DrawSoulFamiliar(SpriteBatch batch, Texture2D pixel, ReaperSkillVisual v, Vector2 center, float p, float opacity, Color primary, Color secondary) { float scale = v.Level switch { 1 => 1f, 2 => 1.28f, _ => 1.58f }; DrawGhost(batch, pixel, center - Vector2.UnitY * 32f, v.Direction, primary * (opacity * 0.62f), secondary * opacity, scale); int gates = v.Level == 1 ? 1 : v.Level == 2 ? 2 : 3; for (int i = 0; i < gates; i++) { float angle = i * MathHelper.TwoPi / gates + p; Vector2 gate = center + angle.ToRotationVector2() * (44f + v.Level * 13f); DrawArc(batch, pixel, gate, 20f + v.Level * 4f, -MathHelper.PiOver2, MathHelper.Pi, 14, primary * (opacity * 0.64f), 5f); } int slashes = v.Level switch { 1 => 2, 2 => 3, _ => 4 }; for (int i = 0; i < slashes; i++) { float angle = v.Direction.ToRotation() + (i - (slashes - 1) * 0.5f) * 0.42f; Vector2 direction = angle.ToRotationVector2(); DrawSegment(batch, pixel, center - direction * 18f, center + direction * (55f + v.Level * 17f), secondary * (opacity * 0.76f), 4f + v.Level); } if (v.Level >= 3) DrawRing(batch, pixel, center, 76f, 32, primary * (opacity * 0.55f), 6f, -p * 1.8f); } private static void DrawSoulFate(SpriteBatch batch, Texture2D pixel, ReaperSkillVisual v, Vector2 center, float p, float opacity, Color primary, Color secondary) { if (v.Variant > 0) { float collapse = 1f - MathHelper.SmoothStep(0f, 1f, MathHelper.Clamp((p - 0.18f) / 0.55f, 0f, 1f)); DrawRing(batch, pixel, center, (54f + v.Level * 15f) * collapse, 34, primary * (opacity * 0.74f), 8f, -p * 1.5f); Vector2 slash = v.Direction.SafeNormalize(Vector2.UnitX); DrawSegment(batch, pixel, center - slash * 84f, center + slash * 84f, new Color(28, 8, 48, 0) * opacity, 18f); DrawSegment(batch, pixel, center - slash * 88f, center + slash * 88f, secondary * opacity, 5f); if (v.Level >= 2) { Vector2 cross = slash.RotatedBy(MathHelper.PiOver2); DrawSegment(batch, pixel, center - cross * 70f, center + cross * 70f, primary * opacity, 4.5f); } return; } int runes = v.Level switch { 1 => 3, 2 => 5, _ => 7 }; float radius = 44f + v.Level * 11f; for (int i = 0; i < runes; i++) { float angle = i * MathHelper.TwoPi / runes - p * 0.8f; Vector2 rune = center + angle.ToRotationVector2() * radius; Vector2 radial = angle.ToRotationVector2(); Vector2 tangent = radial.RotatedBy(MathHelper.PiOver2); DrawSegment(batch, pixel, rune - tangent * 7f, rune + tangent * 7f, secondary * opacity, 2f); DrawSegment(batch, pixel, rune - radial * 5f, rune + radial * 8f, primary * opacity, 3f); if (v.Level >= 2) DrawDiamond(batch, pixel, rune, 9f, primary * (opacity * 0.45f), 1.5f, angle); } if (v.Level >= 2) { Vector2 penStart = center + new Vector2(-55f, -55f); Vector2 penEnd = center + new Vector2(48f, 44f); DrawSegment(batch, pixel, penStart, penEnd, secondary * opacity, 3f); DrawDiamond(batch, pixel, penEnd, 8f, primary * opacity, 2f, (penEnd - penStart).ToRotation()); } if (v.Level >= 3) { Vector2 topLeft = center + new Vector2(-58f, -76f); Vector2 topRight = center + new Vector2(58f, -76f); Vector2 bottomLeft = center + new Vector2(-58f, 70f); Vector2 bottomRight = center + new Vector2(58f, 70f); DrawSegment(batch, pixel, topLeft, topRight, primary * (opacity * 0.62f), 7f); DrawSegment(batch, pixel, topLeft, bottomLeft, primary * (opacity * 0.62f), 7f); DrawSegment(batch, pixel, topRight, bottomRight, primary * (opacity * 0.62f), 7f); DrawSegment(batch, pixel, bottomLeft, bottomRight, primary * (opacity * 0.62f), 7f); DrawSegment(batch, pixel, center + new Vector2(-74f, 42f), center + new Vector2(76f, -35f), secondary * opacity, 9f); } } private static void DrawVoidIntersection(SpriteBatch batch, Texture2D pixel, ReaperSkillVisual v, Vector2 center, float p, float opacity, Color primary, Color secondary) { float length = 92f + v.Level * 22f; float width = 7f + v.Level * 2f; float reveal = MathHelper.SmoothStep(0f, 1f, MathHelper.Clamp(p / 0.28f, 0f, 1f)); Vector2 facing = v.Direction.SafeNormalize(Vector2.UnitX); ReaperVoidBackdropRenderer.DrawHitRift(batch, center, facing.RotatedBy(-0.54f), length, width, v.Seed, reveal, opacity); ReaperVoidBackdropRenderer.DrawHitRift(batch, center, facing.RotatedBy(0.57f), length * (v.Level >= 2 ? 0.94f : 0.78f), width * 0.88f, v.Seed ^ 0x2718, reveal, opacity * 0.92f); } private static void DrawVoidResidual(SpriteBatch batch, Texture2D pixel, ReaperSkillVisual v, Vector2 center, float p, float opacity, Color primary, Color secondary) { int cuts = v.Level; Vector2 tangent = v.Direction.RotatedBy(MathHelper.PiOver2); for (int i = 0; i < cuts; i++) { float offset = (i - (cuts - 1) * 0.5f) * 12f; Vector2 origin = center + tangent * offset + v.Direction * (i * 5f); Vector2 cutDirection = v.Direction.RotatedBy((i - (cuts - 1) * 0.5f) * 0.14f); float length = 104f + v.Level * 18f - i * 6f; ReaperVoidBackdropRenderer.DrawHitRift(batch, origin, cutDirection, length, 7f + v.Level * 1.6f, v.Seed + i * 977, MathHelper.SmoothStep(0f, 1f, MathHelper.Clamp(p / (0.22f + i * 0.04f), 0f, 1f)), opacity * (1f - i * 0.10f)); } if (v.Level >= 3) { float collapse = 1f - MathHelper.SmoothStep(0f, 1f, MathHelper.Clamp((p - 0.42f) / 0.3f, 0f, 1f)); DrawSegment(batch, pixel, center - v.Direction * 94f * collapse, center + v.Direction * 94f * collapse, secondary * (opacity * 0.72f), 1.4f); } } private static void DrawVoidCoordinate(SpriteBatch batch, Texture2D pixel, ReaperSkillVisual v, Vector2 center, float p, float opacity, Color primary, Color secondary) { float radius = v.Level switch { 1 => 40f, 2 => 58f, _ => 82f }; float collapse = v.Variant > 0 ? 1f - MathHelper.SmoothStep(0f, 1f, MathHelper.Clamp(p * 1.5f, 0f, 1f)) : 1f; radius *= Math.Max(0.08f, collapse); DrawCross(batch, pixel, center, radius, primary * opacity, 3f, 0f); DrawRing(batch, pixel, center, radius * 0.72f, 28, secondary * opacity, 2.5f, p); if (v.Level >= 2) { DrawDiamond(batch, pixel, center, radius, primary * opacity, 3f, p * 0.7f); DrawDiamond(batch, pixel, center + new Vector2(9f, -9f), radius, secondary * (opacity * 0.62f), 2f, -p * 0.7f); DrawSegment(batch, pixel, center + new Vector2(-radius, -radius), center + new Vector2(-radius + 9f, -radius - 9f), primary * opacity, 2f); DrawSegment(batch, pixel, center + new Vector2(radius, radius), center + new Vector2(radius + 9f, radius - 9f), primary * opacity, 2f); } if (v.Level >= 3) { for (int grid = -1; grid <= 1; grid++) { float offset = grid * radius * 0.42f; DrawSegment(batch, pixel, center + new Vector2(-radius, offset), center + new Vector2(radius, offset), primary * (opacity * 0.38f), 1.3f); DrawSegment(batch, pixel, center + new Vector2(offset, -radius), center + new Vector2(offset, radius), primary * (opacity * 0.38f), 1.3f); } if (v.Variant > 0 && p > 0.45f) { DrawSegment(batch, pixel, center - Vector2.UnitX * 100f, center + Vector2.UnitX * 100f, secondary * opacity, 3f); DrawSegment(batch, pixel, center - Vector2.UnitY * 100f, center + Vector2.UnitY * 100f, secondary * opacity, 3f); Vector2 depth = new Vector2(1f, -0.62f).SafeNormalize(Vector2.UnitX); DrawSegment(batch, pixel, center - depth * 100f, center + depth * 100f, secondary * opacity, 3f); } } } private static void DrawGhost(SpriteBatch batch, Texture2D pixel, Vector2 center, Vector2 facing, Color body, Color edge, float scale) { facing = facing.SafeNormalize(Vector2.UnitX); Vector2 tangent = facing.RotatedBy(MathHelper.PiOver2); DrawArc(batch, pixel, center, 14f * scale, MathHelper.Pi, MathHelper.Pi, 14, edge, 3f * scale); DrawSegment(batch, pixel, center - tangent * 13f * scale, center + facing * 4f, edge, 2.5f * scale); DrawSegment(batch, pixel, center + tangent * 13f * scale, center + facing * 4f, edge, 2.5f * scale); DrawSegment(batch, pixel, center + facing * 3f, center + facing * 38f * scale, body, 12f * scale); DrawSegment(batch, pixel, center + facing * 25f * scale - tangent * 14f * scale, center + facing * 25f * scale + tangent * 14f * scale, edge * 0.72f, 2f * scale); } private static void DrawChain(SpriteBatch batch, Texture2D pixel, Vector2 start, Vector2 end, Color outer, Color core, int links) { Vector2 delta = end - start; for (int i = 0; i < links; i++) { float t0 = i / (float)links; float t1 = (i + 0.72f) / links; Vector2 linkStart = start + delta * t0; Vector2 linkEnd = start + delta * t1; DrawSegment(batch, pixel, linkStart, linkEnd, outer, i % 2 == 0 ? 4f : 2.4f); DrawSegment(batch, pixel, linkStart, linkEnd, core * 0.72f, 1.2f); } } private static void DrawCross(SpriteBatch batch, Texture2D pixel, Vector2 center, float radius, Color color, float width, float rotation) { Vector2 first = rotation.ToRotationVector2(); Vector2 second = first.RotatedBy(MathHelper.PiOver2); DrawSegment(batch, pixel, center - first * radius, center + first * radius, color, width); DrawSegment(batch, pixel, center - second * radius, center + second * radius, color, width); } private static void DrawDiamond(SpriteBatch batch, Texture2D pixel, Vector2 center, float radius, Color color, float width, float rotation) => DrawPolygon(batch, pixel, center, radius, 4, color, width, rotation + MathHelper.PiOver4); private static void DrawStar(SpriteBatch batch, Texture2D pixel, Vector2 center, float outerRadius, float innerRadius, int points, Color color, float width, float rotation) { int vertices = points * 2; Vector2 previous = center + rotation.ToRotationVector2() * outerRadius; for (int i = 1; i <= vertices; i++) { float angle = rotation + i * MathHelper.TwoPi / vertices; float radius = (i & 1) == 0 ? outerRadius : innerRadius; Vector2 next = center + angle.ToRotationVector2() * radius; DrawSegment(batch, pixel, previous, next, color, width); previous = next; } } private static void DrawPolygon(SpriteBatch batch, Texture2D pixel, Vector2 center, float radius, int sides, Color color, float width, float rotation) { Vector2 previous = center + rotation.ToRotationVector2() * radius; for (int i = 1; i <= sides; i++) { Vector2 next = center + (rotation + i * MathHelper.TwoPi / sides).ToRotationVector2() * radius; DrawSegment(batch, pixel, previous, next, color, width); previous = next; } } private static void DrawRing(SpriteBatch batch, Texture2D pixel, Vector2 center, float radius, int segments, Color color, float width, float rotation) => DrawArc(batch, pixel, center, radius, rotation, MathHelper.TwoPi, segments, color, width); private static void DrawArc(SpriteBatch batch, Texture2D pixel, Vector2 center, float radius, float startAngle, float sweep, int segments, Color color, float width) { if (radius <= 0.1f || segments <= 0) return; segments = Math.Max(segments, Math.Min(160, (int)Math.Ceiling(Math.Abs(sweep) * radius / 18f))); Vector2 previous = center + startAngle.ToRotationVector2() * radius; for (int i = 1; i <= segments; i++) { Vector2 next = center + (startAngle + sweep * i / segments).ToRotationVector2() * radius; DrawSegment(batch, pixel, previous, next, color, width); previous = next; } } private static void DrawSegment(SpriteBatch batch, Texture2D pixel, Vector2 start, Vector2 end, Color color, float width) { DeathDomainPrimitiveTextureSystem.DrawSmoothLine(batch, start - Main.screenPosition, end - Main.screenPosition, color, width); } }