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 System;
using System.IO;
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;

namespace SoulHarvest.Projectiles;

/// <summary>Continuous life siphon used while Blood special is being charged.</summary>
public sealed class ReaperBloodDrainFieldProjectile : ModProjectile
{
    private SickleCombatSnapshot snapshot;
    private float radius;
    private int controllerIdentity;
    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, float radius, int controllerIdentity)
    {
        if (Main.netMode == NetmodeID.MultiplayerClient)
            return -1;
        Player player = Main.player[owner];
        int index = Projectile.NewProjectile(source, player.MountedCenter, Vector2.Zero,
            ModContent.ProjectileType<ReaperBloodDrainFieldProjectile>(),
            Math.Max(1, (int)Math.Round(snapshot.Damage * 0.075f)), 0f, owner);
        if (index < 0 || index >= Main.maxProjectiles)
            return -1;
        Projectile projectile = Main.projectile[index];
        projectile.originalDamage = snapshot.Damage;
        if (projectile.ModProjectile is ReaperBloodDrainFieldProjectile field)
            field.Configure(snapshot, radius, controllerIdentity);
        projectile.GetGlobalProjectile<MyGlobalProjectile>().ConfigureReaperProjectile(
            projectile, snapshot, ReaperHitKind.DamageOverTime, 0, 0f,
            controllerIdentity);
        projectile.localNPCHitCooldown = 12;
        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 = 90;
        Projectile.netImportant = true;
        Projectile.DamageType = DamageClass.Melee;
        Projectile.usesLocalNPCImmunity = true;
        Projectile.localNPCHitCooldown = 12;
    }

    public override bool ShouldUpdatePosition() => false;
    public override bool? CanDamage() => configured && age >= 3 ? null : false;

    public override void AI()
    {
        if (Main.netMode == NetmodeID.Server && !serverAuthorized)
        {
            Projectile.Kill();
            return;
        }
        if (!configured || Projectile.owner < 0 || Projectile.owner >= Main.maxPlayers)
            return;

        Player player = Main.player[Projectile.owner];
        if (!player.active || player.dead || !ControllerStillCharging())
        {
            Projectile.Kill();
            return;
        }
        Projectile.Center = player.MountedCenter;
        Projectile.timeLeft = 2;
        age++;
    }

    public override bool? Colliding(Rectangle projHitbox, Rectangle targetHitbox)
    {
        float closestX = MathHelper.Clamp(Projectile.Center.X, targetHitbox.Left, targetHitbox.Right);
        float closestY = MathHelper.Clamp(Projectile.Center.Y, targetHitbox.Top, targetHitbox.Bottom);
        return Vector2.DistanceSquared(Projectile.Center, new Vector2(closestX, closestY))
            <= radius * radius;
    }

    public override void OnHitNPC(NPC target, NPC.HitInfo hit, int damageDone)
    {
        if (Main.netMode == NetmodeID.MultiplayerClient || Projectile.owner < 0
            || Projectile.owner >= Main.maxPlayers || damageDone <= 0)
        {
            return;
        }
        Player player = Main.player[Projectile.owner];
        if (TryGetController(out ReaperActionControllerProjectile? controller))
            controller?.RecordBloodSiphonDamage(damageDone);
        if (!player.active || player.dead || player.statLife >= player.statLifeMax2)
            return;
        int healing = Math.Clamp((int)Math.Ceiling(damageDone * 0.10f), 1, 4);
        player.Heal(healing);
        if (Main.netMode == NetmodeID.Server)
            NetMessage.SendData(MessageID.PlayerLifeMana, -1, -1, null, player.whoAmI);
    }

    public override bool PreDraw(ref Color lightColor)
    {
        if (!configured || Main.dedServ)
            return false;
        float pulse = 0.78f + (float)Math.Sin(age * 0.18f) * 0.16f;
        DrawSiphonVortex(pulse);
        foreach (NPC npc in Main.ActiveNPCs)
        {
            if (!npc.CanBeChasedBy(Projectile)
                || Vector2.DistanceSquared(npc.Center, Projectile.Center) > radius * radius)
            {
                continue;
            }
            DrawSiphonTendril(npc.Center, Projectile.Center, npc.whoAmI, pulse);
        }
        DrawRippleRing(Projectile.Center, radius * (0.94f + pulse * 0.025f),
            new Color(205, 15, 48, 0) * (0.24f * pulse), 2.2f);
        return false;
    }

    private void DrawSiphonVortex(float opacity)
    {
        Vector2 screenCenter = Projectile.Center - Main.screenPosition;
        DeathDomainPrimitiveTextureSystem.DrawRadialGradient(Main.spriteBatch,
            screenCenter, 82f, new Color(4, 0, 2, 0) * (0.72f * opacity));

        const int arms = 9;
        const int samples = 24;
        float outerRadius = MathHelper.Clamp(radius * 0.34f, 92f, 168f);
        for (int arm = 0; arm < arms; arm++)
        {
            float baseAngle = arm * MathHelper.TwoPi / arms - age * 0.037f;
            Vector2 previous = Projectile.Center + baseAngle.ToRotationVector2() * outerRadius;
            for (int sample = 1; sample <= samples; sample++)
            {
                float progress = sample / (float)samples;
                float eased = progress * progress * (3f - 2f * progress);
                float angle = baseAngle + eased * 2.1f;
                float localRadius = MathHelper.Lerp(outerRadius, 13f, eased);
                float ripple = (float)Math.Sin(progress * MathHelper.TwoPi * 2f
                    - age * 0.14f + arm) * 2.2f * (1f - progress);
                Vector2 current = Projectile.Center + angle.ToRotationVector2()
                    * (localRadius + ripple);
                float width = MathHelper.Lerp(1.1f, 4.2f, progress);
                DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(previous, current,
                    new Color(80, 0, 18, 0) * (opacity * 0.46f), width + 4f);
                DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(previous, current,
                    new Color(236, 24, 58, 0) * (opacity * 0.78f), width);
                previous = current;
            }
        }
        DeathDomainPrimitiveTextureSystem.DrawSmoothDisc(Main.spriteBatch,
            screenCenter, 15f, new Color(2, 0, 1, 245) * opacity);
        DrawRippleRing(Projectile.Center, 22f + (float)Math.Sin(age * 0.12f) * 2f,
            new Color(248, 32, 66, 0) * (0.72f * opacity), 2.3f);
    }

    public override void SendExtraAI(BinaryWriter writer)
    {
        writer.Write(configured);
        if (!configured)
            return;
        snapshot.Write(writer);
        writer.Write(radius);
        writer.Write(controllerIdentity);
        writer.Write((short)Math.Clamp(age, 0, short.MaxValue));
    }

    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 incomingRadius = reader.ReadSingle();
        int incomingIdentity = reader.ReadInt32();
        int incomingAge = reader.ReadInt16();
        if (Main.netMode == NetmodeID.Server)
            return;
        snapshot = incomingSnapshot;
        radius = incomingRadius;
        controllerIdentity = incomingIdentity;
        age = incomingAge;
        configured = true;
    }

    private void Configure(in SickleCombatSnapshot value, float fieldRadius, int identity)
    {
        snapshot = value;
        radius = MathHelper.Clamp(fieldRadius, 120f, 760f);
        controllerIdentity = identity;
        age = 0;
        configured = true;
        serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient;
    }

    private bool ControllerStillCharging()
        => TryGetController(out _);

    private bool TryGetController(out ReaperActionControllerProjectile? activeController)
    {
        foreach (Projectile projectile in Main.ActiveProjectiles)
        {
            if (projectile.owner == Projectile.owner
                && projectile.identity == controllerIdentity
                && projectile.ModProjectile is ReaperActionControllerProjectile controller)
            {
                if (!controller.IsBloodSiphonActive)
                    continue;
                activeController = controller;
                return true;
            }
        }
        activeController = null;
        return false;
    }

    private void DrawSiphonTendril(Vector2 start, Vector2 end, int targetSeed, float opacity)
    {
        Vector2 delta = end - start;
        Vector2 axis = delta.SafeNormalize(Vector2.UnitX);
        Vector2 normal = axis.RotatedBy(MathHelper.PiOver2);
        float side = (targetSeed & 1) == 0 ? 1f : -1f;
        float bend = MathHelper.Clamp(delta.Length() * 0.13f, 18f, 58f);
        Vector2 control = (start + end) * 0.5f + normal * side
            * (bend + (float)Math.Sin(age * 0.075f + targetSeed * 0.83f) * 9f);
        Vector2 previous = start;
        const int segments = 32;
        for (int segment = 1; segment <= segments; segment++)
        {
            float amount = segment / (float)segments;
            Vector2 current = GetPoint(amount);
            DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(previous, current,
                new Color(46, 0, 9, 225) * (opacity * 0.88f), 8.5f);
            DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(previous, current,
                new Color(154, 4, 31, 235) * opacity, 4.2f);
            DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(previous, current,
                new Color(255, 52, 77, 245) * (opacity * 0.92f), 1.35f);
            previous = current;
        }

        // Five visible droplets ride the exact same Bezier curve from every living
        // target into the player. Their short tails make the transfer direction
        // legible even against the dark Blood domain backdrop.
        for (int droplet = 0; droplet < 5; droplet++)
        {
            float amount = (age * 0.046f + droplet / 5f + targetSeed * 0.037f) % 1f;
            Vector2 point = GetPoint(amount);
            Vector2 tail = GetPoint(Math.Max(0f, amount - 0.045f));
            float radius = 2.8f + amount * 1.7f;
            DeathDomainPrimitiveTextureSystem.DrawSmoothWorldLine(tail, point,
                new Color(245, 18, 52, 235) * (opacity * 0.82f), radius * 0.92f);
            DeathDomainPrimitiveTextureSystem.DrawSmoothDisc(Main.spriteBatch,
                point - Main.screenPosition, radius + 3f,
                new Color(86, 0, 18, 220) * (opacity * 0.72f));
            DeathDomainPrimitiveTextureSystem.DrawSmoothDisc(Main.spriteBatch,
                point - Main.screenPosition, radius,
                new Color(255, 58, 82, 250) * opacity);
        }

        Vector2 GetPoint(float amount)
        {
            float inverse = 1f - amount;
            Vector2 curve = inverse * inverse * start
                + 2f * inverse * amount * control
                + amount * amount * end;
            float ripple = (float)Math.Sin(amount * MathHelper.TwoPi * 2f
                - age * 0.16f + targetSeed * 0.43f)
                * 2.6f * (float)Math.Sin(amount * MathHelper.Pi);
            return curve + normal * ripple;
        }
    }

    private void DrawRippleRing(Vector2 center, float baseRadius, Color color, float width)
    {
        const int segments = 72;
        Vector2 previous = center + Vector2.UnitX * baseRadius;
        for (int segment = 1; segment <= segments; segment++)
        {
            float angle = MathHelper.TwoPi * segment / segments;
            float wave = (float)Math.Sin(angle * 7f - age * 0.11f) * 2.5f;
            Vector2 current = center + angle.ToRotationVector2() * (baseRadius + wave);
            DeathDomainPrimitiveTextureSystem.DrawSmoothWorldLine(previous, current,
                color, width);
            previous = current;
        }
    }
}