using SoulHarvest.Common; using Microsoft.Xna.Framework; using System; using System.IO; using Terraria; using Terraria.ID; using Terraria.ModLoader; namespace SoulHarvest.Projectiles; /// Target-locked shatter worth exactly the stored cut-and-domain total. public sealed class ReaperDeathShatterProjectile : ModProjectile { private SickleCombatSnapshot snapshot; private int targetIndex = -1; private int actionId; private int age; private bool configured; private Vector2 visualCenter; private bool drawCenterVisual; private bool serverAuthorized; private bool serverDamageTriggered; internal Vector2 VisualCenter => visualCenter; internal bool DrawsCenterVisual => drawCenterVisual; public override string Texture => "Terraria/Images/Projectile_0"; internal static int Spawn(Terraria.DataStructures.IEntitySource source, int owner, in SickleCombatSnapshot snapshot, NPC target, int accumulatedDamage, int actionId, Vector2 visualCenter, bool drawCenterVisual) { if (Main.netMode == NetmodeID.MultiplayerClient || accumulatedDamage <= 0) return -1; int damage = Math.Max(1, Math.Min(int.MaxValue / 4, accumulatedDamage)); int index = Projectile.NewProjectile(source, target.Center, Vector2.Zero, ModContent.ProjectileType(), damage, snapshot.Knockback * 0.2f, owner); if (index < 0 || index >= Main.maxProjectiles) return -1; Projectile projectile = Main.projectile[index]; projectile.originalDamage = damage; projectile.ArmorPenetration = Math.Max(0, target.defense); if (projectile.ModProjectile is ReaperDeathShatterProjectile shatter) shatter.Configure(snapshot, target.whoAmI, actionId, visualCenter, drawCenterVisual); projectile.GetGlobalProjectile().ConfigureReaperProjectile( projectile, snapshot, ReaperHitKind.UltimateDerived, 63, 0f, actionId); ReaperProjectileHelper.SyncNewProjectile(projectile); return index; } public override void SetDefaults() { Projectile.width = 8; Projectile.height = 8; Projectile.friendly = true; Projectile.hostile = false; Projectile.tileCollide = false; Projectile.ignoreWater = true; Projectile.penetrate = 1; Projectile.timeLeft = 12; Projectile.netImportant = true; Projectile.DamageType = DamageClass.Melee; } public override bool ShouldUpdatePosition() => false; public override bool? CanDamage() { if (Main.netMode == NetmodeID.MultiplayerClient) return false; return configured && age is >= 2 and <= 4 ? null : false; } public override bool? CanHitNPC(NPC target) => configured && target.whoAmI == targetIndex ? null : false; public override void AI() { if (Main.netMode == NetmodeID.Server && !serverAuthorized) { Projectile.Kill(); return; } if (!configured) return; age++; if (targetIndex >= 0 && targetIndex < Main.maxNPCs && Main.npc[targetIndex].active) Projectile.Center = Main.npc[targetIndex].Center; if (Main.netMode == NetmodeID.Server && !serverDamageTriggered && age >= 2) { serverDamageTriggered = true; ReaperProjectileHelper.DamageOnDedicatedServer(Projectile); } } public override bool? Colliding(Rectangle projHitbox, Rectangle targetHitbox) => configured && targetIndex >= 0 && targetIndex < Main.maxNPCs && Main.npc[targetIndex].Hitbox.Intersects(targetHitbox); public override bool PreDraw(ref Color lightColor) { if (!configured || !drawCenterVisual || Main.dedServ) return false; float opacity = 1f - MathHelper.Clamp(age / 11f, 0f, 1f); for (int shard = 0; shard < 12; shard++) { float angle = shard * MathHelper.TwoPi / 12f + actionId * 0.013f; Vector2 direction = angle.ToRotationVector2(); Vector2 start = visualCenter + direction * (8f + age * 1.2f); Vector2 end = start + direction * (30f + age * 5f); DeathDomainPrimitiveTextureSystem.DrawSmoothWorldLine(start, end, new Color(225, 22, 62, 0) * opacity, 4.5f * opacity + 0.8f); DeathDomainPrimitiveTextureSystem.DrawSmoothWorldLine(start, end, Color.White * (opacity * 0.78f), 1.1f); } return false; } public override void SendExtraAI(BinaryWriter writer) { writer.Write(configured); if (!configured) return; snapshot.Write(writer); writer.Write((short)targetIndex); writer.Write(actionId); writer.WriteVector2(visualCenter); writer.Write(drawCenterVisual); writer.Write((byte)Math.Clamp(age, 0, byte.MaxValue)); } public override void ReceiveExtraAI(BinaryReader reader) { bool incoming = reader.ReadBoolean(); if (!incoming) { if (Main.netMode != NetmodeID.Server) configured = false; return; } SickleCombatSnapshot incomingSnapshot = SickleCombatSnapshot.Read(reader); int incomingTarget = reader.ReadInt16(); int incomingAction = reader.ReadInt32(); Vector2 incomingVisualCenter = reader.ReadVector2(); bool incomingDrawCenterVisual = reader.ReadBoolean(); int incomingAge = reader.ReadByte(); if (Main.netMode == NetmodeID.Server) return; snapshot = incomingSnapshot; targetIndex = incomingTarget; actionId = incomingAction; visualCenter = IsFinite(incomingVisualCenter) ? incomingVisualCenter : Projectile.Center; drawCenterVisual = incomingDrawCenterVisual; age = incomingAge; configured = true; } private void Configure(in SickleCombatSnapshot value, int target, int parentActionId, Vector2 center, bool rendersCenterVisual) { snapshot = value; targetIndex = target; actionId = parentActionId; visualCenter = IsFinite(center) ? center : Projectile.Center; drawCenterVisual = rendersCenterVisual; age = 0; configured = true; serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient; serverDamageTriggered = false; } private static bool IsFinite(Vector2 value) => float.IsFinite(value.X) && float.IsFinite(value.Y); }