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 SoulSickleProjectile : ModProjectile
{
private const int HomingDelayFrames = 8;
private SickleCombatSnapshot snapshot;
private ReaperHitKind hitKind;
private int phase;
private int actionId;
private int variant;
private int timer;
private int targetIndex = -1;
private bool configured;
private bool serverAuthorized;
private int authoritativeDamage;
private Vector2 authoritativeCenter;
private Vector2 authoritativeVelocity;
private readonly HashSet<int> hitRoots = [];
internal static int Spawn(Terraria.DataStructures.IEntitySource source, int owner,
in SickleCombatSnapshot snapshot, Vector2 position, Vector2 velocity,
int phase, int actionId, ReaperHitKind hitKind, float damageMultiplier,
int variant = 0)
{
if (Main.netMode == NetmodeID.MultiplayerClient)
return -1;
int projectileIndex = Projectile.NewProjectile(source, position, velocity,
ModContent.ProjectileType<SoulSickleProjectile>(),
Math.Max(1, (int)Math.Round(snapshot.Damage * damageMultiplier)),
snapshot.Knockback * 0.45f, owner);
if (projectileIndex < 0 || projectileIndex >= Main.maxProjectiles)
return -1;
Projectile projectile = Main.projectile[projectileIndex];
projectile.originalDamage = snapshot.Damage;
projectile.CritChance = snapshot.CritChance;
if (projectile.ModProjectile is SoulSickleProjectile soulBlade)
soulBlade.Configure(snapshot, hitKind, phase, actionId, variant);
projectile.GetGlobalProjectile<MyGlobalProjectile>().ConfigureReaperProjectile(
projectile, snapshot, hitKind, phase, velocity.ToRotation(), actionId);
ReaperProjectileHelper.SyncNewProjectile(projectile);
return projectileIndex;
}
public override void SetStaticDefaults()
{
ProjectileID.Sets.TrailCacheLength[Type] = 24;
ProjectileID.Sets.TrailingMode[Type] = 2;
}
public override void SetDefaults()
{
Projectile.width = 42;
Projectile.height = 42;
Projectile.friendly = true;
Projectile.hostile = false;
Projectile.tileCollide = false;
Projectile.penetrate = 4;
Projectile.ignoreWater = true;
Projectile.timeLeft = 150;
Projectile.DamageType = DamageClass.Melee;
Projectile.netImportant = true;
Projectile.usesLocalNPCImmunity = true;
Projectile.localNPCHitCooldown = 14;
}
public override bool ShouldUpdatePosition() => false;
public override bool? CanDamage() => configured ? null : 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;
timer++;
int curveDirection = (variant & 1) == 0 ? 1 : -1;
if (timer <= HomingDelayFrames)
{
Projectile.velocity = Projectile.velocity.RotatedBy(curveDirection
* (0.024f + snapshot.StageNumber * 0.004f));
}
else
{
NPC? target = GetOrFindTarget();
if (target is not null)
{
float speed = Math.Max(14f + snapshot.StageNumber,
Projectile.velocity.Length());
Vector2 desired = (target.Center - Projectile.Center)
.SafeNormalize(Projectile.velocity.SafeNormalize(Vector2.UnitX))
* speed;
Projectile.velocity = Vector2.Lerp(Projectile.velocity, desired,
0.085f + snapshot.StageNumber * 0.018f);
}
else
{
Projectile.velocity = Projectile.velocity.RotatedBy(
curveDirection * 0.006f);
}
}
Projectile.rotation = Projectile.velocity.ToRotation();
Projectile.GetGlobalProjectile<MyGlobalProjectile>()
.UpdateReaperAttackAngle(Projectile.rotation);
Projectile.position += Projectile.velocity;
if (Main.netMode == NetmodeID.Server)
{
authoritativeCenter = Projectile.Center;
authoritativeVelocity = Projectile.velocity;
}
if (!Main.dedServ)
{
Lighting.AddLight(Projectile.Center, new Vector3(0.22f, 0.34f, 0.82f));
if (Main.rand.NextBool(2))
{
Color color = Main.rand.NextBool()
? new Color(85, 225, 255)
: new Color(175, 85, 255);
Dust dust = Dust.NewDustPerfect(Projectile.Center
+ Main.rand.NextVector2Circular(11f, 11f), DustID.AncientLight,
-Projectile.velocity * 0.08f, 55, color, 0.95f);
dust.noGravity = true;
}
}
}
public override bool PreDraw(ref Color lightColor)
=> ReaperProjectileHelper.DrawTrail(Projectile, new Color(120, 115, 255),
scaleMultiplier: 1.04f + snapshot.StageNumber * 0.05f, copies: 22);
public override void OnHitNPC(NPC target, NPC.HitInfo hit, int damageDone)
{
int root = target.realLife >= 0 ? target.realLife : target.whoAmI;
hitRoots.Add(root);
if (targetIndex == target.whoAmI)
targetIndex = -1;
if (Main.netMode != NetmodeID.MultiplayerClient)
Projectile.netUpdate = true;
}
public override void SendExtraAI(BinaryWriter writer)
{
writer.Write(configured);
if (!configured)
return;
snapshot.Write(writer);
writer.Write((byte)hitKind);
writer.Write((byte)Math.Clamp(phase, 0, byte.MaxValue));
writer.Write(actionId);
writer.Write((byte)Math.Clamp(variant, 0, byte.MaxValue));
writer.Write((short)Math.Clamp(timer, 0, short.MaxValue));
writer.Write((short)targetIndex);
}
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);
ReaperHitKind incomingHitKind = (ReaperHitKind)reader.ReadByte();
int incomingPhase = reader.ReadByte();
int incomingActionId = reader.ReadInt32();
int incomingVariant = reader.ReadByte();
int incomingTimer = reader.ReadInt16();
int incomingTarget = reader.ReadInt16();
if (Main.netMode == NetmodeID.Server)
return;
snapshot = incomingSnapshot;
hitKind = incomingHitKind;
phase = incomingPhase;
actionId = incomingActionId;
variant = incomingVariant;
timer = incomingTimer;
targetIndex = incomingTarget;
configured = true;
}
private void Configure(in SickleCombatSnapshot value, ReaperHitKind kind,
int attackPhase, int parentActionId, int projectileVariant)
{
snapshot = value;
hitKind = kind;
phase = Math.Max(0, attackPhase);
actionId = parentActionId;
variant = Math.Max(0, projectileVariant);
timer = 0;
targetIndex = -1;
hitRoots.Clear();
configured = true;
serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient;
authoritativeDamage = Math.Max(1, Projectile.damage);
authoritativeCenter = Projectile.Center;
authoritativeVelocity = Projectile.velocity;
}
private NPC? GetOrFindTarget()
{
float retentionRadius = 760f + snapshot.StageNumber * 80f;
int previousTarget = targetIndex;
if (targetIndex >= 0 && targetIndex < Main.maxNPCs)
{
NPC current = Main.npc[targetIndex];
int currentRoot = current.realLife >= 0
? current.realLife
: current.whoAmI;
if (!hitRoots.Contains(currentRoot)
&& ReaperTargeting.IsValidWeaponTarget(current)
&& (ReaperTargeting.IsTrainingDummy(current)
|| current.CanBeChasedBy(Projectile))
&& Vector2.DistanceSquared(Projectile.Center, current.Center)
<= retentionRadius * retentionRadius)
{
return current;
}
targetIndex = -1;
}
NPC? target = FindUnhitTarget(520f + snapshot.StageNumber * 90f);
targetIndex = target?.whoAmI ?? -1;
if (targetIndex != previousTarget
&& Main.netMode != NetmodeID.MultiplayerClient)
Projectile.netUpdate = true;
return target;
}
private NPC? FindUnhitTarget(float maximumDistance)
{
NPC? target = null;
foreach (NPC npc in Main.ActiveNPCs)
{
int root = npc.realLife >= 0 ? npc.realLife : npc.whoAmI;
if (hitRoots.Contains(root)
|| !ReaperTargeting.IsValidWeaponTarget(npc)
|| !ReaperTargeting.IsTrainingDummy(npc)
&& !npc.CanBeChasedBy(Projectile))
{
continue;
}
float distance = Vector2.Distance(npc.Center, Projectile.Center);
if (distance >= maximumDistance)
continue;
maximumDistance = distance;
target = npc;
}
return target;
}
}
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 SoulSickleProjectile : ModProjectile
{
private const int HomingDelayFrames = 8;
private SickleCombatSnapshot snapshot;
private ReaperHitKind hitKind;
private int phase;
private int actionId;
private int variant;
private int timer;
private int targetIndex = -1;
private bool configured;
private bool serverAuthorized;
private int authoritativeDamage;
private Vector2 authoritativeCenter;
private Vector2 authoritativeVelocity;
private readonly HashSet<int> hitRoots = [];
internal static int Spawn(Terraria.DataStructures.IEntitySource source, int owner,
in SickleCombatSnapshot snapshot, Vector2 position, Vector2 velocity,
int phase, int actionId, ReaperHitKind hitKind, float damageMultiplier,
int variant = 0)
{
if (Main.netMode == NetmodeID.MultiplayerClient)
return -1;
int projectileIndex = Projectile.NewProjectile(source, position, velocity,
ModContent.ProjectileType<SoulSickleProjectile>(),
Math.Max(1, (int)Math.Round(snapshot.Damage * damageMultiplier)),
snapshot.Knockback * 0.45f, owner);
if (projectileIndex < 0 || projectileIndex >= Main.maxProjectiles)
return -1;
Projectile projectile = Main.projectile[projectileIndex];
projectile.originalDamage = snapshot.Damage;
projectile.CritChance = snapshot.CritChance;
if (projectile.ModProjectile is SoulSickleProjectile soulBlade)
soulBlade.Configure(snapshot, hitKind, phase, actionId, variant);
projectile.GetGlobalProjectile<MyGlobalProjectile>().ConfigureReaperProjectile(
projectile, snapshot, hitKind, phase, velocity.ToRotation(), actionId);
ReaperProjectileHelper.SyncNewProjectile(projectile);
return projectileIndex;
}
public override void SetStaticDefaults()
{
ProjectileID.Sets.TrailCacheLength[Type] = 24;
ProjectileID.Sets.TrailingMode[Type] = 2;
}
public override void SetDefaults()
{
Projectile.width = 42;
Projectile.height = 42;
Projectile.friendly = true;
Projectile.hostile = false;
Projectile.tileCollide = false;
Projectile.penetrate = 4;
Projectile.ignoreWater = true;
Projectile.timeLeft = 150;
Projectile.DamageType = DamageClass.Melee;
Projectile.netImportant = true;
Projectile.usesLocalNPCImmunity = true;
Projectile.localNPCHitCooldown = 14;
}
public override bool ShouldUpdatePosition() => false;
public override bool? CanDamage() => configured ? null : 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;
timer++;
int curveDirection = (variant & 1) == 0 ? 1 : -1;
if (timer <= HomingDelayFrames)
{
Projectile.velocity = Projectile.velocity.RotatedBy(curveDirection
* (0.024f + snapshot.StageNumber * 0.004f));
}
else
{
NPC? target = GetOrFindTarget();
if (target is not null)
{
float speed = Math.Max(14f + snapshot.StageNumber,
Projectile.velocity.Length());
Vector2 desired = (target.Center - Projectile.Center)
.SafeNormalize(Projectile.velocity.SafeNormalize(Vector2.UnitX))
* speed;
Projectile.velocity = Vector2.Lerp(Projectile.velocity, desired,
0.085f + snapshot.StageNumber * 0.018f);
}
else
{
Projectile.velocity = Projectile.velocity.RotatedBy(
curveDirection * 0.006f);
}
}
Projectile.rotation = Projectile.velocity.ToRotation();
Projectile.GetGlobalProjectile<MyGlobalProjectile>()
.UpdateReaperAttackAngle(Projectile.rotation);
Projectile.position += Projectile.velocity;
if (Main.netMode == NetmodeID.Server)
{
authoritativeCenter = Projectile.Center;
authoritativeVelocity = Projectile.velocity;
}
if (!Main.dedServ)
{
Lighting.AddLight(Projectile.Center, new Vector3(0.22f, 0.34f, 0.82f));
if (Main.rand.NextBool(2))
{
Color color = Main.rand.NextBool()
? new Color(85, 225, 255)
: new Color(175, 85, 255);
Dust dust = Dust.NewDustPerfect(Projectile.Center
+ Main.rand.NextVector2Circular(11f, 11f), DustID.AncientLight,
-Projectile.velocity * 0.08f, 55, color, 0.95f);
dust.noGravity = true;
}
}
}
public override bool PreDraw(ref Color lightColor)
=> ReaperProjectileHelper.DrawTrail(Projectile, new Color(120, 115, 255),
scaleMultiplier: 1.04f + snapshot.StageNumber * 0.05f, copies: 22);
public override void OnHitNPC(NPC target, NPC.HitInfo hit, int damageDone)
{
int root = target.realLife >= 0 ? target.realLife : target.whoAmI;
hitRoots.Add(root);
if (targetIndex == target.whoAmI)
targetIndex = -1;
if (Main.netMode != NetmodeID.MultiplayerClient)
Projectile.netUpdate = true;
}
public override void SendExtraAI(BinaryWriter writer)
{
writer.Write(configured);
if (!configured)
return;
snapshot.Write(writer);
writer.Write((byte)hitKind);
writer.Write((byte)Math.Clamp(phase, 0, byte.MaxValue));
writer.Write(actionId);
writer.Write((byte)Math.Clamp(variant, 0, byte.MaxValue));
writer.Write((short)Math.Clamp(timer, 0, short.MaxValue));
writer.Write((short)targetIndex);
}
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);
ReaperHitKind incomingHitKind = (ReaperHitKind)reader.ReadByte();
int incomingPhase = reader.ReadByte();
int incomingActionId = reader.ReadInt32();
int incomingVariant = reader.ReadByte();
int incomingTimer = reader.ReadInt16();
int incomingTarget = reader.ReadInt16();
if (Main.netMode == NetmodeID.Server)
return;
snapshot = incomingSnapshot;
hitKind = incomingHitKind;
phase = incomingPhase;
actionId = incomingActionId;
variant = incomingVariant;
timer = incomingTimer;
targetIndex = incomingTarget;
configured = true;
}
private void Configure(in SickleCombatSnapshot value, ReaperHitKind kind,
int attackPhase, int parentActionId, int projectileVariant)
{
snapshot = value;
hitKind = kind;
phase = Math.Max(0, attackPhase);
actionId = parentActionId;
variant = Math.Max(0, projectileVariant);
timer = 0;
targetIndex = -1;
hitRoots.Clear();
configured = true;
serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient;
authoritativeDamage = Math.Max(1, Projectile.damage);
authoritativeCenter = Projectile.Center;
authoritativeVelocity = Projectile.velocity;
}
private NPC? GetOrFindTarget()
{
float retentionRadius = 760f + snapshot.StageNumber * 80f;
int previousTarget = targetIndex;
if (targetIndex >= 0 && targetIndex < Main.maxNPCs)
{
NPC current = Main.npc[targetIndex];
int currentRoot = current.realLife >= 0
? current.realLife
: current.whoAmI;
if (!hitRoots.Contains(currentRoot)
&& ReaperTargeting.IsValidWeaponTarget(current)
&& (ReaperTargeting.IsTrainingDummy(current)
|| current.CanBeChasedBy(Projectile))
&& Vector2.DistanceSquared(Projectile.Center, current.Center)
<= retentionRadius * retentionRadius)
{
return current;
}
targetIndex = -1;
}
NPC? target = FindUnhitTarget(520f + snapshot.StageNumber * 90f);
targetIndex = target?.whoAmI ?? -1;
if (targetIndex != previousTarget
&& Main.netMode != NetmodeID.MultiplayerClient)
Projectile.netUpdate = true;
return target;
}
private NPC? FindUnhitTarget(float maximumDistance)
{
NPC? target = null;
foreach (NPC npc in Main.ActiveNPCs)
{
int root = npc.realLife >= 0 ? npc.realLife : npc.whoAmI;
if (hitRoots.Contains(root)
|| !ReaperTargeting.IsValidWeaponTarget(npc)
|| !ReaperTargeting.IsTrainingDummy(npc)
&& !npc.CanBeChasedBy(Projectile))
{
continue;
}
float distance = Vector2.Distance(npc.Center, Projectile.Center);
if (distance >= maximumDistance)
continue;
maximumDistance = distance;
target = npc;
}
return target;
}
}