diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c72b7a8..ae87a2c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# 2.23.4 +- Fixed GemsDraw only considering the player's slots when determining how many cards to draw +- Fixed Act 2 Tutor sequence softlocking when there are no cards to display +- Fixed Act 2 Tutor sequence not displaying cards when you have less than 7 cards remaining in your deck +- Fixed Gemify affecting Blood cost when it shouldn't +- Added Gems Cost support for ExtendedActivatedAbilityBehaviour class +- Added extension AbilityManager.FullAbility.FlipYIfOpponent +- Add config option to prevent Act 1 card emissions from rendering above the play costs + # 2.23.3 - Fixed custom deck exhaust sequence not performing the requisite checks - Added 'CanAppearRandomly' bool and associated extension method for custom regions diff --git a/InscryptionAPI/Card/AbilityExtensions.cs b/InscryptionAPI/Card/AbilityExtensions.cs index 911261a6..934b31d2 100644 --- a/InscryptionAPI/Card/AbilityExtensions.cs +++ b/InscryptionAPI/Card/AbilityExtensions.cs @@ -424,6 +424,16 @@ public static AbilityInfo SetFlipYIfOpponent(this AbilityInfo abilityInfo, bool return abilityInfo; } /// + /// Sets whether or not the ability's icon should be flipped upside-down when it's on an opponent card. + /// + /// The instance of AbilityInfo. + /// If the icon should be flipped. + /// The same AbilityInfo so a chain can continue. + public static FullAbility SetFlipYIfOpponent(this FullAbility fullAbility, bool flipY = true) { + fullAbility.Info.SetFlipYIfOpponent(flipY); + return fullAbility; + } + /// /// Sets whether or not the ability's icon's colour should be overridden, and what the override colour should be. /// The colour override only applies to the default ability icons; totem and merge icons are unaffected. /// diff --git a/InscryptionAPI/Card/CostProperties.cs b/InscryptionAPI/Card/CostProperties.cs index c89a4cef..f4b3f522 100644 --- a/InscryptionAPI/Card/CostProperties.cs +++ b/InscryptionAPI/Card/CostProperties.cs @@ -128,7 +128,7 @@ public static List ImprovedGemsCost(CardInfo instance) public static bool ReduceGemifiedBlood(PlayableCard card, int? bloodCost = null) { - return (bloodCost ?? OriginalBloodCost(card.Info)) > 0 && !ReduceGemifiedMox(card) && !ReduceGemifiedBones(card) && !ReduceGemifiedMox(card); + return (bloodCost ?? OriginalBloodCost(card.Info)) > 0 && !ReduceGemifiedEnergy(card) && !ReduceGemifiedBones(card) && !ReduceGemifiedMox(card); } public static bool ReduceGemifiedMox(PlayableCard card, List gemsCost = null) { diff --git a/InscryptionAPI/Card/DamageShieldBehaviour.cs b/InscryptionAPI/Card/DamageShieldBehaviour.cs index e6f44468..458d7c5c 100644 --- a/InscryptionAPI/Card/DamageShieldBehaviour.cs +++ b/InscryptionAPI/Card/DamageShieldBehaviour.cs @@ -1,5 +1,6 @@ using DiskCardGame; using GBC; +using InscryptionAPI.Helpers; using InscryptionAPI.Helpers.Extensions; using System.Collections; using UnityEngine; @@ -87,20 +88,34 @@ public abstract class ActivatedDamageShieldBehaviour : DamageShieldBehaviour public int bloodCostMod; public int bonesCostMod; public int energyCostMod; + public List gemsCostMod; public int healthCostMod; public virtual int StartingBloodCost { get; } public virtual int StartingBonesCost { get; } public virtual int StartingEnergyCost { get; } + public virtual List StartingGemsCost { get; } public virtual int StartingHealthCost { get; } public virtual int OnActivateBloodCostMod { get; set; } public virtual int OnActivateBonesCostMod { get; set; } public virtual int OnActivateEnergyCostMod { get; set; } + public virtual List OnActivateGemsCostMod { get; set; } + public virtual bool OnActivateGemsCostModRemovesGems { get; set; } public virtual int OnActivateHealthCostMod { get; set; } public int BloodCost => Mathf.Max(0, StartingBloodCost + bloodCostMod); public int BonesCost => Mathf.Max(0, StartingBonesCost + bonesCostMod); public int EnergyCost => Mathf.Max(0, StartingEnergyCost + energyCostMod); + public List GemsCost { + get { + List retval = new(); + if (StartingGemsCost != null && StartingGemsCost.Count > 0) { + retval.AddRange(StartingGemsCost); + } + retval.AddRange(gemsCostMod); + return retval; + } + } public int HealthCost => Mathf.Max(0, StartingHealthCost + healthCostMod); public Dictionary currentSacrificedCardInfos = new(); @@ -187,12 +202,29 @@ public sealed override IEnumerator OnActivatedAbility() yield break; } } - if (OnActivateEnergyCostMod != 0) - energyCostMod += OnActivateEnergyCostMod; - if (OnActivateBonesCostMod != 0) - bonesCostMod += OnActivateBonesCostMod; - if (OnActivateHealthCostMod != 0) - healthCostMod += OnActivateHealthCostMod; + + int energyMod = OnActivateEnergyCostMod; + int bonesMod = OnActivateBonesCostMod; + List gemsMod = OnActivateGemsCostMod; + int healthMod = OnActivateHealthCostMod; + + if (energyMod != 0) + energyCostMod += energyMod; + + if (bonesMod != 0) + bonesCostMod += bonesMod; + + if (gemsMod != null && gemsMod.Count > 0) { + if (OnActivateGemsCostModRemovesGems) { + gemsMod.ForEach(x => gemsCostMod.Remove(x)); + } + else { + gemsCostMod.AddRange(gemsMod); + } + } + + if (healthMod != 0) + healthCostMod += healthMod; yield return PostActivate(); currentSacrificedCardInfos.Clear(); @@ -296,18 +328,22 @@ private IEnumerator ChooseSacrifices() manager.currentSacrifices.Clear(); } - private bool CanAfford() - { - if (BloodCost <= 0 || SacrificeValue() >= BloodCost) - { - if (base.Card.Health >= HealthCost) - { - if (Singleton.Instance.PlayerEnergy >= EnergyCost) - return Singleton.Instance.PlayerBones >= BonesCost; + private bool CanAfford() { + // if no blood cost or we can fulfill our blood cost + if (BloodCost < 1 || SacrificeValue() >= BloodCost) { + // if we have enough health, Energy, and Bones + if (base.Card.Health >= HealthCost && ResourcesManager.Instance.PlayerEnergy >= EnergyCost && ResourcesManager.Instance.PlayerBones >= BonesCost) { + bool enoughOrange = ResourcesManagerHelpers.GemCount(!base.Card.OpponentCard, GemType.Orange) >= GemsCost.Count(x => x == GemType.Orange); + bool enoughGreen = ResourcesManagerHelpers.GemCount(!base.Card.OpponentCard, GemType.Green) >= GemsCost.Count(x => x == GemType.Green); + bool enoughBlue = ResourcesManagerHelpers.GemCount(!base.Card.OpponentCard, GemType.Blue) >= GemsCost.Count(x => x == GemType.Blue); + + // if we have enough gems + return enoughOrange && enoughGreen && enoughBlue; } } return false; } + private bool LearnMechanic() => SaveManager.SaveFile.IsPart2 && !ProgressionData.LearnedMechanic(MechanicsConcept.GBCActivatedAbilities); private int SacrificeValue() { diff --git a/InscryptionAPI/Card/ExtendedActivatedAbilityBehaviour.cs b/InscryptionAPI/Card/ExtendedActivatedAbilityBehaviour.cs index cba377ee..bee1226c 100644 --- a/InscryptionAPI/Card/ExtendedActivatedAbilityBehaviour.cs +++ b/InscryptionAPI/Card/ExtendedActivatedAbilityBehaviour.cs @@ -1,6 +1,7 @@ using DiskCardGame; using GBC; using HarmonyLib; +using InscryptionAPI.Helpers; using InscryptionAPI.Helpers.Extensions; using System.Collections; using UnityEngine; @@ -11,20 +12,34 @@ public abstract class ExtendedActivatedAbilityBehaviour : AbilityBehaviour public int bloodCostMod; public int bonesCostMod; public int energyCostMod; + public List gemsCostMod = new(); public int healthCostMod; public virtual int StartingBloodCost { get; } public virtual int StartingBonesCost { get; } public virtual int StartingEnergyCost { get; } + public virtual List StartingGemsCost { get; } public virtual int StartingHealthCost { get; } public virtual int OnActivateBloodCostMod { get; set; } public virtual int OnActivateBonesCostMod { get; set; } public virtual int OnActivateEnergyCostMod { get; set; } + public virtual List OnActivateGemsCostMod { get; set; } + public virtual bool OnActivateGemsCostModRemovesGems { get; set; } public virtual int OnActivateHealthCostMod { get; set; } public int BloodCost => Mathf.Max(0, StartingBloodCost + bloodCostMod); public int BonesCost => Mathf.Max(0, StartingBonesCost + bonesCostMod); public int EnergyCost => Mathf.Max(0, StartingEnergyCost + energyCostMod); + public List GemsCost { + get { + List retval = new(); + if (StartingGemsCost != null && StartingGemsCost.Count > 0) { + retval.AddRange(StartingGemsCost); + } + retval.AddRange(gemsCostMod); + return retval; + } + } public int HealthCost => Mathf.Max(0, StartingHealthCost + healthCostMod); public Dictionary currentSacrificedCardInfos = new(); @@ -93,8 +108,9 @@ public sealed override IEnumerator OnActivatedAbility() } } } - if (BonesCost > 0) + if (BonesCost > 0) { yield return Singleton.Instance.SpendBones(BonesCost); + } yield return new WaitForSeconds(0.1f); yield return base.PreSuccessfulTriggerSequence(); @@ -111,12 +127,28 @@ public sealed override IEnumerator OnActivatedAbility() yield break; } } - if (OnActivateEnergyCostMod != 0) - energyCostMod += OnActivateEnergyCostMod; - if (OnActivateBonesCostMod != 0) - bonesCostMod += OnActivateBonesCostMod; - if (OnActivateHealthCostMod != 0) - healthCostMod += OnActivateHealthCostMod; + int energyMod = OnActivateEnergyCostMod; + int bonesMod = OnActivateBonesCostMod; + List gemsMod = OnActivateGemsCostMod; + int healthMod = OnActivateHealthCostMod; + + if (energyMod != 0) + energyCostMod += energyMod; + + if (bonesMod != 0) + bonesCostMod += bonesMod; + + if (gemsMod != null && gemsMod.Count > 0) { + if (OnActivateGemsCostModRemovesGems) { + gemsMod.ForEach(x => gemsCostMod.Remove(x)); + } + else { + gemsCostMod.AddRange(gemsMod); + } + } + + if (healthMod != 0) + healthCostMod += healthMod; yield return PostActivate(); currentSacrificedCardInfos.Clear(); @@ -220,14 +252,17 @@ private IEnumerator ChooseSacrifices() manager.currentSacrifices.Clear(); } - private bool CanAfford() - { - if (BloodCost <= 0 || SacrificeValue() >= BloodCost) - { - if (base.Card.Health >= HealthCost) - { - if (Singleton.Instance.PlayerEnergy >= EnergyCost) - return Singleton.Instance.PlayerBones >= BonesCost; + private bool CanAfford() { + // if no blood cost or we can fulfill our blood cost + if (BloodCost < 1 || SacrificeValue() >= BloodCost) { + // if we have enough health, Energy, and Bones + if (base.Card.Health >= HealthCost && ResourcesManager.Instance.PlayerEnergy >= EnergyCost && ResourcesManager.Instance.PlayerBones >= BonesCost) { + bool enoughOrange = ResourcesManagerHelpers.GemCount(!base.Card.OpponentCard, GemType.Orange) >= GemsCost.Count(x => x == GemType.Orange); + bool enoughGreen = ResourcesManagerHelpers.GemCount(!base.Card.OpponentCard, GemType.Green) >= GemsCost.Count(x => x == GemType.Green); + bool enoughBlue = ResourcesManagerHelpers.GemCount(!base.Card.OpponentCard, GemType.Blue) >= GemsCost.Count(x => x == GemType.Blue); + + // if we have enough gems + return enoughOrange && enoughGreen && enoughBlue; } } return false; diff --git a/InscryptionAPI/Card/TribeManager.cs b/InscryptionAPI/Card/TribeManager.cs index 0b96aca4..a654326e 100644 --- a/InscryptionAPI/Card/TribeManager.cs +++ b/InscryptionAPI/Card/TribeManager.cs @@ -16,6 +16,24 @@ namespace InscryptionAPI.Card; [HarmonyPatch] public static class TribeManager { + /// + /// The internal object used to store all relevant info about a Tribe. + /// guid - The mod GUID that added the Tribe. + /// name - The internal name of the Tribe. + /// tribe - The enum value corresponding to this Tribe. + /// icon - The sprite displayed on cards with this Tribe. + /// tribeChoice - Whether or not this Tribe can appear at card tribe choice nodes. + /// cardBack - The texture displayed at card tribe choice nodes. If null, the API will create one using the icon Sprite. + /// + public class TribeInfo { + public string guid; + public string name; + public Tribe tribe; + public Sprite icon; + public bool tribeChoice; + public Texture2D cardback; + } + private static readonly List tribes = new(); private static readonly List tribeTypes = new(); public static readonly ReadOnlyCollection NewTribes = new(tribes); @@ -23,97 +41,6 @@ public static class TribeManager private static readonly Texture2D TribeIconMissing = TextureHelper.GetImageAsTexture("tribeicon_none.png", Assembly.GetExecutingAssembly()); - [HarmonyPatch(typeof(CardDisplayer3D), nameof(CardDisplayer3D.UpdateTribeIcon))] - [HarmonyPostfix] - private static void UpdateTribeIcon(CardDisplayer3D __instance, CardInfo info) - { - if (info == null || __instance.tribeIconRenderers.Count == 0) - return; - - foreach (TribeInfo tribeInfo in tribes) - { - if (tribeInfo?.icon == null || info.IsNotOfTribe(tribeInfo.tribe)) - continue; - - bool foundSpriteRenderer = false; - foreach (SpriteRenderer spriteRenderer in __instance.tribeIconRenderers) - { - if (spriteRenderer.sprite == null) - { - spriteRenderer.sprite = tribeInfo.icon; - foundSpriteRenderer = true; - break; - } - } - if (!foundSpriteRenderer) - { - SpriteRenderer last = __instance.tribeIconRenderers.Last(); - SpriteRenderer spriteRenderer = UnityObject.Instantiate(last); - spriteRenderer.transform.parent = last.transform.parent; - spriteRenderer.transform.localPosition = last.transform.localPosition + (__instance.tribeIconRenderers[1].transform.localPosition - __instance.tribeIconRenderers[0].transform.localPosition); - } - } - } - - [HarmonyPatch(typeof(CardSingleChoicesSequencer), nameof(CardSingleChoicesSequencer.GetCardbackTexture))] - [HarmonyPostfix] - private static void GetCardbackTexture(ref Texture __result, CardChoice choice) - { - if (choice != null && choice.tribe != Tribe.None && __result == null) - { - __result = tribes.Find(x => x?.tribe == choice.tribe)?.cardback; - } - } - - [HarmonyPatch(typeof(Part1CardChoiceGenerator), nameof(Part1CardChoiceGenerator.GenerateTribeChoices))] - [HarmonyPrefix] - private static bool GenerateTribeChoices(ref List __result, int randomSeed) - { - // create list of chooseable vanilla tribes then add all chooseable custom tribes - List list = new() - { - Tribe.Bird, - Tribe.Canine, - Tribe.Hooved, - Tribe.Insect, - Tribe.Reptile - }; - list.AddRange(TribeManager.tribes.FindAll((x) => x != null && x.tribeChoice).ConvertAll((x) => x.tribe)); - // create a list of this region's dominant tribes - List tribes = new(RunState.CurrentMapRegion.dominantTribes); - // get a list of cards obtainable at choice nodes - List obtainableCards = CardManager.AllCardsCopy.FindAll(c => c.HasCardMetaCategory(CardMetaCategory.ChoiceNode)); - // remove all non-chooseable tribes and all tribes with no cards - tribes.RemoveAll(t => (TribeManager.tribes.Exists(ct => ct.tribe == t && !ct.tribeChoice)) || !obtainableCards.Exists(c => c.IsOfTribe(t))); - list.RemoveAll(t => tribes.Contains(t) || !obtainableCards.Exists(c => c.IsOfTribe(t))); - // if list is empty, add Insect as a fallback - if (list.Count == 0) - list.Add(Tribe.Insect); - - while (tribes.Count < 3) - { - Tribe item = list[SeededRandom.Range(0, list.Count, randomSeed++)]; - tribes.Add(item); - if (list.Count > 1) // prevents softlock - list.Remove(item); - } - while (tribes.Count > 3) // if there are more than 3 tribes, reduce it to 3 - tribes.RemoveAt(SeededRandom.Range(0, tribes.Count, randomSeed++)); - - // randomise the order the Tribes will be displayed - List list2 = new(); - foreach (Tribe tribe in tribes.Randomize()) - { - list2.Add(new CardChoice - { - tribe = tribe - }); - } - - __result = list2; - return false; - } - /// /// Adds a new tribe to the game. /// @@ -140,6 +67,62 @@ public static Tribe Add(string guid, string name, Texture2D tribeIcon = null, bo tribeTypes.Add(tribe); return tribe; } + + /// + /// Adds a new tribe to the game + /// + /// The guid of the mod adding the tribe. + /// The name of the tribe. + /// Path to the tribal icon that will appear as a watermark on all cards belonging to this tribe. + /// Indicates if the card should appear in tribal choice nodes. + /// Path to the card back texture to display if the card should appear in tribal choice nodes. + /// The unique identifier for the new tribe. + public static Tribe Add(string guid, string name, string pathToTribeIcon = null, bool appearInTribeChoices = false, string pathToChoiceCardBackTexture = null) { + // Reason for 'is not null' is because if we pass 'null' to GetImageAsTexture, It will throw an exception. + return Add(guid, name, pathToTribeIcon is not null ? TextureHelper.GetImageAsTexture(pathToTribeIcon) : null, appearInTribeChoices, pathToChoiceCardBackTexture is not null ? TextureHelper.GetImageAsTexture(pathToChoiceCardBackTexture) : null); + } + + public static bool IsCustomTribe(Tribe tribe) => tribeTypes.Contains(tribe); + + /// + /// Retrieves the TribeInfo object associated with the given Tribe enum value. + /// + /// Tribe to retrieve the TribeInfo of. + /// Only custom Tribes have an associated TribeInfo; returns null for vanilla Tribes. + public static TribeInfo GetCustomTribeInfo(Tribe tribe) => tribes.Find(x => x.tribe == tribe); + + /// + /// Returns the icon texture associated with the given Tribe. + /// + /// Tribe to get the icon of. + /// If true, return the missing icon texure when the checked Tribe has no icon texture. + public static Texture2D GetTribeIcon(Tribe tribe, bool useMissingIconIfNull = true) { + Texture2D texture2D = null; + if (IsCustomTribe(tribe)) { + foreach (TribeInfo tribeInfo in NewTribes) { + if (tribeInfo.tribe != tribe) + continue; + + if (tribeInfo.icon != null && tribeInfo.icon.texture != null) + texture2D = tribeInfo.icon.texture; + + break; + } + } + else { + // Vanilla tribe icon + string str = "Art/Cards/TribeIcons/tribeicon_" + tribe.ToString().ToLowerInvariant(); + Sprite sprite = ResourceBank.Get(str); + if (sprite != null) + texture2D = sprite.texture; + } + + if (texture2D == null && useMissingIconIfNull) + texture2D = TribeIconMissing; + + return texture2D; + } + private static Texture2D MakePlaceholderCardback(Texture2D tribeIcon) { Texture2D emptyCardback = TextureHelper.GetImageAsTexture("empty_rewardCardBack.png", InscryptionAPIPlugin.APIAssembly); @@ -174,70 +157,86 @@ private static Texture2D MakePlaceholderCardback(Texture2D tribeIcon) emptyCardback.Apply(); return emptyCardback; } - /// - /// Adds a new tribe to the game - /// - /// The guid of the mod adding the tribe. - /// The name of the tribe. - /// Path to the tribal icon that will appear as a watermark on all cards belonging to this tribe. - /// Indicates if the card should appear in tribal choice nodes. - /// Path to the card back texture to display if the card should appear in tribal choice nodes. - /// The unique identifier for the new tribe. - public static Tribe Add(string guid, string name, string pathToTribeIcon = null, bool appearInTribeChoices = false, string pathToChoiceCardBackTexture = null) - { - // Reason for 'is not null' is because if we pass 'null' to GetImageAsTexture, It will thorw an exception. - return Add(guid, name, pathToTribeIcon is not null ? TextureHelper.GetImageAsTexture(pathToTribeIcon) : null, appearInTribeChoices, pathToChoiceCardBackTexture is not null ? TextureHelper.GetImageAsTexture(pathToChoiceCardBackTexture) : null); - } - public static bool IsCustomTribe(Tribe tribe) => tribeTypes.Contains(tribe); + [HarmonyPatch(typeof(CardDisplayer3D), nameof(CardDisplayer3D.UpdateTribeIcon))] + [HarmonyPostfix] + private static void UpdateTribeIcon(CardDisplayer3D __instance, CardInfo info) { + if (info == null || __instance.tribeIconRenderers.Count == 0) + return; - public static Texture2D GetTribeIcon(Tribe tribe, bool useMissingIconIfNull = true) - { - Texture2D texture2D = null; - if (IsCustomTribe(tribe)) - { - foreach (TribeInfo tribeInfo in NewTribes) - { - if (tribeInfo.tribe != tribe) - continue; + foreach (TribeInfo tribeInfo in tribes) { + if (tribeInfo?.icon == null || info.IsNotOfTribe(tribeInfo.tribe)) + continue; - if (tribeInfo.icon != null && tribeInfo.icon.texture != null) - texture2D = tribeInfo.icon.texture; + bool foundSpriteRenderer = false; + foreach (SpriteRenderer spriteRenderer in __instance.tribeIconRenderers) { + if (spriteRenderer.sprite == null) { + spriteRenderer.sprite = tribeInfo.icon; + foundSpriteRenderer = true; + break; + } + } - break; + // create new sprite renderer to hold custom tribe icon if all current renderers are full + if (!foundSpriteRenderer) { + SpriteRenderer last = __instance.tribeIconRenderers.Last(); + SpriteRenderer spriteRenderer = UnityObject.Instantiate(last); + spriteRenderer.transform.parent = last.transform.parent; + spriteRenderer.transform.localPosition = last.transform.localPosition + (__instance.tribeIconRenderers[1].transform.localPosition - __instance.tribeIconRenderers[0].transform.localPosition); } } - else - { - // Vanilla tribe icon - string str = "Art/Cards/TribeIcons/tribeicon_" + tribe.ToString().ToLowerInvariant(); - Sprite sprite = ResourceBank.Get(str); - if (sprite != null) - texture2D = sprite.texture; + } + + [HarmonyPatch(typeof(CardSingleChoicesSequencer), nameof(CardSingleChoicesSequencer.GetCardbackTexture))] + [HarmonyPostfix] + private static void GetCardbackTexture(ref Texture __result, CardChoice choice) { + if (choice != null && choice.tribe != Tribe.None && __result == null) { + __result = tribes.Find(x => x?.tribe == choice.tribe)?.cardback; } + } - if (texture2D == null && useMissingIconIfNull) - texture2D = TribeIconMissing; + [HarmonyPatch(typeof(Part1CardChoiceGenerator), nameof(Part1CardChoiceGenerator.GenerateTribeChoices))] + [HarmonyPrefix] + private static bool GenerateTribeChoices(ref List __result, int randomSeed) { + // create list of chooseable vanilla tribes then add all chooseable custom tribes + List list = new() + { + Tribe.Bird, + Tribe.Canine, + Tribe.Hooved, + Tribe.Insect, + Tribe.Reptile + }; + list.AddRange(TribeManager.tribes.FindAll((x) => x != null && x.tribeChoice).ConvertAll((x) => x.tribe)); + // create a list of this region's dominant tribes + List tribes = new(RunState.CurrentMapRegion.dominantTribes); + // get a list of cards obtainable at choice nodes + List obtainableCards = CardManager.AllCardsCopy.FindAll(c => c.HasCardMetaCategory(CardMetaCategory.ChoiceNode)); + // remove all non-chooseable tribes and all tribes with no cards + tribes.RemoveAll(t => (TribeManager.tribes.Exists(ct => ct.tribe == t && !ct.tribeChoice)) || !obtainableCards.Exists(c => c.IsOfTribe(t))); + list.RemoveAll(t => tribes.Contains(t) || !obtainableCards.Exists(c => c.IsOfTribe(t))); + // if list is empty, add Insect as a fallback + if (list.Count == 0) + list.Add(Tribe.Insect); - return texture2D; - } + while (tribes.Count < 3) { + Tribe item = list[SeededRandom.Range(0, list.Count, randomSeed++)]; + tribes.Add(item); + if (list.Count > 1) // prevents softlock + list.Remove(item); + } + while (tribes.Count > 3) // if there are more than 3 tribes, reduce it to 3 + tribes.RemoveAt(SeededRandom.Range(0, tribes.Count, randomSeed++)); - /// - /// The internal object used to store all relevant info about a Tribe. - /// guid - The mod GUID that added the Tribe. - /// name - The internal name of the Tribe. - /// tribe - The enum value corresponding to this Tribe. - /// icon - The sprite displayed on cards with this Tribe. - /// tribeChoice - Whether or not this Tribe can appear at card tribe choice nodes. - /// cardBack - The texture displayed at card tribe choice nodes. If null, the API will create one using the icon Sprite. - /// - public class TribeInfo - { - public string guid; - public string name; - public Tribe tribe; - public Sprite icon; - public bool tribeChoice; - public Texture2D cardback; + // randomise the order the Tribes will be displayed + List list2 = new(); + foreach (Tribe tribe in tribes.Randomize()) { + list2.Add(new CardChoice { + tribe = tribe + }); + } + + __result = list2; + return false; } } \ No newline at end of file diff --git a/InscryptionAPI/Compatibility/TypeMapper.cs b/InscryptionAPI/Compatibility/TypeMapper.cs index d7faa67c..e4d0625c 100644 --- a/InscryptionAPI/Compatibility/TypeMapper.cs +++ b/InscryptionAPI/Compatibility/TypeMapper.cs @@ -21,16 +21,16 @@ private static Dictionary FieldAccessors { _accessors = new(); - foreach (var field in AccessTools.GetDeclaredFields(typeof(S)).Where(x => !x.GetCustomAttributes(typeof(IgnoreMappingAttribute), false).Any())) + foreach (var _field in AccessTools.GetDeclaredFields(typeof(S)).Where(x => !x.GetCustomAttributes(typeof(IgnoreMappingAttribute), false).Any())) { - var accessor = new DynamicMethodDefinition("get_" + field.Name, typeof(object), new Type[] { typeof(S) }); + var accessor = new DynamicMethodDefinition("get_" + _field.Name, typeof(object), new Type[] { typeof(S) }); var il = accessor.GetILProcessor(); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldfld, accessor.Module.ImportReference(field)); - if (field.FieldType.IsValueType) - il.Emit(OpCodes.Box, field.FieldType); + il.Emit(OpCodes.Ldfld, accessor.Module.ImportReference(_field)); + if (_field.FieldType.IsValueType) + il.Emit(OpCodes.Box, _field.FieldType); il.Emit(OpCodes.Ret); - _accessors.Add(field.Name, accessor.Generate()); + _accessors.Add(_field.Name, accessor.Generate()); } } return _accessors; @@ -46,16 +46,16 @@ private static Dictionary FieldSetters { _setters = new(); - foreach (var field in AccessTools.GetDeclaredFields(typeof(D))) + foreach (var _field in AccessTools.GetDeclaredFields(typeof(D))) { - var setter = new DynamicMethodDefinition("set_" + field.Name, typeof(void), new Type[] { typeof(D), typeof(object) }); + var setter = new DynamicMethodDefinition("set_" + _field.Name, typeof(void), new Type[] { typeof(D), typeof(object) }); var il = setter.GetILProcessor(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Unbox_Any, setter.Module.ImportReference(field.FieldType)); - il.Emit(OpCodes.Stfld, setter.Module.ImportReference(field)); + il.Emit(OpCodes.Unbox_Any, setter.Module.ImportReference(_field.FieldType)); + il.Emit(OpCodes.Stfld, setter.Module.ImportReference(_field)); il.Emit(OpCodes.Ret); - _setters.Add(field.Name, setter.Generate()); + _setters.Add(_field.Name, setter.Generate()); } } return _setters; @@ -64,12 +64,12 @@ private static Dictionary FieldSetters public static D Convert(S source, D destination) { - foreach (var field in FieldAccessors) + foreach (var _field in FieldAccessors) { - object val = field.Value.Invoke(null, new object[] { source }); - if (val is not null && FieldSetters.ContainsKey(field.Key)) + object val = _field.Value.Invoke(null, new object[] { source }); + if (val is not null && FieldSetters.ContainsKey(_field.Key)) { - FieldSetters[field.Key].Invoke(null, new object[] { destination, val }); + FieldSetters[_field.Key].Invoke(null, new object[] { destination, val }); } } diff --git a/InscryptionAPI/Helpers/ResourcesManagerHelpers.cs b/InscryptionAPI/Helpers/ResourcesManagerHelpers.cs index 95170918..c872e039 100644 --- a/InscryptionAPI/Helpers/ResourcesManagerHelpers.cs +++ b/InscryptionAPI/Helpers/ResourcesManagerHelpers.cs @@ -68,6 +68,13 @@ public static int GemsOfType(this ResourcesManager instance, GemType gem) { return instance.gems.Count(x => x == gem); } + + /// + /// Counts how many gems of the given type are owned by the specified player. + /// + /// True to check the player's gems or false to check the opponent's gems. + /// GemType to get the count of. + /// The number of gems of the given type that are owned by the player/opponent. public static int GemCount(bool playerGems, GemType gemToCheck) { if (playerGems) diff --git a/InscryptionAPI/InscryptionAPI.csproj b/InscryptionAPI/InscryptionAPI.csproj index bea651c4..5a9b03f3 100644 --- a/InscryptionAPI/InscryptionAPI.csproj +++ b/InscryptionAPI/InscryptionAPI.csproj @@ -10,7 +10,7 @@ full false true - 2.23.3 + 2.23.4 diff --git a/InscryptionAPI/InscryptionAPIPlugin.cs b/InscryptionAPI/InscryptionAPIPlugin.cs index 4002ce6c..7ae842b2 100644 --- a/InscryptionAPI/InscryptionAPIPlugin.cs +++ b/InscryptionAPI/InscryptionAPIPlugin.cs @@ -31,7 +31,7 @@ public class InscryptionAPIPlugin : BaseUnityPlugin { public const string ModGUID = "cyantist.inscryption.api"; public const string ModName = "InscryptionAPI"; - public const string ModVer = "2.23.3"; + public const string ModVer = "2.23.4"; public static string Directory = ""; diff --git a/InscryptionCommunityPatch/Assets/act1_gemify_attack.png b/InscryptionCommunityPatch/Assets/act1_gemify_attack.png new file mode 100644 index 00000000..23d8a538 Binary files /dev/null and b/InscryptionCommunityPatch/Assets/act1_gemify_attack.png differ diff --git a/InscryptionCommunityPatch/Assets/act1_gemify_base.png b/InscryptionCommunityPatch/Assets/act1_gemify_base.png new file mode 100644 index 00000000..28372295 Binary files /dev/null and b/InscryptionCommunityPatch/Assets/act1_gemify_base.png differ diff --git a/InscryptionCommunityPatch/Assets/act1_gemify_cost.png b/InscryptionCommunityPatch/Assets/act1_gemify_cost.png new file mode 100644 index 00000000..b604c86a Binary files /dev/null and b/InscryptionCommunityPatch/Assets/act1_gemify_cost.png differ diff --git a/InscryptionCommunityPatch/Assets/act1_gemify_health.png b/InscryptionCommunityPatch/Assets/act1_gemify_health.png new file mode 100644 index 00000000..3e4ba912 Binary files /dev/null and b/InscryptionCommunityPatch/Assets/act1_gemify_health.png differ diff --git a/InscryptionCommunityPatch/Card/Part1GemifyIndicator.cs b/InscryptionCommunityPatch/Card/Part1GemifyIndicator.cs new file mode 100644 index 00000000..6ac57709 --- /dev/null +++ b/InscryptionCommunityPatch/Card/Part1GemifyIndicator.cs @@ -0,0 +1,145 @@ +using DiskCardGame; +using HarmonyLib; +using InscryptionAPI.Card; +using InscryptionAPI.Helpers; +using System; +using System.Collections.Generic; +using System.Text; +using UnityEngine; + +namespace InscryptionCommunityPatch.Card; +[HarmonyPatch] +public class Part1GemifyIndicator { + // 0 = base gemify + // 1 = power + // 2 = health + // 3 = cost + public static Renderer[] GemifyRenderers = new Renderer[4]; + + [HarmonyPostfix, HarmonyPatch(typeof(CardDisplayer3D), nameof(CardDisplayer3D.DisplayInfo))] + private static void HandleGemifyAct1(CardRenderInfo renderInfo, PlayableCard playableCard) { + if (!SaveManager.SaveFile.IsPart1) { + return; + } + + if (GemifyRenderers[0] == null) { + AddAct1GemifyVisuals(CardRenderCamera.Instance); + } + + // if the card we're rendering if gemified + if (renderInfo.baseInfo.Gemified || renderInfo.temporaryMods.Exists(x => x.gemify) || (playableCard != null && playableCard.IsGemified())) { + bool activateAttack = false; + bool activateHealth = false; + bool activateCost = false; + if (playableCard != null) { + if (playableCard.OpponentCard) { + activateAttack = OpponentGemsManager.Instance.HasGem(GemType.Orange); + activateHealth = OpponentGemsManager.Instance.HasGem(GemType.Green); + activateCost = OpponentGemsManager.Instance.HasGem(GemType.Blue); + } + else { + activateAttack = ResourcesManager.Instance.HasGem(GemType.Orange); + activateHealth = ResourcesManager.Instance.HasGem(GemType.Green); + activateCost = ResourcesManager.Instance.HasGem(GemType.Blue); + } + } + + // always render base texture + GemifyRenderers[0].enabled = true; + GemifyRenderers[1].enabled = activateAttack; + GemifyRenderers[2].enabled = activateHealth; + GemifyRenderers[3].enabled = activateCost; + } + else { + // turn off renderers by default + GemifyRenderers[0].enabled = false; + GemifyRenderers[1].enabled = false; + GemifyRenderers[2].enabled = false; + GemifyRenderers[3].enabled = false; + } + } + + public static void AddAct1GemifyVisuals(CardRenderCamera cardRenderCamera) { + CardDisplayer3D dis = cardRenderCamera.cardDisplayer as CardDisplayer3D; + + GameObject obj = new("GemifyTest"); + obj.transform.SetParent(cardRenderCamera.cardDisplayer.transform); + obj.transform.localPosition = Vector3.zero; + obj.transform.localScale = Vector3.one; + + // in order to get the gemify visualisers to appear correctly, we need to change the rendering order + // of basically every element in the card displayer + + // new order: + // portrait: 0 (default) + // gemify renderers: [1, 4] + // most everything else: 5 + // card cost: 6 + // decal renderers: [7, 11] + dis.healthText.gameObject.GetComponent().sortingOrder = 5; + dis.attackText.gameObject.GetComponent().sortingOrder = 5; + dis.nameText.gameObject.GetComponent().sortingOrder = 5; + dis.nameGraphicRenderer.sortingOrder = 5; + dis.emissivePortraitRenderer.sortingOrder = 5; + dis.costRenderer.sortingOrder = 6; + foreach (Renderer r in dis.tribeIconRenderers) { + r.sortingOrder = 5; + } + + for (int i = 0; i < dis.decalRenderers.Count; i++) { + dis.decalRenderers[i].sortingOrder = 7 + i; + dis.decalRenderers[i].GetComponent().sortingOrder = 7 + i; + } + + List acts = dis.AbilityIcons.GetComponentsInChildren(true).ToList(); + foreach (Transform tr in dis.transform) { + acts.Concat(tr.GetComponentsInChildren(true)); + } + foreach (AbilityIconInteractable a in acts) { + Renderer rend = a.GetComponent(); + if (rend.sortingOrder == 0) { + rend.sortingOrder = 5; + + SetSortingLayer layer = a.GetComponent(); + if (layer != null) { + layer.sortingOrder = 5; + } + } + } + + System.Reflection.Assembly asm = typeof(CommunityArtPatches).Assembly; + + // add new renderers for gemify textures, using the decal renderers as the base + GameObject obj1 = GameObject.Instantiate(dis.decalRenderers[0].gameObject, obj.transform); + GemifyRenderers[0] = obj1.GetComponent(); + GemifyRenderers[0].sortingOrder = obj1.GetComponent().sortingOrder = 1; + GemifyRenderers[0].enabled = true; + GemifyRenderers[0].material.mainTexture = TextureHelper.GetImageAsTexture($"act1_gemify_base.png", asm); + + GameObject obj2 = GameObject.Instantiate(dis.decalRenderers[0].gameObject, obj.transform); + GemifyRenderers[1] = obj2.GetComponent(); + GemifyRenderers[1].sortingOrder = obj2.GetComponent().sortingOrder = 2; + GemifyRenderers[1].enabled = true; + GemifyRenderers[1].material.mainTexture = TextureHelper.GetImageAsTexture($"act1_gemify_attack.png", asm); + + GameObject obj3 = GameObject.Instantiate(dis.decalRenderers[0].gameObject, obj.transform); + GemifyRenderers[2] = obj3.GetComponent(); + GemifyRenderers[2].sortingOrder = obj3.GetComponent().sortingOrder = 3; + GemifyRenderers[2].enabled = true; + GemifyRenderers[2].material.mainTexture = TextureHelper.GetImageAsTexture($"act1_gemify_health.png", asm); + + GameObject obj4 = GameObject.Instantiate(dis.decalRenderers[0].gameObject, obj.transform); + GemifyRenderers[3] = obj4.GetComponent(); + GemifyRenderers[3].sortingOrder = obj4.GetComponent().sortingOrder = 4; + GemifyRenderers[3].enabled = true; + GemifyRenderers[3].material.mainTexture = TextureHelper.GetImageAsTexture($"act1_gemify_cost.png", asm); + } + + [HarmonyPrefix, HarmonyPatch(typeof(CardRenderCamera), nameof(CardRenderCamera.UpdateTextureWhenReady))] + private static bool AddGemifyRenderersBeforeUpdateTexture(CardRenderCamera __instance) { + if (SaveManager.SaveFile.IsPart1 && GemifyRenderers[0] == null) { + AddAct1GemifyVisuals(__instance); + } + return true; + } +} diff --git a/InscryptionCommunityPatch/Card/Vanilla Ability Patches/GemsDrawFix.cs b/InscryptionCommunityPatch/Card/Vanilla Ability Patches/GemsDrawFix.cs index 50ae655d..d9ae9a50 100644 --- a/InscryptionCommunityPatch/Card/Vanilla Ability Patches/GemsDrawFix.cs +++ b/InscryptionCommunityPatch/Card/Vanilla Ability Patches/GemsDrawFix.cs @@ -23,7 +23,7 @@ public static IEnumerator BetterGemsDraw(GemsDraw __instance) Singleton.Instance.SwitchToView(SaveManager.SaveFile.IsMagnificus ? View.WizardBattleSlots : View.Default); yield return new WaitForSeconds(0.1f); - int numGems = Singleton.Instance.PlayerSlotsCopy.Count(x => x.Card != null && x.Card.HasTrait(Trait.Gem)); + int numGems = Singleton.Instance.GetSlots(!__instance.Card.OpponentCard).Count(x => x.Card != null && x.Card.HasTrait(Trait.Gem)); if (numGems == 0) { diff --git a/InscryptionCommunityPatch/PixelTutor/PixelPlayableCardArray.cs b/InscryptionCommunityPatch/PixelTutor/PixelPlayableCardArray.cs index 698876c5..e50db0bb 100644 --- a/InscryptionCommunityPatch/PixelTutor/PixelPlayableCardArray.cs +++ b/InscryptionCommunityPatch/PixelTutor/PixelPlayableCardArray.cs @@ -235,6 +235,11 @@ protected IEnumerator CleanUpCards() protected IEnumerator SpawnAndPlaceCards(List cards, int numRows, int pageIndex, bool isDeckReview = false, bool forPositiveEffect = true) { + if (numRows < 1) { + PatchPlugin.Logger.LogDebug($"NumRows for PixelPlayableCardArray is 0, displaying no cards"); + yield break; + } + SetOverlayEnabled(true); SetBoardEnabled(false); displayedCards.ForEach(delegate (PixelPlayableCard x) @@ -244,11 +249,6 @@ protected IEnumerator SpawnAndPlaceCards(List cards, int numRows, int }); displayedCards.Clear(); - if (numRows < 1) - { - PatchPlugin.Logger.LogDebug($"NumRows for PixelPlayableCardArray is 0, displaying no cards"); - yield break; - } // can only show 42 cards per page int maxPerPage = maxRows * maxCardsPerRow; int startingIndex = maxPerPage * pageIndex; @@ -296,6 +296,10 @@ private void InitializeGamepadGrid() private PixelPlayableCard CreateAndPlaceCard(CardInfo info, Vector3 cardPos) { PixelPlayableCard component = Instantiate(gameObjectReference, base.transform); + component.transform.position = Vector3.zeroVector; + component.SetInfo(info); + component.SetFaceDown(false, true); + component.SetEnabled(enabled: false); PixelCardAnimationController controller = component.Anim as PixelCardAnimationController; controller.cardRenderer.sortingGroupID = gameObjectReference.Anim.cardRenderer.sortingGroupID; @@ -303,11 +307,6 @@ private PixelPlayableCard CreateAndPlaceCard(CardInfo info, Vector3 cardPos) controller.cardRenderer.sortingOrder = -9000; controller.cardbackRenderer.sortingOrder = -8000; - component.SetFaceDown(false, true); - component.SetInfo(info); - component.SetEnabled(enabled: false); - component.transform.position = Vector3.zeroVector; - gamepadGrid.Rows[0].interactables.Add(component); displayedCards.Add(component); TweenInCard(component.transform, cardPos); @@ -338,7 +337,15 @@ private void SetOverlayEnabled(bool enabled) } } - private int GetNumRows(int numCards) => Mathf.Min(maxRows, numCards / maxCardsPerRow); + private int GetNumRows(int numCards) { + if (numCards == 0) + return 0; + + if (numCards < maxCardsPerRow) + return 1; + + return Mathf.Min(maxRows, numCards / maxCardsPerRow); + } private void SetCardsEnabled(bool enabled) => displayedCards.ForEach(x => x.SetEnabled(enabled)); private void SetBoardEnabled(bool enabled) {