using SoulHarvest.Common; using Microsoft.Xna.Framework; using System; using System.IO; using Terraria; using Terraria.ID; using Terraria.ModLoader; namespace SoulHarvest.Projectiles; /// /// Server-authoritative, persistent Void tear shared by the Void special, /// passives and ultimate. It damages along the visible opening and pulls normal /// enemies toward the closest point on that opening. /// public sealed class ReaperVoidRiftProjectile : ModProjectile { private SickleCombatSnapshot snapshot; private float riftLength; private float riftWidth; private float pullRadius; 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 center, Vector2 direction, float length, float width, int lifetime, float pullRadius, float damageMultiplier, int phase, int actionId) { if (Main.netMode == NetmodeID.MultiplayerClient) return -1; direction = direction.SafeNormalize(Vector2.UnitX); int index = Projectile.NewProjectile(source, center, direction, ModContent.ProjectileType(), Math.Max(1, (int)Math.Round(snapshot.Damage * damageMultiplier)), snapshot.Knockback * 0.18f, owner); if (index < 0 || index >= Main.maxProjectiles) return -1; Projectile projectile = Main.projectile[index]; projectile.originalDamage = snapshot.Damage; projectile.CritChance = 0; int stableSeed = unchecked(actionId * 486187739 + phase * 16777619 + owner * 31); if (projectile.ModProjectile is ReaperVoidRiftProjectile rift) rift.Configure(snapshot, length, width, lifetime, pullRadius, stableSeed); projectile.GetGlobalProjectile().ConfigureReaperProjectile( projectile, snapshot, ReaperHitKind.DamageOverTime, phase, direction.ToRotation(), actionId); projectile.localNPCHitCooldown = 16; ReaperProjectileHelper.SyncNewProjectile(projectile); return index; } 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 = 180; Projectile.netImportant = true; Projectile.DamageType = DamageClass.Melee; Projectile.usesLocalNPCImmunity = true; Projectile.localNPCHitCooldown = 16; } public override bool ShouldUpdatePosition() => false; public override bool? CanDamage() => configured && age >= 5 && Projectile.timeLeft > 18 ? null : false; public override void AI() { if (Main.netMode == NetmodeID.Server && !serverAuthorized) { Projectile.Kill(); return; } if (!configured) return; age++; Projectile.rotation = Projectile.velocity.ToRotation(); if (Main.netMode != NetmodeID.MultiplayerClient) PullNearbyEnemies(); } public override bool? Colliding(Rectangle projHitbox, Rectangle targetHitbox) { Vector2 axis = Projectile.velocity.SafeNormalize(Vector2.UnitX); Vector2 start = Projectile.Center - axis * riftLength * 0.5f; Vector2 end = Projectile.Center + axis * riftLength * 0.5f; float collisionPoint = 0f; return Collision.CheckAABBvLineCollision(targetHitbox.TopLeft(), targetHitbox.Size(), start, end, Math.Max(3f, riftWidth * 0.72f), ref collisionPoint); } public override bool PreDraw(ref Color lightColor) { if (!configured || Main.dedServ) return false; float reveal = Smooth01(age / 13f); float fade = Smooth01(Projectile.timeLeft / 40f); float breathing = 0.96f + (float)Math.Sin((age + seed * 0.001f) * 0.11f) * 0.04f; ReaperVoidBackdropRenderer.DrawPersistentRift(Main.spriteBatch, Projectile.Center, Projectile.velocity, riftLength, riftWidth * breathing, seed, reveal, fade); return false; } public override void SendExtraAI(BinaryWriter writer) { writer.Write(configured); if (!configured) return; snapshot.Write(writer); writer.Write(riftLength); writer.Write(riftWidth); writer.Write(pullRadius); writer.Write((short)Math.Clamp(lifetime, 1, short.MaxValue)); writer.Write(seed); writer.Write((short)Math.Clamp(age, 0, short.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 incomingLength = reader.ReadSingle(); float incomingWidth = reader.ReadSingle(); float incomingPullRadius = reader.ReadSingle(); int incomingLifetime = reader.ReadInt16(); int incomingSeed = reader.ReadInt32(); int incomingAge = reader.ReadInt16(); if (Main.netMode == NetmodeID.Server) return; snapshot = incomingSnapshot; riftLength = incomingLength; riftWidth = incomingWidth; pullRadius = incomingPullRadius; lifetime = incomingLifetime; seed = incomingSeed; age = incomingAge; configured = true; } private void Configure(in SickleCombatSnapshot value, float length, float width, int activeLifetime, float attractionRadius, int shapeSeed) { snapshot = value; riftLength = MathHelper.Clamp(length, 32f, 2600f); riftWidth = MathHelper.Clamp(width, 4f, 190f); pullRadius = MathHelper.Clamp(attractionRadius, 0f, 820f); lifetime = Math.Clamp(activeLifetime, 30, 600); seed = shapeSeed; age = 0; configured = true; serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient; Projectile.timeLeft = lifetime; } private void PullNearbyEnemies() { if (pullRadius <= 1f || age < 5 || Projectile.timeLeft <= 18) return; Vector2 axis = Projectile.velocity.SafeNormalize(Vector2.UnitX); Vector2 start = Projectile.Center - axis * riftLength * 0.5f; Vector2 end = Projectile.Center + axis * riftLength * 0.5f; Vector2 segment = end - start; float segmentLengthSquared = Math.Max(1f, segment.LengthSquared()); foreach (NPC npc in Main.ActiveNPCs) { if (!npc.CanBeChasedBy(Projectile) || npc.boss || npc.realLife >= 0) continue; float amount = MathHelper.Clamp(Vector2.Dot(npc.Center - start, segment) / segmentLengthSquared, 0f, 1f); Vector2 closest = Vector2.Lerp(start, end, amount); Vector2 toRift = closest - npc.Center; float distance = toRift.Length(); if (distance <= 2f || distance >= pullRadius) continue; float influence = 1f - distance / pullRadius; float maximumSpeed = MathHelper.Lerp(1.2f, 6.2f, influence); Vector2 desired = toRift / distance * maximumSpeed; npc.velocity = Vector2.Lerp(npc.velocity, desired, 0.035f + influence * 0.09f); if (Main.netMode == NetmodeID.Server && Main.GameUpdateCount % 10 == 0) npc.netUpdate = true; } } private static float Smooth01(float value) { value = MathHelper.Clamp(value, 0f, 1f); return value * value * (3f - 2f * value); } }