using System; using System.Collections.Generic; using Terraria; namespace SoulHarvest.Common; public static class ReaperDefinitions { public const int SkillsPerBranch = 3; public const int MaximumSkillLevel = 3; public const int MaximumSoulSealsPerStage = 6; public const float AttackSpeedBonusPerLevel = 0.10f; public const int AssemblyFragmentCount = 16; public const int AssemblyPortalExpandFrames = 15; public const int AssemblyFragmentTravelFrames = 10; public const int AssemblyPortalCloseFrames = 36; public const int AssemblyFragmentReturnFrames = 28; public const int AssemblyCancelFrames = AssemblyFragmentReturnFrames + AssemblyPortalCloseFrames; public const int AssemblyMaximumGapFrames = 8; public const int AssemblyMaximumLevelFrames = 30; public const int AssemblyMaximumLevelPortalCloseFrames = 5; private static readonly ReaperFormId[] BranchFormArray = [ ReaperFormId.Bone, ReaperFormId.Blood, ReaperFormId.Infernal, ReaperFormId.Frost, ReaperFormId.Soul, ReaperFormId.Void ]; private static readonly IReadOnlyList ReadOnlyBranchForms = Array.AsReadOnly(BranchFormArray); private static readonly ReaperFormDefinition[] FormDefinitions = [ new(ReaperFormId.Base, 14, 14, 14, 25), new(ReaperFormId.Bone, 40, 85, 155, 24), new(ReaperFormId.Blood, 38, 81, 148, 22), new(ReaperFormId.Infernal, 36, 77, 140, 20), new(ReaperFormId.Frost, 40, 85, 155, 22), new(ReaperFormId.Soul, 35, 75, 136, 22), new(ReaperFormId.Void, 48, 102, 186, 27), new(ReaperFormId.Death, 214, 214, 214, 24) ]; private static readonly ReaperNodeDefinition[] NodeDefinitions = [ new(ReaperNodeId.BoneFractureEcho, ReaperFormId.Bone, 0, null, ReaperStage.StageI), new(ReaperNodeId.BoneSpectralRestraint, ReaperFormId.Bone, 1, null, ReaperStage.StageII), new(ReaperNodeId.BoneDeadChorus, ReaperFormId.Bone, 2, null, ReaperStage.StageIII), new(ReaperNodeId.BloodDrink, ReaperFormId.Blood, 0, null, ReaperStage.StageI), new(ReaperNodeId.BloodFrenzy, ReaperFormId.Blood, 1, null, ReaperStage.StageII), new(ReaperNodeId.BloodCoagulation, ReaperFormId.Blood, 2, null, ReaperStage.StageIII), new(ReaperNodeId.InfernalWildfireStep, ReaperFormId.Infernal, 0, null, ReaperStage.StageI), new(ReaperNodeId.InfernalSearingBone, ReaperFormId.Infernal, 1, null, ReaperStage.StageII), new(ReaperNodeId.InfernalOverburnCircuit, ReaperFormId.Infernal, 2, null, ReaperStage.StageIII), new(ReaperNodeId.FrostEtchedMark, ReaperFormId.Frost, 0, null, ReaperStage.StageI), new(ReaperNodeId.FrostRefractedSoul, ReaperFormId.Frost, 1, null, ReaperStage.StageII), new(ReaperNodeId.FrostZeroExecution, ReaperFormId.Frost, 2, null, ReaperStage.StageIII), new(ReaperNodeId.SoulEchoChorus, ReaperFormId.Soul, 0, null, ReaperStage.StageI), new(ReaperNodeId.SoulFamiliarPact, ReaperFormId.Soul, 1, null, ReaperStage.StageII), new(ReaperNodeId.SoulFatedInscription, ReaperFormId.Soul, 2, null, ReaperStage.StageIII), new(ReaperNodeId.VoidBoundaryEdge, ReaperFormId.Void, 0, null, ReaperStage.StageI), new(ReaperNodeId.VoidDisplacedAftercut, ReaperFormId.Void, 1, null, ReaperStage.StageII), new(ReaperNodeId.VoidTerminalCoordinate, ReaperFormId.Void, 2, null, ReaperStage.StageIII), new(ReaperNodeId.CommonDamage, ReaperFormId.Base, -1, ReaperCommonNode.Damage, ReaperStage.StageI), new(ReaperNodeId.CommonAttackSpeed, ReaperFormId.Base, -1, ReaperCommonNode.AttackSpeed, ReaperStage.StageI), new(ReaperNodeId.CommonCrit, ReaperFormId.Base, -1, ReaperCommonNode.Crit, ReaperStage.StageI), new(ReaperNodeId.CommonArmorPenetration, ReaperFormId.Base, -1, ReaperCommonNode.ArmorPenetration, ReaperStage.StageI), new(ReaperNodeId.CommonSpecialCooldown, ReaperFormId.Base, -1, ReaperCommonNode.SpecialCooldown, ReaperStage.StageI), new(ReaperNodeId.CommonEnergyGain, ReaperFormId.Base, -1, ReaperCommonNode.EnergyGain, ReaperStage.StageI), new(ReaperNodeId.CommonAssemblySpeed, ReaperFormId.Base, -1, ReaperCommonNode.AssemblySpeed, ReaperStage.StageI), new(ReaperNodeId.DeathDamage, ReaperFormId.Death, -1, ReaperCommonNode.DeathDamage, ReaperStage.StageIII), new(ReaperNodeId.DeathInvocationSlashes, ReaperFormId.Death, -1, ReaperCommonNode.DeathInvocationSlashes, ReaperStage.StageIII), new(ReaperNodeId.DeathTempoCap, ReaperFormId.Death, -1, ReaperCommonNode.DeathTempoCap, ReaperStage.StageIII), new(ReaperNodeId.DeathRange, ReaperFormId.Death, -1, ReaperCommonNode.DeathRange, ReaperStage.StageIII), new(ReaperNodeId.DeathSpaceBreak, ReaperFormId.Death, -1, ReaperCommonNode.DeathSpaceBreak, ReaperStage.StageIII), new(ReaperNodeId.DeathRiftWidth, ReaperFormId.Death, -1, ReaperCommonNode.DeathRiftWidth, ReaperStage.StageIII) ]; private static readonly IReadOnlyList ReadOnlyNodeDefinitions = Array.AsReadOnly(NodeDefinitions); public static IReadOnlyList BranchForms => ReadOnlyBranchForms; public static IReadOnlyList Nodes => ReadOnlyNodeDefinitions; public static bool IsBranchForm(ReaperFormId form) { return form is >= ReaperFormId.Bone and <= ReaperFormId.Void; } public static ReaperFormDefinition GetForm(ReaperFormId form) { int index = (int)form; return index >= 0 && index < FormDefinitions.Length ? FormDefinitions[index] : FormDefinitions[(int)ReaperFormId.Base]; } public static bool TryGetNode(ReaperNodeId id, out ReaperNodeDefinition definition) { foreach (ReaperNodeDefinition candidate in NodeDefinitions) { if (candidate.Id == id) { definition = candidate; return true; } } definition = null!; return false; } public static bool TryGetSkillNode(ReaperFormId form, int skillIndex, out ReaperNodeId id) { foreach (ReaperNodeDefinition definition in NodeDefinitions) { if (!definition.IsCommon && definition.Form == form && definition.SkillIndex == skillIndex) { id = definition.Id; return true; } } id = default; return false; } public static bool TryGetCommonNode(ReaperCommonNode commonNode, out ReaperNodeId id) { foreach (ReaperNodeDefinition definition in NodeDefinitions) { if (definition.CommonNode == commonNode) { id = definition.Id; return true; } } id = default; return false; } public static int GetStageEssenceCost(ReaperStage stage) { return stage switch { ReaperStage.StageI => 1, ReaperStage.StageII => 2, ReaperStage.StageIII => 3, _ => -1 }; } public static int GetSkillSoulCost(int targetLevel) { return targetLevel switch { 2 => 50, 3 => 100, _ => -1 }; } public static int GetCommonNodeMaxLevel(ReaperCommonNode node) { return node switch { ReaperCommonNode.Damage => 10, ReaperCommonNode.DeathDamage => int.MaxValue, ReaperCommonNode.DeathInvocationSlashes => 5, ReaperCommonNode.DeathTempoCap => 5, ReaperCommonNode.DeathRange => 5, ReaperCommonNode.DeathSpaceBreak => 1, ReaperCommonNode.DeathRiftWidth => 5, _ => IsCommonNode(node) ? 5 : 0 }; } public static float GetAttackSpeedMultiplier(int level) => 1f + Math.Max(0, level) * AttackSpeedBonusPerLevel; /// /// Converts the common assembly upgrade into one shared timeline. Levels /// below the cap retain the readable head-to-tail sequence; the final level /// is a qualitative payoff where all rifts and fragments converge together /// in roughly half a second. /// public static int GetAssemblyFrames(int level) { int maximum = GetCommonNodeMaxLevel(ReaperCommonNode.AssemblySpeed); int clampedLevel = Math.Clamp(level, 0, maximum); if (clampedLevel >= maximum) return AssemblyMaximumLevelFrames; float progress = clampedLevel / (float)maximum; int gap = (int)Math.Round(AssemblyMaximumGapFrames * (1f - progress)); int portalInterval = AssemblyPortalExpandFrames + gap; return AssemblyFragmentCount * portalInterval + AssemblyFragmentTravelFrames + AssemblyPortalCloseFrames; } public static bool UsesSimultaneousAssembly(int totalFrames) => totalFrames == AssemblyMaximumLevelFrames; public static int GetAssemblyPortalCloseFrames(int totalFrames) => UsesSimultaneousAssembly(totalFrames) ? AssemblyMaximumLevelPortalCloseFrames : AssemblyPortalCloseFrames; public static bool IsUnlimitedCommonNode(ReaperCommonNode node) => node == ReaperCommonNode.DeathDamage; public static bool IsDeathNode(ReaperCommonNode node) => node is >= ReaperCommonNode.DeathDamage and <= ReaperCommonNode.DeathRiftWidth; public static int GetDeathInvocationSlashCount(int level) => 1 + Math.Clamp(level, 0, GetCommonNodeMaxLevel(ReaperCommonNode.DeathInvocationSlashes)); public static float GetDeathTempoCap(int level) => 1.5f + Math.Clamp(level, 0, GetCommonNodeMaxLevel(ReaperCommonNode.DeathTempoCap)) * 0.3f; public static float GetDeathPrimaryTempoCap(int level) => 1.5f + Math.Clamp(level, 0, GetCommonNodeMaxLevel(ReaperCommonNode.DeathTempoCap)) * 0.7f; public static float GetDeathRangeMultiplier(int level) => 0.5f + Math.Clamp(level, 0, GetCommonNodeMaxLevel(ReaperCommonNode.DeathRange)) * 0.10f; public static float GetDeathRiftWidth(int level) => 60f + Math.Clamp(level, 0, GetCommonNodeMaxLevel(ReaperCommonNode.DeathRiftWidth)) * 12f; public static ReaperProgressionCost? GetCommonNodeCost(ReaperCommonNode node, int targetLevel) { if (!IsCommonNode(node) || targetLevel < 1 || targetLevel > GetCommonNodeMaxLevel(node)) return null; if (node is ReaperCommonNode.Damage or ReaperCommonNode.DeathDamage) { return targetLevel switch { 1 => ReaperProgressionCost.Souls(50), 2 => ReaperProgressionCost.Souls(75), 3 => ReaperProgressionCost.Souls(100), 4 => ReaperProgressionCost.Souls(125), 5 => ReaperProgressionCost.Souls(150), 6 or 7 => ReaperProgressionCost.Essence(1), 8 or 9 => ReaperProgressionCost.Essence(2), 10 => ReaperProgressionCost.Essence(3), _ when node == ReaperCommonNode.DeathDamage => ReaperProgressionCost.Essence(Math.Min(999, 3 + (targetLevel - 11) / 5)), _ => null }; } if (IsDeathNode(node)) { return targetLevel switch { 1 => ReaperProgressionCost.Essence(3), 2 => ReaperProgressionCost.Essence(5), 3 => ReaperProgressionCost.Essence(8), 4 => ReaperProgressionCost.Essence(12), 5 => ReaperProgressionCost.Essence(18), _ => null }; } return targetLevel switch { 1 => ReaperProgressionCost.Souls(50), 2 => ReaperProgressionCost.Souls(100), 3 => ReaperProgressionCost.Essence(1), 4 => ReaperProgressionCost.Essence(2), 5 => ReaperProgressionCost.Essence(3), _ => null }; } public static ReaperStage GetCommonNodeRequiredStage(ReaperCommonNode node, int targetLevel) { if (!IsCommonNode(node) || targetLevel <= 0) return ReaperStage.Locked; if (IsDeathNode(node)) return ReaperStage.StageIII; if (node == ReaperCommonNode.Damage) { if (targetLevel <= 3) return ReaperStage.Locked; if (targetLevel <= 7) return ReaperStage.StageII; return ReaperStage.StageIII; } if (targetLevel <= 2) return ReaperStage.Locked; if (targetLevel <= 4) return ReaperStage.StageII; return ReaperStage.StageIII; } public static bool MeetsWorldGate(ReaperStage stage) { return stage switch { ReaperStage.StageI => NPC.downedBoss3, ReaperStage.StageII => Main.hardMode, ReaperStage.StageIII => NPC.downedMechBoss1 && NPC.downedMechBoss2 && NPC.downedMechBoss3, _ => false }; } public static bool IsCommonNode(ReaperCommonNode node) { return node is >= ReaperCommonNode.Damage and <= ReaperCommonNode.AssemblySpeed; } }