using SoulHarvest.Common; using Microsoft.Xna.Framework; using System; using System.IO; using Terraria; using Terraria.ID; using Terraria.ModLoader; namespace SoulHarvest.Projectiles; /// Continuous life siphon used while Blood special is being charged. public sealed class ReaperBloodDrainFieldProjectile : ModProjectile { private SickleCombatSnapshot snapshot; private float radius; private int controllerIdentity; private int age; private bool configured; private bool serverAuthorized; public override string Texture => "Terraria/Images/Projectile_0"; internal static int Spawn(Terraria.DataStructures.IEntitySource source, int owner, in SickleCombatSnapshot snapshot, float radius, int controllerIdentity) { if (Main.netMode == NetmodeID.MultiplayerClient) return -1; Player player = Main.player[owner]; int index = Projectile.NewProjectile(source, player.MountedCenter, Vector2.Zero, ModContent.ProjectileType(), Math.Max(1, (int)Math.Round(snapshot.Damage * 0.075f)), 0f, owner); if (index < 0 || index >= Main.maxProjectiles) return -1; Projectile projectile = Main.projectile[index]; projectile.originalDamage = snapshot.Damage; if (projectile.ModProjectile is ReaperBloodDrainFieldProjectile field) field.Configure(snapshot, radius, controllerIdentity); projectile.GetGlobalProjectile().ConfigureReaperProjectile( projectile, snapshot, ReaperHitKind.DamageOverTime, 0, 0f, controllerIdentity); projectile.localNPCHitCooldown = 12; ReaperProjectileHelper.SyncNewProjectile(projectile); return index; } public override void SetDefaults() { Projectile.width = 4; Projectile.height = 4; Projectile.friendly = true; Projectile.hostile = false; Projectile.tileCollide = false; Projectile.ignoreWater = true; Projectile.penetrate = -1; Projectile.timeLeft = 90; Projectile.netImportant = true; Projectile.DamageType = DamageClass.Melee; Projectile.usesLocalNPCImmunity = true; Projectile.localNPCHitCooldown = 12; } public override bool ShouldUpdatePosition() => false; public override bool? CanDamage() => configured && age >= 3 ? null : false; public override void AI() { if (Main.netMode == NetmodeID.Server && !serverAuthorized) { Projectile.Kill(); return; } if (!configured || Projectile.owner < 0 || Projectile.owner >= Main.maxPlayers) return; Player player = Main.player[Projectile.owner]; if (!player.active || player.dead || !ControllerStillCharging()) { Projectile.Kill(); return; } Projectile.Center = player.MountedCenter; Projectile.timeLeft = 2; age++; } public override bool? Colliding(Rectangle projHitbox, Rectangle targetHitbox) { float closestX = MathHelper.Clamp(Projectile.Center.X, targetHitbox.Left, targetHitbox.Right); float closestY = MathHelper.Clamp(Projectile.Center.Y, targetHitbox.Top, targetHitbox.Bottom); return Vector2.DistanceSquared(Projectile.Center, new Vector2(closestX, closestY)) <= radius * radius; } public override void OnHitNPC(NPC target, NPC.HitInfo hit, int damageDone) { if (Main.netMode == NetmodeID.MultiplayerClient || Projectile.owner < 0 || Projectile.owner >= Main.maxPlayers || damageDone <= 0) { return; } Player player = Main.player[Projectile.owner]; if (TryGetController(out ReaperActionControllerProjectile? controller)) controller?.RecordBloodSiphonDamage(damageDone); if (!player.active || player.dead || player.statLife >= player.statLifeMax2) return; int healing = Math.Clamp((int)Math.Ceiling(damageDone * 0.10f), 1, 4); player.Heal(healing); if (Main.netMode == NetmodeID.Server) NetMessage.SendData(MessageID.PlayerLifeMana, -1, -1, null, player.whoAmI); } public override bool PreDraw(ref Color lightColor) { if (!configured || Main.dedServ) return false; float pulse = 0.78f + (float)Math.Sin(age * 0.18f) * 0.16f; DrawSiphonVortex(pulse); foreach (NPC npc in Main.ActiveNPCs) { if (!npc.CanBeChasedBy(Projectile) || Vector2.DistanceSquared(npc.Center, Projectile.Center) > radius * radius) { continue; } DrawSiphonTendril(npc.Center, Projectile.Center, npc.whoAmI, pulse); } DrawRippleRing(Projectile.Center, radius * (0.94f + pulse * 0.025f), new Color(205, 15, 48, 0) * (0.24f * pulse), 2.2f); return false; } private void DrawSiphonVortex(float opacity) { Vector2 screenCenter = Projectile.Center - Main.screenPosition; DeathDomainPrimitiveTextureSystem.DrawRadialGradient(Main.spriteBatch, screenCenter, 82f, new Color(4, 0, 2, 0) * (0.72f * opacity)); const int arms = 9; const int samples = 24; float outerRadius = MathHelper.Clamp(radius * 0.34f, 92f, 168f); for (int arm = 0; arm < arms; arm++) { float baseAngle = arm * MathHelper.TwoPi / arms - age * 0.037f; Vector2 previous = Projectile.Center + baseAngle.ToRotationVector2() * outerRadius; for (int sample = 1; sample <= samples; sample++) { float progress = sample / (float)samples; float eased = progress * progress * (3f - 2f * progress); float angle = baseAngle + eased * 2.1f; float localRadius = MathHelper.Lerp(outerRadius, 13f, eased); float ripple = (float)Math.Sin(progress * MathHelper.TwoPi * 2f - age * 0.14f + arm) * 2.2f * (1f - progress); Vector2 current = Projectile.Center + angle.ToRotationVector2() * (localRadius + ripple); float width = MathHelper.Lerp(1.1f, 4.2f, progress); DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(previous, current, new Color(80, 0, 18, 0) * (opacity * 0.46f), width + 4f); DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(previous, current, new Color(236, 24, 58, 0) * (opacity * 0.78f), width); previous = current; } } DeathDomainPrimitiveTextureSystem.DrawSmoothDisc(Main.spriteBatch, screenCenter, 15f, new Color(2, 0, 1, 245) * opacity); DrawRippleRing(Projectile.Center, 22f + (float)Math.Sin(age * 0.12f) * 2f, new Color(248, 32, 66, 0) * (0.72f * opacity), 2.3f); } public override void SendExtraAI(BinaryWriter writer) { writer.Write(configured); if (!configured) return; snapshot.Write(writer); writer.Write(radius); writer.Write(controllerIdentity); writer.Write((short)Math.Clamp(age, 0, short.MaxValue)); } public override void ReceiveExtraAI(BinaryReader reader) { bool incoming = reader.ReadBoolean(); if (!incoming) { if (Main.netMode != NetmodeID.Server) configured = false; return; } SickleCombatSnapshot incomingSnapshot = SickleCombatSnapshot.Read(reader); float incomingRadius = reader.ReadSingle(); int incomingIdentity = reader.ReadInt32(); int incomingAge = reader.ReadInt16(); if (Main.netMode == NetmodeID.Server) return; snapshot = incomingSnapshot; radius = incomingRadius; controllerIdentity = incomingIdentity; age = incomingAge; configured = true; } private void Configure(in SickleCombatSnapshot value, float fieldRadius, int identity) { snapshot = value; radius = MathHelper.Clamp(fieldRadius, 120f, 760f); controllerIdentity = identity; age = 0; configured = true; serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient; } private bool ControllerStillCharging() => TryGetController(out _); private bool TryGetController(out ReaperActionControllerProjectile? activeController) { foreach (Projectile projectile in Main.ActiveProjectiles) { if (projectile.owner == Projectile.owner && projectile.identity == controllerIdentity && projectile.ModProjectile is ReaperActionControllerProjectile controller) { if (!controller.IsBloodSiphonActive) continue; activeController = controller; return true; } } activeController = null; return false; } private void DrawSiphonTendril(Vector2 start, Vector2 end, int targetSeed, float opacity) { Vector2 delta = end - start; Vector2 axis = delta.SafeNormalize(Vector2.UnitX); Vector2 normal = axis.RotatedBy(MathHelper.PiOver2); float side = (targetSeed & 1) == 0 ? 1f : -1f; float bend = MathHelper.Clamp(delta.Length() * 0.13f, 18f, 58f); Vector2 control = (start + end) * 0.5f + normal * side * (bend + (float)Math.Sin(age * 0.075f + targetSeed * 0.83f) * 9f); Vector2 previous = start; const int segments = 32; for (int segment = 1; segment <= segments; segment++) { float amount = segment / (float)segments; Vector2 current = GetPoint(amount); DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(previous, current, new Color(46, 0, 9, 225) * (opacity * 0.88f), 8.5f); DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(previous, current, new Color(154, 4, 31, 235) * opacity, 4.2f); DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(previous, current, new Color(255, 52, 77, 245) * (opacity * 0.92f), 1.35f); previous = current; } // Five visible droplets ride the exact same Bezier curve from every living // target into the player. Their short tails make the transfer direction // legible even against the dark Blood domain backdrop. for (int droplet = 0; droplet < 5; droplet++) { float amount = (age * 0.046f + droplet / 5f + targetSeed * 0.037f) % 1f; Vector2 point = GetPoint(amount); Vector2 tail = GetPoint(Math.Max(0f, amount - 0.045f)); float radius = 2.8f + amount * 1.7f; DeathDomainPrimitiveTextureSystem.DrawSmoothWorldLine(tail, point, new Color(245, 18, 52, 235) * (opacity * 0.82f), radius * 0.92f); DeathDomainPrimitiveTextureSystem.DrawSmoothDisc(Main.spriteBatch, point - Main.screenPosition, radius + 3f, new Color(86, 0, 18, 220) * (opacity * 0.72f)); DeathDomainPrimitiveTextureSystem.DrawSmoothDisc(Main.spriteBatch, point - Main.screenPosition, radius, new Color(255, 58, 82, 250) * opacity); } Vector2 GetPoint(float amount) { float inverse = 1f - amount; Vector2 curve = inverse * inverse * start + 2f * inverse * amount * control + amount * amount * end; float ripple = (float)Math.Sin(amount * MathHelper.TwoPi * 2f - age * 0.16f + targetSeed * 0.43f) * 2.6f * (float)Math.Sin(amount * MathHelper.Pi); return curve + normal * ripple; } } private void DrawRippleRing(Vector2 center, float baseRadius, Color color, float width) { const int segments = 72; Vector2 previous = center + Vector2.UnitX * baseRadius; for (int segment = 1; segment <= segments; segment++) { float angle = MathHelper.TwoPi * segment / segments; float wave = (float)Math.Sin(angle * 7f - age * 0.11f) * 2.5f; Vector2 current = center + angle.ToRotationVector2() * (baseRadius + wave); DeathDomainPrimitiveTextureSystem.DrawSmoothWorldLine(previous, current, color, width); previous = current; } } }