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 ReaperFrostShardProjectile : ModProjectile { private readonly HashSet hitRoots = []; private SickleCombatSnapshot snapshot; private bool configured; private float maximumTravel; private float traveled; private int retargetsRemaining; private bool serverAuthorized; private int authoritativeDamage; private Vector2 authoritativeCenter; private Vector2 authoritativeVelocity; public override string Texture => "SoulHarvest/Projectiles/FrostShardProjectile"; public static void Spawn(Terraria.DataStructures.IEntitySource source, int owner, in SickleCombatSnapshot snapshot, Vector2 origin, Vector2 direction) { if (Main.netMode == NetmodeID.MultiplayerClient) return; int level = Math.Max(1, (int)snapshot.Skill2); float factor = level switch { 2 => 0.90f, >= 3 => 1.05f, _ => 0.75f }; int index = Projectile.NewProjectile(source, origin, direction.SafeNormalize(Vector2.UnitX) * 11f, ModContent.ProjectileType(), Math.Max(1, (int)Math.Round(snapshot.Damage * factor)), snapshot.Knockback, owner); if (index < 0 || index >= Main.maxProjectiles) return; Projectile projectile = Main.projectile[index]; projectile.originalDamage = snapshot.Damage; if (projectile.ModProjectile is ReaperFrostShardProjectile shard) shard.Configure(snapshot); projectile.GetGlobalProjectile().ConfigureReaperProjectile( projectile, snapshot, ReaperHitKind.PrimaryDerived, 2, direction.ToRotation()); ReaperProjectileHelper.SyncNewProjectile(projectile); } public override void SetStaticDefaults() { ProjectileID.Sets.TrailCacheLength[Type] = 12; ProjectileID.Sets.TrailingMode[Type] = 2; } public override void SetDefaults() { Projectile.width = 30; Projectile.height = 30; Projectile.friendly = true; Projectile.tileCollide = true; Projectile.ignoreWater = true; Projectile.penetrate = -1; Projectile.timeLeft = 90; Projectile.DamageType = DamageClass.Melee; Projectile.usesLocalNPCImmunity = true; Projectile.localNPCHitCooldown = 10; } public override bool ShouldUpdatePosition() => 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; traveled += Projectile.velocity.Length(); if (traveled >= maximumTravel) { Projectile.Kill(); return; } NPC? target = FindTarget(maximumTravel - traveled); if (target is not null) { Vector2 desired = (target.Center - Projectile.Center).SafeNormalize(Projectile.velocity.SafeNormalize(Vector2.UnitX)) * 11f; Projectile.velocity = Vector2.Lerp(Projectile.velocity, desired, 0.11f); } Projectile.rotation += 0.28f * Math.Sign(Projectile.velocity.X == 0f ? 1f : Projectile.velocity.X); if (Main.netMode != NetmodeID.Server) { Lighting.AddLight(Projectile.Center, new Vector3(0.12f, 0.45f, 0.72f)); if (Main.rand.NextBool(2)) { Dust dust = Dust.NewDustPerfect(Projectile.Center, DustID.IceTorch, -Projectile.velocity * 0.08f, 60, new Color(125, 210, 255), 0.95f); dust.noGravity = true; } } MoveOneStep(); } public override void OnHitNPC(NPC target, NPC.HitInfo hit, int damageDone) { hitRoots.Add(target.realLife >= 0 ? target.realLife : target.whoAmI); if (retargetsRemaining-- <= 0) { Projectile.Kill(); return; } NPC? next = FindTarget(maximumTravel - traveled); if (next is null) { Projectile.Kill(); return; } Projectile.velocity = (next.Center - Projectile.Center).SafeNormalize(Projectile.velocity.SafeNormalize(Vector2.UnitX)) * 11f; if (Main.netMode == NetmodeID.Server) { authoritativeCenter = Projectile.Center; authoritativeVelocity = Projectile.velocity; } Projectile.netUpdate = true; } public override bool OnTileCollide(Vector2 oldVelocity) => true; public override bool PreDraw(ref Color lightColor) => ReaperProjectileHelper.DrawTrail( Projectile, new Color(105, 205, 255), scaleMultiplier: 1f, copies: 10); public override void SendExtraAI(BinaryWriter writer) { writer.Write(configured); if (!configured) return; snapshot.Write(writer); writer.Write(maximumTravel); writer.Write(traveled); writer.Write((byte)Math.Clamp(retargetsRemaining, 0, byte.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 incomingMaximumTravel = reader.ReadSingle(); float incomingTraveled = reader.ReadSingle(); int incomingRetargets = reader.ReadByte(); if (Main.netMode == NetmodeID.Server) return; configured = true; snapshot = incomingSnapshot; maximumTravel = incomingMaximumTravel; traveled = incomingTraveled; retargetsRemaining = incomingRetargets; } private void Configure(in SickleCombatSnapshot value) { snapshot = value; int level = Math.Max(1, (int)snapshot.Skill2); maximumTravel = (level switch { 2 => 12f, >= 3 => 14f, _ => 10f }) * 16f; retargetsRemaining = level >= 2 ? 2 : 1; traveled = 0f; hitRoots.Clear(); configured = true; serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient; authoritativeDamage = Math.Max(1, Projectile.damage); authoritativeCenter = Projectile.Center; authoritativeVelocity = Projectile.velocity; Projectile.timeLeft = Math.Max(20, (int)Math.Ceiling(maximumTravel / 8f)); } private void MoveOneStep() { Vector2 movement = Collision.TileCollision(Projectile.position, Projectile.velocity, Projectile.width, Projectile.height, fallThrough: false, fall2: false); if (movement != Projectile.velocity) { Projectile.Kill(); return; } Projectile.position += movement; if (Main.netMode == NetmodeID.Server) { authoritativeCenter = Projectile.Center; authoritativeVelocity = Projectile.velocity; } } private NPC? FindTarget(float maximumDistance) { NPC? result = null; foreach (NPC npc in Main.ActiveNPCs) { int root = npc.realLife >= 0 ? npc.realLife : npc.whoAmI; if (hitRoots.Contains(root) || !npc.CanBeChasedBy(Projectile)) continue; float distance = Vector2.Distance(Projectile.Center, npc.Center); if (distance < maximumDistance) { maximumDistance = distance; result = npc; } } return result; } }