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);
}
}