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;
}
}
}