using SoulHarvest.Common;
using Microsoft.Xna.Framework;
using System;
using System.IO;
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;
namespace SoulHarvest.Projectiles;
///
/// Server-authoritative, persistent Void tear shared by the Void special,
/// passives and ultimate. It damages along the visible opening and pulls normal
/// enemies toward the closest point on that opening.
///
public sealed class ReaperVoidRiftProjectile : ModProjectile
{
private SickleCombatSnapshot snapshot;
private float riftLength;
private float riftWidth;
private float pullRadius;
private int lifetime;
private int seed;
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, Vector2 center, Vector2 direction,
float length, float width, int lifetime, float pullRadius,
float damageMultiplier, int phase, int actionId)
{
if (Main.netMode == NetmodeID.MultiplayerClient)
return -1;
direction = direction.SafeNormalize(Vector2.UnitX);
int index = Projectile.NewProjectile(source, center, direction,
ModContent.ProjectileType(),
Math.Max(1, (int)Math.Round(snapshot.Damage * damageMultiplier)),
snapshot.Knockback * 0.18f, owner);
if (index < 0 || index >= Main.maxProjectiles)
return -1;
Projectile projectile = Main.projectile[index];
projectile.originalDamage = snapshot.Damage;
projectile.CritChance = 0;
int stableSeed = unchecked(actionId * 486187739 + phase * 16777619 + owner * 31);
if (projectile.ModProjectile is ReaperVoidRiftProjectile rift)
rift.Configure(snapshot, length, width, lifetime, pullRadius, stableSeed);
projectile.GetGlobalProjectile().ConfigureReaperProjectile(
projectile, snapshot, ReaperHitKind.DamageOverTime, phase,
direction.ToRotation(), actionId);
projectile.localNPCHitCooldown = 16;
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 = 180;
Projectile.netImportant = true;
Projectile.DamageType = DamageClass.Melee;
Projectile.usesLocalNPCImmunity = true;
Projectile.localNPCHitCooldown = 16;
}
public override bool ShouldUpdatePosition() => false;
public override bool? CanDamage()
=> configured && age >= 5 && Projectile.timeLeft > 18 ? null : false;
public override void AI()
{
if (Main.netMode == NetmodeID.Server && !serverAuthorized)
{
Projectile.Kill();
return;
}
if (!configured)
return;
age++;
Projectile.rotation = Projectile.velocity.ToRotation();
if (Main.netMode != NetmodeID.MultiplayerClient)
PullNearbyEnemies();
}
public override bool? Colliding(Rectangle projHitbox, Rectangle targetHitbox)
{
Vector2 axis = Projectile.velocity.SafeNormalize(Vector2.UnitX);
Vector2 start = Projectile.Center - axis * riftLength * 0.5f;
Vector2 end = Projectile.Center + axis * riftLength * 0.5f;
float collisionPoint = 0f;
return Collision.CheckAABBvLineCollision(targetHitbox.TopLeft(), targetHitbox.Size(),
start, end, Math.Max(3f, riftWidth * 0.72f), ref collisionPoint);
}
public override bool PreDraw(ref Color lightColor)
{
if (!configured || Main.dedServ)
return false;
float reveal = Smooth01(age / 13f);
float fade = Smooth01(Projectile.timeLeft / 40f);
float breathing = 0.96f + (float)Math.Sin((age + seed * 0.001f) * 0.11f) * 0.04f;
ReaperVoidBackdropRenderer.DrawPersistentRift(Main.spriteBatch, Projectile.Center,
Projectile.velocity, riftLength, riftWidth * breathing, seed,
reveal, fade);
return false;
}
public override void SendExtraAI(BinaryWriter writer)
{
writer.Write(configured);
if (!configured)
return;
snapshot.Write(writer);
writer.Write(riftLength);
writer.Write(riftWidth);
writer.Write(pullRadius);
writer.Write((short)Math.Clamp(lifetime, 1, short.MaxValue));
writer.Write(seed);
writer.Write((short)Math.Clamp(age, 0, short.MaxValue));
}
public override void ReceiveExtraAI(BinaryReader reader)
{
bool incomingConfigured = reader.ReadBoolean();
if (!incomingConfigured)
{
if (Main.netMode != NetmodeID.Server)
configured = false;
return;
}
SickleCombatSnapshot incomingSnapshot = SickleCombatSnapshot.Read(reader);
float incomingLength = reader.ReadSingle();
float incomingWidth = reader.ReadSingle();
float incomingPullRadius = reader.ReadSingle();
int incomingLifetime = reader.ReadInt16();
int incomingSeed = reader.ReadInt32();
int incomingAge = reader.ReadInt16();
if (Main.netMode == NetmodeID.Server)
return;
snapshot = incomingSnapshot;
riftLength = incomingLength;
riftWidth = incomingWidth;
pullRadius = incomingPullRadius;
lifetime = incomingLifetime;
seed = incomingSeed;
age = incomingAge;
configured = true;
}
private void Configure(in SickleCombatSnapshot value, float length, float width,
int activeLifetime, float attractionRadius, int shapeSeed)
{
snapshot = value;
riftLength = MathHelper.Clamp(length, 32f, 2600f);
riftWidth = MathHelper.Clamp(width, 4f, 190f);
pullRadius = MathHelper.Clamp(attractionRadius, 0f, 820f);
lifetime = Math.Clamp(activeLifetime, 30, 600);
seed = shapeSeed;
age = 0;
configured = true;
serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient;
Projectile.timeLeft = lifetime;
}
private void PullNearbyEnemies()
{
if (pullRadius <= 1f || age < 5 || Projectile.timeLeft <= 18)
return;
Vector2 axis = Projectile.velocity.SafeNormalize(Vector2.UnitX);
Vector2 start = Projectile.Center - axis * riftLength * 0.5f;
Vector2 end = Projectile.Center + axis * riftLength * 0.5f;
Vector2 segment = end - start;
float segmentLengthSquared = Math.Max(1f, segment.LengthSquared());
foreach (NPC npc in Main.ActiveNPCs)
{
if (!npc.CanBeChasedBy(Projectile) || npc.boss || npc.realLife >= 0)
continue;
float amount = MathHelper.Clamp(Vector2.Dot(npc.Center - start, segment)
/ segmentLengthSquared, 0f, 1f);
Vector2 closest = Vector2.Lerp(start, end, amount);
Vector2 toRift = closest - npc.Center;
float distance = toRift.Length();
if (distance <= 2f || distance >= pullRadius)
continue;
float influence = 1f - distance / pullRadius;
float maximumSpeed = MathHelper.Lerp(1.2f, 6.2f, influence);
Vector2 desired = toRift / distance * maximumSpeed;
npc.velocity = Vector2.Lerp(npc.velocity, desired, 0.035f + influence * 0.09f);
if (Main.netMode == NetmodeID.Server && Main.GameUpdateCount % 10 == 0)
npc.netUpdate = true;
}
}
private static float Smooth01(float value)
{
value = MathHelper.Clamp(value, 0f, 1f);
return value * value * (3f - 2f * value);
}
}