using Cinemachine; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using TMPro; using UnityEngine; using UnityEngine.UI; /// <summary> /// Handles mostly the Canvas UI Components of the Battling System, /// such as the Buttons, Dialogues, and Cameras, logic is held mostly in BattleHandler /// </summary> public class EnemyUI : MonoBehaviour { public static EnemyUI Instance { get; private set; } [Header("UI Properties")] public Canvas primaryCanvas; // Canvas for the primary Enemy UI container public Canvas lootingCanvas; // Canvas for the looting aspects public Transform playerField; // The transform that holds the buttons public GameObject normalButton; // The prefab for the smaller button variety public GameObject spellButton; // The prefab for the spell button variety public ActionTextBox actionText; // The text box for the actions being performed in the battle public TextMeshProUGUI currentEntityTurnText; [Header("Looting UI Properties")] [SerializeField] private GameObject gridObjectRef; // Prefab reference for the grid that holds the objects in the loot and inventory fields [SerializeField] private TextMeshProUGUI currentMemberText; [SerializeField] private TextMeshProUGUI currentMemberCurrentInvenCount; [SerializeField] private TextMeshProUGUI goldValueText; [SerializeField] private List<GameObject> activatedIcons; [SerializeField] private List<GameObject> memberIcons; [SerializeField] private GameObject itemContainerPrefab; [SerializeField] private Transform inventoryTransform; [SerializeField] private Transform lootingTransform; [Header("Camera")] [SerializeField] private CinemachineVirtualCamera checkTrackCamera; private BattleHandler battleComponent; // The container for the scene's battle handler for this UI instance private Dictionary<Item, int> currentLoot; // Loot for the current encounter private List<int> experienceGathered; private List<GameObject> lootGrids; private List<GameObject> invenGrids; private Inventory currentInven; private Player player; private GameObject[] EnemyRefs; // The container for the enemy GameObjects within this instance of the enemy UI battling private Enemy[] Enemies; // The container for the enemy components within this instance of the enemy UI battling private Ally[] Allies; // The container for the ally components for this instance of the enemy UI battling private Enemy[] TempEnemies; // The container for the minion enemy components private Ally[] TempAllies; // The container for the minion ally components private List<GameObject> buttons; private bool runUsed; private Task uiTask; // Task call for the enabling and disabling of the player UI /// <summary> /// Retrieves the state of the task for player battling /// null is shown if the left battle side has not been disabled and /// complete if the left side has been disabled /// </summary> /// <returns></returns> public Task GetUITask() => uiTask; /// <summary> /// Switches the battle off /// </summary> private void InvokeBattleOff() => battleComponent.SetBattlingStatus(false); private void Awake() => Instance = this; private async void OnEnable() { // Waits until the enemies are loaded from files await EntityLoad(); // Sets the overlay mode for the canvas as well as the layer SetCanvasComponents(); // Then sets the enemies and player into position // as well as setting the EnemyUI handler within the BattleHandler battleComponent = BattleHandler.Instance; currentLoot = new Dictionary<Item, int>(); battleComponent.SetHandler(this); SetEntities(); await Task.Delay(1000); battleComponent.InitializeOrder(); battleComponent.SetBattlingStatus(true); while (battleComponent.GetBattlingStatus()) await battleComponent.SetActiveEntity(); async Task EntityLoad() { while (EnemyRefs is null) await Task.Yield(); await Task.CompletedTask; } void SetCanvasComponents() { primaryCanvas.worldCamera = Camera.main; primaryCanvas.sortingLayerName = "UI"; lootingCanvas.worldCamera = Camera.main; lootingCanvas.sortingLayerName = "UI"; GeneralUI.Instance.DeactivateInventoryButton(); } } /// <summary> /// Positions the player and enemies into position /// </summary> private void SetEntities() { GameManager gmInstance = GameManager.Instance; GeneralUI.Instance.UpdateGUIs(gmInstance.GetAllyObjects()); if (Globals.IsBossEncounter) { CameraManager.Instance.LightenScreen().GetAwaiter(); Enemies = battleComponent.CallForEntityPositioning(EnemyRefs, true); } else Enemies = battleComponent.CallForEntityPositioning(EnemyRefs, false); PositionHandler.Instance.ShowAndRaisePlayerObject(gmInstance.GetPlayerObject()); PositionHandler.Instance.ShowAndRaiseAllyObjects(gmInstance.GetAllyObjects()); Allies = gmInstance.GetAllyObjects()?.Select(ally => ally.GetComponent<Ally>()).ToArray(); player = gmInstance.GetPlayerObject().GetComponent<Player>(); } /// <summary> /// Intakes the entitiy prefabs into the new enemy UI system to be dispersed among /// other systems, determines if Enemy prefabs or Ally prefabs /// </summary> /// <param name="enemies">Entity prefab instances</param> public void InsertEnemyInfo(GameObject[] enemies) { this.EnemyRefs = new GameObject[enemies.Length]; for (int i = 0; i < enemies.Length; i++) this.EnemyRefs[i] = enemies[i]; } /// <summary> /// Destroys the indicated entity value based on index position and the trigger for /// if the entity destroyed is an enemy or not /// </summary> /// <param name="index">The position in the designated entity collection</param> /// <param name="isEnemy">Trigger for which collection is affected</param> public Task DeactivateEntityValue(int index, bool isEnemy) { // Player value, simply returns after switching the component off if (index is -1) { player.DeactivateVisually(); return Task.CompletedTask; } int loopLength = isEnemy ? Enemies.Length : Allies.Length; // Cycles through the enemies or allies to determine which should be deactivated for (int i = 0; i < loopLength ; i++) { if (isEnemy && i == index) { GenerateLoot(Enemies[i]); GenerateExperience(Enemies[i]); Enemies[i].enabled = false; } else if (!isEnemy && i == index) Allies[i].DeactivateVisually(); } // Searches if all of a side has been deactivated to determine the outcome of the battle bool trigger = true; if (isEnemy) { foreach (Enemy enemyCheck in Enemies) { if (enemyCheck.isActiveAndEnabled) { trigger = false; break; } } } else { if (!player.HasDied) trigger = false; if (trigger) { foreach (Ally ally in Allies) { if (!ally.HasDied) { trigger = false; break; } } } } // If a side is determined to not have any entities then the battle is over if (trigger) battleComponent.SetBattlingStatus(false); return Task.CompletedTask; } /// <summary> /// Destroys all enemies within the scene, meant primarily to be used for the running calls /// </summary> private void DestroyAllEnemies() => battleComponent.DeactivateAllEnemies(); /// <summary> /// Destroys all allies within the scene /// </summary> private void DestroyAllAllies() => battleComponent.DeactivateAllAllies(); /// <summary> /// Provides list of enemies to target with the active weapon of the player along with the cancel option /// </summary> public void Fight(Entity entity, int attackPerformed) { ResetUI(); buttons = new List<GameObject>(); // Enemy options for (int i = 0; i < Enemies.Length; ++i) { if (!Enemies[i].gameObject.activeSelf) continue; GameObject buttonRef = Instantiate(normalButton, playerField); buttons.Add(buttonRef); buttonRef.GetComponentInChildren<TextMeshProUGUI>().text = Enemies[i].GetName(); int tempVar = i; buttonRef.GetComponent<Button>().onClick.AddListener(() => { battleComponent.RollToHit(tempVar, entity, false, attackPerformed); }); } // Enemy Minion options if (TempEnemies != null) { for (int i = 0; i < TempEnemies.Length; ++i) { GameObject buttonRef = Instantiate(normalButton, playerField); buttons.Add(buttonRef); buttonRef.GetComponentInChildren<TextMeshProUGUI>().text = TempEnemies[i].GetName(); // Temp variable needs to include enemy length otherwise buttons will have incorrect correlations int tempVar = Enemies.Length + i; buttonRef.GetComponent<Button>().onClick.AddListener(() => { battleComponent.RollToHit(tempVar, entity, false, attackPerformed); }); } } if (attackPerformed is 0) CreateCancelButton(entity); } /// <summary> /// Creates a spell button for each of the spells available to the player as well as the cancel button /// </summary> public void Magic(Entity entity) { ResetUI(); // Creates a list of the spells that includes the limited spells the entity has access to List<Spell> spellList = new List<Spell>(entity.GetInventory().Spells); spellList.AddRange(entity.GetInventory().limitedSpells); // This cycles through the list in case their are any duplicate spells which shouldn't be listed for (int i = 0; i < spellList.Count - 1; ++i) { for (int j = i + 1; j < spellList.Count; ++j) { try { if (spellList[i] == spellList[j]) { spellList.Remove(spellList[j]); --j; } } catch { throw; } } } buttons = new List<GameObject>(); // Exploitative Check ExploitativeFunction(entity, spellList); for (int i = 0; i < spellList.Count; i++) { // Skips spells that are not meant for battle if (SkipSpell(spellList[i])) continue; GameObject buttonRef = Instantiate(spellButton, playerField); buttons.Add(buttonRef); buttonRef.GetComponentInChildren<TextMeshProUGUI>().text = spellList[i].SpellName; // Puts the spell in the player's spell list into the spell helper conenction buttonRef.GetComponent<SpellHelperConnector>().SpellInjectionStart(spellList[i]); // Disables button if unable to be used CheckSpellUseStatus(spellList, i, buttonRef, entity); int tempVar = i; buttonRef.GetComponent<Button>().onClick.AddListener(() => { // Warning disabled due to method begin called from a listener #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed battleComponent.InjectIntoSpellManager(spellList[tempVar], entity); #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed }); } // Cancel Button CreateCancelButton(entity); // Skips spells that aren't meant for battle bool SkipSpell(Spell spell) { return spell.SpellName switch { "Detect Magic" => true, "Pillage" => Enemies.All(enemy => enemy.Pillaged is true), _ => false, }; } void ExploitativeFunction(Entity entity, List<Spell> spellList) { if (entity.GetInventory().Abilities.Contains(AbilityBrain.GetAbilityList()[126]) || entity.GetInventory().Abilities.Contains(AbilityBrain.GetAbilityList()[126])) { int numberOfAlliesToExploit = 0; if (!player.HasDied) numberOfAlliesToExploit++; if (Allies is not null) { foreach (Ally ally in Allies) { if (!ally.HasDied) numberOfAlliesToExploit++; } } if (TempAllies is not null) { foreach (Ally ally in TempAllies) { if (!ally.HasDied) numberOfAlliesToExploit++; } } // Take one away for the entity being checked currently numberOfAlliesToExploit--; // Percentage compounds, does not straight stack foreach (Spell spell in spellList) { for (int i = 0; i < numberOfAlliesToExploit; ++i) spell.SpellCost -= (int)Math.Floor(spell.SpellCost * .1f); } } } } /// <summary> /// Makes sure that the spell indicated within the button can be activated/used based /// on the user's current mana pool /// </summary> /// <param name="spell_List">The list of spells available</param> /// <param name="index">The index within the spell list</param> /// <param name="button_Ref">The button object reference</param> private void CheckSpellUseStatus(List<Spell> spell_List, int index, GameObject button_Ref, Entity entity) { // Retrieves the current mana value from the player if the current entity is not an ally, otherwise retrieves the passed allies mana int mana = entity is Player player ? player.RetrieveMana() : ((Ally)entity).RetrieveMana(); if (spell_List[index].SpellCost > mana) { button_Ref.GetComponent<Button>().interactable = false; button_Ref.GetComponent<Image>().color = Globals.DisabledColor; } } /// <summary> /// Creates buttons for each consumable in the ally/player's inventory with their value indicated in the buttons /// with listeners to activate their consumabels /// </summary> /// <param name="ally">Ally component if needed</param> public void Items(Entity entity) { ResetUI(); // Gather consumables from needed inventory Dictionary<Consumable, int> consumables = entity.GetInventory().Consumables; buttons = new List<GameObject>(); foreach (KeyValuePair<Consumable, int> con in consumables) { GameObject buttonRef = Instantiate(normalButton, playerField); buttons.Add(buttonRef); buttonRef.GetComponentInChildren<TextMeshProUGUI>().text = $"{con.Key.ItemName} ({con.Value})"; buttonRef.GetComponent<Button>().onClick.AddListener(() => { battleComponent.ActivateConsumable(entity.GetInventory(), con.Key, entity); }); } // Cancel Button CreateCancelButton(entity); } /// <summary> /// Handles analyizing an entity for helpful information /// </summary> /// <param name="ally">Ally component if their turn and available</param> public void Check(Entity entity) { ResetUI(); buttons = new List<GameObject>(); for (int i = 0; i < Enemies.Length; i++) { if (!Enemies[i].gameObject.activeSelf) continue; GameObject bRef = Instantiate(normalButton, playerField); buttons.Add(bRef); bRef.GetComponentInChildren<TextMeshProUGUI>().text = Enemies[i].GetName(); int tempVar = i; bRef.GetComponent<Button>().onClick.AddListener(() => { BeginCheckProcess(Enemies[tempVar]); }); } CreateCancelButton(entity); } /// <summary> /// Calculates running chances and rolls to see if the player (or ally) is able to escape /// </summary> /// <param name="ally">If it is an allies turn their Dex Modifier will be used</param> public async void Run(Entity entity) { DisableOptions(); bool trigger = CalculateSuccess(); await actionText.UpdateActionTextFlee(entity.GetName(), trigger); if (trigger) { DestroyAllEnemies(); DestroyAllAllies(); InvokeBattleOff(); battleComponent.DisableBattle(true); } else { EnableOptions(); buttons[^1].GetComponent<Button>().interactable = false; buttons[^1].GetComponent<Image>().color = Color.gray; runUsed = true; } bool CalculateSuccess(bool isRecalc = false) { if (Globals.IsBossEncounter) return false; int retrievedDexStat = entity.GatherStats().GetStat(EntityStats.Stat.Dexterity, entity.GetBaseStat(EntityStats.Stat.Dexterity)); const float BaseRun = 100; float baseRunChance = BaseRun - entity.GetLevel(); float totalRunChance = baseRunChance + retrievedDexStat; float rolledValue = UnityEngine.Random.Range(0f, 99f); foreach (Enemy enemy in Enemies) { // Foreach enemy that is still alive, their dexterity is added to the roll value for a higher chance of failing if (enemy.HasDied is false) rolledValue += enemy.GatherStats().GetStat(EntityStats.Stat.Dexterity); } // Checks if the entity running away has the ability Evasive Maneuvers Inventory entityInven = entity.GetInventory(); Dictionary<int, Ability> abilityList = AbilityBrain.GetAbilityList(); if (entityInven.Abilities.Contains(abilityList[64]) || entityInven.limitedAbilities.Contains(abilityList[64])) { if (rolledValue > totalRunChance && !isRecalc) return CalculateSuccess(true); } return rolledValue <= totalRunChance; } } /// <summary> /// Simply leaves the current Enemy Encounter to start the next Encounter /// </summary> private void ExitEncounter() { // Destroys all of the disabled enemies foreach (Enemy enemy in Enemies) { // Try catch is implemented if Run was activated and successful try { Destroy(enemy.gameObject); } catch { } } if (Allies != null) GameManager.Instance.EndEncounter(player, Allies); else GameManager.Instance.EndEncounter(player); } /// <summary> /// Creates the cancel button for each of the options listed above /// </summary> public void CreateCancelButton(Entity entity) { // Cancel button GameObject cancelRef = Instantiate(normalButton, playerField); buttons.Add(cancelRef); cancelRef.GetComponentInChildren<TextMeshProUGUI>().text = "Cancel"; cancelRef.GetComponent<Button>().onClick.AddListener(() => { Cancel(entity); }); } /// <summary> /// Cancels the current selection fields to go back to the primary fields /// </summary> public void Cancel(Entity entity) { foreach (Transform child in playerField.transform) Destroy(child.gameObject); buttons?.Clear(); EnableEnemyLayout(entity); } /// <summary> /// The Player UI is enabled for their turn in battle /// </summary> /// <returns>Returns the indication that the task is complete</returns> public async Task EnableLeftBattle(Entity entity, Dictionary<string, object> triggers) { // Task is reset once player is active, resets run available for player turn uiTask = null; runUsed = false; ResetUI(); if (triggers.ContainsKey("CoreSpell")) await battleComponent.InjectIntoSpellManager((Spell)triggers["CoreSpell"], entity, true); EnableEnemyLayout(entity); // The thread awaits until the task has been completed, i.e. the player chooses to fight, use magic, run, etc. while (uiTask == null) await Task.Yield(); await uiTask; } /// <summary> /// The Player UI is disabled and signals that the task is complete to complete the thread call in EnablePlayerBattle() or EnableAllyBattle() /// </summary> public async Task DisableLeftBattle(bool startBattle, bool isCheckProcess = false) { ResetUI(); if (!startBattle && !isCheckProcess) await actionText.GetTextShownStatus(); uiTask = Task.CompletedTask; await uiTask; } /// <summary> /// Retrieves the action text box in the UI for indication of what is happening in the battle /// (NEEDED ESPECIALLY WITHOUT ANIMATIONS) /// </summary> /// <returns>The ActionTextBox within the scene and Enemy UI</returns> public ActionTextBox RetrieveActionText() => actionText; /// <summary> /// Resets the UI back to a clear field /// </summary> public void ResetUI() { foreach (Transform child in playerField) Destroy(child.gameObject); buttons?.Clear(); } /// <summary> /// Creates the enemy layout for the battle, including fighting, magic, items, etc. /// </summary> /// <param name="ally">Indicator if it is an Ally's turn and the data that comes with the indicator</param> public void EnableEnemyLayout(Entity entity) { buttons = new List<GameObject>(); List<string> NumberOfActions = DetermineActionAmount(entity); for (int i = 0; i < NumberOfActions.Count; i++) { buttons.Add(Instantiate(normalButton, playerField)); TextMeshProUGUI retainer = buttons[i].GetComponentInChildren<TextMeshProUGUI>(); switch (NumberOfActions[i]) { case "Fight": retainer.text = "Fight"; buttons[^1].GetComponent<Button>().onClick.AddListener(() => { Fight(entity, 0); }); break; case "Magic": retainer.text = "Magic"; buttons[^1].GetComponent<Button>().onClick.AddListener(() => { Magic(entity); }); // If the entity is silenced, the Magic button simply won't function if (entity.GatherModList().SmartModCheck("Silenced")) { buttons[^1].GetComponent<Button>().interactable = false; buttons[^1].GetComponent<Image>().color = new Color(.65f, .65f, .65f, 1f); } break; case "Items": retainer.text = "Items"; buttons[^1].GetComponent<Button>().onClick.AddListener(() => { Items(entity); }); break; case "Check": retainer.text = "Check"; buttons[^1].GetComponent<Button>().onClick.AddListener(() => { Check(entity); }); break; case "Run": retainer.text = "Run"; buttons[^1].GetComponent<Button>().onClick.AddListener(() => { Run(entity); }); if (runUsed) { buttons[^1].GetComponent<Button>().interactable = false; buttons[^1].GetComponent<Image>().color = Color.gray; } break; case "Dream Master": retainer.text = "Dream Powers"; buttons[^1].GetComponent<Button>().onClick.AddListener(() => { TitlePowers.DreamPowers(this, entity, buttons); }); break; } } } private List<string> DetermineActionAmount(Entity entity) { List<string> amount = new List<string>() { "Fight", "Magic", "Items", "Check", "Run" }; // Minion Check, doesn't have Check, Items, or Run if (entity is Ally || entity is Enemy) { bool isAlly = entity is Ally; if (isAlly) { if (((Ally)entity).IsMinion) { amount.Remove("Check"); amount.Remove("Items"); amount.Remove("Run"); return amount; } } else { if (((Enemy)entity).IsMinion) { amount.Remove("Check"); amount.Remove("Items"); amount.Remove("Run"); return amount; } } } try { foreach (string title in entity.EntityFlags) { switch (title) { case "Dream Master": amount.Insert(2, "Dream Master"); break; default: break; } } } catch { } return amount; } /// <summary> /// Grays out and makes the player's options uninteractable /// </summary> public void DisableOptions() { foreach (Transform child in playerField) { child.GetComponent<Button>().interactable = false; child.GetComponent<Image>().color = Color.gray; } } /// <summary> /// Enables the player's options and colors them back to their normal white mode /// </summary> public void EnableOptions() { foreach (Transform child in playerField) { child.GetComponent<Button>().interactable = true; child.GetComponent<Image>().color = Color.white; } } /// <summary> /// Creates post-battle buttons depending on the state of the enemy encounter if the player ran away before killing all enemies /// </summary> /// <param name="ranAway">Indicator trigger if the loot button should be left out</param> public void CreatePostBattleButtons(bool ranAway = false) { // Cleans the slate ResetUI(); buttons = new List<GameObject>(); // Indicate stats after battle player.ClearModifiers(); player.StatusChanged(); foreach (Ally ally in Allies) { ally.ClearModifiers(); ally.StatusChanged(); } // Adds two buttons that will give the option to loot the enemies killed or to start the next encounter const int NumberOfActions = 2; for (int i = 1; i <= NumberOfActions; i++) { // If the player runs away than the loot action is simply not given if (i == 1 && ranAway) continue; buttons.Add(Instantiate(normalButton, playerField)); TextMeshProUGUI retainer = buttons[^1].GetComponentInChildren<TextMeshProUGUI>(); switch (i) { // When all enemies are killed the player can loot the items that the enemies randomly dropped during the battle case 1: retainer.text = "Loot"; buttons[^1].GetComponent<Button>().onClick.AddListener(() => { LootMonstersCall(); }); break; // Starts the next encounter case 2: retainer.text = "Leave"; buttons[^1].GetComponent<Button>().onClick.AddListener(() => { ExitEncounter(); }); break; } } GeneralUI.Instance.ReactivateInventoryButton(); } /// <summary> /// Generates the loot stash when an enemy is killed by cycling through possible loot items /// </summary> /// <param name="enemy">The enemy that holds the loot information</param> private void GenerateLoot(Enemy enemy) { Dictionary<ItemSO, float> lootAndChances = enemy.GetPotentialLoot(); Vector2Int goldRange = enemy.GetGoldRange(); // False Idol Relic Flag bool idolActive = Globals.RelicFlags[1]; int itemCutoff = idolActive ? 2 : 3; // If the enemy is a boss more loot is dropped from the boss if (enemy.GetEnemyDifficulty() is Enemy.DifficultyRating.Boss) itemCutoff += 3; int counter = 0; // Based on the chances adds items to the loot stash to be collected at the end of the battle // If the itemSO is detected to be gold, the gold range is used instead to add to the stash foreach (KeyValuePair<ItemSO, float> loot in lootAndChances) { float v = UnityEngine.Random.Range(0f, 100f); bool check = v <= loot.Value; if (!check) continue; Item item; if (loot.Key is WeaponSO weapon) item = Globals.TurnWeaponFromObject(weapon); else if (loot.Key is ApparelSO apparel) item = Globals.TurnApparelFromObject(apparel); else if (loot.Key is ConsumableSO consume) item = Globals.TurnConsumableFromObject(consume); else item = Globals.TurnItemFromObject(loot.Key); if (currentLoot.ContainsKey(item) && item.IsUniqueItem) continue; if (!currentLoot.ContainsKey(item)) { if (item.ItemName != "Gold") currentLoot.Add(item, 1); else { int g = UnityEngine.Random.Range(goldRange.x, goldRange.y + 1); if (idolActive) g += (int)(Math.Floor(g * .1f)); currentLoot.Add(item, g); } ++counter; if (counter == itemCutoff) break; } else { if (item.ItemName != "Gold") currentLoot[item]++; else { int g = UnityEngine.Random.Range(goldRange.x, goldRange.y + 1); if (idolActive) g += (int)(Math.Floor(g * .1f)); currentLoot[item] += g; } ++counter; if (counter == itemCutoff) break; } } } /// <summary> /// Generates experience points based on the enemies current experience level /// </summary> /// <param name="enemy">The enemy to calculate experience from</param> private void GenerateExperience(Enemy enemy) { experienceGathered ??= new List<int>(); experienceGathered.Add(enemy.GetExperienceValue()); } /// <summary> /// Shows the loot screen and provides a button to exit /// </summary> private void LootMonstersCall() { actionText.enabled = false; GeneralUI.Instance.DeactivateInventoryButton(); ResetUI(); buttons = new List<GameObject>(); GameObject go = Instantiate(normalButton, playerField); Button buttonRef = go.GetComponent<Button>(); go.GetComponentInChildren<TextMeshProUGUI>().text = "Exit Looting"; buttonRef.onClick.AddListener(() => { ExitLootingScreen(); }); buttons.Add(go); FillLootingAspects(); lootingCanvas.enabled = true; } /// <summary> /// Spawns in the items for both the active inventory as well as the loot available after the battle /// </summary> private void FillLootingAspects(bool lootAllUsed = false) { // Clear GUI screen if loot all button was used before reinitializing if (lootAllUsed) { int max = lootGrids.Count < invenGrids.Count ? invenGrids.Count : lootGrids.Count; for (int i = 0; i < max; ++i) { try { Destroy(lootGrids[i]); } catch { } try { Destroy(invenGrids[i]); } catch { } } } currentInven = player.GetInventory(); // Fill the gold text goldValueText.text = player.GetInventory().GetGold().ToString(); // Start on main player's inventory currentMemberText.text = player.GetName(); currentMemberCurrentInvenCount.text = $"{currentInven.GetInvenCount()} / {currentInven.GetInvenLimit()}"; // Turns on the activation icon for the main player and off for the rest to indicate who's inventory is open for (int i = 0; i < activatedIcons.Count; i++) { if (i == 0) activatedIcons[i].GetComponent<Image>().enabled = true; else activatedIcons[i].GetComponent<Image>().enabled = false; } // Cycles through each of the icons in the buttons int counter = 0; foreach (GameObject icon in memberIcons) { // Tries until either all of the member icons are filled or a Arugment out of range exception is created which just turns off the buttons try { icon.GetComponentsInChildren<Image>()[1].enabled = true; icon.GetComponentsInChildren<Image>()[1].sprite = counter == 0 ? player.GetSprite() : Allies[counter - 1].GetSprite(); icon.GetComponent<Button>().interactable = true; int tempVar = counter; icon.GetComponent<Button>().onClick.AddListener(() => { if (tempVar is 0) SwitchInventories(player.GetInventory(), tempVar, player); else SwitchInventories(Allies[tempVar - 1].GetInventory(), tempVar, Allies[tempVar - 1]); }); } catch { icon.GetComponent<Button>().interactable = false; icon.GetComponent<Image>().enabled = false; icon.GetComponentsInChildren<Image>()[1].enabled = false; } counter++; } lootGrids = new List<GameObject>(); invenGrids = new List<GameObject>(); // Spawns loot within the looting canvas object with their values foreach (KeyValuePair<Item, int> pair in currentLoot) { GameObject grid = Instantiate(gridObjectRef, lootingTransform); grid.GetComponent<DropBox>().DetectItemUISize(DropBox.DropLocations.LootStock); lootGrids.Add(grid); GameObject gen = Instantiate(itemContainerPrefab, grid.transform); gen.GetComponent<DraggableItem>().Type = DraggableItem.DraggableItemType.LootStock; ItemGUI gui = gen.GetComponent<ItemGUI>(); gui.SwitchCostToValueText(); gui.InjectItemInformation(pair.Key, pair.Value); gui.IndicateNoNameLargeSpriteWithValue(); AspectHelperManager ahm = gen.GetComponent<AspectHelperManager>(); ahm.InjectTransform(lootingCanvas.transform); ahm.InjectAspect(pair.Key); ahm.InjectOffset(new Vector2(-4.25f, -1.55f)); } // Spawns in the player's inventory within the inventory transform indicator Dictionary<Item, int> currentInvenItems = currentInven.GetAllItems(); foreach (KeyValuePair<Item, int> item in currentInvenItems) { if (item.Key.CantBeSold) continue; GameObject grid = Instantiate(gridObjectRef, inventoryTransform); grid.GetComponent<DropBox>().DetectItemUISize(DropBox.DropLocations.LootInventory); invenGrids.Add(grid); GameObject gen = Instantiate(itemContainerPrefab, grid.transform); gen.GetComponent<DraggableItem>().Type = DraggableItem.DraggableItemType.InventoryInLootScreen; ItemGUI gui = gen.GetComponent<ItemGUI>(); gui.SwitchCostToValueText(); gui.InjectItemInformation(item.Key, item.Value); gui.IndicateNoNameLargeSpriteWithValue(); AspectHelperManager ahm = gen.GetComponent<AspectHelperManager>(); ahm.InjectTransform(lootingCanvas.transform); ahm.InjectAspect(item.Key); ahm.InjectOffset(new Vector2(4.25f, -1.55f)); } } /// <summary> /// Moves the full amount of the loot item into the active inventory /// </summary> /// <param name="itemGen">The item to move into the inventory</param> /// <param name="value">The amount of the item to move into the inventory</param> public void MoveToActiveInven(GameObject itemObject, Item itemGen, int value) { // Adds the gold to the collective pile if (itemGen.ItemName.Equals("Gold")) { player.GetInventory().AddGold(value); goldValueText.text = player.GetInventory().GetGold().ToString(); } // Adds the item to the active inventory and instantiates it to the inventory transform else { bool? addedToPresentKey = DecipherItem(itemGen, value); if (addedToPresentKey is null) return; else if ((bool)addedToPresentKey) { // Childern are the grid holders foreach (Transform child in inventoryTransform) { ItemGUI itemGUIInfo = child.transform.GetChild(0).GetComponent<ItemGUI>(); if (itemGUIInfo.GetAspect().ItemId == itemGen.ItemId) { int amount = itemGen is Weapon weap ? currentInven.Weapons[weap] : (itemGen is Apparel app ? currentInven.Apparel[app] : (itemGen is Consumable con ? currentInven.Consumables[con] : currentInven.Items[itemGen])); itemGUIInfo.InjectItemInformation(itemGen, amount); Destroy(itemObject); break; } } } else { GameObject grid = Instantiate(gridObjectRef, inventoryTransform); grid.GetComponent<DropBox>().Type = DropBox.DropLocations.LootInventory; GameObject movedItem = Instantiate(itemContainerPrefab, grid.transform); movedItem.GetComponent<DraggableItem>().Type = DraggableItem.DraggableItemType.InventoryInLootScreen; ItemGUI gui = movedItem.GetComponent<ItemGUI>(); gui.InjectItemInformation(itemGen, value); gui.IndicateNoNameLargeSpriteWithValue(); AspectHelperManager aspectMan = movedItem.GetComponent<AspectHelperManager>(); aspectMan.InjectTransform(lootingCanvas.transform); aspectMan.SwitchOffsetSign(true); aspectMan.InjectAspect(gui.GetAspect()); currentMemberCurrentInvenCount.text = $"{currentInven.GetInvenCount()} / {currentInven.GetInvenLimit()}"; } } currentLoot.Remove(itemGen); foreach (Transform child in lootingTransform) { try { if (itemGen.ItemName.Equals(child.GetComponentInChildren<ItemGUI>().GetItemName())) { Destroy(child.gameObject); break; } } catch { Destroy(child.gameObject); break; } } } private bool? DecipherItem(Item itemGen, int value) { bool? addedItem = null; if (itemGen is Weapon weapon) { if (currentInven.Weapons.ContainsKey(weapon)) { currentInven.Weapons[weapon] += value; addedItem = true; } else if (currentInven.HaveRoom()) { currentInven.Add(weapon, value); addedItem = false; } } else if (itemGen is Apparel apparel) { if (currentInven.Apparel.ContainsKey(apparel)) { currentInven.Apparel[apparel] += value; addedItem = true; } else if (currentInven.HaveRoom()) { currentInven.Add(apparel, value); addedItem = false; } } else if (itemGen is Consumable consumable) { if (currentInven.Consumables.ContainsKey(consumable)) { currentInven.Consumables[consumable] += value; addedItem = true; } else if (currentInven.HaveRoom()) { currentInven.Add(consumable, value); addedItem = false; } } else { if (currentInven.Items.ContainsKey(itemGen)) { currentInven.Items[itemGen] += value; addedItem = true; } else if (currentInven.HaveRoom()) { currentInven.Add(itemGen, value); addedItem = false; } } return addedItem; } /// <summary> /// Move the item indicated in the player's inventory to the looting inventory transform /// </summary> /// <param name="itemPlayer">The item in the player's inventory that is wanted to be moved</param> /// <param name="value">The amount of the item</param> public void MoveToLootingInven(Item itemPlayer, int value) { if (itemPlayer is Weapon) currentInven.Remove(itemPlayer as Weapon, value); else if (itemPlayer is Apparel) currentInven.Remove(itemPlayer as Apparel, value); else if (itemPlayer is Consumable) currentInven.Remove(itemPlayer as Consumable, value); else currentInven.Remove(itemPlayer, value); bool lootContainsItem = false; if (currentLoot.ContainsKey(itemPlayer)) { currentLoot[itemPlayer] += value; lootContainsItem = true; } else currentLoot.Add(itemPlayer, value); foreach (Transform t in inventoryTransform) { try { if (itemPlayer.ItemName.Equals(t.GetComponentInChildren<ItemGUI>().GetItemName())) { Destroy(t.gameObject); break; } } catch { Destroy(t.gameObject); break; } } if (lootContainsItem) { foreach (Transform child in lootingTransform) { ItemGUI itemGUIReference = child.GetChild(0).GetComponent<ItemGUI>(); if (itemGUIReference.GetAspect().ItemId == itemPlayer.ItemId) { itemGUIReference.InjectItemInformation(itemPlayer, currentLoot[itemPlayer]); break; } } } else { GameObject grid = Instantiate(gridObjectRef, lootingTransform); grid.GetComponent<DropBox>().Type = DropBox.DropLocations.LootStock; GameObject newLootItem = Instantiate(itemContainerPrefab, grid.transform); newLootItem.GetComponent<DraggableItem>().Type = DraggableItem.DraggableItemType.LootStock; ItemGUI gui = newLootItem.GetComponent<ItemGUI>(); gui.InjectItemInformation(itemPlayer, value); gui.IndicateNoNameLargeSpriteWithValue(); AspectHelperManager aspectMan = newLootItem.GetComponent<AspectHelperManager>(); aspectMan.InjectTransform(lootingCanvas.transform); aspectMan.SwitchOffsetSign(false); aspectMan.InjectAspect(gui.GetAspect()); } } /// <summary> /// Exits the looting screen for the player /// </summary> private void ExitLootingScreen() { lootingCanvas.enabled = false; ClearLootingTransforms(); CreatePostBattleButtons(); actionText.enabled = true; GeneralUI.Instance.ReactivateInventoryButton(); } /// <summary> /// Switches the inventory active by the buttons on the left of the looting screen designated by party member icons /// </summary> /// <param name="inven">The inventory to switch to</param> /// <param name="index">The reference to which activation symbol should highlight whose inventory is active</param> /// <param name="ally">Data for if an allies inventory is selected</param> private void SwitchInventories(Inventory inven, int index, Entity entity) { if (currentInven == inven) return; ClearLootingTransforms(true); currentInven = inven; // Fill the gold text goldValueText.text = player.GetInventory().GetGold().ToString(); // Start on main player's inventory currentMemberText.text = entity.GetName(); currentMemberCurrentInvenCount.text = $"{currentInven.GetInvenCount()} / {currentInven.GetInvenLimit()}"; // Turns on the activation icon for the main player and off for the rest to indicate who's inventory is open for (int i = 0; i < activatedIcons.Count; i++) { if (i == index) activatedIcons[i].GetComponent<Image>().enabled = true; else activatedIcons[i].GetComponent<Image>().enabled = false; } // Spawns in the player's inventory within the inventory transform indicator Dictionary<Item, int> currentInvenItems = currentInven.GetAllItems(); invenGrids?.Clear(); invenGrids = new List<GameObject>(); foreach (KeyValuePair<Item, int> item in currentInvenItems) { if (item.Key.CantBeSold) return; GameObject grid = Instantiate(gridObjectRef, inventoryTransform); grid.GetComponent<DropBox>().DetectItemUISize(DropBox.DropLocations.LootInventory); invenGrids.Add(grid); GameObject gen = Instantiate(itemContainerPrefab, grid.transform); gen.GetComponent<Button>().onClick.AddListener(() => { MoveToLootingInven(item.Key, item.Value); }); ItemGUI gui = gen.GetComponent<ItemGUI>(); gui.SwitchCostToValueText(); gui.InjectItemInformation(item.Key, item.Value); AspectHelperManager ahm = gen.GetComponent<AspectHelperManager>(); ahm.InjectAspect(gui.GetAspect()); ahm.InjectTransform(lootingCanvas.transform); ahm.InjectOffset(new Vector2(4.25f, -1.55f)); } } /// <summary> /// Takes all of the items in the loot stash up to the player's max inventory, to which items will remain in the stash /// </summary> public void LootAll() { // No loot, don't bother checking rest if (currentLoot.Count is 0) return; Dictionary<Item, int> allItems = currentInven.GetAllItems(); List<Item> itemsCollected = new List<Item>(); foreach (KeyValuePair<Item, int> item in currentLoot) { // Special check if it is gold if (item.Key.ItemName.Equals("Gold")) { player.GetInventory().AddGold(item.Value); itemsCollected.Add(item.Key); continue; } bool? addedItem = DecipherItem(item.Key, item.Value); if (addedItem is null) break; // Add to the garbage list itemsCollected.Add(item.Key); } // Throw away all of the items that were retrieved in the current loot stash itemsCollected.ForEach(item => currentLoot.Remove(item)); // Then refresh the GUIs to show the loot collected FillLootingAspects(true); } /// <summary> /// Clears the looting transforms, both the player's inventory and looting inventory /// </summary> /// <param name="clearOnlyPlayerInven">Trigger for if just the inventory should be cleared</param> private void ClearLootingTransforms(bool clearOnlyPlayerInven = false) { if (clearOnlyPlayerInven) { foreach (Transform t in inventoryTransform) Destroy(t.gameObject); } else { foreach (Transform t in lootingTransform) Destroy(t.gameObject); foreach (Transform t in inventoryTransform) Destroy(t.gameObject); } } public void DisableSpellButtonInteractivity(Entity entity) { buttons.ForEach(b => { Button button = b.GetComponent<Button>(); // When the cancel button is found than a new listener replaces the current one to Re-enable the buttons when clicked if (b.GetComponentInChildren<TextMeshProUGUI>().text == "Cancel") { b.GetComponentInChildren<TextMeshProUGUI>().text = "Cancel Spell"; button.onClick.RemoveAllListeners(); button.onClick.AddListener(() => { ReEnableSpellButtons(entity); }); button.interactable = true; } else { // Disables the buttons and grays them out button.interactable = false; b.GetComponent<Image>().color = Globals.DisabledColor; } }); } /// <summary> /// Switches methods on the Cancel button and disables the other item buttons /// </summary> /// <param name="entity">The entity that is currently active, ally or player</param> public void DisableItemButtonInteractivity(Entity entity) { buttons.ForEach(b => { Button button = b.GetComponent<Button>(); if (b.GetComponentInChildren<TextMeshProUGUI>().text == "Cancel") { b.GetComponentInChildren<TextMeshProUGUI>().text = "Cancel Item"; button.onClick.RemoveAllListeners(); button.onClick.AddListener(() => { ReEnableItemButtons(entity); }); button.interactable = true; } else { // Disable item buttons and gray them out button.interactable = false; b.GetComponent<Image>().color = Globals.DisabledColor; } }); } /// <summary> /// Colors in the buttons and sets their interactivity if they are able to be used based on the /// player's current mana pool, also switches the Cancel buttons listener method /// </summary> public void ReEnableSpellButtons(Entity entity) { // Cancel's the spell attempting to be used in the battle handler battleComponent.CancelSpellUse(); List<Spell> spellList = new List<Spell>(entity.GetInventory().Spells); spellList.AddRange(entity.GetInventory().limitedSpells); for (int i = 0; i < buttons.Count; i++) { Button button = buttons[i].GetComponent<Button>(); // Resets the Cancel button to the proper Cancel listener if (buttons[i].GetComponentInChildren<TextMeshProUGUI>().text == "Cancel Spell") { buttons[i].GetComponentInChildren<TextMeshProUGUI>().text = "Cancel"; button.onClick.RemoveAllListeners(); button.onClick.AddListener(() => { if (entity is not Ally) Cancel(entity as Player); else Cancel(entity as Ally); }); return; } // Re-enables the buttons before checking to be sure that the button needs to be active based on the user's mana button.interactable = true; buttons[i].GetComponent<Image>().color = Color.white; CheckSpellUseStatus(spellList, i, buttons[i], entity); } } /// <summary> /// Reactivates the Item Buttons for the designated entity, which should be the active ally or player, /// before switching back the Cancel button method and the interactibility of the rest of the item buttons /// </summary> /// <param name="entity">The current active player or ally</param> public void ReEnableItemButtons(Entity entity) { battleComponent.CancelItemUse(); foreach (GameObject gObj in buttons) { Button b = gObj.GetComponent<Button>(); // Resets the Cancel button to the proper Cancel listener if (gObj.GetComponentInChildren<TextMeshProUGUI>().text == "Cancel Item") { gObj.GetComponentInChildren<TextMeshProUGUI>().text = "Cancel"; b.onClick.RemoveAllListeners(); b.onClick.AddListener(() => { if (entity is not Ally) Cancel(null); else Cancel(entity as Ally); }); return; } b.interactable = true; b.GetComponent<Image>().color = Color.white; } } /// <summary> /// Initializes the check process that targets an enemy for information /// </summary> /// <param name="enemy">The enemy which is being analyzed</param> private async void BeginCheckProcess(Enemy enemy) { // Enemy component requires CheckCameraUtil CheckCameraUtil ccUtil = enemy.GetComponent<CheckCameraUtil>(); checkTrackCamera.Follow = ccUtil.AcquireTrackingPoint(); ResetUI(); await ccUtil.StartCheckDialogue(checkTrackCamera); battleComponent.DisablePlayerTurn(true); } /// <summary> /// Injects a minion component that is deciphered into either TempAllies or TempEnemies /// </summary> /// <param name="entity">The entity component</param> public void InjectTempMinion(Entity entity) { // Checks if an allied minion is being added bool isAlly = entity is Ally; // Simply resize and append the entity component to the end of either // needed array if (isAlly) { if (TempAllies is null) TempAllies = new Ally[1]; else Array.Resize(ref TempAllies, TempAllies.Length + 1); TempAllies[^1] = entity as Ally; } else { if (TempEnemies is null) TempEnemies = new Enemy[1]; else Array.Resize(ref TempEnemies, TempEnemies.Length + 1); TempEnemies[^1] = entity as Enemy; } } /// <summary> /// Erases the entity component value from either TempAllies or TempEnemies depending on the child factor of the Entity component /// </summary> /// <param name="entity">The entity component to clear</param> public void DestroyTempMinion(Entity entity) { bool isAlly = entity is Ally; if (isAlly) { Ally allyComp = entity as Ally; int minionArrayLength = TempAllies.Length; // Resizes the ally minion array to disinclude the entity component parameter for (int i = 0; i < minionArrayLength; ++i) { if (TempAllies[i].GetUniqueID() == allyComp.GetUniqueID()) { for (int k = i; k < minionArrayLength - 1; ++k) { TempAllies[k] = TempAllies[k + 1]; TempAllies[k + 1] = null; } Array.Resize(ref TempAllies, minionArrayLength - 1); } } if (TempAllies.Length is 0) TempAllies = null; } else { Enemy enemyComp = entity as Enemy; int minionArrayLength = TempEnemies.Length; // Resizes the enemy minion array to disinclude the entity component parameter for (int i = 0; i < TempEnemies.Length; ++i) { if (TempEnemies[i].GetUniqueID() == enemyComp.GetUniqueID()) { for (int k = i; k < minionArrayLength - 1; ++k) { TempEnemies[k] = TempEnemies[k + 1]; TempEnemies[k + 1] = null; } Array.Resize(ref TempEnemies, minionArrayLength - 1); } } if (TempEnemies.Length is 0) TempEnemies = null; } } /// <summary> /// Simply just wipes out the arrays for minions /// </summary> public void ClearTempMinions() { TempAllies = null; TempEnemies = null; } /// <summary> /// Deactivates the post battle buttons when the inventory is opened /// </summary> public void DeactivatePostBattleButtons() { foreach (GameObject button in buttons) { button.GetComponent<Button>().interactable = false; button.GetComponent<Image>().color = new Color(.65f, .65f, .65f, 1f); } } /// <summary> /// Reactivates the post battle buttons when the inventory is closed /// </summary> public void ReactivatePostBattleButtons() { foreach (GameObject button in buttons) { button.GetComponent<Button>().interactable = true; button.GetComponent<Image>().color = Color.white; } } public void SwitchMemberTurnText(bool status) => currentEntityTurnText.enabled = status; /// <summary> /// Gives any experience that was collected in this enemy Encounter /// </summary> /// <param name="player">The player component</param> /// <param name="allyComps">The ally components</param> public void GiveExperience(Player player, Ally[] allyComps) { int totalExp = 0; if (experienceGathered != null) { // Check for any experience modifiers foreach (int expVal in experienceGathered) { int retainedExpVal = expVal; if (player.GetInventory().Abilities.Contains(AbilityBrain.GetAbilityList()[26]) || player.GetInventory().limitedAbilities.Contains(AbilityBrain.GetAbilityList()[26])) retainedExpVal += (int)(expVal * .25f); foreach (Ally ally in allyComps) { if (ally.GetInventory().Abilities.Contains(AbilityBrain.GetAbilityList()[26]) || ally.GetInventory().limitedAbilities.Contains(AbilityBrain.GetAbilityList()[26])) retainedExpVal += (int)(expVal * .25f); } totalExp += retainedExpVal; } } if (totalExp is not 0) { // Increase experience by the total amount and check for any level ups Dictionary<int, bool> levelUpMap = new Dictionary<int, bool> { { 0, player.IncreaseExperience(totalExp) } }; int counter = 1; foreach (Ally ally in allyComps) levelUpMap.Add(counter++, ally.IncreaseExperience(totalExp)); } } private Entity revivalTarget; private bool canceledChoice; public async Task<(bool choice, Entity spellTarget)> ProvideDeadChoices(List<Entity> deadEntities) { ResetUI(); int counter = 0; foreach (Entity deadEntity in deadEntities) { GameObject buttons = Instantiate(normalButton, playerField); deadEntity.gameObject.SetActive(true); buttons.GetComponentInChildren<TextMeshProUGUI>().text = deadEntity.GetName(); buttons.GetComponent<Button>().onClick.AddListener(() => { InjectDeadTarget(counter++, deadEntities); }); } canceledChoice = false; revivalTarget = null; while (revivalTarget == null) await Task.Yield(); ResetUI(); return (!canceledChoice, revivalTarget); } public void InjectDeadTarget(int indexValue, List<Entity> deadEntities) { try { revivalTarget = deadEntities[indexValue]; deadEntities.Remove(revivalTarget); } catch { canceledChoice = true; } } }