using SoulHarvest.Common; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using ReLogic.Content; using System; using System.IO; using Terraria; using Terraria.ID; using Terraria.ModLoader; namespace SoulHarvest.Projectiles; /// A Stage-III branch scythe summoned by Death's held special. public sealed class ReaperDeathEchoScytheProjectile : ModProjectile { private const int HistoryLength = 16; private const int SlashCycleFrames = 20; private const int StrikeFrame = 8; private const int ResidualFrame = 15; private static readonly ReaperFormId[] BranchForms = [ ReaperFormId.Bone, ReaperFormId.Blood, ReaperFormId.Infernal, ReaperFormId.Frost, ReaperFormId.Soul, ReaperFormId.Void ]; private readonly Vector2[] tipHistory = new Vector2[HistoryLength]; private SickleCombatSnapshot snapshot; private ReaperFormId branchForm; private Vector2 summonOrigin; private Vector2 strikeFocus; private Vector2 focus; private Vector2 aim; private float weaponAngle; private int phase; private int actionId; private int age; private int validHistory; private int lastStrikeCycle = -1; private int lastResidualCycle = -1; private bool configured; private bool serverAuthorized; public override string Texture => "Terraria/Images/Projectile_0"; internal static int Spawn(Terraria.DataStructures.IEntitySource source, int owner, in SickleCombatSnapshot deathSnapshot, int phase, Vector2 origin, Vector2 targetFocus, int actionId) { if (Main.netMode == NetmodeID.MultiplayerClient) return -1; phase = Math.Clamp(phase, 0, BranchForms.Length - 1); Vector2 direction = (targetFocus - origin).SafeNormalize(Vector2.UnitX); int index = Projectile.NewProjectile(source, origin, direction, ModContent.ProjectileType(), 0, 0f, owner); if (index < 0 || index >= Main.maxProjectiles) return -1; Projectile projectile = Main.projectile[index]; if (projectile.ModProjectile is ReaperDeathEchoScytheProjectile echo) echo.Configure(deathSnapshot, phase, origin, targetFocus, actionId); projectile.netUpdate = true; if (Main.netMode == NetmodeID.Server) { // The echo is configured after NewProjectile. Push that completed // snapshot immediately so multiplayer clients see and animate every // summoned right-click scythe from its first cycle. NetMessage.SendData(MessageID.SyncProjectile, -1, -1, null, index); } return index; } 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 = 190; 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++; int slashCount = snapshot.DeathInvocationSlashCount; int cycle = Math.Min(slashCount - 1, Math.Max(0, (age - 1) / SlashCycleFrames)); int cycleAge = Math.Max(1, (age - 1) % SlashCycleFrames + 1); float travel = Smooth01(MathHelper.Clamp(age / 11f, 0f, 1f)); Vector2 arrival = strikeFocus - aim * 42f; focus = Vector2.Lerp(summonOrigin, arrival, travel); Projectile.Center = focus; if (cycleAge == 1) validHistory = 0; float progress = MathHelper.Clamp((cycleAge - 2f) / 14f, 0f, 1f); float eased = progress * progress * (3f - 2f * progress); int facing = aim.X >= 0f ? 1 : -1; int swingDirection = (cycle & 1) == 0 ? facing : -facing; weaponAngle = MathHelper.Lerp(aim.ToRotation() - 2.35f * swingDirection, aim.ToRotation() + 2.15f * swingDirection, eased); RecordTip(focus + weaponAngle.ToRotationVector2() * ReaperCombatRegistry.GetBladeTipLength(branchForm, ReaperStage.StageIII) * snapshot.DeathRangeMultiplier); if (lastStrikeCycle < cycle && cycleAge >= StrikeFrame && Main.netMode != NetmodeID.MultiplayerClient) { lastStrikeCycle = cycle; SpawnBranchExecution(cycle); } if (lastResidualCycle < cycle && cycleAge >= ResidualFrame && validHistory >= 2 && Main.netMode != NetmodeID.MultiplayerClient && branchForm is ReaperFormId.Blood or ReaperFormId.Void) { lastResidualCycle = cycle; if (branchForm == ReaperFormId.Blood) { ReaperBloodArcScarProjectile.Spawn(Projectile.GetSource_FromThis(), Projectile.owner, snapshot, tipHistory, validHistory, phase, actionId + cycle, 0.25f); } else { ReaperVoidArcRiftProjectile.Spawn(Projectile.GetSource_FromThis(), Projectile.owner, snapshot, tipHistory, validHistory, phase, actionId + cycle, 0.25f); } } if (age >= slashCount * SlashCycleFrames + 2) Projectile.Kill(); } public override bool PreDraw(ref Color lightColor) { if (!configured || Main.dedServ) return false; int cycleAge = Math.Max(1, (age - 1) % SlashCycleFrames + 1); float opacity = Smooth01(MathHelper.Clamp(cycleAge / 3f, 0f, 1f)) * Smooth01(MathHelper.Clamp((SlashCycleFrames + 1f - cycleAge) / 5f, 0f, 1f)); if (age <= 35) { float portalReveal = Smooth01(age / 15f); float portalClose = 1f - Smooth01((age - 20f) / 15f); DeathDomainPortalRenderer.Draw(summonOrigin, aim, 1.16f * snapshot.DeathRangeMultiplier, Math.Min(portalReveal, portalClose), actionId); } DrawBranchTrail(opacity); Asset asset = ModContent.Request( ReaperCombatRegistry.GetTexturePath(branchForm, ReaperStage.StageIII)); Texture2D texture = asset.Value; Vector2 anchor = ReaperCombatRegistry.GetHandleAnchor(branchForm, ReaperStage.StageIII); Vector2 origin = new(texture.Width * anchor.X, texture.Height * anchor.Y); int swingDirection = aim.X >= 0f ? 1 : -1; SpriteEffects effects = swingDirection < 0 ? SpriteEffects.FlipVertically : SpriteEffects.None; if ((effects & SpriteEffects.FlipVertically) != 0) origin.Y = texture.Height - origin.Y; float correction = ReaperCombatRegistry.GetTextureRotationCorrection( branchForm, ReaperStage.StageIII); Main.EntitySpriteDraw(texture, focus - Main.screenPosition, null, ReaperCombatRegistry.GetPrimaryColor(branchForm) with { A = 0 }, weaponAngle + correction, origin, 0.74f * snapshot.DeathRangeMultiplier, effects); Main.EntitySpriteDraw(texture, focus - Main.screenPosition, null, Color.White * opacity, weaponAngle + correction, origin, 0.62f * snapshot.DeathRangeMultiplier, effects); return false; } public override void SendExtraAI(BinaryWriter writer) { writer.Write(configured); if (!configured) return; snapshot.Write(writer); writer.Write((byte)branchForm); writer.Write((byte)phase); writer.Write(actionId); writer.WriteVector2(summonOrigin); writer.WriteVector2(strikeFocus); writer.WriteVector2(aim); writer.Write((byte)Math.Clamp(age, 0, byte.MaxValue)); writer.Write((sbyte)Math.Clamp(lastStrikeCycle, -1, sbyte.MaxValue)); writer.Write((sbyte)Math.Clamp(lastResidualCycle, -1, sbyte.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); ReaperFormId incomingForm = (ReaperFormId)reader.ReadByte(); int incomingPhase = reader.ReadByte(); int incomingAction = reader.ReadInt32(); Vector2 incomingOrigin = reader.ReadVector2(); Vector2 incomingStrikeFocus = reader.ReadVector2(); Vector2 incomingAim = reader.ReadVector2(); int incomingAge = reader.ReadByte(); int incomingStrikeCycle = reader.ReadSByte(); int incomingResidualCycle = reader.ReadSByte(); if (Main.netMode == NetmodeID.Server) return; snapshot = incomingSnapshot; branchForm = incomingForm; phase = incomingPhase; actionId = incomingAction; summonOrigin = incomingOrigin; strikeFocus = incomingStrikeFocus; focus = Vector2.Lerp(summonOrigin, strikeFocus - incomingAim * 42f, Smooth01(MathHelper.Clamp(incomingAge / 11f, 0f, 1f))); aim = incomingAim; age = incomingAge; lastStrikeCycle = incomingStrikeCycle; lastResidualCycle = incomingResidualCycle; configured = true; } private void Configure(in SickleCombatSnapshot deathSnapshot, int attackPhase, Vector2 origin, Vector2 targetFocus, int parentActionId) { phase = Math.Clamp(attackPhase, 0, BranchForms.Length - 1); branchForm = BranchForms[phase]; snapshot = deathSnapshot with { Form = branchForm, Stage = ReaperStage.StageIII, Skill1 = 3, Skill2 = 3, Skill3 = 3 }; summonOrigin = origin; strikeFocus = targetFocus; focus = origin; aim = (targetFocus - origin).SafeNormalize(Vector2.UnitX); actionId = parentActionId; age = 0; validHistory = 0; lastStrikeCycle = -1; lastResidualCycle = -1; Projectile.timeLeft = snapshot.DeathInvocationSlashCount * SlashCycleFrames + 6; configured = true; serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient; } private void SpawnBranchExecution(int slashCycle) { Vector2 direction = weaponAngle.ToRotationVector2(); Vector2 executionFocus = strikeFocus; ReaperStrikeShape shape = branchForm switch { ReaperFormId.Bone or ReaperFormId.Frost => ReaperStrikeShape.Cross, ReaperFormId.Soul => ReaperStrikeShape.Circle, _ => ReaperStrikeShape.Line }; float length = (shape == ReaperStrikeShape.Circle ? 94f : 205f) * snapshot.DeathRangeMultiplier; Vector2 start = shape == ReaperStrikeShape.Line ? executionFocus - direction * length * 0.5f : executionFocus; ReaperStrikeProjectile.Spawn(Projectile.GetSource_FromThis(), Projectile.owner, snapshot, ReaperHitKind.Primary, 0, start, direction, shape, length, 25f * snapshot.DeathRangeMultiplier, 0.75f, actionId: actionId + slashCycle); ReaperSkillCueProjectile.Spawn(Projectile.GetSource_FromThis(), Projectile.owner, snapshot, phase % 3, executionFocus, direction, variant: phase, actionId: unchecked(actionId * 8 + phase + slashCycle * 47)); } private void RecordTip(Vector2 point) { for (int index = HistoryLength - 1; index > 0; index--) tipHistory[index] = tipHistory[index - 1]; tipHistory[0] = point; validHistory = Math.Min(HistoryLength, validHistory + 1); } private void DrawBranchTrail(float opacity) { for (int index = 1; index < validHistory; index++) { Vector2 start = tipHistory[index]; Vector2 end = tipHistory[index - 1]; float progress = 1f - index / (float)validHistory; float width = 4f + 13f * progress; float alpha = opacity * progress; switch (branchForm) { case ReaperFormId.Bone: DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(start, end, new Color(18, 96, 128, 0) * (alpha * 0.72f), width * 1.18f); DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(start, end, new Color(226, 239, 211, 235) * alpha, width * 0.32f); if (index % 3 == 0) DrawTrailNode(end, width * 0.34f, new Color(231, 241, 216, 230) * alpha); break; case ReaperFormId.Blood: ReaperBloodTrailRenderer.DrawSegment(start, end, width * 0.72f, alpha); break; case ReaperFormId.Infernal: DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(start, end, new Color(18, 4, 0, 245) * alpha, width * 1.48f); DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(start, end, new Color(255, 77, 5, 0) * alpha, width * 0.84f); DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(start, end, new Color(255, 238, 150, 235) * alpha, width * 0.17f); break; case ReaperFormId.Frost: DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(start, end, new Color(33, 123, 205, 0) * (alpha * 0.64f), width * 1.22f); DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(start, end, new Color(197, 244, 255, 235) * alpha, width * 0.24f); if ((index & 3) == 0) DrawTrailNode(end, width * 0.27f, new Color(218, 251, 255, 220) * alpha); break; case ReaperFormId.Soul: { Vector2 normal = (end - start).SafeNormalize(Vector2.UnitX) .RotatedBy(MathHelper.PiOver2) * (float)Math.Sin(index * 1.7f) * 3f; DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(start + normal, end + normal, new Color(80, 24, 154, 0) * (alpha * 0.52f), width * 1.5f); DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(start - normal, end - normal, new Color(126, 235, 255, 0) * alpha, width * 0.23f); break; } case ReaperFormId.Void: ReaperVoidBackdropRenderer.DrawTrailStrip(Main.spriteBatch, start, end, width * 0.72f, alpha, true, actionId, index); break; } } } private static void DrawTrailNode(Vector2 worldPosition, float radius, Color color) { DeathDomainPrimitiveTextureSystem.DrawSmoothDisc(Main.spriteBatch, worldPosition - Main.screenPosition, radius, color); } private static float Smooth01(float value) { value = MathHelper.Clamp(value, 0f, 1f); return value * value * (3f - 2f * value); } }