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

namespace SoulHarvest.Projectiles;

/// <summary>
/// A server-authoritative, immutable section of the path cut open by the Death
/// Wings. The geometry remains at the world positions where it was created and
/// periodically asks the owner's character-bound Death Domain to harvest enemies.
/// </summary>
public sealed class DeathWingDomainTrailProjectile : ModProjectile
{
    private const int Lifetime = 300;
    private const int MaximumPoints = 16;
    private readonly List<Vector2> points = [];
    private readonly HashSet<int> selectedTargets = [];
    private Vector4 pathBounds;
    private float trailWidth;
    private float baseOpacity;
    private int trailSeed;
    private int age;
    private bool configured;
    private bool serverAuthorized;

#if DEBUG
    internal int DebugAge => age;
    internal int DebugPointCount => points.Count;
    internal int DebugLifetime => Lifetime;
#endif

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

    internal static int Spawn(IEntitySource source, int owner,
        IReadOnlyList<Vector2> orderedPoints, float width, float opacity,
        int seed)
    {
        if (Main.netMode == NetmodeID.MultiplayerClient
            || orderedPoints.Count < 2)
        {
            return -1;
        }

        List<Vector2> path = [];
        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 (path.Count == 0
                || Vector2.DistanceSquared(path[^1], point) >= 2f * 2f)
            {
                path.Add(point);
            }
        }
        if (path.Count < 2)
            return -1;

        Vector2 center = Vector2.Zero;
        foreach (Vector2 point in path)
            center += point;
        center /= path.Count;

        int projectileIndex = Projectile.NewProjectile(source, center,
            Vector2.Zero,
            ModContent.ProjectileType<DeathWingDomainTrailProjectile>(),
            0, 0f, owner);
        if (projectileIndex < 0 || projectileIndex >= Main.maxProjectiles)
            return -1;

        Projectile projectile = Main.projectile[projectileIndex];
        if (projectile.ModProjectile is DeathWingDomainTrailProjectile trail)
            trail.Configure(path, width, opacity, seed);
        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++;
        if (Main.netMode != NetmodeID.Server)
        {
            DeathDomainTrailVisualSystem.Record(Projectile.owner,
                Projectile.identity, points, trailWidth, GetOpacity(),
                mergeOverlappingRims: true, taperEnds: false,
                noiseFadeEnds: 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;

        writer.Write(trailWidth);
        writer.Write(baseOpacity);
        writer.Write(trailSeed);
        writer.Write((short)Math.Clamp(age, 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;
        }

        float incomingWidth = reader.ReadSingle();
        float incomingOpacity = reader.ReadSingle();
        int incomingSeed = reader.ReadInt32();
        int incomingAge = 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;

        points.Clear();
        points.AddRange(incomingPoints);
        trailWidth = MathHelper.Clamp(incomingWidth, 8f, 96f);
        baseOpacity = MathHelper.Clamp(incomingOpacity, 0f, 1f);
        trailSeed = incomingSeed;
        age = Math.Clamp(incomingAge, 0, Lifetime);
        Projectile.timeLeft = Math.Max(1, Lifetime - age);
        pathBounds = CalculatePathBounds(points, trailWidth);
        configured = points.Count >= 2;
    }

    private void Configure(List<Vector2> path, float width, float opacity,
        int seed)
    {
        points.Clear();
        points.AddRange(path);
        trailWidth = MathHelper.Clamp(width, 8f, 96f);
        baseOpacity = MathHelper.Clamp(opacity, 0f, 1f);
        trailSeed = seed;
        age = 0;
        pathBounds = CalculatePathBounds(points, trailWidth);
        configured = points.Count >= 2;
        serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient;
        Projectile.timeLeft = Lifetime;
    }

    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>();

        selectedTargets.Clear();
        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))
            {
                continue;
            }
            modPlayer.TrySpawnDeathWingDomainTrailHarvest(target, trailSeed);
        }
    }

    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]);
        }
        float padding = width * 0.5f;
        return new Vector4(minimum.X - padding, minimum.Y - padding,
            maximum.X + padding, maximum.Y + padding);
    }

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

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