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;
///
/// Server-authoritative mutations for the character progression tree. Every method
/// validates the complete transaction before consuming currency.
///
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();
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();
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 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();
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());
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();
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();
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 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().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().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().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();
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 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);
}
}
}