using SoulHarvest.Common;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using Terraria;
using Terraria.DataStructures;
using Terraria.ID;
using Terraria.ModLoader;
namespace SoulHarvest.Projectiles;
/// <summary>
/// A server-authoritative, immutable section of the path cut open by the Death
/// Wings. The geometry remains at the world positions where it was created and
/// periodically asks the owner's character-bound Death Domain to harvest enemies.
/// </summary>
public sealed class DeathWingDomainTrailProjectile : ModProjectile
{
private const int Lifetime = 300;
private const int MaximumPoints = 16;
private readonly List<Vector2> points = [];
private readonly HashSet<int> selectedTargets = [];
private Vector4 pathBounds;
private float trailWidth;
private float baseOpacity;
private int trailSeed;
private int age;
private bool configured;
private bool serverAuthorized;
#if DEBUG
internal int DebugAge => age;
internal int DebugPointCount => points.Count;
internal int DebugLifetime => Lifetime;
#endif
public override string Texture => "Terraria/Images/Projectile_0";
internal static int Spawn(IEntitySource source, int owner,
IReadOnlyList<Vector2> orderedPoints, float width, float opacity,
int seed)
{
if (Main.netMode == NetmodeID.MultiplayerClient
|| orderedPoints.Count < 2)
{
return -1;
}
List<Vector2> path = [];
int usable = Math.Min(orderedPoints.Count, MaximumPoints);
for (int index = 0; index < usable; index++)
{
Vector2 point = orderedPoints[index];
if (!float.IsFinite(point.X) || !float.IsFinite(point.Y))
continue;
if (path.Count == 0
|| Vector2.DistanceSquared(path[^1], point) >= 2f * 2f)
{
path.Add(point);
}
}
if (path.Count < 2)
return -1;
Vector2 center = Vector2.Zero;
foreach (Vector2 point in path)
center += point;
center /= path.Count;
int projectileIndex = Projectile.NewProjectile(source, center,
Vector2.Zero,
ModContent.ProjectileType<DeathWingDomainTrailProjectile>(),
0, 0f, owner);
if (projectileIndex < 0 || projectileIndex >= Main.maxProjectiles)
return -1;
Projectile projectile = Main.projectile[projectileIndex];
if (projectile.ModProjectile is DeathWingDomainTrailProjectile trail)
trail.Configure(path, width, opacity, seed);
ReaperProjectileHelper.SyncNewProjectile(projectile);
return projectileIndex;
}
public override void SetStaticDefaults()
{
ProjectileID.Sets.DrawScreenCheckFluff[Type] = 2400;
}
public override void SetDefaults()
{
Projectile.width = 4;
Projectile.height = 4;
Projectile.friendly = false;
Projectile.hostile = false;
Projectile.tileCollide = false;
Projectile.ignoreWater = true;
Projectile.penetrate = -1;
Projectile.timeLeft = Lifetime;
Projectile.netImportant = true;
}
public override bool ShouldUpdatePosition() => false;
public override bool? CanDamage() => false;
public override void AI()
{
if (Main.netMode == NetmodeID.Server && !serverAuthorized)
{
Projectile.Kill();
return;
}
if (!configured)
return;
age++;
if (Main.netMode != NetmodeID.Server)
{
DeathDomainTrailVisualSystem.Record(Projectile.owner,
Projectile.identity, points, trailWidth, GetOpacity(),
mergeOverlappingRims: true, taperEnds: false,
noiseFadeEnds: true);
}
if (Main.netMode != NetmodeID.MultiplayerClient)
TryHarvestIntersectingEnemies();
}
public override bool PreDraw(ref Color lightColor) => false;
public override void SendExtraAI(BinaryWriter writer)
{
writer.Write(configured);
if (!configured)
return;
writer.Write(trailWidth);
writer.Write(baseOpacity);
writer.Write(trailSeed);
writer.Write((short)Math.Clamp(age, 0, short.MaxValue));
writer.Write((byte)Math.Min(points.Count, MaximumPoints));
for (int index = 0; index < points.Count && index < MaximumPoints;
index++)
{
writer.WriteVector2(points[index]);
}
}
public override void ReceiveExtraAI(BinaryReader reader)
{
bool incoming = reader.ReadBoolean();
if (!incoming)
{
if (Main.netMode != NetmodeID.Server)
configured = false;
return;
}
float incomingWidth = reader.ReadSingle();
float incomingOpacity = reader.ReadSingle();
int incomingSeed = reader.ReadInt32();
int incomingAge = reader.ReadInt16();
int count = reader.ReadByte();
List<Vector2> incomingPoints = [];
for (int index = 0; index < count; index++)
incomingPoints.Add(reader.ReadVector2());
if (Main.netMode == NetmodeID.Server)
return;
points.Clear();
points.AddRange(incomingPoints);
trailWidth = MathHelper.Clamp(incomingWidth, 8f, 96f);
baseOpacity = MathHelper.Clamp(incomingOpacity, 0f, 1f);
trailSeed = incomingSeed;
age = Math.Clamp(incomingAge, 0, Lifetime);
Projectile.timeLeft = Math.Max(1, Lifetime - age);
pathBounds = CalculatePathBounds(points, trailWidth);
configured = points.Count >= 2;
}
private void Configure(List<Vector2> path, float width, float opacity,
int seed)
{
points.Clear();
points.AddRange(path);
trailWidth = MathHelper.Clamp(width, 8f, 96f);
baseOpacity = MathHelper.Clamp(opacity, 0f, 1f);
trailSeed = seed;
age = 0;
pathBounds = CalculatePathBounds(points, trailWidth);
configured = points.Count >= 2;
serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient;
Projectile.timeLeft = Lifetime;
}
private void TryHarvestIntersectingEnemies()
{
if (age < 6 || Projectile.timeLeft <= 15
|| Projectile.owner < 0 || Projectile.owner >= Main.maxPlayers)
{
return;
}
Player owner = Main.player[Projectile.owner];
if (!owner.active || owner.dead)
return;
MyPlayer modPlayer = owner.GetModPlayer<MyPlayer>();
selectedTargets.Clear();
foreach (NPC npc in Main.ActiveNPCs)
{
if (!ReaperTargeting.IsValidWeaponTarget(npc)
|| !IntersectsPathBounds(npc.Hitbox)
|| !IntersectsPath(npc.Hitbox))
{
continue;
}
NPC target = npc.realLife >= 0 && npc.realLife < Main.maxNPCs
&& Main.npc[npc.realLife].active
? Main.npc[npc.realLife]
: npc;
if (!ReaperTargeting.IsValidWeaponTarget(target)
|| !selectedTargets.Add(target.whoAmI))
{
continue;
}
modPlayer.TrySpawnDeathWingDomainTrailHarvest(target, trailSeed);
}
}
private bool IntersectsPath(Rectangle target)
{
float collisionPoint = 0f;
for (int index = 1; index < points.Count; index++)
{
if (Collision.CheckAABBvLineCollision(target.TopLeft(),
target.Size(), points[index - 1], points[index], trailWidth,
ref collisionPoint))
{
return true;
}
}
return false;
}
private bool IntersectsPathBounds(Rectangle target)
=> target.Right >= pathBounds.X && target.Bottom >= pathBounds.Y
&& target.Left <= pathBounds.Z && target.Top <= pathBounds.W;
private static Vector4 CalculatePathBounds(IReadOnlyList<Vector2> path,
float width)
{
if (path.Count == 0)
return Vector4.Zero;
Vector2 minimum = path[0];
Vector2 maximum = path[0];
for (int index = 1; index < path.Count; index++)
{
minimum = Vector2.Min(minimum, path[index]);
maximum = Vector2.Max(maximum, path[index]);
}
float padding = width * 0.5f;
return new Vector4(minimum.X - padding, minimum.Y - padding,
maximum.X + padding, maximum.Y + padding);
}
private float GetOpacity()
=> baseOpacity * Smooth01(age / 6f)
* Smooth01(Projectile.timeLeft / 44f);
private static float Smooth01(float value)
{
value = MathHelper.Clamp(value, 0f, 1f);
return value * value * (3f - 2f * value);
}
}
using SoulHarvest.Common;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using Terraria;
using Terraria.DataStructures;
using Terraria.ID;
using Terraria.ModLoader;
namespace SoulHarvest.Projectiles;
/// <summary>
/// A server-authoritative, immutable section of the path cut open by the Death
/// Wings. The geometry remains at the world positions where it was created and
/// periodically asks the owner's character-bound Death Domain to harvest enemies.
/// </summary>
public sealed class DeathWingDomainTrailProjectile : ModProjectile
{
private const int Lifetime = 300;
private const int MaximumPoints = 16;
private readonly List<Vector2> points = [];
private readonly HashSet<int> selectedTargets = [];
private Vector4 pathBounds;
private float trailWidth;
private float baseOpacity;
private int trailSeed;
private int age;
private bool configured;
private bool serverAuthorized;
#if DEBUG
internal int DebugAge => age;
internal int DebugPointCount => points.Count;
internal int DebugLifetime => Lifetime;
#endif
public override string Texture => "Terraria/Images/Projectile_0";
internal static int Spawn(IEntitySource source, int owner,
IReadOnlyList<Vector2> orderedPoints, float width, float opacity,
int seed)
{
if (Main.netMode == NetmodeID.MultiplayerClient
|| orderedPoints.Count < 2)
{
return -1;
}
List<Vector2> path = [];
int usable = Math.Min(orderedPoints.Count, MaximumPoints);
for (int index = 0; index < usable; index++)
{
Vector2 point = orderedPoints[index];
if (!float.IsFinite(point.X) || !float.IsFinite(point.Y))
continue;
if (path.Count == 0
|| Vector2.DistanceSquared(path[^1], point) >= 2f * 2f)
{
path.Add(point);
}
}
if (path.Count < 2)
return -1;
Vector2 center = Vector2.Zero;
foreach (Vector2 point in path)
center += point;
center /= path.Count;
int projectileIndex = Projectile.NewProjectile(source, center,
Vector2.Zero,
ModContent.ProjectileType<DeathWingDomainTrailProjectile>(),
0, 0f, owner);
if (projectileIndex < 0 || projectileIndex >= Main.maxProjectiles)
return -1;
Projectile projectile = Main.projectile[projectileIndex];
if (projectile.ModProjectile is DeathWingDomainTrailProjectile trail)
trail.Configure(path, width, opacity, seed);
ReaperProjectileHelper.SyncNewProjectile(projectile);
return projectileIndex;
}
public override void SetStaticDefaults()
{
ProjectileID.Sets.DrawScreenCheckFluff[Type] = 2400;
}
public override void SetDefaults()
{
Projectile.width = 4;
Projectile.height = 4;
Projectile.friendly = false;
Projectile.hostile = false;
Projectile.tileCollide = false;
Projectile.ignoreWater = true;
Projectile.penetrate = -1;
Projectile.timeLeft = Lifetime;
Projectile.netImportant = true;
}
public override bool ShouldUpdatePosition() => false;
public override bool? CanDamage() => false;
public override void AI()
{
if (Main.netMode == NetmodeID.Server && !serverAuthorized)
{
Projectile.Kill();
return;
}
if (!configured)
return;
age++;
if (Main.netMode != NetmodeID.Server)
{
DeathDomainTrailVisualSystem.Record(Projectile.owner,
Projectile.identity, points, trailWidth, GetOpacity(),
mergeOverlappingRims: true, taperEnds: false,
noiseFadeEnds: true);
}
if (Main.netMode != NetmodeID.MultiplayerClient)
TryHarvestIntersectingEnemies();
}
public override bool PreDraw(ref Color lightColor) => false;
public override void SendExtraAI(BinaryWriter writer)
{
writer.Write(configured);
if (!configured)
return;
writer.Write(trailWidth);
writer.Write(baseOpacity);
writer.Write(trailSeed);
writer.Write((short)Math.Clamp(age, 0, short.MaxValue));
writer.Write((byte)Math.Min(points.Count, MaximumPoints));
for (int index = 0; index < points.Count && index < MaximumPoints;
index++)
{
writer.WriteVector2(points[index]);
}
}
public override void ReceiveExtraAI(BinaryReader reader)
{
bool incoming = reader.ReadBoolean();
if (!incoming)
{
if (Main.netMode != NetmodeID.Server)
configured = false;
return;
}
float incomingWidth = reader.ReadSingle();
float incomingOpacity = reader.ReadSingle();
int incomingSeed = reader.ReadInt32();
int incomingAge = reader.ReadInt16();
int count = reader.ReadByte();
List<Vector2> incomingPoints = [];
for (int index = 0; index < count; index++)
incomingPoints.Add(reader.ReadVector2());
if (Main.netMode == NetmodeID.Server)
return;
points.Clear();
points.AddRange(incomingPoints);
trailWidth = MathHelper.Clamp(incomingWidth, 8f, 96f);
baseOpacity = MathHelper.Clamp(incomingOpacity, 0f, 1f);
trailSeed = incomingSeed;
age = Math.Clamp(incomingAge, 0, Lifetime);
Projectile.timeLeft = Math.Max(1, Lifetime - age);
pathBounds = CalculatePathBounds(points, trailWidth);
configured = points.Count >= 2;
}
private void Configure(List<Vector2> path, float width, float opacity,
int seed)
{
points.Clear();
points.AddRange(path);
trailWidth = MathHelper.Clamp(width, 8f, 96f);
baseOpacity = MathHelper.Clamp(opacity, 0f, 1f);
trailSeed = seed;
age = 0;
pathBounds = CalculatePathBounds(points, trailWidth);
configured = points.Count >= 2;
serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient;
Projectile.timeLeft = Lifetime;
}
private void TryHarvestIntersectingEnemies()
{
if (age < 6 || Projectile.timeLeft <= 15
|| Projectile.owner < 0 || Projectile.owner >= Main.maxPlayers)
{
return;
}
Player owner = Main.player[Projectile.owner];
if (!owner.active || owner.dead)
return;
MyPlayer modPlayer = owner.GetModPlayer<MyPlayer>();
selectedTargets.Clear();
foreach (NPC npc in Main.ActiveNPCs)
{
if (!ReaperTargeting.IsValidWeaponTarget(npc)
|| !IntersectsPathBounds(npc.Hitbox)
|| !IntersectsPath(npc.Hitbox))
{
continue;
}
NPC target = npc.realLife >= 0 && npc.realLife < Main.maxNPCs
&& Main.npc[npc.realLife].active
? Main.npc[npc.realLife]
: npc;
if (!ReaperTargeting.IsValidWeaponTarget(target)
|| !selectedTargets.Add(target.whoAmI))
{
continue;
}
modPlayer.TrySpawnDeathWingDomainTrailHarvest(target, trailSeed);
}
}
private bool IntersectsPath(Rectangle target)
{
float collisionPoint = 0f;
for (int index = 1; index < points.Count; index++)
{
if (Collision.CheckAABBvLineCollision(target.TopLeft(),
target.Size(), points[index - 1], points[index], trailWidth,
ref collisionPoint))
{
return true;
}
}
return false;
}
private bool IntersectsPathBounds(Rectangle target)
=> target.Right >= pathBounds.X && target.Bottom >= pathBounds.Y
&& target.Left <= pathBounds.Z && target.Top <= pathBounds.W;
private static Vector4 CalculatePathBounds(IReadOnlyList<Vector2> path,
float width)
{
if (path.Count == 0)
return Vector4.Zero;
Vector2 minimum = path[0];
Vector2 maximum = path[0];
for (int index = 1; index < path.Count; index++)
{
minimum = Vector2.Min(minimum, path[index]);
maximum = Vector2.Max(maximum, path[index]);
}
float padding = width * 0.5f;
return new Vector4(minimum.X - padding, minimum.Y - padding,
maximum.X + padding, maximum.Y + padding);
}
private float GetOpacity()
=> baseOpacity * Smooth01(age / 6f)
* Smooth01(Projectile.timeLeft / 44f);
private static float Smooth01(float value)
{
value = MathHelper.Clamp(value, 0f, 1f);
return value * value * (3f - 2f * value);
}
}