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 SoulHarvest.Tiles;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;

namespace SoulHarvest.Common;

/// <summary>
/// Server-authoritative mutations for the character progression tree. Every method
/// validates the complete transaction before consuming currency.
/// </summary>
public static class ReaperProgressionService
{
    private const int InventorySlotCount = 58;
    private const float MaximumAltarDistance = 12f * 16f;

    public static bool TryUnlockNextStage(
        Player player,
        ReaperFormId form,
        ReaperStage expectedTargetStage,
        out ReaperProgressionFailure failure)
    {
        failure = ValidateAuthority(player);
        if (failure != ReaperProgressionFailure.None)
            return false;
        if (!IsNearDeathAltar(player))
        {
            failure = ReaperProgressionFailure.NotNearAltar;
            return false;
        }
        if (!ReaperDefinitions.IsBranchForm(form))
        {
            failure = ReaperProgressionFailure.InvalidForm;
            return false;
        }

        MyPlayer modPlayer = player.GetModPlayer<MyPlayer>();
        ReaperProgressionState progression = modPlayer.ReaperProgression;
        ReaperStage currentStage = progression.GetStage(form);
        if (currentStage >= ReaperStage.StageIII)
        {
            failure = ReaperProgressionFailure.Maxed;
            return false;
        }

        ReaperStage targetStage = (ReaperStage)((int)currentStage + 1);
        if (expectedTargetStage != targetStage)
        {
            failure = ReaperProgressionFailure.InvalidStage;
            return false;
        }
        int essenceCost = ReaperDefinitions.GetStageEssenceCost(targetStage);
        if (essenceCost < 0)
        {
            failure = ReaperProgressionFailure.InvalidStage;
            return false;
        }
        if (!ReaperDefinitions.MeetsWorldGate(targetStage))
        {
            failure = ReaperProgressionFailure.WorldProgressionLocked;
            return false;
        }
        if (progression.GetSoulSealAvailable(targetStage) <= 0)
        {
            failure = ReaperProgressionFailure.NotEnoughSoulSeals;
            return false;
        }

        int soulSealType = ReaperSoulSealCatalog.GetItemType(targetStage);
        if (CountInventoryItem(player, soulSealType) < 1)
        {
            failure = ReaperProgressionFailure.NotEnoughSoulSeals;
            return false;
        }

        int essenceType = ModContent.ItemType<Soul>();
        if (CountInventoryItem(player, essenceType) < essenceCost)
        {
            failure = ReaperProgressionFailure.NotEnoughEssence;
            return false;
        }

        // All three operations are guaranteed by the checks above and cannot race on
        // Terraria's world thread. Currency is removed only after state validation.
        if (!progression.TryUnlockNextStage(form) || !progression.TryConsumeSoulSeal(targetStage))
        {
            failure = ReaperProgressionFailure.InvalidStage;
            return false;
        }

        HashSet<int> changedSlots = [];
        ConsumeInventoryItem(player, essenceType, essenceCost, changedSlots);
        ConsumeInventoryItem(player, soulSealType, 1, changedSlots);
        SoulHarvest.SendReaperProgressionTransaction(player, changedSlots);
        return true;
    }

    public static bool TryUpgradeSkill(
        Player player,
        ReaperFormId form,
        int skillIndex,
        int expectedTargetLevel,
        out ReaperProgressionFailure failure)
    {
        failure = ValidateAuthority(player);
        if (failure != ReaperProgressionFailure.None)
            return false;
        if (!IsNearDeathAltar(player))
        {
            failure = ReaperProgressionFailure.NotNearAltar;
            return false;
        }
        if (!ReaperDefinitions.IsBranchForm(form))
        {
            failure = ReaperProgressionFailure.InvalidForm;
            return false;
        }
        if (skillIndex < 0 || skillIndex >= ReaperDefinitions.SkillsPerBranch)
        {
            failure = ReaperProgressionFailure.InvalidStage;
            return false;
        }

        MyPlayer modPlayer = player.GetModPlayer<MyPlayer>();
        ReaperProgressionState progression = modPlayer.ReaperProgression;
        int currentLevel = progression.GetSkillLevel(form, skillIndex);
        if (skillIndex >= (int)progression.GetStage(form) || currentLevel <= 0)
        {
            failure = ReaperProgressionFailure.SkillLocked;
            return false;
        }
        if (currentLevel >= ReaperDefinitions.MaximumSkillLevel)
        {
            failure = ReaperProgressionFailure.Maxed;
            return false;
        }

        if (expectedTargetLevel != currentLevel + 1)
        {
            failure = ReaperProgressionFailure.InvalidStage;
            return false;
        }

        int soulCost = ReaperDefinitions.GetSkillSoulCost(currentLevel + 1);
        if (soulCost < 0 || modPlayer.Souls < soulCost)
        {
            failure = ReaperProgressionFailure.NotEnoughSouls;
            return false;
        }

        if (!progression.TryUpgradeSkill(form, skillIndex))
        {
            // This cannot occur after validation on the world thread. It is kept as a
            // defensive failure rather than allowing a duplicated upgrade.
            failure = ReaperProgressionFailure.InvalidStage;
            return false;
        }
        // The preflight makes this spend infallible on Terraria's world thread.
        // Its balance and the mutated tree are sent together below.
        if (!modPlayer.TrySpendSouls(soulCost, sync: false))
        {
            failure = ReaperProgressionFailure.NotEnoughSouls;
            return false;
        }

        SoulHarvest.SendReaperProgressionTransaction(player, Array.Empty<int>());
        return true;
    }

    public static bool TryUpgradeCommonNode(
        Player player,
        ReaperCommonNode node,
        int expectedTargetLevel,
        out ReaperProgressionFailure failure)
    {
        failure = ValidateAuthority(player);
        if (failure != ReaperProgressionFailure.None)
            return false;
        if (!IsNearDeathAltar(player))
        {
            failure = ReaperProgressionFailure.NotNearAltar;
            return false;
        }
        if (!ReaperDefinitions.IsCommonNode(node))
        {
            failure = ReaperProgressionFailure.InvalidStage;
            return false;
        }

        MyPlayer modPlayer = player.GetModPlayer<MyPlayer>();
        ReaperProgressionState progression = modPlayer.ReaperProgression;
        if (ReaperDefinitions.IsDeathNode(node) && !progression.DeathFormUnlocked)
        {
            failure = ReaperProgressionFailure.DeathFormLocked;
            return false;
        }
        if (node == ReaperCommonNode.DeathRiftWidth
            && progression.GetCommonNodeLevel(ReaperCommonNode.DeathSpaceBreak) <= 0)
        {
            failure = ReaperProgressionFailure.SkillLocked;
            return false;
        }
        if (progression.GetCommonNodeLevel(node) == int.MaxValue)
        {
            failure = ReaperProgressionFailure.Maxed;
            return false;
        }
        int targetLevel = progression.GetCommonNodeLevel(node) + 1;
        if (targetLevel > ReaperDefinitions.GetCommonNodeMaxLevel(node))
        {
            failure = ReaperProgressionFailure.Maxed;
            return false;
        }

        if (expectedTargetLevel != targetLevel)
        {
            failure = ReaperProgressionFailure.InvalidStage;
            return false;
        }

        ReaperStage requiredStage = ReaperDefinitions.GetCommonNodeRequiredStage(node, targetLevel);
        if (!progression.HasAnyBranchAtLeast(requiredStage))
        {
            failure = ReaperProgressionFailure.SkillLocked;
            return false;
        }

        ReaperProgressionCost? nullableCost = ReaperDefinitions.GetCommonNodeCost(node, targetLevel);
        if (nullableCost is not ReaperProgressionCost cost)
        {
            failure = ReaperProgressionFailure.InvalidStage;
            return false;
        }

        int essenceType = ModContent.ItemType<Soul>();
        if (cost.Currency == ReaperProgressionCurrency.Souls && modPlayer.Souls < cost.Amount)
        {
            failure = ReaperProgressionFailure.NotEnoughSouls;
            return false;
        }
        if (cost.Currency == ReaperProgressionCurrency.SoulEssence
            && CountInventoryItem(player, essenceType) < cost.Amount)
        {
            failure = ReaperProgressionFailure.NotEnoughEssence;
            return false;
        }

        if (!progression.TryUpgradeCommonNode(node))
        {
            failure = ReaperProgressionFailure.InvalidStage;
            return false;
        }

        HashSet<int> changedSlots = [];
        if (cost.Currency == ReaperProgressionCurrency.Souls
            && !modPlayer.TrySpendSouls(cost.Amount, sync: false))
        {
            failure = ReaperProgressionFailure.NotEnoughSouls;
            return false;
        }

        if (cost.Currency == ReaperProgressionCurrency.SoulEssence)
            ConsumeInventoryItem(player, essenceType, cost.Amount, changedSlots);

        SoulHarvest.SendReaperProgressionTransaction(player, changedSlots);
        return true;
    }

    public static bool TrySwitchForm(Player player, ReaperFormId form, out ReaperProgressionFailure failure)
    {
        failure = ValidateAuthority(player);
        if (failure != ReaperProgressionFailure.None)
            return false;

        ReaperProgressionState progression = player.GetModPlayer<MyPlayer>().ReaperProgression;
        if (form == ReaperFormId.Death && !progression.DeathFormUnlocked)
        {
            failure = ReaperProgressionFailure.DeathFormLocked;
            return false;
        }
        if (form != ReaperFormId.Base
            && form != ReaperFormId.Death
            && (!ReaperDefinitions.IsBranchForm(form) || progression.GetStage(form) == ReaperStage.Locked))
        {
            failure = ReaperProgressionFailure.InvalidForm;
            return false;
        }
        if (!player.GetModPlayer<MyPlayer>().TrySwitchOrQueueReaperForm(form))
        {
            failure = ReaperProgressionFailure.InvalidForm;
            return false;
        }

        return true;
    }

    private static ReaperProgressionFailure ValidateAuthority(Player player)
    {
        if (Main.netMode == NetmodeID.MultiplayerClient)
            return ReaperProgressionFailure.ClientAuthority;
        if (player is null || !player.active)
            return ReaperProgressionFailure.InvalidForm;
        if (!player.GetModPlayer<MyPlayer>().ServerCharacterStateReady)
            return ReaperProgressionFailure.ClientAuthority;
        return ReaperProgressionFailure.None;
    }

    private static int CountInventoryItem(Player player, int itemType)
    {
        if (itemType <= ItemID.None)
            return 0;

        int count = 0;
        int slotCount = Math.Min(InventorySlotCount, player.inventory.Length);
        for (int slot = 0; slot < slotCount; slot++)
        {
            Item item = player.inventory[slot];
            if (item.type == itemType && item.stack > 0)
                count = (int)Math.Min(int.MaxValue, (long)count + item.stack);
        }
        return count;
    }

    private static bool IsNearDeathAltar(Player player)
    {
        Point center = player.Center.ToTileCoordinates();
        int tileRadius = (int)Math.Ceiling(MaximumAltarDistance / 16f);
        int altarType = ModContent.TileType<DeathAltarTile>();

        for (int x = center.X - tileRadius; x <= center.X + tileRadius; x++)
        {
            for (int y = center.Y - tileRadius; y <= center.Y + tileRadius; y++)
            {
                if (!WorldGen.InWorld(x, y, 1))
                    continue;

                Tile tile = Framing.GetTileSafely(x, y);
                if (!tile.HasTile || tile.TileType != altarType)
                    continue;

                Vector2 altarPosition = new((x + 0.5f) * 16f, (y + 0.5f) * 16f);
                if (Vector2.DistanceSquared(player.Center, altarPosition)
                    <= MaximumAltarDistance * MaximumAltarDistance)
                {
                    return true;
                }
            }
        }

        return false;
    }

    private static void ConsumeInventoryItem(Player player, int itemType, int amount, HashSet<int> changedSlots)
    {
        if (itemType <= ItemID.None || amount <= 0)
            return;

        int slotCount = Math.Min(InventorySlotCount, player.inventory.Length);
        for (int slot = 0; slot < slotCount && amount > 0; slot++)
        {
            Item item = player.inventory[slot];
            if (item.type != itemType || item.stack <= 0)
                continue;

            int consumed = Math.Min(amount, item.stack);
            item.stack -= consumed;
            amount -= consumed;
            if (item.stack <= 0)
                item.TurnToAir();
            changedSlots.Add(slot);
        }
    }

}