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 authoritative residual cut carved by a Void primary swing. Its geometry
/// is copied from the held weapon's actual blade-tip history, so drawing,
/// collision and attraction all use one curved world-space path.
/// </summary>
public sealed class ReaperVoidArcRiftProjectile : ModProjectile
{
    private const int MaximumPoints = 24;
    private readonly List<Vector2> points = [];
    private SickleCombatSnapshot snapshot;
    private float baseWidth;
    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 GetLifetime(int skillLevel) => skillLevel switch
    {
        >= 3 => 300,
        2 => 210,
        1 => 150,
        _ => 120
    };

    internal static int Spawn(Terraria.DataStructures.IEntitySource source, int owner,
        in SickleCombatSnapshot snapshot, Vector2[] newestFirstPoints, int validPointCount,
        int phase, int actionId, float damageMultiplier = 0.50f)
    {
        if (Main.netMode == NetmodeID.MultiplayerClient || validPointCount < 2)
            return -1;

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

        Vector2 center = Vector2.Zero;
        foreach (Vector2 point in ordered)
            center += point;
        center /= ordered.Count;
        Vector2 direction = (ordered[^1] - ordered[0]).SafeNormalize(Vector2.UnitX);
        int indexProjectile = Projectile.NewProjectile(source, center, direction,
            ModContent.ProjectileType<ReaperVoidArcRiftProjectile>(),
            Math.Max(1, (int)Math.Ceiling(snapshot.Damage
                * MathHelper.Clamp(damageMultiplier, 0.05f, 2f))),
            snapshot.Knockback * 0.10f, owner);
        if (indexProjectile < 0 || indexProjectile >= Main.maxProjectiles)
            return -1;

        Projectile projectile = Main.projectile[indexProjectile];
        projectile.originalDamage = snapshot.Damage;
        int skillLevel = Math.Clamp(Math.Max(1, (int)snapshot.Skill2), 1, 3);
        int stableSeed = unchecked(actionId * 486187739 + phase * 16777619 + owner * 97);
        if (projectile.ModProjectile is ReaperVoidArcRiftProjectile rift)
        {
            rift.Configure(snapshot, ordered,
                6f + snapshot.StageNumber * 2.2f,
                120f + skillLevel * 38f, GetLifetime(skillLevel), stableSeed);
        }
        projectile.GetGlobalProjectile<MyGlobalProjectile>().ConfigureReaperProjectile(
            projectile, snapshot, ReaperHitKind.DamageOverTime, phase,
            direction.ToRotation(), actionId);
        projectile.localNPCHitCooldown = 30;
        ReaperProjectileHelper.SyncNewProjectile(projectile);
        return indexProjectile;
    }

    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 = 30;
    }

    public override bool ShouldUpdatePosition() => false;
    public override bool? CanDamage()
        => configured && age >= 4 && Projectile.timeLeft > 15 ? 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();
    }

    public override bool? Colliding(Rectangle projHitbox, Rectangle targetHitbox)
    {
        float collisionPoint = 0f;
        for (int index = 1; index < points.Count; index++)
        {
            float taper = GetTaper(index - 0.5f);
            if (Collision.CheckAABBvLineCollision(targetHitbox.TopLeft(), targetHitbox.Size(),
                points[index - 1], points[index], Math.Max(2f, baseWidth * taper * 0.75f),
                ref collisionPoint))
            {
                return true;
            }
        }
        return false;
    }

    public override bool PreDraw(ref Color lightColor)
    {
        if (!configured || Main.dedServ || points.Count < 2)
            return false;
        float reveal = Smooth01(age / 9f);
        float fade = Smooth01(Projectile.timeLeft / 36f);
        int visibleSegments = Math.Clamp((int)Math.Ceiling((points.Count - 1) * reveal), 1,
            points.Count - 1);
        float visualBaseWidth = snapshot.Stage >= ReaperStage.StageIII
            ? baseWidth * 2f
            : baseWidth;
        for (int index = 1; index <= visibleSegments; index++)
        {
            float width = Math.Max(0.55f, visualBaseWidth * GetTaper(index - 0.5f));
            ReaperVoidBackdropRenderer.DrawTrailStrip(Main.spriteBatch,
                points[index - 1], points[index], width, fade,
                drawStars: true, branchSeed: seed, branchIndex: index);
        }
        return false;
    }

    public override void SendExtraAI(BinaryWriter writer)
    {
        writer.Write(configured);
        if (!configured)
            return;
        snapshot.Write(writer);
        writer.Write(baseWidth);
        writer.Write(pullRadius);
        writer.Write((short)lifetime);
        writer.Write(seed);
        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;
        }
        SickleCombatSnapshot incomingSnapshot = SickleCombatSnapshot.Read(reader);
        float incomingWidth = reader.ReadSingle();
        float incomingPullRadius = reader.ReadSingle();
        int incomingLifetime = reader.ReadInt16();
        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;
        snapshot = incomingSnapshot;
        baseWidth = incomingWidth;
        pullRadius = incomingPullRadius;
        lifetime = incomingLifetime;
        seed = incomingSeed;
        age = incomingAge;
        points.Clear();
        points.AddRange(incomingPoints);
        configured = points.Count >= 2;
    }

    private void Configure(in SickleCombatSnapshot value, List<Vector2> path,
        float width, float attractionRadius, int activeLifetime, int shapeSeed)
    {
        snapshot = value;
        points.Clear();
        points.AddRange(path);
        baseWidth = MathHelper.Clamp(width, 4f, 22f);
        pullRadius = MathHelper.Clamp(attractionRadius, 80f, 360f);
        lifetime = Math.Clamp(activeLifetime, 60, 300);
        seed = shapeSeed;
        age = 0;
        configured = points.Count >= 2;
        serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient;
        Projectile.timeLeft = lifetime;
    }

    private void PullNearbyEnemies()
    {
        if (age < 4 || Projectile.timeLeft <= 15)
            return;
        foreach (NPC npc in Main.ActiveNPCs)
        {
            if (!npc.CanBeChasedBy(Projectile) || npc.boss || npc.realLife >= 0)
                continue;
            Vector2 closest = points[0];
            float closestDistanceSquared = float.MaxValue;
            for (int index = 1; index < points.Count; index++)
            {
                Vector2 candidate = ClosestPoint(points[index - 1], points[index], npc.Center);
                float distanceSquared = Vector2.DistanceSquared(candidate, npc.Center);
                if (distanceSquared < closestDistanceSquared)
                {
                    closestDistanceSquared = distanceSquared;
                    closest = candidate;
                }
            }
            float distance = (float)Math.Sqrt(closestDistanceSquared);
            if (distance <= 2f || distance >= pullRadius)
                continue;
            float influence = 1f - distance / pullRadius;
            Vector2 desired = (closest - npc.Center) / distance
                * MathHelper.Lerp(1.2f, 6.4f, influence);
            npc.velocity = Vector2.Lerp(npc.velocity, desired, 0.04f + influence * 0.10f);
            if (Main.netMode == NetmodeID.Server && Main.GameUpdateCount % 10 == 0)
                npc.netUpdate = true;
        }
    }

    private float GetTaper(float segmentIndex)
    {
        float progress = segmentIndex / Math.Max(1f, points.Count - 1f);
        return (float)Math.Pow(Math.Max(0f, Math.Sin(progress * MathHelper.Pi)), 0.46f);
    }

    private static Vector2 ClosestPoint(Vector2 start, Vector2 end, Vector2 point)
    {
        Vector2 segment = end - start;
        float lengthSquared = Math.Max(1f, segment.LengthSquared());
        float amount = MathHelper.Clamp(Vector2.Dot(point - start, segment) / lengthSquared, 0f, 1f);
        return Vector2.Lerp(start, end, amount);
    }

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