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; /// /// 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. /// public sealed class DeathWingDomainTrailProjectile : ModProjectile { private const int Lifetime = 300; private const int MaximumPoints = 16; private readonly List points = []; private readonly HashSet 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 orderedPoints, float width, float opacity, int seed) { if (Main.netMode == NetmodeID.MultiplayerClient || orderedPoints.Count < 2) { return -1; } List 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(), 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 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 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(); 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 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); } }