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;
/// <summary>
/// A straight, double-pointed execution blade: crimson at the edge and completely
/// black through its core. Damage remains server-authoritative in Common.MyPlayer.
/// </summary>
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<MyPlayer>()
.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<MyPlayer>().TriggerDeathDomainImpact(
Projectile.Center,
normal,
2.2f + Mastery * 1.8f + (IsRequiem ? 1.2f : 0f));
Main.LocalPlayer.GetModPlayer<MyPlayer>().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);
}
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;
/// <summary>
/// A straight, double-pointed execution blade: crimson at the edge and completely
/// black through its core. Damage remains server-authoritative in Common.MyPlayer.
/// </summary>
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<MyPlayer>()
.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<MyPlayer>().TriggerDeathDomainImpact(
Projectile.Center,
normal,
2.2f + Mastery * 1.8f + (IsRequiem ? 1.2f : 0f));
Main.LocalPlayer.GetModPlayer<MyPlayer>().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);
}