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.Audio;
using Terraria.GameContent;
using Terraria.ID;
using Terraria.ModLoader;

namespace SoulHarvest.Projectiles;

public sealed class ReaperStrikeProjectile : ModProjectile
{
    private const int DeathCutPointCount = 65;
    private readonly Vector2[] deathCutPoints = new Vector2[DeathCutPointCount];
    private SickleCombatSnapshot snapshot;
    private ReaperHitKind hitKind;
    private ReaperStrikeShape shape;
    private int phase;
    private int delay;
    private int age;
    private float length;
    private float width;
    private bool configured;
    private bool serverAuthorized;
    private bool serverDamageTriggered;
    private int authoritativeDamage;
    private Vector2 authoritativeCenter;
    private Vector2 authoritativeVelocity;

    public override string Texture => "Terraria/Images/Projectile_0";

    public static int Spawn(Terraria.DataStructures.IEntitySource source, int owner,
        in SickleCombatSnapshot snapshot, ReaperHitKind hitKind, int phase,
        Vector2 origin, Vector2 direction, ReaperStrikeShape shape, float length, float width,
        float damageMultiplier, int delay = 0, int actionId = -1)
    {
        if (Main.netMode == NetmodeID.MultiplayerClient)
            return -1;
        direction = direction.SafeNormalize(Vector2.UnitX);
        int index = Projectile.NewProjectile(source, origin, direction, ModContent.ProjectileType<ReaperStrikeProjectile>(),
            Math.Max(1, (int)Math.Round(snapshot.Damage * damageMultiplier)), snapshot.Knockback, owner);
        if (index < 0 || index >= Main.maxProjectiles)
            return -1;
        Projectile projectile = Main.projectile[index];
        projectile.originalDamage = snapshot.Damage;
        projectile.CritChance = hitKind == ReaperHitKind.Special ? snapshot.CritChance : 0;
        if (projectile.ModProjectile is ReaperStrikeProjectile strike)
            strike.Configure(snapshot, hitKind, phase, shape, length, width, delay);
        projectile.GetGlobalProjectile<MyGlobalProjectile>().ConfigureReaperProjectile(
            projectile, snapshot, hitKind, phase, direction.ToRotation(), actionId);
        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 = 240;
        // This projectile owns the client-side slash telegraph.  `hide` projectiles
        // are not submitted to Terraria's normal projectile draw cache unless a
        // DrawBehind hook explicitly adds them, so PreDraw never ran for any of the
        // server-authoritative form strikes.
        Projectile.hide = false;
        Projectile.netImportant = true;
        Projectile.DamageType = DamageClass.Melee;
        Projectile.usesLocalNPCImmunity = true;
        Projectile.localNPCHitCooldown = 10;
    }

    public override bool ShouldUpdatePosition() => false;

    // The complete visible slash window is authoritative. Local immunity is
    // longer than the remaining lifetime, so each actor still hits a target once
    // instead of leaving five decorative frames with no collision.
    public override bool? CanDamage()
    {
        // ReaperStrike is created by the server but retains the casting player's
        // owner for attribution. Terraria normally lets that owning client run
        // friendly-projectile collision, which makes the slash look successful
        // locally while the dedicated server never executes the Reaper hit hooks.
        // The server explicitly calls Damage below; clients are presentation-only.
        if (Main.netMode == NetmodeID.MultiplayerClient)
            return false;
        return configured && age >= delay && age < delay + 8 ? null : false;
    }

    public override void AI()
    {
        if (Main.netMode == NetmodeID.Server)
        {
            if (!serverAuthorized)
            {
                Projectile.Kill();
                return;
            }
            Projectile.damage = authoritativeDamage;
            Projectile.Center = authoritativeCenter;
            Projectile.velocity = authoritativeVelocity;
        }
        if (!configured)
            return;
        age++;
        if (Main.netMode == NetmodeID.Server && !serverDamageTriggered
            && age >= Math.Max(1, delay))
        {
            serverDamageTriggered = true;
            ReaperProjectileHelper.DamageOnDedicatedServer(Projectile);
        }
        int visualLifetime = GetVisualLifetime();
        if (IsDeathUltimateWorldCut && Main.netMode != NetmodeID.Server)
            RecordDeathUltimateWorldCut(visualLifetime);
        Projectile.timeLeft = Math.Max(2, visualLifetime + 1 - age);
        if (age == Math.Max(1, delay) && Main.netMode != NetmodeID.Server)
            SpawnBurst();
        if (age >= visualLifetime)
            Projectile.Kill();
    }

    public override bool? Colliding(Rectangle projHitbox, Rectangle targetHitbox)
    {
        Vector2 direction = Projectile.velocity.SafeNormalize(Vector2.UnitX);
        return shape switch
        {
            ReaperStrikeShape.Circle => CircleIntersects(targetHitbox, Projectile.Center, length),
            ReaperStrikeShape.Cross => LineIntersects(targetHitbox, direction) || LineIntersects(targetHitbox, direction.RotatedBy(MathHelper.PiOver2)),
            _ => LineIntersects(targetHitbox, direction)
        };
    }

    public override void OnHitNPC(NPC target, NPC.HitInfo hit, int damageDone)
    {
        if (Main.netMode == NetmodeID.Server || !configured
            || snapshot.Form != ReaperFormId.Base
            || hitKind != ReaperHitKind.Special)
        {
            return;
        }

        Vector2 axis = Projectile.velocity.SafeNormalize(Vector2.UnitX);
        Vector2 slash = axis.RotatedBy(-0.48f)
            .SafeNormalize(Vector2.UnitY);
        ReaperVfxDirector.TriggerGlobalImpact(slash, 3.2f, 4,
            new Color(90, 220, 230), 0f, 0, 0.08f);
        SoundEngine.PlaySound(SoundID.Item71 with
        {
            Volume = 0.52f,
            Pitch = 0.3f,
            PitchVariance = 0.07f
        }, target.Center);

        for (int index = 0; index < 13; index++)
        {
            Vector2 velocity = slash.RotatedBy(
                    Main.rand.NextFloat(-0.68f, 0.68f))
                * Main.rand.NextFloat(3f, 7.8f);
            Dust dust = Dust.NewDustPerfect(
                target.Center + Main.rand.NextVector2Circular(8f, 8f),
                DustID.AncientLight, velocity, 25,
                index % 4 == 0 ? new Color(225, 255, 250)
                    : new Color(65, 220, 235),
                Main.rand.NextFloat(0.78f, 1.16f));
            dust.noGravity = true;
        }

        Vector2 towardOwner = Projectile.owner >= 0
            && Projectile.owner < Main.maxPlayers
            ? (Main.player[Projectile.owner].Center - target.Center)
                .SafeNormalize(-axis)
            : -axis;
        for (int index = 0; index < 5; index++)
        {
            Dust soul = Dust.NewDustPerfect(
                target.Center + Main.rand.NextVector2Circular(11f, 11f),
                DustID.AncientLight,
                towardOwner.RotatedBy(Main.rand.NextFloat(-0.34f, 0.34f))
                    * Main.rand.NextFloat(1.4f, 3.2f),
                80, new Color(85, 225, 235),
                Main.rand.NextFloat(0.62f, 0.92f));
            soul.noGravity = true;
        }
    }

    public override bool PreDraw(ref Color lightColor)
    {
        if (!configured || age < delay || age > delay + 7)
        {
            if (!IsDeathUltimateWorldCut)
                return false;
        }
        if (IsDeathUltimateWorldCut)
        {
            DrawDeathUltimateShatterFragments();
            return false;
        }
        // Void circle strikes are the invisible collision sweep of the held
        // weapon. Its exact blade-tip tear is already drawn by the shared trail;
        // a conventional purple ring would contradict the cut-out background.
        if (snapshot.Form == ReaperFormId.Void && shape == ReaperStrikeShape.Circle)
            return false;
        float opacity = 1f - (age - delay) / 8f;
        Color outer = ReaperCombatRegistry.GetPrimaryColor(snapshot.Form) with { A = 0 };
        Color core = ReaperCombatRegistry.GetSecondaryColor(snapshot.Form) with { A = 0 };
        Vector2 direction = Projectile.velocity.SafeNormalize(Vector2.UnitX);
        if (shape == ReaperStrikeShape.Circle)
        {
            const int segments = 28;
            Vector2 previous = Projectile.Center + Vector2.UnitX * length;
            for (int index = 1; index <= segments; index++)
            {
                Vector2 next = Projectile.Center + (MathHelper.TwoPi * index / segments).ToRotationVector2() * length;
                DrawLine(previous, next, width * 0.42f, outer * opacity);
                DrawLine(previous, next, Math.Max(2f, width * 0.12f), core * opacity);
                previous = next;
            }
        }
        else
        {
            DrawSlash(direction, outer, core, opacity);
            if (shape == ReaperStrikeShape.Cross)
                DrawSlash(direction.RotatedBy(MathHelper.PiOver2), outer, core, opacity);
        }
        DrawEnergyMotes(direction, core, opacity);
        return false;
    }

    public override void SendExtraAI(BinaryWriter writer)
    {
        writer.Write(configured);
        if (!configured)
            return;
        snapshot.Write(writer);
        writer.Write((byte)hitKind);
        writer.Write((byte)shape);
        writer.Write((byte)Math.Clamp(phase, 0, byte.MaxValue));
        writer.Write((short)Math.Clamp(delay, 0, short.MaxValue));
        writer.Write(length);
        writer.Write(width);
    }

    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);
        ReaperHitKind incomingHitKind = (ReaperHitKind)reader.ReadByte();
        ReaperStrikeShape incomingShape = (ReaperStrikeShape)reader.ReadByte();
        int incomingPhase = reader.ReadByte();
        int incomingDelay = reader.ReadInt16();
        float incomingLength = reader.ReadSingle();
        float incomingWidth = reader.ReadSingle();
        if (Main.netMode == NetmodeID.Server)
            return;
        configured = true;
        snapshot = incomingSnapshot;
        hitKind = incomingHitKind;
        shape = incomingShape;
        phase = incomingPhase;
        delay = incomingDelay;
        length = incomingLength;
        width = incomingWidth;
    }

    private void Configure(in SickleCombatSnapshot value, ReaperHitKind kind, int hitPhase,
        ReaperStrikeShape strikeShape, float strikeLength, float strikeWidth, int strikeDelay)
    {
        snapshot = value;
        hitKind = kind;
        phase = hitPhase;
        shape = strikeShape;
        length = Math.Max(8f, strikeLength);
        width = Math.Max(2f, strikeWidth);
        delay = Math.Max(0, strikeDelay);
        configured = true;
        serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient;
        serverDamageTriggered = false;
        authoritativeDamage = Math.Max(1, Projectile.damage);
        authoritativeCenter = Projectile.Center;
        authoritativeVelocity = Projectile.velocity;
        Projectile.timeLeft = delay + 10;
    }

    private bool LineIntersects(Rectangle targetHitbox, Vector2 direction)
    {
        Vector2 start = shape == ReaperStrikeShape.Cross ? Projectile.Center - direction * length : Projectile.Center;
        Vector2 end = Projectile.Center + direction * length;
        float collisionPoint = 0f;
        return Collision.CheckAABBvLineCollision(targetHitbox.TopLeft(), targetHitbox.Size(), start, end, width, ref collisionPoint);
    }

    private static bool CircleIntersects(Rectangle target, Vector2 center, float radius)
    {
        float closestX = MathHelper.Clamp(center.X, target.Left, target.Right);
        float closestY = MathHelper.Clamp(center.Y, target.Top, target.Bottom);
        return Vector2.DistanceSquared(center, new Vector2(closestX, closestY)) <= radius * radius;
    }

    private void DrawSlash(Vector2 direction, Color outer, Color core, float opacity)
    {
        Vector2 start = shape == ReaperStrikeShape.Cross ? Projectile.Center - direction * length : Projectile.Center;
        Vector2 end = Projectile.Center + direction * length;
        DrawLine(start, end, width, outer * opacity * 0.72f);
        DrawLine(start, end, Math.Max(2f, width * 0.22f), core * opacity);
    }

    private bool IsDeathUltimateWorldCut => configured
        && snapshot.Form == ReaperFormId.Death
        && hitKind == ReaperHitKind.Ultimate
        && shape == ReaperStrikeShape.Line;

    private int GetVisualLifetime()
    {
        if (!IsDeathUltimateWorldCut)
            return delay + 8;
        // Every cut remains fixed in the world until the shared shatter tick,
        // then fractures for the rest of the controller timeline.
        return Math.Max(18, ReaperDeathUltimateGeometry.ShatterTick
            - ReaperDeathUltimateGeometry.GetCutTick(phase) + 18);
    }

    private void RecordDeathUltimateWorldCut(int lifetime)
    {
        Vector2 direction = Projectile.velocity.SafeNormalize(Vector2.UnitX);
        for (int index = 0; index < deathCutPoints.Length; index++)
        {
            deathCutPoints[index] = Projectile.Center + direction * length
                * (index / (float)(deathCutPoints.Length - 1));
        }
        float completion = Smooth01(age / 4f);
        float fracture = Smooth01(MathHelper.Clamp(
            (age - (lifetime - 18f)) / 18f, 0f, 1f));
        float terminalFade = 1f - Smooth01(MathHelper.Clamp(
            (fracture - 0.78f) / 0.22f, 0f, 1f));
        DeathDomainTrailVisualSystem.Record(Projectile.owner,
            Projectile.identity, deathCutPoints,
            Math.Max(ReaperDeathUltimateGeometry.CutVisualWidth, width * 3.1f),
            completion * terminalFade * 0.98f,
            mergeOverlappingRims: true, fracture: fracture);
    }

    private void DrawDeathUltimateShatterFragments()
    {
        int lifetime = GetVisualLifetime();
        float fracture = Smooth01(MathHelper.Clamp(
            (age - (lifetime - 18f)) / 18f, 0f, 1f));
        if (fracture <= 0.001f)
            return;

        Vector2 axis = Projectile.velocity.SafeNormalize(Vector2.UnitX);
        Vector2 normal = axis.RotatedBy(MathHelper.PiOver2);
        for (int shard = 0; shard < 7; shard++)
        {
            uint hash = unchecked((uint)(Projectile.identity * 747796405
                + shard * 2891336453 + phase * 97));
            hash ^= hash >> 16;
            float along = 0.08f + (hash & 0xFFFFu) / 65535f * 0.84f;
            float side = (shard & 1) == 0 ? -1f : 1f;
            float tilt = 0.22f + ((hash >> 16) & 0xFFu) / 255f * 0.48f;
            Vector2 origin = Projectile.Center + axis * length * along;
            Vector2 direction = (normal * side + axis * tilt)
                .SafeNormalize(normal * side);
            float branchLength = (22f + shard % 3 * 9f)
                * MathHelper.Lerp(0.35f, 1f, fracture);
            Vector2 end = origin + direction * branchLength;
            DeathDomainPrimitiveTextureSystem.DrawSmoothWorldLine(origin, end,
                new Color(30, 0, 14) * (fracture * 0.84f), 5.5f);
            DeathDomainPrimitiveTextureSystem.DrawSmoothWorldLine(origin, end,
                new Color(255, 34, 82) * (fracture * 0.78f), 1.7f);
        }
    }

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

    private static void DrawLine(Vector2 start, Vector2 end, float drawWidth, Color color)
    {
        DeathDomainPrimitiveTextureSystem.DrawSmoothWorldLine(start, end, color, drawWidth);
    }

    private void DrawEnergyMotes(Vector2 direction, Color color, float opacity)
    {
        int motes = shape == ReaperStrikeShape.Circle ? 5 : 3;
        for (int index = 0; index < motes; index++)
        {
            float travel = (age * 0.13f + index / (float)motes + phase * 0.071f) % 1f;
            Vector2 point;
            if (shape == ReaperStrikeShape.Circle)
            {
                float angle = travel * MathHelper.TwoPi + phase * 0.19f;
                point = Projectile.Center + angle.ToRotationVector2() * length;
            }
            else
            {
                Vector2 start = shape == ReaperStrikeShape.Cross
                    ? Projectile.Center - direction * length
                    : Projectile.Center;
                Vector2 end = Projectile.Center + direction * length;
                point = Vector2.Lerp(start, end, travel);
                if (shape == ReaperStrikeShape.Cross && (index & 1) != 0)
                {
                    Vector2 cross = direction.RotatedBy(MathHelper.PiOver2);
                    point = Vector2.Lerp(Projectile.Center - cross * length,
                        Projectile.Center + cross * length, travel);
                }
            }
            float radius = 1.8f + (index % 2) * 0.8f;
            DeathDomainPrimitiveTextureSystem.DrawSmoothDisc(Main.spriteBatch,
                point - Main.screenPosition, radius + 2.2f,
                color * (opacity * 0.25f));
            DeathDomainPrimitiveTextureSystem.DrawSmoothDisc(Main.spriteBatch,
                point - Main.screenPosition, radius,
                color * (opacity * 0.82f));
        }
    }

    private void SpawnBurst()
    {
        Color color = ReaperCombatRegistry.GetPrimaryColor(snapshot.Form);
        int dustType = ReaperCombatRegistry.GetDust(snapshot.Form);
        int count = shape == ReaperStrikeShape.Circle ? 24 : 14;
        for (int index = 0; index < count; index++)
        {
            Vector2 velocity = (MathHelper.TwoPi * index / count).ToRotationVector2() * Main.rand.NextFloat(1.2f, 4.2f);
            Dust dust = Dust.NewDustPerfect(Projectile.Center, dustType, velocity, 70, color, 0.9f);
            dust.noGravity = true;
        }
    }
}