using SoulHarvest.Common; using Microsoft.Xna.Framework; using System; using System.IO; using Terraria; using Terraria.ID; using Terraria.ModLoader; namespace SoulHarvest.Projectiles; public sealed class ReaperSoulFamiliarProjectile : ModProjectile { private SickleCombatSnapshot snapshot; private bool configured; private int timer; private int attacksMade; private bool serverAuthorized; private Vector2 authoritativeCenter; public override string Texture => "SoulHarvest/Projectiles/SoulSickleProjectile"; public static void Spawn(Terraria.DataStructures.IEntitySource source, int owner, in SickleCombatSnapshot snapshot, Vector2 origin) { if (Main.netMode == NetmodeID.MultiplayerClient) return; int type = ModContent.ProjectileType(); for (int index = 0; index < Main.maxProjectiles; index++) { Projectile existing = Main.projectile[index]; if (existing.active && existing.owner == owner && existing.type == type) existing.Kill(); } int projectileIndex = Projectile.NewProjectile(source, origin, Vector2.Zero, type, 0, 0f, owner); if (projectileIndex < 0 || projectileIndex >= Main.maxProjectiles) return; Projectile projectile = Main.projectile[projectileIndex]; if (projectile.ModProjectile is ReaperSoulFamiliarProjectile familiar) familiar.Configure(snapshot); ReaperProjectileHelper.SyncNewProjectile(projectile); if (snapshot.Skill2 > 0) { ReaperSkillCueProjectile.Spawn(source, owner, snapshot, 1, origin, Vector2.UnitY * -1f, actionId: projectile.identity); } } public override void SetDefaults() { Projectile.width = 34; Projectile.height = 34; Projectile.friendly = false; Projectile.hostile = false; Projectile.tileCollide = false; Projectile.ignoreWater = true; Projectile.penetrate = -1; Projectile.timeLeft = 360; Projectile.DamageType = DamageClass.Melee; } public override bool? CanDamage() => false; public override bool ShouldUpdatePosition() => false; public override void AI() { if (Main.netMode == NetmodeID.Server && !serverAuthorized) { Projectile.Kill(); return; } if (!configured || Projectile.owner < 0 || Projectile.owner >= Main.maxPlayers) return; if (Main.netMode == NetmodeID.Server) Projectile.Center = authoritativeCenter; Player owner = Main.player[Projectile.owner]; if (!owner.active || owner.dead) { Projectile.Kill(); return; } timer++; float angle = timer * 0.045f + Projectile.identity * 0.31f; Vector2 desired = owner.MountedCenter + new Vector2(owner.direction * -46f, -38f) + angle.ToRotationVector2() * 12f; Projectile.Center = Vector2.Lerp(Projectile.Center, desired, 0.16f); Projectile.velocity = Vector2.Zero; if (Main.netMode == NetmodeID.Server) authoritativeCenter = Projectile.Center; Projectile.rotation += 0.08f; int maximumAttacks = snapshot.Skill2 switch { 2 => 3, >= 3 => 4, _ => 2 }; int duration = (snapshot.Skill2 switch { 2 => 4, >= 3 => 5, _ => 3 }) * 60; int interval = Math.Max(22, duration / (maximumAttacks + 1)); if (timer % interval == 0 && attacksMade < maximumAttacks && Main.netMode != NetmodeID.MultiplayerClient) { NPC? target = FindTarget(640f); if (target is not null) { float factor = snapshot.Skill2 switch { 2 => 0.45f, >= 3 => 0.5f, _ => 0.4f }; Vector2 direction = (target.Center - Projectile.Center).SafeNormalize(Vector2.UnitX * owner.direction); ReaperStrikeProjectile.Spawn(Projectile.GetSource_FromThis(), Projectile.owner, snapshot, ReaperHitKind.SoulFamiliar, attacksMade, Projectile.Center, direction, ReaperStrikeShape.Line, Math.Min(340f, Vector2.Distance(Projectile.Center, target.Center) + 36f), 19f, factor); attacksMade++; } } if (timer >= duration) Projectile.Kill(); if (Main.netMode != NetmodeID.Server && Main.rand.NextBool(2)) { Dust dust = Dust.NewDustPerfect(Projectile.Center + Main.rand.NextVector2Circular(12f, 12f), DustID.AncientLight, -Projectile.velocity * 0.1f, 60, Main.rand.NextBool() ? new Color(85, 225, 255) : new Color(175, 85, 255), 0.85f); dust.noGravity = true; } } public override void SendExtraAI(BinaryWriter writer) { writer.Write(configured); if (configured) { snapshot.Write(writer); writer.Write((short)timer); writer.Write((byte)attacksMade); } } 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); int incomingTimer = reader.ReadInt16(); int incomingAttacksMade = reader.ReadByte(); if (Main.netMode == NetmodeID.Server) return; configured = true; snapshot = incomingSnapshot; timer = incomingTimer; attacksMade = incomingAttacksMade; } private void Configure(in SickleCombatSnapshot value) { snapshot = value; configured = true; serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient; Projectile.GetGlobalProjectile().AuthorizeServerReaperProjectile(Projectile); authoritativeCenter = Projectile.Center; timer = 0; attacksMade = 0; Projectile.timeLeft = (snapshot.Skill2 switch { 2 => 4, >= 3 => 5, _ => 3 }) * 60 + 10; } private NPC? FindTarget(float maximumDistance) { NPC? result = null; foreach (NPC npc in Main.ActiveNPCs) { if (!npc.CanBeChasedBy(Projectile)) continue; float distance = Vector2.Distance(Projectile.Center, npc.Center); if (distance < maximumDistance && Collision.CanHitLine(Projectile.Center, 1, 1, npc.Center, 1, 1)) { maximumDistance = distance; result = npc; } } return result; } }