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

namespace SoulHarvest.Projectiles;

/// <summary>
/// The immutable world-space wound traced by the tip of a max-tempo Death swing.
/// Its red edges enclose the Death Domain backdrop; enemies crossing the exact
/// recorded curve receive the owner's character-bound Death Domain harvest.
/// </summary>
public sealed class ReaperDeathDomainRiftProjectile : ModProjectile
{
    private const int Lifetime = 300;
    private const int MaximumPoints = 72;
    private readonly ulong[] nextHarvestTicks = new ulong[Main.maxNPCs];
    private readonly List<Vector2> points = [];
    private readonly HashSet<int> selectedTargets = [];
    private readonly List<NPC> harvestTargets = [];
    private Vector4 pathBounds;
    private SickleCombatSnapshot snapshot;
    private int phase;
    private int actionId;
    private int age;
    private int volleyCounter;
    private bool configured;
    private bool serverAuthorized;
    private float TrailWidth => snapshot.DeathRiftWidth;

#if DEBUG
    internal int DebugAge => age;
    internal int DebugPointCount => points.Count;
    internal int DebugLifetime => Lifetime;
    internal float DebugPathLength
    {
        get
        {
            float total = 0f;
            for (int index = 1; index < points.Count; index++)
                total += Vector2.Distance(points[index - 1], points[index]);
            return total;
        }
    }
#endif

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

    internal static int Spawn(Terraria.DataStructures.IEntitySource source,
        int owner, in SickleCombatSnapshot snapshot,
        IReadOnlyList<Vector2> orderedPoints, int phase, int actionId)
    {
        if (Main.netMode == NetmodeID.MultiplayerClient || orderedPoints.Count < 2)
            return -1;

        List<Vector2> ordered = [];
        int usable = Math.Min(orderedPoints.Count, MaximumPoints);
        for (int index = 0; index < usable; index++)
        {
            Vector2 point = orderedPoints[index];
            if (!float.IsFinite(point.X) || !float.IsFinite(point.Y))
                continue;
            if (ordered.Count == 0
                || Vector2.DistanceSquared(ordered[^1], point) >= 5f * 5f)
            {
                ordered.Add(point);
            }
        }
        if (ordered.Count < 2)
            return -1;

        Vector2 center = Vector2.Zero;
        foreach (Vector2 point in ordered)
            center += point;
        center /= ordered.Count;
        int projectileIndex = Projectile.NewProjectile(source, center,
            Vector2.Zero, ModContent.ProjectileType<ReaperDeathDomainRiftProjectile>(),
            0, 0f, owner);
        if (projectileIndex < 0 || projectileIndex >= Main.maxProjectiles)
            return -1;
        Projectile projectile = Main.projectile[projectileIndex];
        if (projectile.ModProjectile is ReaperDeathDomainRiftProjectile rift)
            rift.Configure(snapshot, ordered, phase, actionId);
        ReaperProjectileHelper.SyncNewProjectile(projectile);
        return projectileIndex;
    }

    public override void SetStaticDefaults()
    {
        ProjectileID.Sets.DrawScreenCheckFluff[Type] = 2400;
    }

    public override void SetDefaults()
    {
        Projectile.width = 4;
        Projectile.height = 4;
        Projectile.friendly = false;
        Projectile.hostile = false;
        Projectile.tileCollide = false;
        Projectile.ignoreWater = true;
        Projectile.penetrate = -1;
        Projectile.timeLeft = Lifetime;
        Projectile.netImportant = true;
    }

    public override bool ShouldUpdatePosition() => false;
    public override bool? CanDamage() => false;

    public override void AI()
    {
        if (Main.netMode == NetmodeID.Server && !serverAuthorized)
        {
            Projectile.Kill();
            return;
        }
        if (!configured)
            return;
        age++;
        float opacity = GetOpacity();
        if (Main.netMode != NetmodeID.Server)
        {
            // The lingering rift is the completed blade locus: keep the whole
            // arc intact and fade it uniformly instead of deleting random
            // segments during its final frames.
            DeathDomainTrailVisualSystem.Record(Projectile.owner,
                Projectile.identity, points, TrailWidth, opacity,
                mergeOverlappingRims: true);
        }
        if (Main.netMode != NetmodeID.MultiplayerClient)
            TryHarvestIntersectingEnemies();
    }

    public override bool PreDraw(ref Color lightColor)
        => false;

    public override void SendExtraAI(BinaryWriter writer)
    {
        writer.Write(configured);
        if (!configured)
            return;
        snapshot.Write(writer);
        writer.Write((byte)Math.Clamp(phase, 0, byte.MaxValue));
        writer.Write(actionId);
        writer.Write((short)Math.Clamp(age, 0, short.MaxValue));
        writer.Write((short)Math.Clamp(volleyCounter, 0, short.MaxValue));
        writer.Write((byte)Math.Min(points.Count, MaximumPoints));
        for (int index = 0; index < points.Count && index < MaximumPoints; index++)
            writer.WriteVector2(points[index]);
    }

    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);
        int incomingPhase = reader.ReadByte();
        int incomingAction = reader.ReadInt32();
        int incomingAge = reader.ReadInt16();
        int incomingVolley = reader.ReadInt16();
        int count = reader.ReadByte();
        List<Vector2> incomingPoints = [];
        for (int index = 0; index < count; index++)
            incomingPoints.Add(reader.ReadVector2());
        if (Main.netMode == NetmodeID.Server)
            return;
        snapshot = incomingSnapshot;
        phase = incomingPhase;
        actionId = incomingAction;
        age = Math.Clamp(incomingAge, 0, Lifetime);
        Projectile.timeLeft = Math.Max(1, Lifetime - age);
        volleyCounter = incomingVolley;
        points.Clear();
        points.AddRange(incomingPoints);
        pathBounds = CalculatePathBounds(points, TrailWidth);
        configured = points.Count >= 2;
    }

    private void Configure(in SickleCombatSnapshot value, List<Vector2> path,
        int sourcePhase, int sourceActionId)
    {
        snapshot = value;
        points.Clear();
        points.AddRange(path);
        pathBounds = CalculatePathBounds(points, TrailWidth);
        phase = Math.Max(0, sourcePhase);
        actionId = sourceActionId;
        age = 0;
        volleyCounter = 0;
        Projectile.timeLeft = Lifetime;
        configured = points.Count >= 2;
        serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient;
    }

    private void TryHarvestIntersectingEnemies()
    {
        if (age < 6 || Projectile.timeLeft <= 15
            || Projectile.owner < 0 || Projectile.owner >= Main.maxPlayers)
        {
            return;
        }
        Player owner = Main.player[Projectile.owner];
        if (!owner.active || owner.dead)
            return;
        MyPlayer modPlayer = owner.GetModPlayer<MyPlayer>();
        DeathDomainProgression progression = modPlayer.DeathDomainProgression;

        selectedTargets.Clear();
        harvestTargets.Clear();
        ulong currentTick = Main.GameUpdateCount;
        foreach (NPC npc in Main.ActiveNPCs)
        {
            if (!ReaperTargeting.IsValidWeaponTarget(npc)
                || !IntersectsPathBounds(npc.Hitbox)
                || !IntersectsPath(npc.Hitbox))
            {
                continue;
            }
            NPC target = npc.realLife >= 0 && npc.realLife < Main.maxNPCs
                && Main.npc[npc.realLife].active ? Main.npc[npc.realLife] : npc;
            if (!ReaperTargeting.IsValidWeaponTarget(target)
                || !selectedTargets.Add(target.whoAmI)
                || nextHarvestTicks[target.whoAmI] > currentTick)
            {
                continue;
            }
            nextHarvestTicks[target.whoAmI] = currentTick
                + (ulong)Math.Max(1, progression.SpawnInterval);
            harvestTargets.Add(target);
        }
        if (harvestTargets.Count == 0)
            return;

        volleyCounter++;
        bool requiem = modPlayer.IsDeathDomainDescended(modPlayer.GetEquippedDeathNecklace())
            && volleyCounter % 4 == 0;
        foreach (NPC target in harvestTargets)
        {
            uint hash = unchecked((uint)(actionId * 16777619
                + target.whoAmI * 486187739 + volleyCounter * 97));
            hash ^= hash >> 16;
            float rotation = (hash & 0x00FFFFFFu) / 16777215f
                * MathHelper.TwoPi;
            modPlayer.SpawnDeathDomainHarvestFromReaperRift(target, requiem,
                rotation);
        }
    }

    private bool IntersectsPath(Rectangle target)
    {
        float collisionPoint = 0f;
        for (int index = 1; index < points.Count; index++)
        {
            if (Collision.CheckAABBvLineCollision(target.TopLeft(), target.Size(),
                points[index - 1], points[index], TrailWidth, ref collisionPoint))
            {
                return true;
            }
        }
        return false;
    }

    private bool IntersectsPathBounds(Rectangle target)
        => target.Right >= pathBounds.X && target.Bottom >= pathBounds.Y
            && target.Left <= pathBounds.Z && target.Top <= pathBounds.W;

    private static Vector4 CalculatePathBounds(IReadOnlyList<Vector2> path,
        float width)
    {
        if (path.Count == 0)
            return Vector4.Zero;
        Vector2 minimum = path[0];
        Vector2 maximum = path[0];
        for (int index = 1; index < path.Count; index++)
        {
            minimum = Vector2.Min(minimum, path[index]);
            maximum = Vector2.Max(maximum, path[index]);
        }
        return new Vector4(minimum.X - width, minimum.Y - width,
            maximum.X + width, maximum.Y + width);
    }

    private float GetOpacity()
        => Smooth01(age / 8f) * Smooth01(Projectile.timeLeft / 44f);

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