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

DeathMod

【Terraria】DeathMod (now aka Soul Harvest)

公开
关注 0 Fork 0 Star 0
UTF-8
using SoulHarvest.Items;
using System;
using System.Collections.Generic;
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;

namespace SoulHarvest.Common;


/// <summary>
/// Issues character-bound seal entitlements and mirrors them with inventory items.
/// The ledger is authoritative; the item is only a visible receipt and cannot be
/// copied or traded to create progression.
/// </summary>
public static class ReaperProgressionSoulSealService
{
    private const int InventorySlotCount = 58;

    public static bool TryGrant(Player player, ReaperStage stage)
    {
        if (Main.netMode == NetmodeID.MultiplayerClient
            || player is null
            || !player.active
            || !player.GetModPlayer<MyPlayer>().ServerCharacterStateReady
            || stage is < ReaperStage.StageI or > ReaperStage.StageIII)
        {
            return false;
        }

        MyPlayer modPlayer = player.GetModPlayer<MyPlayer>();
        ReaperProgressionState progression = modPlayer.ReaperProgression;
        if (!progression.CanIssueSoulSeal(stage))
            return false;

        bool hasInventorySpace = TryFindInsertionSlot(player, stage, out int slot);
        if (!progression.TryRecordSoulSealIssued(stage, pending: !hasInventorySpace))
            return false;

        if (hasInventorySpace)
        {
            InsertToken(player, stage, slot);
        }

        SoulHarvest.SendReaperProgressionTransaction(
            player,
            hasInventorySpace ? new[] { slot } : Array.Empty<int>());
        return true;
    }

    /// <summary>
    /// Delivers as many pending seals as currently fit. Call from the owning
    /// ModPlayer's server-side update loop; it performs no work when the ledger is
    /// empty or the inventory is still full.
    /// </summary>
    public static bool TryDeliverPending(Player player)
    {
        if (Main.netMode == NetmodeID.MultiplayerClient
            || player is null
            || !player.active
            || !player.GetModPlayer<MyPlayer>().ServerCharacterStateReady)
            return false;

        ReaperProgressionState progression = player.GetModPlayer<MyPlayer>().ReaperProgression;
        bool hasOutstandingSeal = false;
        for (ReaperStage stage = ReaperStage.StageI; stage <= ReaperStage.StageIII; stage++)
        {
            if (progression.GetSoulSealIssued(stage) > progression.GetSoulSealSpent(stage))
            {
                hasOutstandingSeal = true;
                break;
            }
        }
        if (!hasOutstandingSeal)
            return false;

        bool ledgerChanged = ReconcileMissingReceipts(player, progression);
        bool deliveredAny = false;
        HashSet<int> changedSlots = [];
        for (ReaperStage stage = ReaperStage.StageI; stage <= ReaperStage.StageIII; stage++)
        {
            while (progression.GetSoulSealPending(stage) > 0
                && TryFindInsertionSlot(player, stage, out int slot))
            {
                InsertToken(player, stage, slot);
                progression.TryMarkPendingSoulSealDelivered(stage);
                changedSlots.Add(slot);
                deliveredAny = true;
            }
        }

        if (deliveredAny || ledgerChanged)
            SoulHarvest.SendReaperProgressionTransaction(player, changedSlots);
        return deliveredAny;
    }

    private static bool ReconcileMissingReceipts(Player player, ReaperProgressionState progression)
    {
        bool changed = false;
        for (ReaperStage stage = ReaperStage.StageI; stage <= ReaperStage.StageIII; stage++)
        {
            int outstanding = progression.GetSoulSealIssued(stage) - progression.GetSoulSealSpent(stage);
            if (outstanding <= progression.GetSoulSealPending(stage))
                continue;

            int itemType = ReaperSoulSealCatalog.GetItemType(stage);
            int physicalCount = CountReceipts(player.inventory, Math.Min(InventorySlotCount, player.inventory.Length), itemType)
                + CountReceipts(player.bank.item, player.bank.item.Length, itemType)
                + CountReceipts(player.bank2.item, player.bank2.item.Length, itemType)
                + CountReceipts(player.bank3.item, player.bank3.item.Length, itemType)
                + CountReceipts(player.bank4.item, player.bank4.item.Length, itemType);

            int missing = Math.Max(0, outstanding - progression.GetSoulSealPending(stage) - physicalCount);
            changed |= progression.MarkMissingSoulSealsPending(stage, missing) > 0;
        }
        return changed;
    }

    private static int CountReceipts(Item[] items, int count, int itemType)
    {
        int total = 0;
        for (int slot = 0; slot < count; slot++)
        {
            Item item = items[slot];
            if (item.type == itemType && item.stack > 0)
                total += item.stack;
        }
        return total;
    }

    private static bool TryFindInsertionSlot(Player player, ReaperStage stage, out int slot)
    {
        slot = -1;
        int itemType = ReaperSoulSealCatalog.GetItemType(stage);
        if (itemType <= ItemID.None)
            return false;

        int emptySlot = -1;
        int slotCount = Math.Min(InventorySlotCount, player.inventory.Length);
        for (int index = 0; index < slotCount; index++)
        {
            Item item = player.inventory[index];
            if (item.type == itemType && item.stack > 0 && item.stack < item.maxStack)
            {
                slot = index;
                return true;
            }
            if (emptySlot < 0 && item.IsAir)
                emptySlot = index;
        }

        slot = emptySlot;
        return slot >= 0;
    }

    private static void InsertToken(Player player, ReaperStage stage, int slot)
    {
        int itemType = ReaperSoulSealCatalog.GetItemType(stage);
        Item item = player.inventory[slot];
        if (item.type == itemType && item.stack > 0)
        {
            item.stack++;
            return;
        }

        item.SetDefaults(itemType);
        item.stack = 1;
    }

}