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; /// /// 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. /// public sealed class ReaperBloodArcScarProjectile : ModProjectile { private const int MaximumPoints = 24; private readonly List 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 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(), 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().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 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 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); } }