using SoulHarvest.Common; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.IO; using Terraria; using Terraria.DataStructures; using Terraria.GameContent; using Terraria.ID; using Terraria.ModLoader; namespace SoulHarvest.Projectiles; /// /// A straight, double-pointed execution blade: crimson at the edge and completely /// black through its core. Damage remains server-authoritative in Common.MyPlayer. /// public class DeathDomainHarvestSlashProjectile : ModProjectile { private const int SlashActiveFrames = 60; internal const int SlashDelayFrames = 5; internal const int SlashImpactFrame = 5; private const int EndLingerFrames = 7; private const int MaximumSlashCount = 5; private int deathUltimateActionId = -1; private int TargetIndex => (int)Projectile.ai[0]; private int SlashCount => Math.Clamp((int)Projectile.ai[1], 1, MaximumSlashCount); private bool IsServerController => Projectile.ai[2] >= 10f; private float VisualState => IsServerController ? Projectile.ai[2] - 10f : Projectile.ai[2]; private bool IsRequiem => VisualState >= 2f; private float Mastery => MathHelper.Clamp(IsRequiem ? VisualState - 2f : VisualState, 0f, 1f); private int TotalLifetime => SlashActiveFrames + (SlashCount - 1) * SlashDelayFrames + EndLingerFrames; public override string Texture => "Terraria/Images/Projectile_0"; public override void SetStaticDefaults() { ProjectileID.Sets.DrawScreenCheckFluff[Type] = 3600; } public override void SetDefaults() { Projectile.width = 2; Projectile.height = 2; Projectile.friendly = false; Projectile.hostile = false; Projectile.tileCollide = false; Projectile.ignoreWater = true; Projectile.penetrate = -1; Projectile.timeLeft = 60; Projectile.netImportant = false; } public override void OnSpawn(IEntitySource source) { Projectile.timeLeft = TotalLifetime; } public override bool? CanDamage() => false; public override void SendExtraAI(BinaryWriter writer) { writer.Write(Projectile.rotation); writer.Write(Projectile.scale); } public override void ReceiveExtraAI(BinaryReader reader) { Projectile.rotation = reader.ReadSingle(); Projectile.scale = reader.ReadSingle(); } public override void AI() { if (TargetIndex >= 0 && TargetIndex < Main.maxNPCs && Main.npc[TargetIndex].active) Projectile.Center = Main.npc[TargetIndex].Center; int age = TotalLifetime - Projectile.timeLeft; int emittedMask = (int)Projectile.localAI[0]; bool createClientEffects = !Main.dedServ && !IsServerController; for (int slash = 0; slash < SlashCount; slash++) { int slashAge = age - slash * SlashDelayFrames; int bit = 1 << slash; // The visible blade reaches the target centre after five ticks. Apply // damage at that moment instead of before the slash has visibly landed. if ((emittedMask & bit) == 0 && slashAge >= SlashImpactFrame) { emittedMask |= bit; if (Projectile.owner >= 0 && Projectile.owner < Main.maxPlayers) { Main.player[Projectile.owner] .GetModPlayer() .ApplyDeathDomainStrike( TargetIndex, Projectile.damage, Main.netMode == NetmodeID.MultiplayerClient ? 0 : (int)Projectile.localAI[1], deathUltimateActionId); } if (createClientEffects) SpawnExecutionBurst(slash); } if (createClientEffects && slashAge is >= 0 and < 10) SpawnTravellingEdge(slash, GetSlashReveal(slashAge / (float)SlashActiveFrames)); } Projectile.localAI[0] = emittedMask; } public override bool PreDraw(ref Color lightColor) { if (IsServerController) return false; int age = TotalLifetime - Projectile.timeLeft; for (int slash = 0; slash < SlashCount; slash++) { float progress = (age - slash * SlashDelayFrames) / (float)SlashActiveFrames; if (progress <= 0f) continue; if (progress >= 1f) continue; DrawExecutionBlade(progress, slash); } return false; } private void DrawExecutionBlade(float progress, int slash) { // Begin as a single point, then visibly stretch it along the warning line. // SmoothStep preserves acceleration while keeping enough intermediate // lengths on screen for the eye to read the complete cutting motion. float reveal = GetSlashReveal(progress); float disappear = 1f - Smooth01((progress - 0.15f) / 0.81f); float strength = Smooth01((progress - 0.008f) / 0.055f) * disappear; float impactPulse = 1f + (float)Math.Exp(-Math.Pow((progress - 0.083f) / 0.034f, 2f)) * 0.24f; float rotation = GetSlashRotation(Projectile.rotation, slash); float length = GetSlashLength(Mastery, Projectile.scale, slash); float width = (21f + Mastery * 13f + (IsRequiem ? 8f : 0f)) * Projectile.scale * 0.78f; Vector2 center = Projectile.Center - Main.screenPosition; DrawTaperedBladeLayer(center, rotation, length, width * 1.52f * impactPulse, reveal, new Color(92, 0, 22, 0) * (strength * 0.54f)); DrawTaperedBladeLayer(center, rotation, length, width * impactPulse, reveal, new Color(255, 8, 48, 255) * strength); DrawTaperedBladeLayer(center, rotation, length * 0.985f, width * 0.62f * impactPulse, reveal, Color.Black * Math.Min(1f, strength * 1.32f)); } private static void DrawTaperedBladeLayer(Vector2 center, float rotation, float length, float maximumWidth, float reveal, Color color) { if (maximumWidth <= 0.05f || reveal <= 0f) return; Vector2 start = center - rotation.ToRotationVector2() * length * 0.5f; DeathDomainPrimitiveTextureSystem.DrawBlade(start, rotation, length, maximumWidth, color, reveal); } private void SpawnExecutionBurst(int slash) { float rotation = GetSlashRotation(Projectile.rotation, slash); Vector2 direction = rotation.ToRotationVector2(); Vector2 normal = direction.RotatedBy(MathHelper.PiOver2); float length = GetSlashLength(Mastery, Projectile.scale, slash); int count = 16 + (int)(Mastery * 10f) + (IsRequiem ? 7 : 0); for (int i = 0; i < count; i++) { Vector2 position = Projectile.Center + direction * Main.rand.NextFloat(-length * 0.06f, length * 0.06f) + normal * Main.rand.NextFloat(-5f, 5f) * Projectile.scale; Vector2 velocity = normal * Main.rand.NextFloat(-3.8f, 3.8f) + direction * Main.rand.NextFloat(3.8f, 9.2f); int dustType = i % 3 == 0 ? DustID.Shadowflame : DustID.RedTorch; Dust dust = Dust.NewDustPerfect( position, dustType, velocity, 75, i % 3 == 0 ? new Color(30, 0, 12) : new Color(255, 12, 54), Main.rand.NextFloat(0.75f, 1.35f)); dust.noGravity = true; dust.fadeIn = 0.35f; } Main.LocalPlayer.GetModPlayer().TriggerDeathDomainImpact( Projectile.Center, normal, 2.2f + Mastery * 1.8f + (IsRequiem ? 1.2f : 0f)); Main.LocalPlayer.GetModPlayer().PlayDeathDomainSlashSound(Projectile.Center, Mastery, IsRequiem); } private void SpawnTravellingEdge(int slash, float reveal) { float rotation = GetSlashRotation(Projectile.rotation, slash); Vector2 direction = rotation.ToRotationVector2(); Vector2 normal = direction.RotatedBy(MathHelper.PiOver2); float length = GetSlashLength(Mastery, Projectile.scale, slash); Vector2 start = Projectile.Center - direction * length * 0.5f; Vector2 position = start + direction * (length * reveal); Dust dust = Dust.NewDustPerfect( position + normal * Main.rand.NextFloat(-4f, 4f) * Projectile.scale, Main.rand.NextBool(3) ? DustID.Shadowflame : DustID.RedTorch, direction * Main.rand.NextFloat(2.2f, 4.6f) + normal * Main.rand.NextFloat(-2.4f, 2.4f), 70, new Color(255, 10, 50), Main.rand.NextFloat(0.62f, 1.05f)); dust.noGravity = true; } internal static float GetSlashRotation(float baseRotation, int slash) { if (slash <= 0) return baseRotation; uint state = (uint)BitConverter.SingleToInt32Bits(baseRotation) + (uint)slash * 0x9E3779B9u; state ^= state >> 16; state *= 0x7FEB352Du; state ^= state >> 15; float jitter = (state & 0xFFFFu) / 65535f - 0.5f; return baseRotation + slash * 2.39996323f + jitter * 0.58f; } internal static float GetSlashLength(float mastery, float scale, int slash) { float variation = 0.94f + 0.06f * (float)Math.Sin((slash + 1) * 2.7f); return (188f + mastery * 108f) * scale * variation; } internal void SetDeathUltimateActionId(int actionId) => deathUltimateActionId = Math.Max(-1, actionId); private static float Smooth01(float value) { value = MathHelper.Clamp(value, 0f, 1f); return value * value * (3f - 2f * value); } private static float GetSlashReveal(float progress) => Smooth01((progress - 0.008f) / 0.125f); }