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;
/// <summary>
/// A continuous blood wound copied from a summoned Blood scythe's real blade-tip
/// path. Collision and rendering share the same ordered world-space curve.
/// </summary>
public sealed class ReaperBloodArcScarProjectile : ModProjectile
{
private const int MaximumPoints = 24;
private readonly List<Vector2> points = [];
private SickleCombatSnapshot snapshot;
private float baseWidth;
private int lifetime;
private int seed;
private int age;
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[] newestFirstPoints,
int validPointCount, int phase, int actionId, float damageMultiplier)
{
if (Main.netMode == NetmodeID.MultiplayerClient || validPointCount < 2)
return -1;
List<Vector2> ordered = [];
int usable = Math.Min(Math.Min(validPointCount, newestFirstPoints.Length),
MaximumPoints);
for (int index = usable - 1; index >= 0; index--)
{
Vector2 point = newestFirstPoints[index];
if (!float.IsFinite(point.X) || !float.IsFinite(point.Y))
continue;
if (ordered.Count == 0
|| Vector2.DistanceSquared(ordered[^1], point) >= 3f * 3f)
{
ordered.Add(point);
}
}
if (ordered.Count < 2)
return -1;
Vector2 center = Vector2.Zero;
foreach (Vector2 point in ordered)
center += point;
center /= ordered.Count;
Vector2 direction = (ordered[^1] - ordered[0]).SafeNormalize(Vector2.UnitX);
int indexProjectile = Projectile.NewProjectile(source, center, direction,
ModContent.ProjectileType<ReaperBloodArcScarProjectile>(),
Math.Max(1, (int)Math.Round(snapshot.Damage
* MathHelper.Clamp(damageMultiplier, 0.05f, 2f))),
snapshot.Knockback * 0.10f, owner);
if (indexProjectile < 0 || indexProjectile >= Main.maxProjectiles)
return -1;
Projectile projectile = Main.projectile[indexProjectile];
projectile.originalDamage = snapshot.Damage;
int level = Math.Clamp(Math.Max(1, (int)snapshot.Skill1), 1, 3);
int stableSeed = unchecked(actionId * 16777619 + phase * 486187739
+ owner * 97);
if (projectile.ModProjectile is ReaperBloodArcScarProjectile scar)
{
scar.Configure(snapshot, ordered, 10f + snapshot.StageNumber * 2.5f,
ReaperBloodBladeProjectile.GetScarLifetime(level), stableSeed);
}
projectile.GetGlobalProjectile<MyGlobalProjectile>().ConfigureReaperProjectile(
projectile, snapshot, ReaperHitKind.DamageOverTime, phase,
direction.ToRotation(), actionId);
projectile.localNPCHitCooldown = 24;
ReaperProjectileHelper.SyncNewProjectile(projectile);
return indexProjectile;
}
public override void SetDefaults()
{
Projectile.width = 4;
Projectile.height = 4;
Projectile.friendly = true;
Projectile.hostile = false;
Projectile.tileCollide = false;
Projectile.ignoreWater = true;
Projectile.penetrate = -1;
Projectile.timeLeft = 300;
Projectile.netImportant = true;
Projectile.DamageType = DamageClass.Melee;
Projectile.usesLocalNPCImmunity = true;
Projectile.localNPCHitCooldown = 24;
}
public override bool ShouldUpdatePosition() => false;
public override bool? CanDamage()
=> configured && age >= 4 && Projectile.timeLeft > 15 ? null : false;
public override void AI()
{
if (Main.netMode == NetmodeID.Server && !serverAuthorized)
{
Projectile.Kill();
return;
}
if (configured)
age++;
}
public override bool? Colliding(Rectangle projHitbox, Rectangle targetHitbox)
{
float collisionPoint = 0f;
for (int index = 1; index < points.Count; index++)
{
float taper = GetTaper(index - 0.5f);
if (Collision.CheckAABBvLineCollision(targetHitbox.TopLeft(),
targetHitbox.Size(), points[index - 1], points[index],
Math.Max(2f, baseWidth * taper * 0.72f), ref collisionPoint))
{
return true;
}
}
return false;
}
public override bool PreDraw(ref Color lightColor)
{
if (!configured || Main.dedServ || points.Count < 2)
return false;
float reveal = Smooth01(age / 9f);
float fade = Smooth01(Projectile.timeLeft / 36f);
int visible = Math.Clamp((int)Math.Ceiling((points.Count - 1) * reveal),
1, points.Count - 1);
for (int index = 1; index <= visible; index++)
{
float width = Math.Max(0.7f, baseWidth * GetTaper(index - 0.5f));
ReaperBloodTrailRenderer.DrawSegment(points[index - 1], points[index],
width, fade);
}
return false;
}
public override void SendExtraAI(BinaryWriter writer)
{
writer.Write(configured);
if (!configured)
return;
snapshot.Write(writer);
writer.Write(baseWidth);
writer.Write((short)Math.Clamp(lifetime, 1, short.MaxValue));
writer.Write(seed);
writer.Write((short)Math.Clamp(age, 0, short.MaxValue));
writer.Write((byte)Math.Min(points.Count, MaximumPoints));
for (int index = 0; index < points.Count && index < MaximumPoints; index++)
writer.WriteVector2(points[index]);
}
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);
float incomingWidth = reader.ReadSingle();
int incomingLifetime = reader.ReadInt16();
int incomingSeed = reader.ReadInt32();
int incomingAge = reader.ReadInt16();
int count = reader.ReadByte();
List<Vector2> incomingPoints = [];
for (int index = 0; index < count; index++)
incomingPoints.Add(reader.ReadVector2());
if (Main.netMode == NetmodeID.Server)
return;
snapshot = incomingSnapshot;
baseWidth = incomingWidth;
lifetime = incomingLifetime;
seed = incomingSeed;
age = incomingAge;
points.Clear();
points.AddRange(incomingPoints);
configured = points.Count >= 2;
}
private void Configure(in SickleCombatSnapshot value, List<Vector2> path,
float width, int activeLifetime, int stableSeed)
{
snapshot = value;
points.Clear();
points.AddRange(path);
baseWidth = MathHelper.Clamp(width, 5f, 24f);
lifetime = Math.Clamp(activeLifetime, 60, 300);
seed = stableSeed;
age = 0;
configured = points.Count >= 2;
serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient;
Projectile.timeLeft = lifetime;
}
private float GetTaper(float segmentIndex)
{
float progress = segmentIndex / Math.Max(1f, points.Count - 1f);
return (float)Math.Pow(Math.Max(0f, Math.Sin(progress * MathHelper.Pi)), 0.46f);
}
private static float Smooth01(float value)
{
value = MathHelper.Clamp(value, 0f, 1f);
return value * value * (3f - 2f * value);
}
}
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;
/// <summary>
/// A continuous blood wound copied from a summoned Blood scythe's real blade-tip
/// path. Collision and rendering share the same ordered world-space curve.
/// </summary>
public sealed class ReaperBloodArcScarProjectile : ModProjectile
{
private const int MaximumPoints = 24;
private readonly List<Vector2> points = [];
private SickleCombatSnapshot snapshot;
private float baseWidth;
private int lifetime;
private int seed;
private int age;
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[] newestFirstPoints,
int validPointCount, int phase, int actionId, float damageMultiplier)
{
if (Main.netMode == NetmodeID.MultiplayerClient || validPointCount < 2)
return -1;
List<Vector2> ordered = [];
int usable = Math.Min(Math.Min(validPointCount, newestFirstPoints.Length),
MaximumPoints);
for (int index = usable - 1; index >= 0; index--)
{
Vector2 point = newestFirstPoints[index];
if (!float.IsFinite(point.X) || !float.IsFinite(point.Y))
continue;
if (ordered.Count == 0
|| Vector2.DistanceSquared(ordered[^1], point) >= 3f * 3f)
{
ordered.Add(point);
}
}
if (ordered.Count < 2)
return -1;
Vector2 center = Vector2.Zero;
foreach (Vector2 point in ordered)
center += point;
center /= ordered.Count;
Vector2 direction = (ordered[^1] - ordered[0]).SafeNormalize(Vector2.UnitX);
int indexProjectile = Projectile.NewProjectile(source, center, direction,
ModContent.ProjectileType<ReaperBloodArcScarProjectile>(),
Math.Max(1, (int)Math.Round(snapshot.Damage
* MathHelper.Clamp(damageMultiplier, 0.05f, 2f))),
snapshot.Knockback * 0.10f, owner);
if (indexProjectile < 0 || indexProjectile >= Main.maxProjectiles)
return -1;
Projectile projectile = Main.projectile[indexProjectile];
projectile.originalDamage = snapshot.Damage;
int level = Math.Clamp(Math.Max(1, (int)snapshot.Skill1), 1, 3);
int stableSeed = unchecked(actionId * 16777619 + phase * 486187739
+ owner * 97);
if (projectile.ModProjectile is ReaperBloodArcScarProjectile scar)
{
scar.Configure(snapshot, ordered, 10f + snapshot.StageNumber * 2.5f,
ReaperBloodBladeProjectile.GetScarLifetime(level), stableSeed);
}
projectile.GetGlobalProjectile<MyGlobalProjectile>().ConfigureReaperProjectile(
projectile, snapshot, ReaperHitKind.DamageOverTime, phase,
direction.ToRotation(), actionId);
projectile.localNPCHitCooldown = 24;
ReaperProjectileHelper.SyncNewProjectile(projectile);
return indexProjectile;
}
public override void SetDefaults()
{
Projectile.width = 4;
Projectile.height = 4;
Projectile.friendly = true;
Projectile.hostile = false;
Projectile.tileCollide = false;
Projectile.ignoreWater = true;
Projectile.penetrate = -1;
Projectile.timeLeft = 300;
Projectile.netImportant = true;
Projectile.DamageType = DamageClass.Melee;
Projectile.usesLocalNPCImmunity = true;
Projectile.localNPCHitCooldown = 24;
}
public override bool ShouldUpdatePosition() => false;
public override bool? CanDamage()
=> configured && age >= 4 && Projectile.timeLeft > 15 ? null : false;
public override void AI()
{
if (Main.netMode == NetmodeID.Server && !serverAuthorized)
{
Projectile.Kill();
return;
}
if (configured)
age++;
}
public override bool? Colliding(Rectangle projHitbox, Rectangle targetHitbox)
{
float collisionPoint = 0f;
for (int index = 1; index < points.Count; index++)
{
float taper = GetTaper(index - 0.5f);
if (Collision.CheckAABBvLineCollision(targetHitbox.TopLeft(),
targetHitbox.Size(), points[index - 1], points[index],
Math.Max(2f, baseWidth * taper * 0.72f), ref collisionPoint))
{
return true;
}
}
return false;
}
public override bool PreDraw(ref Color lightColor)
{
if (!configured || Main.dedServ || points.Count < 2)
return false;
float reveal = Smooth01(age / 9f);
float fade = Smooth01(Projectile.timeLeft / 36f);
int visible = Math.Clamp((int)Math.Ceiling((points.Count - 1) * reveal),
1, points.Count - 1);
for (int index = 1; index <= visible; index++)
{
float width = Math.Max(0.7f, baseWidth * GetTaper(index - 0.5f));
ReaperBloodTrailRenderer.DrawSegment(points[index - 1], points[index],
width, fade);
}
return false;
}
public override void SendExtraAI(BinaryWriter writer)
{
writer.Write(configured);
if (!configured)
return;
snapshot.Write(writer);
writer.Write(baseWidth);
writer.Write((short)Math.Clamp(lifetime, 1, short.MaxValue));
writer.Write(seed);
writer.Write((short)Math.Clamp(age, 0, short.MaxValue));
writer.Write((byte)Math.Min(points.Count, MaximumPoints));
for (int index = 0; index < points.Count && index < MaximumPoints; index++)
writer.WriteVector2(points[index]);
}
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);
float incomingWidth = reader.ReadSingle();
int incomingLifetime = reader.ReadInt16();
int incomingSeed = reader.ReadInt32();
int incomingAge = reader.ReadInt16();
int count = reader.ReadByte();
List<Vector2> incomingPoints = [];
for (int index = 0; index < count; index++)
incomingPoints.Add(reader.ReadVector2());
if (Main.netMode == NetmodeID.Server)
return;
snapshot = incomingSnapshot;
baseWidth = incomingWidth;
lifetime = incomingLifetime;
seed = incomingSeed;
age = incomingAge;
points.Clear();
points.AddRange(incomingPoints);
configured = points.Count >= 2;
}
private void Configure(in SickleCombatSnapshot value, List<Vector2> path,
float width, int activeLifetime, int stableSeed)
{
snapshot = value;
points.Clear();
points.AddRange(path);
baseWidth = MathHelper.Clamp(width, 5f, 24f);
lifetime = Math.Clamp(activeLifetime, 60, 300);
seed = stableSeed;
age = 0;
configured = points.Count >= 2;
serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient;
Projectile.timeLeft = lifetime;
}
private float GetTaper(float segmentIndex)
{
float progress = segmentIndex / Math.Max(1f, points.Count - 1f);
return (float)Math.Pow(Math.Max(0f, Math.Sin(progress * MathHelper.Pi)), 0.46f);
}
private static float Smooth01(float value)
{
value = MathHelper.Clamp(value, 0f, 1f);
return value * value * (3f - 2f * value);
}
}