using SoulHarvest.Common; using SoulHarvest.Items; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using ReLogic.Content; using System; using System.Collections.Generic; using System.Text; using Terraria; using Terraria.Audio; using Terraria.GameContent; using Terraria.GameContent.UI.Elements; using Terraria.GameInput; using Terraria.ID; using Terraria.Localization; using Terraria.ModLoader; using Terraria.UI; namespace SoulHarvest.UI; internal sealed class ReaperTreeNodeModel { public ReaperTreeNodeKind Kind { get; } public ReaperFormId Form { get; } public int Index { get; } public ReaperCommonNode CommonNode { get; } public Vector2 Position { get; } public float Radius { get; } public int CurrentLevel { get; private set; } public int MaximumLevel { get; private set; } = 1; public int ExpectedTarget { get; private set; } public bool Completed { get; private set; } public bool CanRequest { get; private set; } public bool Affordable { get; private set; } public bool Lit => Kind == ReaperTreeNodeKind.Stage ? Completed : CurrentLevel > 0; public string Title { get; private set; } = string.Empty; public string Category { get; private set; } = string.Empty; public string Description { get; private set; } = string.Empty; public string Status { get; private set; } = string.Empty; public string Cost { get; private set; } = string.Empty; private ReaperTreeNodeModel(ReaperTreeNodeKind kind, ReaperFormId form, int index, ReaperCommonNode commonNode, Vector2 position, float radius) { Kind = kind; Form = form; Index = index; CommonNode = commonNode; Position = position; Radius = radius; } public static ReaperTreeNodeModel Base(Vector2 position) => new(ReaperTreeNodeKind.Base, ReaperFormId.Base, 0, default, position, 46f); public static ReaperTreeNodeModel Death(Vector2 position) => new(ReaperTreeNodeKind.Death, ReaperFormId.Death, 0, default, position, 50f); public static ReaperTreeNodeModel Stage(ReaperFormId form, int stage, Vector2 position) => new(ReaperTreeNodeKind.Stage, form, stage, default, position, 31f); public static ReaperTreeNodeModel Skill(ReaperFormId form, int skill, Vector2 position) => new(ReaperTreeNodeKind.Skill, form, skill, default, position, 24f); public static ReaperTreeNodeModel Common(ReaperCommonNode node, Vector2 position) => new(ReaperTreeNodeKind.Common, ReaperFormId.Base, 0, node, position, 23f); public static ReaperTreeNodeModel DeathUpgrade(ReaperCommonNode node, Vector2 position) => new(ReaperTreeNodeKind.DeathUpgrade, ReaperFormId.Death, 0, node, position, 25f); public void Refresh(Player player, ReaperProgressionState progression, int essenceCount, int souls) { CanRequest = false; Affordable = false; Cost = string.Empty; switch (Kind) { case ReaperTreeNodeKind.Base: CurrentLevel = MaximumLevel = 1; Completed = true; Title = ReaperUIData.GetFormName(ReaperFormId.Base); Category = Text("CategoryBase"); Description = ReaperUIData.GetFormSummary(ReaperFormId.Base); Status = Text("BaseAlwaysAvailable"); break; case ReaperTreeNodeKind.Death: RefreshDeath(progression); break; case ReaperTreeNodeKind.Stage: RefreshStage(player, progression, essenceCount); break; case ReaperTreeNodeKind.Skill: RefreshSkill(progression, souls); break; case ReaperTreeNodeKind.Common: case ReaperTreeNodeKind.DeathUpgrade: RefreshCommon(progression, essenceCount, souls); break; } } public void RequestUpgrade() { if (!CanRequest || !Affordable) return; switch (Kind) { case ReaperTreeNodeKind.Stage: SoulHarvest.RequestReaperStageUpgrade(Form, (ReaperStage)ExpectedTarget); break; case ReaperTreeNodeKind.Skill: SoulHarvest.RequestReaperSkillUpgrade(Form, Index, ExpectedTarget); break; case ReaperTreeNodeKind.Common: case ReaperTreeNodeKind.DeathUpgrade: SoulHarvest.RequestReaperCommonUpgrade(CommonNode, ExpectedTarget); break; } } private void RefreshDeath(ReaperProgressionState progression) { CurrentLevel = progression.DeathFormUnlocked ? 1 : 0; MaximumLevel = 1; Completed = progression.DeathFormUnlocked; Title = ReaperUIData.GetFormName(ReaperFormId.Death); Category = Text("CategoryFinal"); Description = $"{ReaperUIData.GetFormSummary(ReaperFormId.Death)}\n{Text("UltimateLabel", ReaperUIData.GetUltimateName(ReaperFormId.Death))}"; Status = Text(progression.DeathFormUnlocked ? "DeathUnlocked" : "DeathLocked"); int completedBranches = 0; int masteredSkills = 0; foreach (ReaperFormId form in ReaperDefinitions.BranchForms) { if (progression.GetStage(form) >= ReaperStage.StageIII) completedBranches++; for (int skill = 0; skill < ReaperDefinitions.SkillsPerBranch; skill++) if (progression.GetSkillLevel(form, skill) >= ReaperDefinitions.MaximumSkillLevel) masteredSkills++; } Cost = Text("DeathMasteryProgress", completedBranches, masteredSkills); } private void RefreshStage(Player player, ReaperProgressionState progression, int essenceCount) { int current = (int)progression.GetStage(Form); // Each stage node is a binary unlock, not one shared three-rank node. CurrentLevel = current >= Index ? 1 : 0; MaximumLevel = 1; ExpectedTarget = Index; Completed = current >= Index; CanRequest = current == Index - 1; Title = $"{ReaperUIData.GetFormName(Form)} ยท {ReaperUIData.GetStageName(Index)}"; Category = Text("CategoryStage"); ReaperFormDefinition definition = ReaperDefinitions.GetForm(Form); Description = Text("StageCombatStats", ReaperUIData.GetFormSummary(Form), definition.GetBaseDamage((ReaperStage)Index), definition.BaseUseTime); if (Index == 3) Description += $"\n{Text("UltimateLabel", ReaperUIData.GetUltimateName(Form))}"; int seals = progression.GetSoulSealAvailable((ReaperStage)Index); int receiptCount = CountInventoryItem(player, ReaperSoulSealCatalog.GetItemType((ReaperStage)Index)); int essenceCost = ReaperDefinitions.GetStageEssenceCost((ReaperStage)Index); bool worldReady = ReaperDefinitions.MeetsWorldGate((ReaperStage)Index); Affordable = Completed || worldReady && seals > 0 && receiptCount > 0 && essenceCount >= essenceCost; if (Completed) { Status = Text("StatusComplete"); Cost = Text("Completed"); } else if (!CanRequest) { Status = Text("StatusLocked"); Cost = Text("StagePrerequisite", Index - 1); } else if (!worldReady) { Status = Text("StatusLocked"); Cost = Language.GetTextValue($"Mods.SoulHarvest.UI.ReaperTree.StageGates.Stage{Index}"); } else if (seals <= 0) { Status = Text("StatusInsufficient"); Cost = Text("NoStageSeal"); } else if (receiptCount <= 0) { Status = Text("StatusInsufficient"); Cost = Text("NoStageSealItem"); } else { Status = Affordable ? Text("StatusReady") : Text("StatusInsufficient"); Cost = Text("StageCost", essenceCost, seals, receiptCount); } } private void RefreshSkill(ReaperProgressionState progression, int souls) { int stage = (int)progression.GetStage(Form); int level = progression.GetSkillLevel(Form, Index); int requiredStage = Index + 1; CurrentLevel = level; MaximumLevel = ReaperDefinitions.MaximumSkillLevel; ExpectedTarget = level + 1; Completed = level >= MaximumLevel; CanRequest = stage >= requiredStage && !Completed; int soulCost = ReaperDefinitions.GetSkillSoulCost(level + 1); Affordable = Completed || CanRequest && soulCost >= 0 && souls >= soulCost; Title = ReaperUIData.GetSkillName(Form, Index); Category = Text("CategorySkill", ReaperUIData.GetFormName(Form)); Description = ReaperUIData.GetSkillDescription(Form, Index); if (stage < requiredStage) { Status = Text("StatusLocked"); Cost = Text("SkillStageLocked", requiredStage); } else if (Completed) { Status = Text("StatusComplete"); Cost = Text("CompletedLevel", level, MaximumLevel); } else { Status = Affordable ? Text("StatusReady") : Text("StatusInsufficient"); Cost = Text("SkillLevel", level, MaximumLevel, soulCost); } } private void RefreshCommon(ReaperProgressionState progression, int essenceCount, int souls) { int level = progression.GetCommonNodeLevel(CommonNode); int maximum = ReaperDefinitions.GetCommonNodeMaxLevel(CommonNode); bool unlimited = ReaperDefinitions.IsUnlimitedCommonNode(CommonNode); int targetLevel = level + 1; ReaperStage requiredStage = ReaperDefinitions.GetCommonNodeRequiredStage(CommonNode, targetLevel); ReaperProgressionCost? price = ReaperDefinitions.GetCommonNodeCost(CommonNode, targetLevel); CurrentLevel = level; MaximumLevel = maximum; ExpectedTarget = targetLevel; Completed = !unlimited && level >= maximum; CanRequest = !Completed && progression.HasAnyBranchAtLeast(requiredStage); if (ReaperDefinitions.IsDeathNode(CommonNode)) { CanRequest &= progression.DeathFormUnlocked; if (CommonNode == ReaperCommonNode.DeathRiftWidth) { CanRequest &= progression.GetCommonNodeLevel( ReaperCommonNode.DeathSpaceBreak) > 0; } } Affordable = Completed || CanRequest && price is ReaperProgressionCost cost && (cost.Currency == ReaperProgressionCurrency.Souls ? souls >= cost.Amount : essenceCount >= cost.Amount); Title = ReaperUIData.GetCommonNodeName(CommonNode); Category = Text(ReaperDefinitions.IsDeathNode(CommonNode) ? "CategoryDeathUpgrade" : "CategoryCommon"); Description = ReaperUIData.GetCommonNodeDescription(CommonNode); if (Completed) { Status = Text("StatusComplete"); Cost = Text("CompletedLevel", level, maximum); } else if (!CanRequest) { Status = Text("StatusLocked"); Cost = ReaperDefinitions.IsDeathNode(CommonNode) ? Text("DeathUpgradeLocked") : Text("CommonStageLocked", (int)requiredStage); } else if (price is ReaperProgressionCost commonCost) { Status = Affordable ? Text("StatusReady") : Text("StatusInsufficient"); Cost = unlimited ? Text("CommonUnlimitedLevel", level, FormatCost(commonCost)) : Text("CommonLevel", level, maximum, FormatCost(commonCost)); } else { Status = Text("StatusLocked"); Cost = Text("Unavailable"); } } private static int CountInventoryItem(Player player, int itemType) { if (itemType <= ItemID.None) return 0; int count = 0; int slots = Math.Min(58, player.inventory.Length); for (int slot = 0; slot < slots; slot++) { Item item = player.inventory[slot]; if (item.type == itemType && item.stack > 0) count += item.stack; } return count; } private static string FormatCost(ReaperProgressionCost cost) => Text(cost.Currency == ReaperProgressionCurrency.Souls ? "CostSouls" : "CostEssence", cost.Amount); private static string Text(string key, params object[] args) => Language.GetTextValue($"Mods.SoulHarvest.UI.ReaperTree.{key}", args); }