using SoulHarvest.Common;
using Microsoft.Xna.Framework;
using System;
using System.IO;
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;
namespace SoulHarvest.Projectiles;
public sealed class ReaperSoulFamiliarProjectile : ModProjectile
{
private SickleCombatSnapshot snapshot;
private bool configured;
private int timer;
private int attacksMade;
private bool serverAuthorized;
private Vector2 authoritativeCenter;
public override string Texture => "SoulHarvest/Projectiles/SoulSickleProjectile";
public static void Spawn(Terraria.DataStructures.IEntitySource source, int owner,
in SickleCombatSnapshot snapshot, Vector2 origin)
{
if (Main.netMode == NetmodeID.MultiplayerClient)
return;
int type = ModContent.ProjectileType<ReaperSoulFamiliarProjectile>();
for (int index = 0; index < Main.maxProjectiles; index++)
{
Projectile existing = Main.projectile[index];
if (existing.active && existing.owner == owner && existing.type == type)
existing.Kill();
}
int projectileIndex = Projectile.NewProjectile(source, origin, Vector2.Zero, type, 0, 0f, owner);
if (projectileIndex < 0 || projectileIndex >= Main.maxProjectiles)
return;
Projectile projectile = Main.projectile[projectileIndex];
if (projectile.ModProjectile is ReaperSoulFamiliarProjectile familiar)
familiar.Configure(snapshot);
ReaperProjectileHelper.SyncNewProjectile(projectile);
if (snapshot.Skill2 > 0)
{
ReaperSkillCueProjectile.Spawn(source, owner, snapshot, 1, origin,
Vector2.UnitY * -1f, actionId: projectile.identity);
}
}
public override void SetDefaults()
{
Projectile.width = 34;
Projectile.height = 34;
Projectile.friendly = false;
Projectile.hostile = false;
Projectile.tileCollide = false;
Projectile.ignoreWater = true;
Projectile.penetrate = -1;
Projectile.timeLeft = 360;
Projectile.DamageType = DamageClass.Melee;
}
public override bool? CanDamage() => false;
public override bool ShouldUpdatePosition() => false;
public override void AI()
{
if (Main.netMode == NetmodeID.Server && !serverAuthorized)
{
Projectile.Kill();
return;
}
if (!configured || Projectile.owner < 0 || Projectile.owner >= Main.maxPlayers)
return;
if (Main.netMode == NetmodeID.Server)
Projectile.Center = authoritativeCenter;
Player owner = Main.player[Projectile.owner];
if (!owner.active || owner.dead)
{
Projectile.Kill();
return;
}
timer++;
float angle = timer * 0.045f + Projectile.identity * 0.31f;
Vector2 desired = owner.MountedCenter + new Vector2(owner.direction * -46f, -38f) + angle.ToRotationVector2() * 12f;
Projectile.Center = Vector2.Lerp(Projectile.Center, desired, 0.16f);
Projectile.velocity = Vector2.Zero;
if (Main.netMode == NetmodeID.Server)
authoritativeCenter = Projectile.Center;
Projectile.rotation += 0.08f;
int maximumAttacks = snapshot.Skill2 switch { 2 => 3, >= 3 => 4, _ => 2 };
int duration = (snapshot.Skill2 switch { 2 => 4, >= 3 => 5, _ => 3 }) * 60;
int interval = Math.Max(22, duration / (maximumAttacks + 1));
if (timer % interval == 0 && attacksMade < maximumAttacks && Main.netMode != NetmodeID.MultiplayerClient)
{
NPC? target = FindTarget(640f);
if (target is not null)
{
float factor = snapshot.Skill2 switch { 2 => 0.45f, >= 3 => 0.5f, _ => 0.4f };
Vector2 direction = (target.Center - Projectile.Center).SafeNormalize(Vector2.UnitX * owner.direction);
ReaperStrikeProjectile.Spawn(Projectile.GetSource_FromThis(), Projectile.owner, snapshot,
ReaperHitKind.SoulFamiliar, attacksMade, Projectile.Center, direction,
ReaperStrikeShape.Line, Math.Min(340f, Vector2.Distance(Projectile.Center, target.Center) + 36f), 19f, factor);
attacksMade++;
}
}
if (timer >= duration)
Projectile.Kill();
if (Main.netMode != NetmodeID.Server && Main.rand.NextBool(2))
{
Dust dust = Dust.NewDustPerfect(Projectile.Center + Main.rand.NextVector2Circular(12f, 12f),
DustID.AncientLight, -Projectile.velocity * 0.1f, 60,
Main.rand.NextBool() ? new Color(85, 225, 255) : new Color(175, 85, 255), 0.85f);
dust.noGravity = true;
}
}
public override void SendExtraAI(BinaryWriter writer)
{
writer.Write(configured);
if (configured)
{
snapshot.Write(writer);
writer.Write((short)timer);
writer.Write((byte)attacksMade);
}
}
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);
int incomingTimer = reader.ReadInt16();
int incomingAttacksMade = reader.ReadByte();
if (Main.netMode == NetmodeID.Server)
return;
configured = true;
snapshot = incomingSnapshot;
timer = incomingTimer;
attacksMade = incomingAttacksMade;
}
private void Configure(in SickleCombatSnapshot value)
{
snapshot = value;
configured = true;
serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient;
Projectile.GetGlobalProjectile<MyGlobalProjectile>().AuthorizeServerReaperProjectile(Projectile);
authoritativeCenter = Projectile.Center;
timer = 0;
attacksMade = 0;
Projectile.timeLeft = (snapshot.Skill2 switch { 2 => 4, >= 3 => 5, _ => 3 }) * 60 + 10;
}
private NPC? FindTarget(float maximumDistance)
{
NPC? result = null;
foreach (NPC npc in Main.ActiveNPCs)
{
if (!npc.CanBeChasedBy(Projectile))
continue;
float distance = Vector2.Distance(Projectile.Center, npc.Center);
if (distance < maximumDistance && Collision.CanHitLine(Projectile.Center, 1, 1, npc.Center, 1, 1))
{
maximumDistance = distance;
result = npc;
}
}
return result;
}
}
using SoulHarvest.Common;
using Microsoft.Xna.Framework;
using System;
using System.IO;
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;
namespace SoulHarvest.Projectiles;
public sealed class ReaperSoulFamiliarProjectile : ModProjectile
{
private SickleCombatSnapshot snapshot;
private bool configured;
private int timer;
private int attacksMade;
private bool serverAuthorized;
private Vector2 authoritativeCenter;
public override string Texture => "SoulHarvest/Projectiles/SoulSickleProjectile";
public static void Spawn(Terraria.DataStructures.IEntitySource source, int owner,
in SickleCombatSnapshot snapshot, Vector2 origin)
{
if (Main.netMode == NetmodeID.MultiplayerClient)
return;
int type = ModContent.ProjectileType<ReaperSoulFamiliarProjectile>();
for (int index = 0; index < Main.maxProjectiles; index++)
{
Projectile existing = Main.projectile[index];
if (existing.active && existing.owner == owner && existing.type == type)
existing.Kill();
}
int projectileIndex = Projectile.NewProjectile(source, origin, Vector2.Zero, type, 0, 0f, owner);
if (projectileIndex < 0 || projectileIndex >= Main.maxProjectiles)
return;
Projectile projectile = Main.projectile[projectileIndex];
if (projectile.ModProjectile is ReaperSoulFamiliarProjectile familiar)
familiar.Configure(snapshot);
ReaperProjectileHelper.SyncNewProjectile(projectile);
if (snapshot.Skill2 > 0)
{
ReaperSkillCueProjectile.Spawn(source, owner, snapshot, 1, origin,
Vector2.UnitY * -1f, actionId: projectile.identity);
}
}
public override void SetDefaults()
{
Projectile.width = 34;
Projectile.height = 34;
Projectile.friendly = false;
Projectile.hostile = false;
Projectile.tileCollide = false;
Projectile.ignoreWater = true;
Projectile.penetrate = -1;
Projectile.timeLeft = 360;
Projectile.DamageType = DamageClass.Melee;
}
public override bool? CanDamage() => false;
public override bool ShouldUpdatePosition() => false;
public override void AI()
{
if (Main.netMode == NetmodeID.Server && !serverAuthorized)
{
Projectile.Kill();
return;
}
if (!configured || Projectile.owner < 0 || Projectile.owner >= Main.maxPlayers)
return;
if (Main.netMode == NetmodeID.Server)
Projectile.Center = authoritativeCenter;
Player owner = Main.player[Projectile.owner];
if (!owner.active || owner.dead)
{
Projectile.Kill();
return;
}
timer++;
float angle = timer * 0.045f + Projectile.identity * 0.31f;
Vector2 desired = owner.MountedCenter + new Vector2(owner.direction * -46f, -38f) + angle.ToRotationVector2() * 12f;
Projectile.Center = Vector2.Lerp(Projectile.Center, desired, 0.16f);
Projectile.velocity = Vector2.Zero;
if (Main.netMode == NetmodeID.Server)
authoritativeCenter = Projectile.Center;
Projectile.rotation += 0.08f;
int maximumAttacks = snapshot.Skill2 switch { 2 => 3, >= 3 => 4, _ => 2 };
int duration = (snapshot.Skill2 switch { 2 => 4, >= 3 => 5, _ => 3 }) * 60;
int interval = Math.Max(22, duration / (maximumAttacks + 1));
if (timer % interval == 0 && attacksMade < maximumAttacks && Main.netMode != NetmodeID.MultiplayerClient)
{
NPC? target = FindTarget(640f);
if (target is not null)
{
float factor = snapshot.Skill2 switch { 2 => 0.45f, >= 3 => 0.5f, _ => 0.4f };
Vector2 direction = (target.Center - Projectile.Center).SafeNormalize(Vector2.UnitX * owner.direction);
ReaperStrikeProjectile.Spawn(Projectile.GetSource_FromThis(), Projectile.owner, snapshot,
ReaperHitKind.SoulFamiliar, attacksMade, Projectile.Center, direction,
ReaperStrikeShape.Line, Math.Min(340f, Vector2.Distance(Projectile.Center, target.Center) + 36f), 19f, factor);
attacksMade++;
}
}
if (timer >= duration)
Projectile.Kill();
if (Main.netMode != NetmodeID.Server && Main.rand.NextBool(2))
{
Dust dust = Dust.NewDustPerfect(Projectile.Center + Main.rand.NextVector2Circular(12f, 12f),
DustID.AncientLight, -Projectile.velocity * 0.1f, 60,
Main.rand.NextBool() ? new Color(85, 225, 255) : new Color(175, 85, 255), 0.85f);
dust.noGravity = true;
}
}
public override void SendExtraAI(BinaryWriter writer)
{
writer.Write(configured);
if (configured)
{
snapshot.Write(writer);
writer.Write((short)timer);
writer.Write((byte)attacksMade);
}
}
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);
int incomingTimer = reader.ReadInt16();
int incomingAttacksMade = reader.ReadByte();
if (Main.netMode == NetmodeID.Server)
return;
configured = true;
snapshot = incomingSnapshot;
timer = incomingTimer;
attacksMade = incomingAttacksMade;
}
private void Configure(in SickleCombatSnapshot value)
{
snapshot = value;
configured = true;
serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient;
Projectile.GetGlobalProjectile<MyGlobalProjectile>().AuthorizeServerReaperProjectile(Projectile);
authoritativeCenter = Projectile.Center;
timer = 0;
attacksMade = 0;
Projectile.timeLeft = (snapshot.Skill2 switch { 2 => 4, >= 3 => 5, _ => 3 }) * 60 + 10;
}
private NPC? FindTarget(float maximumDistance)
{
NPC? result = null;
foreach (NPC npc in Main.ActiveNPCs)
{
if (!npc.CanBeChasedBy(Projectile))
continue;
float distance = Vector2.Distance(Projectile.Center, npc.Center);
if (distance < maximumDistance && Collision.CanHitLine(Projectile.Center, 1, 1, npc.Center, 1, 1))
{
maximumDistance = distance;
result = npc;
}
}
return result;
}
}