XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

DeathMod

【Terraria】DeathMod (now aka Soul Harvest)

公开
关注 0 Fork 0 Star 0
UTF-8
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);
}