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>
/// Server-authoritative Void ultimate. The gameplay surface is a stationary
/// circular tear; its client presentation uses layered continuous noise waves
/// instead of a rotating sprite.
/// </summary>
public sealed class ReaperVoidBlackHoleProjectile : ModProjectile
{
    private SickleCombatSnapshot snapshot;
    private float radius;
    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, float radius,
        int lifetime, float pullRadius, float damageMultiplier, int actionId)
    {
        if (Main.netMode == NetmodeID.MultiplayerClient)
            return -1;

        int index = Projectile.NewProjectile(source, center, Vector2.Zero,
            ModContent.ProjectileType<ReaperVoidBlackHoleProjectile>(),
            Math.Max(1, (int)Math.Round(snapshot.Damage
                * MathHelper.Clamp(damageMultiplier, 0.05f, 2f))),
            snapshot.Knockback * 0.12f, 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 + owner * 16777619);
        if (projectile.ModProjectile is ReaperVoidBlackHoleProjectile blackHole)
            blackHole.Configure(snapshot, radius, lifetime, pullRadius, stableSeed);
        projectile.GetGlobalProjectile<MyGlobalProjectile>().ConfigureReaperProjectile(
            projectile, snapshot, ReaperHitKind.DamageOverTime, 0, 0f, actionId);
        projectile.localNPCHitCooldown = 18;
        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 = 300;
        Projectile.netImportant = true;
        Projectile.DamageType = DamageClass.Melee;
        Projectile.usesLocalNPCImmunity = true;
        Projectile.localNPCHitCooldown = 18;
    }

    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++;
        if (Main.netMode != NetmodeID.MultiplayerClient)
            PullNearbyEnemies();
        Lighting.AddLight(Projectile.Center,
            new Vector3(0.24f, 0.025f, 0.42f) * MathHelper.Clamp(radius / 220f, 0.6f, 1.35f));
    }

    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);
        float damageRadius = radius * 0.78f;
        return Vector2.DistanceSquared(Projectile.Center,
            new Vector2(closestX, closestY)) <= damageRadius * damageRadius;
    }

    public override bool PreDraw(ref Color lightColor)
    {
        if (!configured || Main.dedServ)
            return false;

        float reveal = Smooth01(age / 14f);
        float fade = Smooth01(Projectile.timeLeft / 42f);
        float expansion = 1f - (float)Math.Pow(1f - reveal, 3f);
        float breathing = 0.985f + (float)Math.Sin(age * 0.075f + seed * 0.001f) * 0.015f;
        float visualRadius = radius * expansion * breathing;
        float opacity = reveal * fade;
        if (visualRadius <= 1f || opacity <= 0.001f)
            return false;

        Vector2 screenCenter = Projectile.Center - Main.screenPosition;
        DeathDomainPrimitiveTextureSystem.DrawRadialGradient(Main.spriteBatch,
            screenCenter, visualRadius * 1.55f,
            new Color(105, 14, 175, 0) * (opacity * 0.22f));

        DrawNoiseRipples(visualRadius, opacity);
        DrawAccretionStreams(visualRadius, opacity);

        float coreRadius = visualRadius * 0.74f;
        DeathDomainPrimitiveTextureSystem.DrawSmoothDisc(Main.spriteBatch,
            screenCenter, coreRadius + 7f,
            new Color(50, 3, 78, 220) * (opacity * 0.72f));
        DeathDomainPrimitiveTextureSystem.DrawSmoothDisc(Main.spriteBatch,
            screenCenter, coreRadius,
            new Color(1, 0, 4, 252) * opacity);
        DeathDomainPrimitiveTextureSystem.DrawRadialGradient(Main.spriteBatch,
            screenCenter, coreRadius * 0.72f,
            new Color(35, 3, 62, 0) * (opacity * 0.34f));

        float time = age * 0.055f;
        DrawNoiseBoundary(coreRadius + 4f, 5.5f, 5.2f,
            opacity * 0.82f, -time * 1.35f, 11.3f);
        DrawNoiseBoundary(coreRadius, 3.1f, 2.1f,
            opacity, time * 1.7f, 29.7f);
        DrawNoiseBoundary(visualRadius, 8.5f, 3.4f,
            opacity * 0.66f, time, 47.1f);
        return false;
    }

    public override void SendExtraAI(BinaryWriter writer)
    {
        writer.Write(configured);
        if (!configured)
            return;
        snapshot.Write(writer);
        writer.Write(radius);
        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 incomingRadius = 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;
        radius = incomingRadius;
        pullRadius = incomingPullRadius;
        lifetime = incomingLifetime;
        seed = incomingSeed;
        age = incomingAge;
        configured = true;
    }

    private void Configure(in SickleCombatSnapshot value, float activeRadius,
        int activeLifetime, float attractionRadius, int shapeSeed)
    {
        snapshot = value;
        radius = MathHelper.Clamp(activeRadius, 72f, 420f);
        pullRadius = MathHelper.Clamp(attractionRadius, radius, 960f);
        lifetime = Math.Clamp(activeLifetime, 60, 600);
        seed = shapeSeed;
        age = 0;
        configured = true;
        serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient;
        Projectile.timeLeft = lifetime;
    }

    private void PullNearbyEnemies()
    {
        if (age < 5 || Projectile.timeLeft <= 18)
            return;

        foreach (NPC npc in Main.ActiveNPCs)
        {
            if (!npc.CanBeChasedBy(Projectile) || npc.boss || npc.realLife >= 0)
                continue;

            Vector2 toCenter = Projectile.Center - npc.Center;
            float distance = toCenter.Length();
            if (distance <= 2f || distance >= pullRadius)
                continue;

            float influence = 1f - distance / pullRadius;
            float maximumSpeed = MathHelper.Lerp(1.6f, 9.2f, influence);
            Vector2 desired = toCenter / distance * maximumSpeed;
            npc.velocity = Vector2.Lerp(npc.velocity, desired,
                0.055f + influence * 0.14f);
            if (distance < radius * 0.30f)
                npc.velocity *= 0.72f;
            if (Main.netMode == NetmodeID.Server && Main.GameUpdateCount % 8 == 0)
                npc.netUpdate = true;
        }
    }

    private void DrawNoiseRipples(float visualRadius, float opacity)
    {
        for (int ripple = 0; ripple < 3; ripple++)
        {
            float progress = (age * 0.026f + ripple / 3f) % 1f;
            float rippleOpacity = (1f - progress) * (1f - progress) * opacity;
            float rippleRadius = visualRadius * MathHelper.Lerp(1.02f, 1.42f, progress);
            DrawNoiseBoundary(rippleRadius, 5f + progress * 7f,
                MathHelper.Lerp(3.2f, 1.1f, progress), rippleOpacity * 0.42f,
                age * 0.035f * (ripple % 2 == 0 ? 1f : -1f),
                71.3f + ripple * 19.7f);
        }
    }

    private void DrawAccretionStreams(float visualRadius, float opacity)
    {
        const int arms = 6;
        const int segments = 22;
        float rotation = age * 0.018f + seed * 0.00013f;
        for (int arm = 0; arm < arms; arm++)
        {
            float startAngle = rotation + arm * MathHelper.TwoPi / arms;
            Vector2 previous = Projectile.Center + startAngle.ToRotationVector2()
                * visualRadius * 1.38f;
            for (int segment = 1; segment <= segments; segment++)
            {
                float progress = segment / (float)segments;
                float angle = startAngle + progress * 1.32f;
                float streamRadius = visualRadius * MathHelper.Lerp(1.38f, 0.78f, progress);
                Vector2 current = Projectile.Center + angle.ToRotationVector2() * streamRadius;
                float envelope = (float)Math.Sin(progress * MathHelper.Pi);
                Color color = Color.Lerp(new Color(70, 10, 120, 0),
                    new Color(190, 62, 255, 0), progress);
                DeathDomainPrimitiveTextureSystem.DrawSmoothWorldLine(previous, current,
                    color * (opacity * envelope * 0.25f),
                    MathHelper.Lerp(5.5f, 1.4f, progress));
                previous = current;
            }
        }
    }

    private void DrawNoiseBoundary(float baseRadius, float amplitude, float width,
        float opacity, float time, float seedOffset)
    {
        if (baseRadius <= 1f || opacity <= 0.001f)
            return;

        const int segments = 112;
        float previousAngle = 0f;
        float previousRadius = baseRadius
            + BoundaryNoise(previousAngle, time, seedOffset) * amplitude;
        Vector2 previous = Projectile.Center + Vector2.UnitX * previousRadius;
        for (int segment = 1; segment <= segments; segment++)
        {
            float angle = MathHelper.TwoPi * segment / segments;
            float noisyRadius = baseRadius
                + BoundaryNoise(angle, time, seedOffset) * amplitude;
            Vector2 current = Projectile.Center
                + angle.ToRotationVector2() * noisyRadius;
            float flow = 0.5f + 0.5f * (float)Math.Sin(
                angle * 4f - time * 2.1f + seedOffset);
            Color color = Color.Lerp(new Color(78, 10, 132, 0),
                new Color(218, 82, 255, 0), flow);
            DeathDomainPrimitiveTextureSystem.DrawSmoothWorldLine(previous, current,
                color * opacity, width * (0.76f + flow * 0.38f));
            previous = current;
        }
    }

    private static float BoundaryNoise(float angle, float time, float noiseSeed)
    {
        float low = (float)Math.Sin(angle * 3f + time * 0.83f + noiseSeed) * 0.49f;
        float middle = (float)Math.Sin(angle * 7f - time * 1.27f
            + noiseSeed * 1.71f) * 0.29f;
        float high = (float)Math.Sin(angle * 13f + time * 1.81f
            + noiseSeed * 0.39f) * 0.15f;
        float grain = (float)Math.Sin(angle * 29f - time * 2.37f
            + noiseSeed * 2.13f) * 0.07f;
        return low + middle + high + grain;
    }

    private static float Smooth01(float value)
    {
        value = MathHelper.Clamp(value, 0f, 1f);
        return value * value * (3f - 2f * value);
    }
}