using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using System.Threading.Tasks; using TMPro; using UnityEngine; using UnityEngine.UI; public class SkillTreeManager : MonoBehaviour { public static SkillTreeManager Instance { get; private set; } [Header("UI Components")] [SerializeField] private StatusScreenHandler statusComp; [SerializeField] private TextMeshProUGUI skillPointsLeft; [SerializeField] private GameObject choicePrefab; [SerializeField] private GameObject objectToInitialize; // References the background object that holds the rest of the components for the skill tree screen [SerializeField] private GameObject choiceObject; [SerializeField] private Button nextButton; [SerializeField] private Button prevButton; [SerializeField] private Transform contentFieldTransform; [SerializeField] private Transform choiceFieldTransform; private GameObject objectToSwitchBackOn; private List<GameObject> choiceObjectsInstanced; private Entity entityReference; private List<GameObject> skillTreeObjects; private Dictionary<string, Guid> entitySkillListsInit; private GameObject activeSkillTreeObject; private int index; private readonly Color DisabledColor = new Color(.65f, .65f, .65f, 1f); private void Awake() { Instance = this; entitySkillListsInit = new Dictionary<string, Guid>(); } public Task Initialize(List<GameObject> skillTrees, Entity entityAffected, GameObject objectToTurnBackOn) { objectToInitialize.SetActive(true); GetComponent<Image>().enabled = true; entityReference = entityAffected; skillTreeObjects = new List<GameObject>(skillTrees); index = 0; objectToSwitchBackOn = objectToTurnBackOn; IndicateSkillPointsRemaining(); // If only one skill tree is available then deactivate the buttons if (skillTrees.Count is 1) { nextButton.interactable = false; nextButton.GetComponent<Image>().color = DisabledColor; prevButton.interactable = false; prevButton.GetComponent<Image>().color = DisabledColor; } // If at least two are available then activate the buttons else { nextButton.interactable = true; nextButton.GetComponent<Image>().color = Color.white; prevButton.interactable = true; prevButton.GetComponent<Image>().color = Color.white; } StartDefaultSkillTree(); return Task.CompletedTask; } /// <summary> /// Determines if any skill points are remaining and if so determines the layout of the text object /// </summary> private void IndicateSkillPointsRemaining() { if (entityReference.HasSkillPoints()) { skillPointsLeft.gameObject.SetActive(true); if (entityReference.GetSkillPoints() is 1) skillPointsLeft.text = $"{entityReference.GetSkillPoints()} Skill Point Available"; else skillPointsLeft.text = $"{entityReference.GetSkillPoints()} Skill Points Available"; } else skillPointsLeft.gameObject.SetActive(false); } /// <summary> /// Starts on the first tree in the list, the default /// </summary> private void StartDefaultSkillTree() { activeSkillTreeObject = Instantiate(skillTreeObjects.FirstOrDefault(), contentFieldTransform); CheckForExistingData(); } /// <summary> /// Checks for any existing data regarding the active entity skill set /// </summary> private void CheckForExistingData() { GraphicalSkillTree graphicalComp = activeSkillTreeObject.GetComponent<GraphicalSkillTree>(); if (entitySkillListsInit.ContainsKey(activeSkillTreeObject.name)) { string nameRef = graphicalComp.GetEntityNameReference(); Guid id = entitySkillListsInit[activeSkillTreeObject.name]; graphicalComp.InjectExistingData(Globals.SearchForEntitySkillData(nameRef, id)); } else graphicalComp.InitializeTree(); } /// <summary> /// Iterates forward in the list of skill trees /// </summary> public void NextSkillTree() { RetainSkillTreeData(); // If already at last in list, go to first if (index == skillTreeObjects.Count - 1) index = 0; else ++index; ShowSkillTree(); } /// <summary> /// Iterates backward in the list of skill trees /// </summary> public void PrevSkillTree() { RetainSkillTreeData(); // If already at first in list, go to last if (index == 0) index = skillTreeObjects.Count - 1; else --index; ShowSkillTree(); } /// <summary> /// Saves the currently active skill tree /// </summary> private void RetainSkillTreeData() { GraphicalSkillTree graphComp = activeSkillTreeObject.GetComponent<GraphicalSkillTree>(); string skillTreeName = activeSkillTreeObject.name; Guid id = graphComp.GetSkillSet().UniqueId; if (!entitySkillListsInit.ContainsKey(skillTreeName)) entitySkillListsInit.Add(skillTreeName, id); Globals.SaveEntitySkillData(graphComp.GetEntityNameReference(), graphComp.GetSkillSet()); } /// <summary> /// Shows the newly indexed skill tree /// </summary> private void ShowSkillTree() { // Destroys the prior skill tree if (activeSkillTreeObject != null) Destroy(activeSkillTreeObject); activeSkillTreeObject = Instantiate(skillTreeObjects[index], contentFieldTransform); CheckForExistingData(); } /// <summary> /// Saves the current skill tree on screen before switching off components to go back to the general screen /// </summary> public void BackToGeneralStatusScreen() { // Save skill tree data and destroy the object instanced RetainSkillTreeData(); Destroy(activeSkillTreeObject); GameObject[] allyObjects = GameManager.Instance.GetAllyObjects(); Ally[] allies = new Ally[allyObjects.Length]; for (int i = 0; i < allies.Length; ++i) allies[i] = allyObjects[i].GetComponent<Ally>(); AbilityBrain.ModificationChecks(GameManager.Instance.GetPlayerObject().GetComponent<Player>(), allies); // Then go back to the general status screen GetComponent<Image>().enabled = false; objectToInitialize.SetActive(false); objectToSwitchBackOn.SetActive(true); statusComp.UpdateStatusScreen(); } public void SignalChoice(SkillSO data, object choices) { choiceObjectsInstanced?.ForEach(choice => Destroy(choice)); choiceObjectsInstanced = new List<GameObject>(); choiceObject.SetActive(true); if (choices is List<Vector2Int> statChoices) { foreach (Vector2Int choice in statChoices) { GameObject instancedChoice = Instantiate(choicePrefab, choiceFieldTransform); string choiceName = DetermineStatChoiceString(choice); instancedChoice.GetComponent<SkillChoiceHandler>().InjectInformation(data.ChoiceSprites[choice.x], choiceName, choice, data); choiceObjectsInstanced.Add(instancedChoice); } } else if (choices is List<Spell> spellChoices) { int index = 0; foreach (Spell spell in spellChoices) { GameObject instancedChoice = Instantiate(choicePrefab, choiceFieldTransform); instancedChoice.GetComponent<SkillChoiceHandler>().InjectInformation(data.ChoiceSprites[index++], spell.SpellName, spell, data); choiceObjectsInstanced.Add(instancedChoice); } } else if (choices is List<Ability> abilityChoices) { int index = 0; foreach (Ability ability in abilityChoices) { GameObject instancedChoice = Instantiate(choicePrefab, choiceFieldTransform); instancedChoice.GetComponent<SkillChoiceHandler>().InjectInformation(data.ChoiceSprites[index++], ability.AbilityName, ability, data); choiceObjectsInstanced.Add(instancedChoice); } } else if (choices is List<WeaponSO.Type> weaponResistChoices) { int index = 0; foreach (WeaponSO.Type type in weaponResistChoices) { GameObject instancedChoice = Instantiate(choicePrefab, choiceFieldTransform); string choiceName = DetermineWeaponResistChoiceString(type); instancedChoice.GetComponent<SkillChoiceHandler>().InjectInformation(data.ChoiceSprites[index++], choiceName, type, data); choiceObjectsInstanced.Add(instancedChoice); } } else if (choices is List<SpellSO.Element> elementResistChoices) { int index = 0; foreach (SpellSO.Element element in elementResistChoices) { GameObject instancedChoice = Instantiate(choicePrefab, choiceFieldTransform); string choiceName = DetermineElementResistChoiceString(element); instancedChoice.GetComponent<SkillChoiceHandler>().InjectInformation(data.ChoiceSprites[index++], choiceName, element, data); choiceObjectsInstanced.Add(instancedChoice); } } else if (choices is List<int> flags) DecipherFlagValues(data, flags, choiceObjectsInstanced); else throw new TypeLoadException(); string DetermineStatChoiceString(Vector2Int statModifier) { char sign = statModifier.y > 0 ? '+' : '-'; return statModifier.x switch { 0 => $"{sign}{statModifier.y} STR", 1 => $"{sign}{statModifier.y} INT", 2 => $"{sign}{statModifier.y} CON", 3 => $"{sign}{statModifier.y} DEX", 4 => $"{sign}{statModifier.y} CHA", 5 => $"{sign}{statModifier.y} WIS", 6 => $"{sign}{statModifier.y} LCK", _ => "Error Detected", }; } string DetermineWeaponResistChoiceString(WeaponSO.Type resistType) { return resistType switch { WeaponSO.Type.Blunt => "Blunt", WeaponSO.Type.Sharp => "Sharp", WeaponSO.Type.Ranged => "Ranged", WeaponSO.Type.Magic => "Magic", WeaponSO.Type.Extended => "Extended", WeaponSO.Type.Firearm => "Firearm", _ => "Error Detected", }; } string DetermineElementResistChoiceString(SpellSO.Element resistType) { return resistType switch { SpellSO.Element.Fire => "Fire", SpellSO.Element.Wind => "Wind", SpellSO.Element.Normal => "Normal", SpellSO.Element.Water => "Water", SpellSO.Element.Earth => "Earth", SpellSO.Element.Poison => "Poison", SpellSO.Element.Light => "Light", SpellSO.Element.Darkness => "Darkness", SpellSO.Element.Necro => "Necro", SpellSO.Element.Strange => "Strange", SpellSO.Element.Energy => "Energy", SpellSO.Element.Ice => "Ice", _ => "Error Detected", }; } } private void DecipherFlagValues(SkillSO data, List<int> flags, List<GameObject> choiceObjectsInstanced) { switch (data.SkillName) { case "Favored Enemy": // Flag values convert to different enemy types foreach (int flag in flags) { GameObject instancedChoice = Instantiate(choicePrefab, choiceFieldTransform); string choiceName = flag switch { 0 => "Golem Enemies", 1 => "Undead Enemies", 2 => "Beast Enemies", 3 => "Humanoid Enemies", 4 => "Demonic Enemies", _ => string.Empty, }; instancedChoice.GetComponent<SkillChoiceHandler>().InjectInformation(data.ChoiceSprites[flag], choiceName, flag, data); choiceObjectsInstanced.Add(instancedChoice); } break; case "Field Focus": foreach (int flag in flags) { GameObject instancedChoice = Instantiate(choicePrefab, choiceFieldTransform); string choiceName = flag switch { 0 => "Normal Element", 1 => "Fire Element", 2 => "Water Element", 3 => "Earth Element", 4 => "Wind Element", 5 => "Poison Element", 6 => "Ice Element", 7 => "Light Element", 8 => "Darkness Element", 9 => "Necro Element", 10 => "Energy Element", 11 => "Strange Element", _ => string.Empty, }; instancedChoice.GetComponent<SkillChoiceHandler>().InjectInformation(data.ChoiceSprites[flag], choiceName, flag, data); choiceObjectsInstanced.Add(instancedChoice); } break; default: Debug.LogError($"Unknown Flag Choice Data Set: ({data.SkillName})"); break; } } public void ChoiceMade(object data, SkillSO skillData = null) { if (data is Vector2Int statMod) { EntityStats.Stat filteredStat = GetStatMod(statMod); int newStatValue = entityReference.GetBaseStat(filteredStat) + statMod.y; string statReference = filteredStat switch { EntityStats.Stat.Strength => "STR", EntityStats.Stat.Intelligence => "INT", EntityStats.Stat.Wisdom => "WIS", EntityStats.Stat.Dexterity => "DEX", EntityStats.Stat.Constitution => "CON", EntityStats.Stat.Luck => "LCK", EntityStats.Stat.Charisma => "CHA", _ => throw new InvalidOperationException(), }; entityReference.GatherStats().SetStat(statReference, newStatValue); } else if (data is Ability ability) { entityReference.GetInventory().Add(ability); ManipulateSecondaryDetails(ability); } else if (data is Spell spell) entityReference.GetInventory().Add(spell); else if (data is WeaponSO.Type weaponResist) entityReference.GatherStats().AddTypeResistance(weaponResist); else if (data is SpellSO.Element elementResist) entityReference.GatherStats().AddElementalResistance(elementResist); else if (data is int value) { if (skillData.SkillName is "Favored Enemy") { if (entityReference is not Player) throw new InvalidOperationException(); string enemyType = value switch { 0 => "Golem", 1 => "Undead", 2 => "Beast", 3 => "Humanoid", 4 => "Demonic", _ => throw new InvalidOperationException(), }; // List object goes ---> Type Name, Added Damage, Added To Hit entityReference.SignalPlayerModification("Favored Enemy", new List<object> { enemyType, new DSix(), new DFour() }); } else if (skillData.SkillName is "Field Focus") { Ability newFieldAbility = new Ability(Resources.Load<AbilitySO>("Abilities/93_Field_Focus")) { ReferenceObject = new List<object>() { value switch { 0 => SpellSO.Element.Normal, 1 => SpellSO.Element.Fire, 2 => SpellSO.Element.Water, 3 => SpellSO.Element.Earth, 4 => SpellSO.Element.Wind, 5 => SpellSO.Element.Poison, 6 => SpellSO.Element.Ice, 7 => SpellSO.Element.Light, 8 => SpellSO.Element.Darkness, 9 => SpellSO.Element.Necro, 10 => SpellSO.Element.Energy, 11 => SpellSO.Element.Strange, _ => throw new InvalidOperationException(), } } }; entityReference.GetInventory().Add(newFieldAbility); } } TurnOffChoicePanel(); static EntityStats.Stat GetStatMod(Vector2Int modification) { return modification.x switch { 0 => EntityStats.Stat.Strength, 1 => EntityStats.Stat.Intelligence, 2 => EntityStats.Stat.Constitution, 3 => EntityStats.Stat.Dexterity, 4 => EntityStats.Stat.Charisma, 5 => EntityStats.Stat.Wisdom, 6 => EntityStats.Stat.Luck, _ => throw new InvalidOperationException(), }; } } private void ManipulateSecondaryDetails(Ability ability) { switch (ability.AbilityID) { case 91: // Rapid Blasts Spell rapidBlast = entityReference.GetInventory().Spells.Find(spell => spell.SpellID is 2); rapidBlast.AlterHelperEffect(0, "3D6 Energy"); rapidBlast.AlterDescription("A blast of magic to a single target that deals 3D6 Energy damage."); break; case 92: // Concentrated Blasts Spell concentratedBlast = entityReference.GetInventory().Spells.Find(spell => spell.SpellID is 2); concentratedBlast.AlterHelperEffect(0, "2D8 Energy"); concentratedBlast.AlterDescription("A blast of magic to a single target that deals 2D8 Energy damage."); break; default: break; } } private void TurnOffChoicePanel() { choiceObject.SetActive(false); } /// <summary> /// Public call for updating the skill points available in the skill tree screen /// </summary> public void SignalPointUsage() => IndicateSkillPointsRemaining(); }