XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

DeathMod

【Terraria】DeathMod (now aka Soul Harvest)

公开
关注 0 Fork 0 Star 0
UTF-8
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using SoulHarvest.Common;
using SoulHarvest.Items;
using System;
using System.IO;
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;

namespace SoulHarvest.Projectiles;

/// <summary>
/// Server-authored, non-damaging form-switch clock. The target form is not
/// committed to character progression until this projectile completes, so its
/// synchronized timer is also the visual and gameplay definition of “assembled”.
/// </summary>
public sealed class ReaperFormAssemblyProjectile : ModProjectile
{
    private ReaperFormId targetForm;
    private ReaperStage targetStage;
    private int duration;
    private int timer;
    private byte deathRangeLevel;
    private bool configured;
    private bool serverAuthorized;
    private float weaponAngle;
    private int facing = 1;

    internal ReaperFormId TargetForm => targetForm;
    internal ReaperStage TargetStage => targetStage;
    internal int Duration => duration;
    internal int Elapsed => timer;
    internal bool AssemblyComplete => configured && timer >= duration;

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

    internal static bool TryGetActive(Player player,
        out ReaperFormAssemblyProjectile? assembly)
    {
        int type = ModContent.ProjectileType<ReaperFormAssemblyProjectile>();
        foreach (Projectile projectile in Main.ActiveProjectiles)
        {
            if (projectile.owner == player.whoAmI && projectile.type == type
                && projectile.ModProjectile
                    is ReaperFormAssemblyProjectile candidate)
            {
                assembly = candidate;
                return true;
            }
        }
        assembly = null;
        return false;
    }

    internal static int Spawn(Player player, ReaperFormId form,
        ReaperStage stage, int assemblyFrames, int rangeLevel)
    {
        if (Main.netMode == NetmodeID.MultiplayerClient)
            return -1;

        int index = Projectile.NewProjectile(
            player.GetSource_Misc("SoulHarvest:ReaperFormAssembly"),
            player.MountedCenter, Vector2.Zero,
            ModContent.ProjectileType<ReaperFormAssemblyProjectile>(),
            0, 0f, player.whoAmI);
        if (index < 0 || index >= Main.maxProjectiles)
            return -1;

        Projectile projectile = Main.projectile[index];
        if (projectile.ModProjectile is not ReaperFormAssemblyProjectile assembly)
        {
            projectile.Kill();
            return -1;
        }

        assembly.Configure(form, stage, assemblyFrames, rangeLevel,
            authorizeServer: true);
        ReaperProjectileHelper.SyncNewProjectile(projectile);
        return index;
    }

    public override void SetDefaults()
    {
        Projectile.width = 2;
        Projectile.height = 2;
        Projectile.friendly = false;
        Projectile.hostile = false;
        Projectile.tileCollide = false;
        Projectile.ignoreWater = true;
        Projectile.penetrate = -1;
        Projectile.timeLeft = 360;
        Projectile.hide = true;
        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 || Projectile.owner < 0
            || Projectile.owner >= Main.maxPlayers)
        {
            return;
        }

        Player player = Main.player[Projectile.owner];
        if (!player.active || player.dead
            || player.HeldItem.ModItem is not NormalSickle)
        {
            Projectile.Kill();
            return;
        }

        Projectile.Center = player.MountedCenter;
        Projectile.timeLeft = 2;
        facing = player.direction == 0 ? 1 : player.direction;
        float aimAngle = facing > 0 ? 0f : MathHelper.Pi;
        weaponAngle = aimAngle - 1.45f * facing;
        player.ChangeDir(facing);
        player.heldProj = Projectile.whoAmI;
        player.itemTime = Math.Max(player.itemTime, 2);
        player.itemAnimation = Math.Max(player.itemAnimation, 2);
        player.SetCompositeArmFront(true, Player.CompositeArmStretchAmount.Full,
            weaponAngle - MathHelper.PiOver2);
        player.itemRotation = MathHelper.WrapAngle(weaponAngle);

        if (timer < duration)
        {
            timer++;
            if (timer == duration)
                Projectile.netUpdate = true;
            return;
        }

        if (Main.netMode == NetmodeID.MultiplayerClient)
        {
            // The owner client can reach the last visual tick a frame before the
            // server. Never let that predicted copy send a projectile kill back
            // and cancel the authoritative commit; hold the completed weapon
            // until the server changes the form and removes the controller.
            timer = duration;
            return;
        }

        bool completed = player.GetModPlayer<MyPlayer>()
            .TryCompleteReaperFormAssembly(Projectile.whoAmI, targetForm);
#if DEBUG
        Mod.Logger.Info(
            $"SOULHARVEST_FORM_ASSEMBLY: complete player={player.whoAmI} target={targetForm} elapsed={timer}/{duration} committed={completed}");
#endif
        Projectile.Kill();
    }

    public override bool PreDraw(ref Color lightColor)
    {
        if (Main.dedServ || !configured || Projectile.owner < 0
            || Projectile.owner >= Main.maxPlayers)
        {
            return false;
        }

        Player player = Main.player[Projectile.owner];
        if (!player.active)
            return false;

        Texture2D texture = ModContent.Request<Texture2D>(
            ReaperCombatRegistry.GetTexturePath(targetForm, targetStage)).Value;
        Vector2 normalizedAnchor = ReaperCombatRegistry.GetHandleAnchor(
            targetForm, targetStage);
        Vector2 anchor = new(texture.Width * normalizedAnchor.X,
            texture.Height * normalizedAnchor.Y);
        SpriteEffects effects = facing < 0
            ? SpriteEffects.FlipVertically : SpriteEffects.None;
        if ((effects & SpriteEffects.FlipVertically) != 0)
            anchor.Y = texture.Height - anchor.Y;

        int stageNumber = targetForm == ReaperFormId.Base ? 0
            : targetForm == ReaperFormId.Death ? 3 : (int)targetStage;
        float scale = targetForm == ReaperFormId.Death
            ? ReaperCombatRegistry.DeathWeaponDrawScale
                * ReaperDefinitions.GetDeathRangeMultiplier(deathRangeLevel)
            : 0.86f + stageNumber * 0.045f;
        float rotation = weaponAngle
            + ReaperCombatRegistry.GetTextureRotationCorrection(targetForm,
                targetStage);
        DeathSickleAssemblyTextureSystem.Draw(texture, player.MountedCenter,
            rotation, anchor, scale, effects, lightColor, timer, duration,
            Projectile.identity * 47 + (int)targetForm * 1291,
            targetForm == ReaperFormId.Death);
        return false;
    }

    public override void SendExtraAI(BinaryWriter writer)
    {
        writer.Write(configured);
        if (!configured)
            return;
        writer.Write((byte)targetForm);
        writer.Write((byte)targetStage);
        writer.Write((short)Math.Clamp(duration, 1, short.MaxValue));
        writer.Write((short)Math.Clamp(timer, 0, short.MaxValue));
        writer.Write(deathRangeLevel);
    }

    public override void ReceiveExtraAI(BinaryReader reader)
    {
        bool incomingConfigured = reader.ReadBoolean();
        if (!incomingConfigured)
        {
            if (Main.netMode != NetmodeID.Server)
                configured = false;
            return;
        }

        ReaperFormId incomingForm = (ReaperFormId)reader.ReadByte();
        ReaperStage incomingStage = (ReaperStage)reader.ReadByte();
        int incomingDuration = Math.Max(1, (int)reader.ReadInt16());
        int incomingTimer = Math.Clamp(reader.ReadInt16(), 0,
            incomingDuration);
        byte incomingRangeLevel = reader.ReadByte();
        if (Main.netMode == NetmodeID.Server)
            return;

        Configure(incomingForm, incomingStage, incomingDuration,
            incomingRangeLevel, authorizeServer: false);
        timer = incomingTimer;
    }

    public override void OnKill(int timeLeft)
    {
#if DEBUG
        if (Main.netMode != NetmodeID.MultiplayerClient)
        {
            string ownerState = Projectile.owner >= 0
                    && Projectile.owner < Main.maxPlayers
                ? $"active={Main.player[Projectile.owner].active} dead={Main.player[Projectile.owner].dead} held={Main.player[Projectile.owner].HeldItem.type}"
                : "owner-invalid";
            Mod.Logger.Info(
                $"SOULHARVEST_FORM_ASSEMBLY: killed player={Projectile.owner} target={targetForm} elapsed={timer}/{duration} authorized={serverAuthorized} configured={configured} {ownerState}");
        }
#endif
        if (Projectile.owner < 0 || Projectile.owner >= Main.maxPlayers)
            return;
        Player player = Main.player[Projectile.owner];
        if (player.heldProj == Projectile.whoAmI)
            player.heldProj = -1;
    }

    private void Configure(ReaperFormId form, ReaperStage stage,
        int assemblyFrames, int rangeLevel, bool authorizeServer)
    {
        targetForm = Enum.IsDefined(form) ? form : ReaperFormId.Base;
        targetStage = stage is >= ReaperStage.StageI and <= ReaperStage.StageIII
            ? stage : ReaperStage.StageI;
        duration = Math.Clamp(assemblyFrames, 1, 600);
        deathRangeLevel = (byte)Math.Clamp(rangeLevel, 0, byte.MaxValue);
        timer = 0;
        configured = true;
        serverAuthorized = authorizeServer;
        // Only the authoritative creator publishes state. Marking the replicated
        // owner copy dirty echoes a late spawn packet back after the server has
        // already committed and killed it, creating a short-lived ghost.
        if (authorizeServer)
            Projectile.netUpdate = true;
    }
}