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

DeathMod

【Terraria】DeathMod (now aka Soul Harvest)

公开
关注 0 Fork 0 Star 0
UTF-8
using System;
using System.IO;
using Terraria.ModLoader.IO;

namespace SoulHarvest.Common;


/// <summary>
/// Persisted, character-owned reaper progression. Combat energy, cooldowns and
/// action state intentionally do not live here and therefore reset on death/rejoin.
/// </summary>
public sealed class ReaperProgressionState
{
    public const int CurrentDataVersion = 7;
    public const string DefaultSaveKey = "ReaperProgression";

    private const int BranchCount = 6;
    private const int CommonNodeCount = 13;
    private const int SoulSealStageCount = 3;

    private readonly ReaperBranchState[] branches = new ReaperBranchState[BranchCount];
    private readonly int[] commonNodeLevels = new int[CommonNodeCount];
    private readonly byte[] soulSealsIssued = new byte[SoulSealStageCount];
    private readonly byte[] soulSealsSpent = new byte[SoulSealStageCount];
    private readonly byte[] soulSealsPending = new byte[SoulSealStageCount];

    public ReaperProgressionState()
    {
        for (int index = 0; index < branches.Length; index++)
            branches[index] = new ReaperBranchState();
    }

    public int LoadedDataVersion { get; private set; } = CurrentDataVersion;

    public ReaperFormId CurrentForm { get; private set; } = ReaperFormId.Base;

    public bool DeathFormUnlocked
    {
        get
        {
            foreach (ReaperBranchState branch in branches)
            {
                if (branch.Stage != ReaperStage.StageIII)
                    return false;

                for (int skill = 0; skill < ReaperDefinitions.SkillsPerBranch; skill++)
                {
                    if (branch.GetSkillLevel(skill) != ReaperDefinitions.MaximumSkillLevel)
                        return false;
                }
            }

            return true;
        }
    }

    public ReaperBranchState GetBranch(ReaperFormId form)
    {
        int index = GetBranchIndex(form);
        if (index < 0)
            throw new ArgumentOutOfRangeException(nameof(form), form, "Only the six branch forms own branch progression.");
        return branches[index];
    }

    public ReaperStage GetStage(ReaperFormId form)
    {
        if (form == ReaperFormId.Base)
            return ReaperStage.StageI;
        if (form == ReaperFormId.Death)
            return DeathFormUnlocked ? ReaperStage.StageIII : ReaperStage.Locked;

        int index = GetBranchIndex(form);
        return index >= 0 ? branches[index].Stage : ReaperStage.Locked;
    }

    public int GetSkillLevel(ReaperFormId form, int skillIndex)
    {
        int index = GetBranchIndex(form);
        return index >= 0 ? branches[index].GetSkillLevel(skillIndex) : 0;
    }

    public int GetCommonNodeLevel(ReaperCommonNode node)
    {
        int index = (int)node;
        return index >= 0 && index < commonNodeLevels.Length ? commonNodeLevels[index] : 0;
    }

    public int GetNodeLevel(ReaperNodeId nodeId)
    {
        if (!ReaperDefinitions.TryGetNode(nodeId, out ReaperNodeDefinition definition))
            return 0;

        return definition.CommonNode is ReaperCommonNode commonNode
            ? GetCommonNodeLevel(commonNode)
            : GetSkillLevel(definition.Form, definition.SkillIndex);
    }

    public int GetSoulSealIssued(ReaperStage stage)
    {
        int index = GetSoulSealIndex(stage);
        return index >= 0 ? soulSealsIssued[index] : 0;
    }

    public int GetSoulSealSpent(ReaperStage stage)
    {
        int index = GetSoulSealIndex(stage);
        return index >= 0 ? soulSealsSpent[index] : 0;
    }

    public int GetSoulSealPending(ReaperStage stage)
    {
        int index = GetSoulSealIndex(stage);
        return index >= 0 ? soulSealsPending[index] : 0;
    }

    public int GetSoulSealAvailable(ReaperStage stage)
    {
        int index = GetSoulSealIndex(stage);
        return index >= 0
            ? Math.Max(0, soulSealsIssued[index] - soulSealsSpent[index] - soulSealsPending[index])
            : 0;
    }

    // Plural aliases keep UI/call sites readable and preserve the public contract
    // used by the initial implementation plan.
    public int GetPendingSoulSeals(ReaperStage stage) => GetSoulSealPending(stage);

    public int GetAvailableSoulSeals(ReaperStage stage) => GetSoulSealAvailable(stage);

    public bool CanIssueSoulSeal(ReaperStage stage)
    {
        int index = GetSoulSealIndex(stage);
        return index >= 0 && soulSealsIssued[index] < ReaperDefinitions.MaximumSoulSealsPerStage;
    }

    /// <summary>
    /// Records a server-authoritative boss reward. Pending means the representative
    /// item could not be placed in inventory yet; the entitlement itself is already
    /// capped and persisted here.
    /// </summary>
    public bool TryRecordSoulSealIssued(ReaperStage stage, bool pending)
    {
        int index = GetSoulSealIndex(stage);
        if (index < 0 || soulSealsIssued[index] >= ReaperDefinitions.MaximumSoulSealsPerStage)
            return false;

        soulSealsIssued[index]++;
        if (pending)
            soulSealsPending[index]++;
        return true;
    }

    public bool TryMarkPendingSoulSealDelivered(ReaperStage stage)
    {
        int index = GetSoulSealIndex(stage);
        if (index < 0 || soulSealsPending[index] <= 0)
            return false;

        soulSealsPending[index]--;
        return true;
    }

    internal int MarkMissingSoulSealsPending(ReaperStage stage, int amount)
    {
        int index = GetSoulSealIndex(stage);
        if (index < 0 || amount <= 0)
            return 0;

        int maximumMissing = Math.Max(0, soulSealsIssued[index] - soulSealsSpent[index] - soulSealsPending[index]);
        int marked = Math.Min(amount, maximumMissing);
        soulSealsPending[index] += (byte)marked;
        return marked;
    }

    public bool TrySetCurrentForm(ReaperFormId form)
    {
        if (form == ReaperFormId.Base)
        {
            CurrentForm = form;
            return true;
        }

        if (form == ReaperFormId.Death)
        {
            if (!DeathFormUnlocked)
                return false;
            CurrentForm = form;
            return true;
        }

        if (!ReaperDefinitions.IsBranchForm(form) || GetStage(form) == ReaperStage.Locked)
            return false;

        CurrentForm = form;
        return true;
    }

    public void Reset()
    {
        foreach (ReaperBranchState branch in branches)
            branch.Reset();
        Array.Clear(commonNodeLevels);
        Array.Clear(soulSealsIssued);
        Array.Clear(soulSealsSpent);
        Array.Clear(soulSealsPending);
        CurrentForm = ReaperFormId.Base;
        LoadedDataVersion = CurrentDataVersion;
    }

    /// <summary>
    /// Debug-only rollback of every sickle progression node. Issued soul seals are
    /// retained and marked unspent so the same test character can replay all six
    /// branches without farming bosses again.
    /// </summary>
    internal void ResetTreeForDebug()
    {
        foreach (ReaperBranchState branch in branches)
            branch.Reset();
        Array.Clear(commonNodeLevels);
        Array.Clear(soulSealsSpent);
        Array.Clear(soulSealsPending);
        CurrentForm = ReaperFormId.Base;
        LoadedDataVersion = CurrentDataVersion;
        NormalizeSoulSealLedger();
    }

    public void SaveData(TagCompound rootTag, string key = DefaultSaveKey)
    {
        byte[] branchStages = new byte[branches.Length];
        byte[] branchSkills = new byte[branches.Length * ReaperDefinitions.SkillsPerBranch];
        for (int branch = 0; branch < branches.Length; branch++)
        {
            branchStages[branch] = (byte)branches[branch].Stage;
            branches[branch].CopySkillLevelsTo(branchSkills, branch * ReaperDefinitions.SkillsPerBranch);
        }

        byte[] nodeIds = new byte[ReaperDefinitions.Nodes.Count];
        byte[] legacyNodeLevels = new byte[ReaperDefinitions.Nodes.Count];
        int[] nodeLevels = new int[ReaperDefinitions.Nodes.Count];
        for (int index = 0; index < ReaperDefinitions.Nodes.Count; index++)
        {
            ReaperNodeDefinition definition = ReaperDefinitions.Nodes[index];
            nodeIds[index] = (byte)definition.Id;
            nodeLevels[index] = GetNodeLevel(definition.Id);
            legacyNodeLevels[index] = (byte)Math.Clamp(nodeLevels[index], 0, byte.MaxValue);
        }

        rootTag[key] = new TagCompound
        {
            ["Version"] = CurrentDataVersion,
            ["CurrentForm"] = (byte)CurrentForm,
            ["BranchStages"] = branchStages,
            ["NodeIds"] = nodeIds,
            ["NodeLevelsInt"] = nodeLevels,
            ["NodeLevels"] = legacyNodeLevels,
            // Retained as a downgrade fallback for pre-id 3.0 development saves.
            ["BranchSkills"] = branchSkills,
            ["CommonNodesInt"] = (int[])commonNodeLevels.Clone(),
            ["CommonNodes"] = Array.ConvertAll(commonNodeLevels, value => (byte)Math.Clamp(value, 0, byte.MaxValue)),
            ["SoulSealsIssued"] = (byte[])soulSealsIssued.Clone(),
            ["SoulSealsSpent"] = (byte[])soulSealsSpent.Clone(),
            ["SoulSealsPending"] = (byte[])soulSealsPending.Clone()
        };
    }

    public void LoadData(TagCompound rootTag, string key = DefaultSaveKey)
    {
        Reset();
        if (!rootTag.ContainsKey(key))
        {
            LoadedDataVersion = 0;
            return;
        }

        TagCompound tag = rootTag.GetCompound(key);
        LoadedDataVersion = Math.Max(0, tag.GetInt("Version"));
        CopyLoadedBytes(tag.GetByteArray("BranchStages"), (index, value) =>
        {
            branches[index].Stage = (ReaperStage)Math.Clamp(value, (byte)ReaperStage.Locked, (byte)ReaperStage.StageIII);
        }, branches.Length);

        bool hasStableNodes = tag.ContainsKey("NodeIds")
            && (tag.ContainsKey("NodeLevelsInt") || tag.ContainsKey("NodeLevels"));
        if (hasStableNodes)
        {
            byte[] nodeIds = tag.GetByteArray("NodeIds");
            int[] nodeLevels = tag.ContainsKey("NodeLevelsInt")
                ? tag.Get<int[]>("NodeLevelsInt")
                : Array.ConvertAll(tag.GetByteArray("NodeLevels"), value => (int)value);
            int nodeCount = Math.Min(nodeIds.Length, nodeLevels.Length);
            int migratedDeathDamage = 0;
            for (int index = 0; index < nodeCount; index++)
            {
                ReaperNodeId nodeId = (ReaperNodeId)nodeIds[index];
                if (nodeId == ReaperNodeId.CommonDamage
                    && nodeLevels[index] > ReaperDefinitions.GetCommonNodeMaxLevel(ReaperCommonNode.Damage))
                {
                    migratedDeathDamage = Math.Max(migratedDeathDamage,
                        nodeLevels[index] - ReaperDefinitions.GetCommonNodeMaxLevel(ReaperCommonNode.Damage));
                }
                if (ReaperDefinitions.TryGetNode(nodeId, out _))
                    SetNodeLevel(nodeId, nodeLevels[index]);
            }
            if (migratedDeathDamage > 0)
            {
                SetCommonNodeLevel(ReaperCommonNode.DeathDamage,
                    Math.Max(GetCommonNodeLevel(ReaperCommonNode.DeathDamage),
                        migratedDeathDamage));
            }
        }
        else
        {
            byte[] skills = tag.GetByteArray("BranchSkills");
            for (int branch = 0; branch < branches.Length; branch++)
            {
                for (int skill = 0; skill < ReaperDefinitions.SkillsPerBranch; skill++)
                {
                    int flatIndex = branch * ReaperDefinitions.SkillsPerBranch + skill;
                    if (flatIndex < skills.Length)
                        branches[branch].SetSkillLevel(skill, skills[flatIndex]);
                }
            }

            if (tag.ContainsKey("CommonNodesInt"))
            {
                int[] loadedCommon = tag.Get<int[]>("CommonNodesInt");
                int migratedDeathDamage = loadedCommon.Length > (int)ReaperCommonNode.Damage
                    ? Math.Max(0, loadedCommon[(int)ReaperCommonNode.Damage]
                        - ReaperDefinitions.GetCommonNodeMaxLevel(ReaperCommonNode.Damage))
                    : 0;
                LoadClampedArray(loadedCommon, commonNodeLevels, index =>
                    ReaperDefinitions.GetCommonNodeMaxLevel((ReaperCommonNode)index));
                if (migratedDeathDamage > 0)
                    SetCommonNodeLevel(ReaperCommonNode.DeathDamage, migratedDeathDamage);
            }
            else
                LoadClampedArray(Array.ConvertAll(tag.GetByteArray("CommonNodes"), value => (int)value), commonNodeLevels, index =>
                    ReaperDefinitions.GetCommonNodeMaxLevel((ReaperCommonNode)index));
        }

        for (int branch = 0; branch < branches.Length; branch++)
            branches[branch].EnsureStageSkills();

        LoadClampedArray(tag.GetByteArray("SoulSealsIssued"), soulSealsIssued, _ => ReaperDefinitions.MaximumSoulSealsPerStage);
        LoadClampedArray(tag.GetByteArray("SoulSealsSpent"), soulSealsSpent, _ => ReaperDefinitions.MaximumSoulSealsPerStage);
        LoadClampedArray(tag.GetByteArray("SoulSealsPending"), soulSealsPending, _ => ReaperDefinitions.MaximumSoulSealsPerStage);
        NormalizeSoulSealLedger();

        ReaperFormId loadedForm = (ReaperFormId)tag.GetByte("CurrentForm");
        CurrentForm = ReaperFormId.Base;
        TrySetCurrentForm(loadedForm);
    }

    public void NetSend(BinaryWriter writer)
    {
        writer.Write((byte)CurrentDataVersion);
        writer.Write((byte)CurrentForm);
        for (int branch = 0; branch < branches.Length; branch++)
        {
            writer.Write((byte)branches[branch].Stage);
            for (int skill = 0; skill < ReaperDefinitions.SkillsPerBranch; skill++)
                writer.Write((byte)branches[branch].GetSkillLevel(skill));
        }

        for (int index = 0; index < commonNodeLevels.Length; index++)
            writer.Write(commonNodeLevels[index]);
        writer.Write(soulSealsIssued);
        writer.Write(soulSealsSpent);
        writer.Write(soulSealsPending);
    }

    public void NetReceive(BinaryReader reader)
    {
        Reset();
        LoadedDataVersion = reader.ReadByte();
        ReaperFormId receivedForm = (ReaperFormId)reader.ReadByte();
        for (int branch = 0; branch < branches.Length; branch++)
        {
            branches[branch].Stage = (ReaperStage)Math.Clamp(reader.ReadByte(), (byte)ReaperStage.Locked, (byte)ReaperStage.StageIII);
            for (int skill = 0; skill < ReaperDefinitions.SkillsPerBranch; skill++)
                branches[branch].SetSkillLevel(skill, reader.ReadByte());
            branches[branch].EnsureStageSkills();
        }

        for (int index = 0; index < commonNodeLevels.Length; index++)
            commonNodeLevels[index] = Math.Clamp(reader.ReadInt32(), 0,
                ReaperDefinitions.GetCommonNodeMaxLevel((ReaperCommonNode)index));
        ReadClampedArray(reader, soulSealsIssued, _ => ReaperDefinitions.MaximumSoulSealsPerStage);
        ReadClampedArray(reader, soulSealsSpent, _ => ReaperDefinitions.MaximumSoulSealsPerStage);
        ReadClampedArray(reader, soulSealsPending, _ => ReaperDefinitions.MaximumSoulSealsPerStage);
        NormalizeSoulSealLedger();

        CurrentForm = ReaperFormId.Base;
        TrySetCurrentForm(receivedForm);
    }

    internal bool TryUnlockNextStage(ReaperFormId form)
    {
        int branchIndex = GetBranchIndex(form);
        if (branchIndex < 0 || branches[branchIndex].Stage >= ReaperStage.StageIII)
            return false;

        branches[branchIndex].Stage++;
        branches[branchIndex].EnsureStageSkills();
        return true;
    }

    internal bool TryUpgradeSkill(ReaperFormId form, int skillIndex)
    {
        return ReaperDefinitions.TryGetSkillNode(form, skillIndex, out ReaperNodeId nodeId)
            && TryUpgradeNode(nodeId);
    }

    internal bool TryUpgradeCommonNode(ReaperCommonNode node)
    {
        return ReaperDefinitions.TryGetCommonNode(node, out ReaperNodeId nodeId)
            && TryUpgradeNode(nodeId);
    }

    internal bool TryUpgradeNode(ReaperNodeId nodeId)
    {
        if (!ReaperDefinitions.TryGetNode(nodeId, out ReaperNodeDefinition definition))
            return false;

        if (!definition.IsCommon)
        {
            int branchIndex = GetBranchIndex(definition.Form);
            return branchIndex >= 0 && branches[branchIndex].TryUpgradeSkill(definition.SkillIndex);
        }

        ReaperCommonNode commonNode = definition.CommonNode!.Value;
        int index = (int)commonNode;
        if (index < 0 || index >= commonNodeLevels.Length
            || commonNodeLevels[index] >= ReaperDefinitions.GetCommonNodeMaxLevel(commonNode))
        {
            return false;
        }

        commonNodeLevels[index]++;
        return true;
    }

    internal bool TryConsumeSoulSeal(ReaperStage stage)
    {
        int index = GetSoulSealIndex(stage);
        if (index < 0 || GetSoulSealAvailable(stage) <= 0)
            return false;

        soulSealsSpent[index]++;
        return true;
    }

    internal bool HasAnyBranchAtLeast(ReaperStage stage)
    {
        foreach (ReaperBranchState branch in branches)
        {
            if (branch.Stage >= stage)
                return true;
        }
        return false;
    }

    internal void SetBranchStage(ReaperFormId form, ReaperStage stage)
    {
        int index = GetBranchIndex(form);
        if (index < 0)
            return;
        branches[index].Stage = (ReaperStage)Math.Clamp((byte)stage, (byte)ReaperStage.Locked, (byte)ReaperStage.StageIII);
        branches[index].EnsureStageSkills();
    }

    internal void SetSkillLevel(ReaperFormId form, int skillIndex, int level)
    {
        if (ReaperDefinitions.TryGetSkillNode(form, skillIndex, out ReaperNodeId nodeId))
            SetNodeLevel(nodeId, level);
    }

    internal void SetCommonNodeLevel(ReaperCommonNode node, int level)
    {
        if (ReaperDefinitions.TryGetCommonNode(node, out ReaperNodeId nodeId))
            SetNodeLevel(nodeId, level);
    }

    internal void SetNodeLevel(ReaperNodeId nodeId, int level)
    {
        if (!ReaperDefinitions.TryGetNode(nodeId, out ReaperNodeDefinition definition))
            return;

        if (!definition.IsCommon)
        {
            int branchIndex = GetBranchIndex(definition.Form);
            if (branchIndex >= 0)
                branches[branchIndex].SetSkillLevel(definition.SkillIndex, level);
            return;
        }

        ReaperCommonNode commonNode = definition.CommonNode!.Value;
        int index = (int)commonNode;
        if (index >= 0 && index < commonNodeLevels.Length)
            commonNodeLevels[index] = Math.Clamp(level, 0, ReaperDefinitions.GetCommonNodeMaxLevel(commonNode));
    }

    internal void MarkMigrationComplete()
    {
        LoadedDataVersion = CurrentDataVersion;
    }

    internal void BackfillSoulSealConsumptionFromBranches()
    {
        for (ReaperStage stage = ReaperStage.StageI; stage <= ReaperStage.StageIII; stage++)
        {
            int index = GetSoulSealIndex(stage);
            int consumed = 0;
            foreach (ReaperBranchState branch in branches)
            {
                if (branch.Stage >= stage)
                    consumed++;
            }

            soulSealsIssued[index] = (byte)Math.Max(soulSealsIssued[index], consumed);
            soulSealsSpent[index] = (byte)Math.Max(soulSealsSpent[index], consumed);
        }
        NormalizeSoulSealLedger();
    }

    internal void UnlockDeathMasteryForMigration()
    {
        foreach (ReaperFormId form in ReaperDefinitions.BranchForms)
        {
            SetBranchStage(form, ReaperStage.StageIII);
            for (int skill = 0; skill < ReaperDefinitions.SkillsPerBranch; skill++)
                SetSkillLevel(form, skill, ReaperDefinitions.MaximumSkillLevel);
        }
        TrySetCurrentForm(ReaperFormId.Death);
        MarkMigrationComplete();
    }

    private static int GetBranchIndex(ReaperFormId form)
    {
        return ReaperDefinitions.IsBranchForm(form) ? (int)form - 1 : -1;
    }

    private static int GetSoulSealIndex(ReaperStage stage)
    {
        return stage is >= ReaperStage.StageI and <= ReaperStage.StageIII ? (int)stage - 1 : -1;
    }

    private static void CopyLoadedBytes(byte[] source, Action<int, byte> setter, int count)
    {
        int copyCount = Math.Min(source.Length, count);
        for (int index = 0; index < copyCount; index++)
            setter(index, source[index]);
    }

    private static void LoadClampedArray(byte[] source, byte[] destination, Func<int, int> maximum)
    {
        int copyCount = Math.Min(source.Length, destination.Length);
        for (int index = 0; index < copyCount; index++)
            destination[index] = (byte)Math.Clamp(source[index], 0, maximum(index));
    }

    private static void LoadClampedArray(int[] source, int[] destination, Func<int, int> maximum)
    {
        int copyCount = Math.Min(source.Length, destination.Length);
        for (int index = 0; index < copyCount; index++)
            destination[index] = Math.Clamp(source[index], 0, maximum(index));
    }

    private static void ReadClampedArray(BinaryReader reader, byte[] destination, Func<int, int> maximum)
    {
        byte[] source = reader.ReadBytes(destination.Length);
        LoadClampedArray(source, destination, maximum);
    }

    private void NormalizeSoulSealLedger()
    {
        for (int index = 0; index < SoulSealStageCount; index++)
        {
            soulSealsIssued[index] = (byte)Math.Min((int)soulSealsIssued[index], ReaperDefinitions.MaximumSoulSealsPerStage);
            soulSealsSpent[index] = (byte)Math.Min((int)soulSealsSpent[index], soulSealsIssued[index]);
            int unspent = soulSealsIssued[index] - soulSealsSpent[index];
            soulSealsPending[index] = (byte)Math.Min((int)soulSealsPending[index], unspent);
        }
    }
}