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 ReaperFrostShardProjectile : ModProjectile
{
private readonly HashSet<int> hitRoots = [];
private SickleCombatSnapshot snapshot;
private bool configured;
private float maximumTravel;
private float traveled;
private int retargetsRemaining;
private bool serverAuthorized;
private int authoritativeDamage;
private Vector2 authoritativeCenter;
private Vector2 authoritativeVelocity;
public override string Texture => "SoulHarvest/Projectiles/FrostShardProjectile";
public static void Spawn(Terraria.DataStructures.IEntitySource source, int owner,
in SickleCombatSnapshot snapshot, Vector2 origin, Vector2 direction)
{
if (Main.netMode == NetmodeID.MultiplayerClient)
return;
int level = Math.Max(1, (int)snapshot.Skill2);
float factor = level switch { 2 => 0.90f, >= 3 => 1.05f, _ => 0.75f };
int index = Projectile.NewProjectile(source, origin, direction.SafeNormalize(Vector2.UnitX) * 11f,
ModContent.ProjectileType<ReaperFrostShardProjectile>(),
Math.Max(1, (int)Math.Round(snapshot.Damage * factor)), snapshot.Knockback, owner);
if (index < 0 || index >= Main.maxProjectiles)
return;
Projectile projectile = Main.projectile[index];
projectile.originalDamage = snapshot.Damage;
if (projectile.ModProjectile is ReaperFrostShardProjectile shard)
shard.Configure(snapshot);
projectile.GetGlobalProjectile<MyGlobalProjectile>().ConfigureReaperProjectile(
projectile, snapshot, ReaperHitKind.PrimaryDerived, 2, direction.ToRotation());
ReaperProjectileHelper.SyncNewProjectile(projectile);
}
public override void SetStaticDefaults()
{
ProjectileID.Sets.TrailCacheLength[Type] = 12;
ProjectileID.Sets.TrailingMode[Type] = 2;
}
public override void SetDefaults()
{
Projectile.width = 30;
Projectile.height = 30;
Projectile.friendly = true;
Projectile.tileCollide = true;
Projectile.ignoreWater = true;
Projectile.penetrate = -1;
Projectile.timeLeft = 90;
Projectile.DamageType = DamageClass.Melee;
Projectile.usesLocalNPCImmunity = true;
Projectile.localNPCHitCooldown = 10;
}
public override bool ShouldUpdatePosition() => 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;
traveled += Projectile.velocity.Length();
if (traveled >= maximumTravel)
{
Projectile.Kill();
return;
}
NPC? target = FindTarget(maximumTravel - traveled);
if (target is not null)
{
Vector2 desired = (target.Center - Projectile.Center).SafeNormalize(Projectile.velocity.SafeNormalize(Vector2.UnitX)) * 11f;
Projectile.velocity = Vector2.Lerp(Projectile.velocity, desired, 0.11f);
}
Projectile.rotation += 0.28f * Math.Sign(Projectile.velocity.X == 0f ? 1f : Projectile.velocity.X);
if (Main.netMode != NetmodeID.Server)
{
Lighting.AddLight(Projectile.Center, new Vector3(0.12f, 0.45f, 0.72f));
if (Main.rand.NextBool(2))
{
Dust dust = Dust.NewDustPerfect(Projectile.Center, DustID.IceTorch, -Projectile.velocity * 0.08f,
60, new Color(125, 210, 255), 0.95f);
dust.noGravity = true;
}
}
MoveOneStep();
}
public override void OnHitNPC(NPC target, NPC.HitInfo hit, int damageDone)
{
hitRoots.Add(target.realLife >= 0 ? target.realLife : target.whoAmI);
if (retargetsRemaining-- <= 0)
{
Projectile.Kill();
return;
}
NPC? next = FindTarget(maximumTravel - traveled);
if (next is null)
{
Projectile.Kill();
return;
}
Projectile.velocity = (next.Center - Projectile.Center).SafeNormalize(Projectile.velocity.SafeNormalize(Vector2.UnitX)) * 11f;
if (Main.netMode == NetmodeID.Server)
{
authoritativeCenter = Projectile.Center;
authoritativeVelocity = Projectile.velocity;
}
Projectile.netUpdate = true;
}
public override bool OnTileCollide(Vector2 oldVelocity) => true;
public override bool PreDraw(ref Color lightColor) => ReaperProjectileHelper.DrawTrail(
Projectile, new Color(105, 205, 255), scaleMultiplier: 1f, copies: 10);
public override void SendExtraAI(BinaryWriter writer)
{
writer.Write(configured);
if (!configured)
return;
snapshot.Write(writer);
writer.Write(maximumTravel);
writer.Write(traveled);
writer.Write((byte)Math.Clamp(retargetsRemaining, 0, byte.MaxValue));
}
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);
float incomingMaximumTravel = reader.ReadSingle();
float incomingTraveled = reader.ReadSingle();
int incomingRetargets = reader.ReadByte();
if (Main.netMode == NetmodeID.Server)
return;
configured = true;
snapshot = incomingSnapshot;
maximumTravel = incomingMaximumTravel;
traveled = incomingTraveled;
retargetsRemaining = incomingRetargets;
}
private void Configure(in SickleCombatSnapshot value)
{
snapshot = value;
int level = Math.Max(1, (int)snapshot.Skill2);
maximumTravel = (level switch { 2 => 12f, >= 3 => 14f, _ => 10f }) * 16f;
retargetsRemaining = level >= 2 ? 2 : 1;
traveled = 0f;
hitRoots.Clear();
configured = true;
serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient;
authoritativeDamage = Math.Max(1, Projectile.damage);
authoritativeCenter = Projectile.Center;
authoritativeVelocity = Projectile.velocity;
Projectile.timeLeft = Math.Max(20, (int)Math.Ceiling(maximumTravel / 8f));
}
private void MoveOneStep()
{
Vector2 movement = Collision.TileCollision(Projectile.position, Projectile.velocity,
Projectile.width, Projectile.height, fallThrough: false, fall2: false);
if (movement != Projectile.velocity)
{
Projectile.Kill();
return;
}
Projectile.position += movement;
if (Main.netMode == NetmodeID.Server)
{
authoritativeCenter = Projectile.Center;
authoritativeVelocity = Projectile.velocity;
}
}
private NPC? FindTarget(float maximumDistance)
{
NPC? result = null;
foreach (NPC npc in Main.ActiveNPCs)
{
int root = npc.realLife >= 0 ? npc.realLife : npc.whoAmI;
if (hitRoots.Contains(root) || !npc.CanBeChasedBy(Projectile))
continue;
float distance = Vector2.Distance(Projectile.Center, npc.Center);
if (distance < maximumDistance)
{
maximumDistance = distance;
result = npc;
}
}
return result;
}
}
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 ReaperFrostShardProjectile : ModProjectile
{
private readonly HashSet<int> hitRoots = [];
private SickleCombatSnapshot snapshot;
private bool configured;
private float maximumTravel;
private float traveled;
private int retargetsRemaining;
private bool serverAuthorized;
private int authoritativeDamage;
private Vector2 authoritativeCenter;
private Vector2 authoritativeVelocity;
public override string Texture => "SoulHarvest/Projectiles/FrostShardProjectile";
public static void Spawn(Terraria.DataStructures.IEntitySource source, int owner,
in SickleCombatSnapshot snapshot, Vector2 origin, Vector2 direction)
{
if (Main.netMode == NetmodeID.MultiplayerClient)
return;
int level = Math.Max(1, (int)snapshot.Skill2);
float factor = level switch { 2 => 0.90f, >= 3 => 1.05f, _ => 0.75f };
int index = Projectile.NewProjectile(source, origin, direction.SafeNormalize(Vector2.UnitX) * 11f,
ModContent.ProjectileType<ReaperFrostShardProjectile>(),
Math.Max(1, (int)Math.Round(snapshot.Damage * factor)), snapshot.Knockback, owner);
if (index < 0 || index >= Main.maxProjectiles)
return;
Projectile projectile = Main.projectile[index];
projectile.originalDamage = snapshot.Damage;
if (projectile.ModProjectile is ReaperFrostShardProjectile shard)
shard.Configure(snapshot);
projectile.GetGlobalProjectile<MyGlobalProjectile>().ConfigureReaperProjectile(
projectile, snapshot, ReaperHitKind.PrimaryDerived, 2, direction.ToRotation());
ReaperProjectileHelper.SyncNewProjectile(projectile);
}
public override void SetStaticDefaults()
{
ProjectileID.Sets.TrailCacheLength[Type] = 12;
ProjectileID.Sets.TrailingMode[Type] = 2;
}
public override void SetDefaults()
{
Projectile.width = 30;
Projectile.height = 30;
Projectile.friendly = true;
Projectile.tileCollide = true;
Projectile.ignoreWater = true;
Projectile.penetrate = -1;
Projectile.timeLeft = 90;
Projectile.DamageType = DamageClass.Melee;
Projectile.usesLocalNPCImmunity = true;
Projectile.localNPCHitCooldown = 10;
}
public override bool ShouldUpdatePosition() => 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;
traveled += Projectile.velocity.Length();
if (traveled >= maximumTravel)
{
Projectile.Kill();
return;
}
NPC? target = FindTarget(maximumTravel - traveled);
if (target is not null)
{
Vector2 desired = (target.Center - Projectile.Center).SafeNormalize(Projectile.velocity.SafeNormalize(Vector2.UnitX)) * 11f;
Projectile.velocity = Vector2.Lerp(Projectile.velocity, desired, 0.11f);
}
Projectile.rotation += 0.28f * Math.Sign(Projectile.velocity.X == 0f ? 1f : Projectile.velocity.X);
if (Main.netMode != NetmodeID.Server)
{
Lighting.AddLight(Projectile.Center, new Vector3(0.12f, 0.45f, 0.72f));
if (Main.rand.NextBool(2))
{
Dust dust = Dust.NewDustPerfect(Projectile.Center, DustID.IceTorch, -Projectile.velocity * 0.08f,
60, new Color(125, 210, 255), 0.95f);
dust.noGravity = true;
}
}
MoveOneStep();
}
public override void OnHitNPC(NPC target, NPC.HitInfo hit, int damageDone)
{
hitRoots.Add(target.realLife >= 0 ? target.realLife : target.whoAmI);
if (retargetsRemaining-- <= 0)
{
Projectile.Kill();
return;
}
NPC? next = FindTarget(maximumTravel - traveled);
if (next is null)
{
Projectile.Kill();
return;
}
Projectile.velocity = (next.Center - Projectile.Center).SafeNormalize(Projectile.velocity.SafeNormalize(Vector2.UnitX)) * 11f;
if (Main.netMode == NetmodeID.Server)
{
authoritativeCenter = Projectile.Center;
authoritativeVelocity = Projectile.velocity;
}
Projectile.netUpdate = true;
}
public override bool OnTileCollide(Vector2 oldVelocity) => true;
public override bool PreDraw(ref Color lightColor) => ReaperProjectileHelper.DrawTrail(
Projectile, new Color(105, 205, 255), scaleMultiplier: 1f, copies: 10);
public override void SendExtraAI(BinaryWriter writer)
{
writer.Write(configured);
if (!configured)
return;
snapshot.Write(writer);
writer.Write(maximumTravel);
writer.Write(traveled);
writer.Write((byte)Math.Clamp(retargetsRemaining, 0, byte.MaxValue));
}
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);
float incomingMaximumTravel = reader.ReadSingle();
float incomingTraveled = reader.ReadSingle();
int incomingRetargets = reader.ReadByte();
if (Main.netMode == NetmodeID.Server)
return;
configured = true;
snapshot = incomingSnapshot;
maximumTravel = incomingMaximumTravel;
traveled = incomingTraveled;
retargetsRemaining = incomingRetargets;
}
private void Configure(in SickleCombatSnapshot value)
{
snapshot = value;
int level = Math.Max(1, (int)snapshot.Skill2);
maximumTravel = (level switch { 2 => 12f, >= 3 => 14f, _ => 10f }) * 16f;
retargetsRemaining = level >= 2 ? 2 : 1;
traveled = 0f;
hitRoots.Clear();
configured = true;
serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient;
authoritativeDamage = Math.Max(1, Projectile.damage);
authoritativeCenter = Projectile.Center;
authoritativeVelocity = Projectile.velocity;
Projectile.timeLeft = Math.Max(20, (int)Math.Ceiling(maximumTravel / 8f));
}
private void MoveOneStep()
{
Vector2 movement = Collision.TileCollision(Projectile.position, Projectile.velocity,
Projectile.width, Projectile.height, fallThrough: false, fall2: false);
if (movement != Projectile.velocity)
{
Projectile.Kill();
return;
}
Projectile.position += movement;
if (Main.netMode == NetmodeID.Server)
{
authoritativeCenter = Projectile.Center;
authoritativeVelocity = Projectile.velocity;
}
}
private NPC? FindTarget(float maximumDistance)
{
NPC? result = null;
foreach (NPC npc in Main.ActiveNPCs)
{
int root = npc.realLife >= 0 ? npc.realLife : npc.whoAmI;
if (hitRoots.Contains(root) || !npc.CanBeChasedBy(Projectile))
continue;
float distance = Vector2.Distance(Projectile.Center, npc.Center);
if (distance < maximumDistance)
{
maximumDistance = distance;
result = npc;
}
}
return result;
}
}