using SoulHarvest.Common; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.IO; using Terraria; using Terraria.ID; using Terraria.ModLoader; namespace SoulHarvest.Projectiles; public sealed class SoulSickleProjectile : ModProjectile { private const int HomingDelayFrames = 8; private SickleCombatSnapshot snapshot; private ReaperHitKind hitKind; private int phase; private int actionId; private int variant; private int timer; private int targetIndex = -1; private bool configured; private bool serverAuthorized; private int authoritativeDamage; private Vector2 authoritativeCenter; private Vector2 authoritativeVelocity; private readonly HashSet hitRoots = []; internal static int Spawn(Terraria.DataStructures.IEntitySource source, int owner, in SickleCombatSnapshot snapshot, Vector2 position, Vector2 velocity, int phase, int actionId, ReaperHitKind hitKind, float damageMultiplier, int variant = 0) { if (Main.netMode == NetmodeID.MultiplayerClient) return -1; int projectileIndex = Projectile.NewProjectile(source, position, velocity, ModContent.ProjectileType(), Math.Max(1, (int)Math.Round(snapshot.Damage * damageMultiplier)), snapshot.Knockback * 0.45f, owner); if (projectileIndex < 0 || projectileIndex >= Main.maxProjectiles) return -1; Projectile projectile = Main.projectile[projectileIndex]; projectile.originalDamage = snapshot.Damage; projectile.CritChance = snapshot.CritChance; if (projectile.ModProjectile is SoulSickleProjectile soulBlade) soulBlade.Configure(snapshot, hitKind, phase, actionId, variant); projectile.GetGlobalProjectile().ConfigureReaperProjectile( projectile, snapshot, hitKind, phase, velocity.ToRotation(), actionId); ReaperProjectileHelper.SyncNewProjectile(projectile); return projectileIndex; } public override void SetStaticDefaults() { ProjectileID.Sets.TrailCacheLength[Type] = 24; ProjectileID.Sets.TrailingMode[Type] = 2; } public override void SetDefaults() { Projectile.width = 42; Projectile.height = 42; Projectile.friendly = true; Projectile.hostile = false; Projectile.tileCollide = false; Projectile.penetrate = 4; Projectile.ignoreWater = true; Projectile.timeLeft = 150; Projectile.DamageType = DamageClass.Melee; Projectile.netImportant = true; Projectile.usesLocalNPCImmunity = true; Projectile.localNPCHitCooldown = 14; } public override bool ShouldUpdatePosition() => false; public override bool? CanDamage() => configured ? null : false; public override void AI() { if (Main.netMode == NetmodeID.Server) { if (!serverAuthorized) { Projectile.Kill(); return; } Projectile.damage = authoritativeDamage; Projectile.Center = authoritativeCenter; Projectile.velocity = authoritativeVelocity; } if (!configured) return; timer++; int curveDirection = (variant & 1) == 0 ? 1 : -1; if (timer <= HomingDelayFrames) { Projectile.velocity = Projectile.velocity.RotatedBy(curveDirection * (0.024f + snapshot.StageNumber * 0.004f)); } else { NPC? target = GetOrFindTarget(); if (target is not null) { float speed = Math.Max(14f + snapshot.StageNumber, Projectile.velocity.Length()); Vector2 desired = (target.Center - Projectile.Center) .SafeNormalize(Projectile.velocity.SafeNormalize(Vector2.UnitX)) * speed; Projectile.velocity = Vector2.Lerp(Projectile.velocity, desired, 0.085f + snapshot.StageNumber * 0.018f); } else { Projectile.velocity = Projectile.velocity.RotatedBy( curveDirection * 0.006f); } } Projectile.rotation = Projectile.velocity.ToRotation(); Projectile.GetGlobalProjectile() .UpdateReaperAttackAngle(Projectile.rotation); Projectile.position += Projectile.velocity; if (Main.netMode == NetmodeID.Server) { authoritativeCenter = Projectile.Center; authoritativeVelocity = Projectile.velocity; } if (!Main.dedServ) { Lighting.AddLight(Projectile.Center, new Vector3(0.22f, 0.34f, 0.82f)); if (Main.rand.NextBool(2)) { Color color = Main.rand.NextBool() ? new Color(85, 225, 255) : new Color(175, 85, 255); Dust dust = Dust.NewDustPerfect(Projectile.Center + Main.rand.NextVector2Circular(11f, 11f), DustID.AncientLight, -Projectile.velocity * 0.08f, 55, color, 0.95f); dust.noGravity = true; } } } public override bool PreDraw(ref Color lightColor) => ReaperProjectileHelper.DrawTrail(Projectile, new Color(120, 115, 255), scaleMultiplier: 1.04f + snapshot.StageNumber * 0.05f, copies: 22); public override void OnHitNPC(NPC target, NPC.HitInfo hit, int damageDone) { int root = target.realLife >= 0 ? target.realLife : target.whoAmI; hitRoots.Add(root); if (targetIndex == target.whoAmI) targetIndex = -1; if (Main.netMode != NetmodeID.MultiplayerClient) Projectile.netUpdate = true; } public override void SendExtraAI(BinaryWriter writer) { writer.Write(configured); if (!configured) return; snapshot.Write(writer); writer.Write((byte)hitKind); writer.Write((byte)Math.Clamp(phase, 0, byte.MaxValue)); writer.Write(actionId); writer.Write((byte)Math.Clamp(variant, 0, byte.MaxValue)); writer.Write((short)Math.Clamp(timer, 0, short.MaxValue)); writer.Write((short)targetIndex); } 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); ReaperHitKind incomingHitKind = (ReaperHitKind)reader.ReadByte(); int incomingPhase = reader.ReadByte(); int incomingActionId = reader.ReadInt32(); int incomingVariant = reader.ReadByte(); int incomingTimer = reader.ReadInt16(); int incomingTarget = reader.ReadInt16(); if (Main.netMode == NetmodeID.Server) return; snapshot = incomingSnapshot; hitKind = incomingHitKind; phase = incomingPhase; actionId = incomingActionId; variant = incomingVariant; timer = incomingTimer; targetIndex = incomingTarget; configured = true; } private void Configure(in SickleCombatSnapshot value, ReaperHitKind kind, int attackPhase, int parentActionId, int projectileVariant) { snapshot = value; hitKind = kind; phase = Math.Max(0, attackPhase); actionId = parentActionId; variant = Math.Max(0, projectileVariant); timer = 0; targetIndex = -1; hitRoots.Clear(); configured = true; serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient; authoritativeDamage = Math.Max(1, Projectile.damage); authoritativeCenter = Projectile.Center; authoritativeVelocity = Projectile.velocity; } private NPC? GetOrFindTarget() { float retentionRadius = 760f + snapshot.StageNumber * 80f; int previousTarget = targetIndex; if (targetIndex >= 0 && targetIndex < Main.maxNPCs) { NPC current = Main.npc[targetIndex]; int currentRoot = current.realLife >= 0 ? current.realLife : current.whoAmI; if (!hitRoots.Contains(currentRoot) && ReaperTargeting.IsValidWeaponTarget(current) && (ReaperTargeting.IsTrainingDummy(current) || current.CanBeChasedBy(Projectile)) && Vector2.DistanceSquared(Projectile.Center, current.Center) <= retentionRadius * retentionRadius) { return current; } targetIndex = -1; } NPC? target = FindUnhitTarget(520f + snapshot.StageNumber * 90f); targetIndex = target?.whoAmI ?? -1; if (targetIndex != previousTarget && Main.netMode != NetmodeID.MultiplayerClient) Projectile.netUpdate = true; return target; } private NPC? FindUnhitTarget(float maximumDistance) { NPC? target = null; foreach (NPC npc in Main.ActiveNPCs) { int root = npc.realLife >= 0 ? npc.realLife : npc.whoAmI; if (hitRoots.Contains(root) || !ReaperTargeting.IsValidWeaponTarget(npc) || !ReaperTargeting.IsTrainingDummy(npc) && !npc.CanBeChasedBy(Projectile)) { continue; } float distance = Vector2.Distance(npc.Center, Projectile.Center); if (distance >= maximumDistance) continue; maximumDistance = distance; target = npc; } return target; } }