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;
/// <summary>
/// A Death-scythe afterimage anchored at a position cut by the primary attack.
/// It repeatedly acquires enemies from that exact position and performs real,
/// server-authoritative follow-up cuts without summoning any branch weapon.
/// </summary>
public sealed class ReaperDeathPhantomProjectile : ModProjectile
{
private const int MaximumPhantomsPerOwner = 20;
private SickleCombatSnapshot snapshot;
private Vector2 anchor;
private Vector2 attackDirection = Vector2.UnitX;
private float tempo = 1f;
private float weaponAngle;
private int age;
private int attackClock;
private int seed;
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 snapshot, Vector2 position, Vector2 initialDirection,
float tempo, int stableSeed)
{
if (Main.netMode == NetmodeID.MultiplayerClient)
return -1;
TrimOldest(owner);
int index = Projectile.NewProjectile(source, position, Vector2.Zero,
ModContent.ProjectileType<ReaperDeathPhantomProjectile>(), 0, 0f, owner);
if (index < 0 || index >= Main.maxProjectiles)
return -1;
Projectile projectile = Main.projectile[index];
if (projectile.ModProjectile is ReaperDeathPhantomProjectile phantom)
phantom.Configure(snapshot, position, initialDirection, tempo, stableSeed);
ReaperProjectileHelper.SyncNewProjectile(projectile);
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 = 240;
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++;
attackClock++;
Projectile.Center = anchor;
NPC? target = FindTarget(720f);
if (target is not null)
{
Vector2 desired = (target.Center - anchor).SafeNormalize(attackDirection);
attackDirection = Vector2.Lerp(attackDirection, desired, 0.18f)
.SafeNormalize(desired);
}
int interval = Math.Clamp((int)Math.Round(MathHelper.Lerp(30f, 12f,
MathHelper.Clamp((tempo - 1f) / 1.6f, 0f, 1f))), 12, 30);
float cycle = (attackClock % interval) / (float)interval;
int facing = attackDirection.X >= 0f ? 1 : -1;
weaponAngle = attackDirection.ToRotation()
+ MathHelper.Lerp(-1.65f * facing, 1.35f * facing,
cycle * cycle * (3f - 2f * cycle));
if (Main.netMode != NetmodeID.MultiplayerClient && target is not null
&& age >= 10 && attackClock >= interval)
{
attackClock = 0;
Vector2 direction = (target.Center - anchor).SafeNormalize(attackDirection);
float length = MathHelper.Clamp(Vector2.Distance(anchor, target.Center) + 34f,
72f, 720f);
ReaperStrikeProjectile.Spawn(Projectile.GetSource_FromThis(), Projectile.owner,
snapshot, ReaperHitKind.PrimaryDerived, seed % 6, anchor, direction,
ReaperStrikeShape.Line, length, 22f + tempo * 3f,
MathHelper.Lerp(0.30f, 0.48f, (tempo - 1f) / 1.6f),
actionId: seed * 509 + age);
Projectile.netUpdate = true;
}
}
public override bool PreDraw(ref Color lightColor)
{
if (!configured || Main.dedServ)
return false;
float fadeIn = MathHelper.Clamp(age / 10f, 0f, 1f);
float fadeOut = MathHelper.Clamp(Projectile.timeLeft / 35f, 0f, 1f);
float opacity = fadeIn * fadeOut;
Asset<Texture2D> asset = ModContent.Request<Texture2D>(
ReaperCombatRegistry.GetTexturePath(ReaperFormId.Death, ReaperStage.StageIII));
Texture2D texture = asset.Value;
Vector2 anchorRatio = ReaperCombatRegistry.GetHandleAnchor(
ReaperFormId.Death, ReaperStage.StageIII);
Vector2 origin = new(texture.Width * anchorRatio.X, texture.Height * anchorRatio.Y);
SpriteEffects effects = attackDirection.X < 0f
? SpriteEffects.FlipVertically : SpriteEffects.None;
if ((effects & SpriteEffects.FlipVertically) != 0)
origin.Y = texture.Height - origin.Y;
float correction = ReaperCombatRegistry.GetTextureRotationCorrection(
ReaperFormId.Death, ReaperStage.StageIII);
Vector2 drawPosition = anchor - Main.screenPosition;
Color shadow = new Color(32, 0, 18, 0) * (opacity * 0.58f);
Color edge = new Color(245, 24, 80, 0) * (opacity * 0.72f);
Main.EntitySpriteDraw(texture, drawPosition, null, shadow,
weaponAngle + correction - 0.12f, origin, 1.08f, effects);
Main.EntitySpriteDraw(texture, drawPosition, null, edge,
weaponAngle + correction, origin, 0.93f, effects);
Main.EntitySpriteDraw(texture, drawPosition, null, Color.White * (opacity * 0.34f),
weaponAngle + correction, origin, 0.855f, effects);
return false;
}
public override void SendExtraAI(BinaryWriter writer)
{
writer.Write(configured);
if (!configured)
return;
snapshot.Write(writer);
writer.WriteVector2(anchor);
writer.WriteVector2(attackDirection);
writer.Write(tempo);
writer.Write(seed);
writer.Write((short)Math.Clamp(age, 0, short.MaxValue));
writer.Write((short)Math.Clamp(attackClock, 0, short.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);
Vector2 incomingAnchor = reader.ReadVector2();
Vector2 incomingDirection = reader.ReadVector2();
float incomingTempo = reader.ReadSingle();
int incomingSeed = reader.ReadInt32();
int incomingAge = reader.ReadInt16();
int incomingClock = reader.ReadInt16();
if (Main.netMode == NetmodeID.Server)
return;
snapshot = incomingSnapshot;
anchor = incomingAnchor;
attackDirection = incomingDirection.SafeNormalize(Vector2.UnitX);
tempo = MathHelper.Clamp(incomingTempo, 1f, 2.6f);
seed = incomingSeed;
age = incomingAge;
attackClock = incomingClock;
configured = true;
}
private void Configure(in SickleCombatSnapshot value, Vector2 position,
Vector2 initialDirection, float attackTempo, int stableSeed)
{
snapshot = value;
anchor = position;
attackDirection = initialDirection.SafeNormalize(Vector2.UnitX);
tempo = MathHelper.Clamp(attackTempo, 1f, 2.6f);
seed = stableSeed;
age = 0;
attackClock = 0;
configured = true;
serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient;
Projectile.Center = anchor;
Projectile.timeLeft = 240;
}
private NPC? FindTarget(float maximumDistance)
{
NPC? nearest = null;
float best = maximumDistance * maximumDistance;
foreach (NPC npc in Main.ActiveNPCs)
{
if (!ReaperTargeting.IsValidWeaponTarget(npc)
|| !ReaperTargeting.IsTrainingDummy(npc)
&& !npc.CanBeChasedBy(Projectile))
continue;
float distance = Vector2.DistanceSquared(npc.Center, anchor);
if (distance >= best)
continue;
best = distance;
nearest = npc;
}
return nearest;
}
private static void TrimOldest(int owner)
{
int type = ModContent.ProjectileType<ReaperDeathPhantomProjectile>();
Projectile? oldest = null;
int count = 0;
foreach (Projectile projectile in Main.ActiveProjectiles)
{
if (projectile.owner != owner || projectile.type != type)
continue;
count++;
if (oldest is null || projectile.timeLeft < oldest.timeLeft)
oldest = projectile;
}
if (count >= MaximumPhantomsPerOwner)
oldest?.Kill();
}
}
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;
/// <summary>
/// A Death-scythe afterimage anchored at a position cut by the primary attack.
/// It repeatedly acquires enemies from that exact position and performs real,
/// server-authoritative follow-up cuts without summoning any branch weapon.
/// </summary>
public sealed class ReaperDeathPhantomProjectile : ModProjectile
{
private const int MaximumPhantomsPerOwner = 20;
private SickleCombatSnapshot snapshot;
private Vector2 anchor;
private Vector2 attackDirection = Vector2.UnitX;
private float tempo = 1f;
private float weaponAngle;
private int age;
private int attackClock;
private int seed;
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 snapshot, Vector2 position, Vector2 initialDirection,
float tempo, int stableSeed)
{
if (Main.netMode == NetmodeID.MultiplayerClient)
return -1;
TrimOldest(owner);
int index = Projectile.NewProjectile(source, position, Vector2.Zero,
ModContent.ProjectileType<ReaperDeathPhantomProjectile>(), 0, 0f, owner);
if (index < 0 || index >= Main.maxProjectiles)
return -1;
Projectile projectile = Main.projectile[index];
if (projectile.ModProjectile is ReaperDeathPhantomProjectile phantom)
phantom.Configure(snapshot, position, initialDirection, tempo, stableSeed);
ReaperProjectileHelper.SyncNewProjectile(projectile);
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 = 240;
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++;
attackClock++;
Projectile.Center = anchor;
NPC? target = FindTarget(720f);
if (target is not null)
{
Vector2 desired = (target.Center - anchor).SafeNormalize(attackDirection);
attackDirection = Vector2.Lerp(attackDirection, desired, 0.18f)
.SafeNormalize(desired);
}
int interval = Math.Clamp((int)Math.Round(MathHelper.Lerp(30f, 12f,
MathHelper.Clamp((tempo - 1f) / 1.6f, 0f, 1f))), 12, 30);
float cycle = (attackClock % interval) / (float)interval;
int facing = attackDirection.X >= 0f ? 1 : -1;
weaponAngle = attackDirection.ToRotation()
+ MathHelper.Lerp(-1.65f * facing, 1.35f * facing,
cycle * cycle * (3f - 2f * cycle));
if (Main.netMode != NetmodeID.MultiplayerClient && target is not null
&& age >= 10 && attackClock >= interval)
{
attackClock = 0;
Vector2 direction = (target.Center - anchor).SafeNormalize(attackDirection);
float length = MathHelper.Clamp(Vector2.Distance(anchor, target.Center) + 34f,
72f, 720f);
ReaperStrikeProjectile.Spawn(Projectile.GetSource_FromThis(), Projectile.owner,
snapshot, ReaperHitKind.PrimaryDerived, seed % 6, anchor, direction,
ReaperStrikeShape.Line, length, 22f + tempo * 3f,
MathHelper.Lerp(0.30f, 0.48f, (tempo - 1f) / 1.6f),
actionId: seed * 509 + age);
Projectile.netUpdate = true;
}
}
public override bool PreDraw(ref Color lightColor)
{
if (!configured || Main.dedServ)
return false;
float fadeIn = MathHelper.Clamp(age / 10f, 0f, 1f);
float fadeOut = MathHelper.Clamp(Projectile.timeLeft / 35f, 0f, 1f);
float opacity = fadeIn * fadeOut;
Asset<Texture2D> asset = ModContent.Request<Texture2D>(
ReaperCombatRegistry.GetTexturePath(ReaperFormId.Death, ReaperStage.StageIII));
Texture2D texture = asset.Value;
Vector2 anchorRatio = ReaperCombatRegistry.GetHandleAnchor(
ReaperFormId.Death, ReaperStage.StageIII);
Vector2 origin = new(texture.Width * anchorRatio.X, texture.Height * anchorRatio.Y);
SpriteEffects effects = attackDirection.X < 0f
? SpriteEffects.FlipVertically : SpriteEffects.None;
if ((effects & SpriteEffects.FlipVertically) != 0)
origin.Y = texture.Height - origin.Y;
float correction = ReaperCombatRegistry.GetTextureRotationCorrection(
ReaperFormId.Death, ReaperStage.StageIII);
Vector2 drawPosition = anchor - Main.screenPosition;
Color shadow = new Color(32, 0, 18, 0) * (opacity * 0.58f);
Color edge = new Color(245, 24, 80, 0) * (opacity * 0.72f);
Main.EntitySpriteDraw(texture, drawPosition, null, shadow,
weaponAngle + correction - 0.12f, origin, 1.08f, effects);
Main.EntitySpriteDraw(texture, drawPosition, null, edge,
weaponAngle + correction, origin, 0.93f, effects);
Main.EntitySpriteDraw(texture, drawPosition, null, Color.White * (opacity * 0.34f),
weaponAngle + correction, origin, 0.855f, effects);
return false;
}
public override void SendExtraAI(BinaryWriter writer)
{
writer.Write(configured);
if (!configured)
return;
snapshot.Write(writer);
writer.WriteVector2(anchor);
writer.WriteVector2(attackDirection);
writer.Write(tempo);
writer.Write(seed);
writer.Write((short)Math.Clamp(age, 0, short.MaxValue));
writer.Write((short)Math.Clamp(attackClock, 0, short.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);
Vector2 incomingAnchor = reader.ReadVector2();
Vector2 incomingDirection = reader.ReadVector2();
float incomingTempo = reader.ReadSingle();
int incomingSeed = reader.ReadInt32();
int incomingAge = reader.ReadInt16();
int incomingClock = reader.ReadInt16();
if (Main.netMode == NetmodeID.Server)
return;
snapshot = incomingSnapshot;
anchor = incomingAnchor;
attackDirection = incomingDirection.SafeNormalize(Vector2.UnitX);
tempo = MathHelper.Clamp(incomingTempo, 1f, 2.6f);
seed = incomingSeed;
age = incomingAge;
attackClock = incomingClock;
configured = true;
}
private void Configure(in SickleCombatSnapshot value, Vector2 position,
Vector2 initialDirection, float attackTempo, int stableSeed)
{
snapshot = value;
anchor = position;
attackDirection = initialDirection.SafeNormalize(Vector2.UnitX);
tempo = MathHelper.Clamp(attackTempo, 1f, 2.6f);
seed = stableSeed;
age = 0;
attackClock = 0;
configured = true;
serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient;
Projectile.Center = anchor;
Projectile.timeLeft = 240;
}
private NPC? FindTarget(float maximumDistance)
{
NPC? nearest = null;
float best = maximumDistance * maximumDistance;
foreach (NPC npc in Main.ActiveNPCs)
{
if (!ReaperTargeting.IsValidWeaponTarget(npc)
|| !ReaperTargeting.IsTrainingDummy(npc)
&& !npc.CanBeChasedBy(Projectile))
continue;
float distance = Vector2.DistanceSquared(npc.Center, anchor);
if (distance >= best)
continue;
best = distance;
nearest = npc;
}
return nearest;
}
private static void TrimOldest(int owner)
{
int type = ModContent.ProjectileType<ReaperDeathPhantomProjectile>();
Projectile? oldest = null;
int count = 0;
foreach (Projectile projectile in Main.ActiveProjectiles)
{
if (projectile.owner != owner || projectile.type != type)
continue;
count++;
if (oldest is null || projectile.timeLeft < oldest.timeLeft)
oldest = projectile;
}
if (count >= MaximumPhantomsPerOwner)
oldest?.Kill();
}
}