diff --git a/CardSets/ExampleSet/images/pack.png b/CardSets/ExampleSet/images/pack.png new file mode 100644 index 0000000..b343c3f Binary files /dev/null and b/CardSets/ExampleSet/images/pack.png differ diff --git a/TradingCardMod.csproj b/TradingCardMod.csproj index 76f4029..f41943c 100644 --- a/TradingCardMod.csproj +++ b/TradingCardMod.csproj @@ -50,6 +50,9 @@ $(DuckovPath)$(SubPath)UnityEngine.InputLegacyModule.dll + + $(DuckovPath)$(SubPath)UniTask.dll + diff --git a/src/CardPack.cs b/src/CardPack.cs new file mode 100644 index 0000000..bf518a0 --- /dev/null +++ b/src/CardPack.cs @@ -0,0 +1,155 @@ +using System.Collections.Generic; + +namespace TradingCardMod +{ + /// + /// Represents a card pack that can be opened to receive random cards. + /// + public class CardPack + { + /// + /// Display name of the pack. + /// + public string PackName { get; set; } = string.Empty; + + /// + /// The card set this pack draws from. + /// + public string SetName { get; set; } = string.Empty; + + /// + /// Path to the pack's icon image. + /// + public string ImagePath { get; set; } = string.Empty; + + /// + /// Filename of the pack image (before full path is set). + /// + public string ImageFile { get; set; } = string.Empty; + + /// + /// In-game currency value of the pack. + /// + public int Value { get; set; } + + /// + /// Weight for appearing in loot (0 = not lootable). + /// + public float Weight { get; set; } = 0.1f; + + /// + /// The slots in this pack, each with their own drop weights. + /// + public List Slots { get; set; } = new List(); + + /// + /// Whether this is an auto-generated default pack. + /// + public bool IsDefault { get; set; } + + /// + /// Generates a unique TypeID for this pack. + /// Uses hash of set name + pack name for uniqueness. + /// + public int GenerateTypeID() + { + // Start from 300000 range for packs (cards are 100000+, storage is 200000+) + string uniqueKey = $"CardPack_{SetName}_{PackName}"; + return 300000 + System.Math.Abs(uniqueKey.GetHashCode() % 100000); + } + } + + /// + /// Represents a single slot in a card pack with drop weights. + /// Each slot can use either rarity-based or card-specific weights. + /// + public class PackSlot + { + /// + /// Whether this slot uses rarity-based weights (true) or specific card weights (false). + /// + public bool UseRarityWeights { get; set; } = true; + + /// + /// Rarity-based weights. Key = rarity string (e.g., "Common"), Value = weight. + /// Used when UseRarityWeights is true. + /// + public Dictionary RarityWeights { get; set; } = new Dictionary(); + + /// + /// Specific card weights. Key = card name, Value = weight. + /// Used when UseRarityWeights is false. + /// + public Dictionary CardWeights { get; set; } = new Dictionary(); + } + + /// + /// Default slot configurations for auto-generated packs. + /// + public static class DefaultPackSlots + { + /// + /// Slot 1: Favors common cards. + /// + public static PackSlot CommonSlot => new PackSlot + { + UseRarityWeights = true, + RarityWeights = new Dictionary + { + { "Common", 100f }, + { "Uncommon", 30f }, + { "Rare", 5f }, + { "Very Rare", 1f }, + { "Ultra Rare", 0f }, + { "Legendary", 0f } + } + }; + + /// + /// Slot 2: Balanced towards uncommon. + /// + public static PackSlot UncommonSlot => new PackSlot + { + UseRarityWeights = true, + RarityWeights = new Dictionary + { + { "Common", 60f }, + { "Uncommon", 80f }, + { "Rare", 20f }, + { "Very Rare", 5f }, + { "Ultra Rare", 1f }, + { "Legendary", 1f } + } + }; + + /// + /// Slot 3: Better odds for rare+ cards. + /// + public static PackSlot RareSlot => new PackSlot + { + UseRarityWeights = true, + RarityWeights = new Dictionary + { + { "Common", 30f }, + { "Uncommon", 60f }, + { "Rare", 60f }, + { "Very Rare", 20f }, + { "Ultra Rare", 5f }, + { "Legendary", 5f } + } + }; + + /// + /// Gets the default slots for an auto-generated pack (3 slots). + /// + public static List GetDefaultSlots() + { + return new List + { + CommonSlot, + UncommonSlot, + RareSlot + }; + } + } +} diff --git a/src/ModBehaviour.cs b/src/ModBehaviour.cs index fb5a60b..304d144 100644 --- a/src/ModBehaviour.cs +++ b/src/ModBehaviour.cs @@ -17,6 +17,42 @@ namespace TradingCardMod private static ModBehaviour? _instance; public static ModBehaviour Instance => _instance!; + /// + /// Gets the list of registered card items (for loot injection). + /// + public List GetRegisteredItems() => _registeredItems; + + /// + /// Gets cards for a specific set (used by PackUsageBehavior). + /// + public List GetCardsBySet(string setName) + { + if (_cardsBySet.TryGetValue(setName, out var cards)) + { + return cards; + } + return new List(); + } + + /// + /// Gets the card name to TypeID mapping (used by PackUsageBehavior). + /// + public Dictionary GetCardTypeIds() => _cardNameToTypeId; + + /// + /// Gets slot configuration for a specific pack (used by PackUsageBehavior). + /// + public List GetPackSlots(string setName, string packName) + { + string key = $"{setName}|{packName}"; + if (_packDefinitions.TryGetValue(key, out var pack)) + { + return pack.Slots; + } + Debug.LogWarning($"[TradingCardMod] Pack definition not found: {key}"); + return new List(); + } + // Base game item ID to clone (135 is commonly used for collectibles) private const int BASE_ITEM_ID = 135; @@ -32,6 +68,14 @@ namespace TradingCardMod private Item? _binderItem; private Item? _cardBoxItem; + // Track cards and IDs per set for pack creation + private Dictionary> _cardsBySet = new Dictionary>(); + private Dictionary _cardNameToTypeId = new Dictionary(); + private List _registeredPacks = new List(); + + // Store pack definitions for runtime lookup (key = "SetName|PackName") + private Dictionary _packDefinitions = new Dictionary(); + // Debug: track spawn cycling private int _debugSpawnIndex = 0; private List _allSpawnableItems = new List(); @@ -54,6 +98,9 @@ namespace TradingCardMod try { + // Log all available tags for reference + TagHelper.LogAvailableTags(); + // Create our custom tag first _tradingCardTag = TagHelper.GetOrCreateTradingCardTag(); @@ -63,10 +110,14 @@ namespace TradingCardMod // Create storage items CreateStorageItems(); - // Build spawnable items list (cards + storage) + // Create card packs + CreateCardPacks(); + + // Build spawnable items list (cards + storage + packs) _allSpawnableItems.AddRange(_registeredItems); if (_binderItem != null) _allSpawnableItems.Add(_binderItem); if (_cardBoxItem != null) _allSpawnableItems.Add(_cardBoxItem); + _allSpawnableItems.AddRange(_registeredPacks); Debug.Log("[TradingCardMod] Mod initialized successfully!"); } @@ -102,6 +153,34 @@ namespace TradingCardMod Debug.Log($"[TradingCardMod] Total cards loaded: {_loadedCards.Count}"); Debug.Log($"[TradingCardMod] Total items registered: {_registeredItems.Count}"); Debug.Log("[TradingCardMod] DEBUG: Press F9 to spawn items (cycles through cards, then binder, then box)"); + + // Clear the search cache so our items can be found + ClearSearchCache(); + } + + /// + /// Clears the ItemAssetsCollection search cache so dynamically registered items can be found. + /// + private void ClearSearchCache() + { + try + { + var cacheField = typeof(ItemAssetsCollection).GetField("cachedSearchResults", + System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic); + if (cacheField != null) + { + var cache = cacheField.GetValue(null) as System.Collections.IDictionary; + if (cache != null) + { + cache.Clear(); + Debug.Log("[TradingCardMod] Cleared search cache for loot table integration"); + } + } + } + catch (Exception ex) + { + Debug.LogWarning($"[TradingCardMod] Could not clear search cache: {ex.Message}"); + } } /// @@ -138,6 +217,61 @@ namespace TradingCardMod ); } + /// + /// Creates card packs for each loaded card set. + /// + private void CreateCardPacks() + { + foreach (var setEntry in _cardsBySet) + { + string setName = setEntry.Key; + var cards = setEntry.Value; + + if (cards.Count == 0) + { + Debug.LogWarning($"[TradingCardMod] No cards in set {setName}, skipping pack creation"); + continue; + } + + // Create default pack for this set + string setDirectory = Path.Combine(_modPath, "CardSets", setName); + string imagesDirectory = Path.Combine(setDirectory, "images"); + + CardPack defaultPack = PackParser.CreateDefaultPack(setName, imagesDirectory); + + // Check for user-defined packs + string packsFile = Path.Combine(setDirectory, "packs.txt"); + var userPacks = PackParser.ParseFile(packsFile, setName, imagesDirectory); + + // Create all packs + var allPacks = new List { defaultPack }; + allPacks.AddRange(userPacks); + + foreach (var pack in allPacks) + { + // Store pack definition for runtime lookup + string packKey = $"{pack.SetName}|{pack.PackName}"; + _packDefinitions[packKey] = pack; + + // Load pack icon + Sprite? packIcon = null; + if (File.Exists(pack.ImagePath)) + { + packIcon = LoadSpriteFromFile(pack.ImagePath, pack.GenerateTypeID()); + } + + // Create pack item + Item? packItem = PackHelper.CreatePackItem(pack, packIcon); + if (packItem != null) + { + _registeredPacks.Add(packItem); + } + } + } + + Debug.Log($"[TradingCardMod] Created {_registeredPacks.Count} card packs"); + } + /// /// Update is called every frame. Used for debug input handling. /// @@ -162,14 +296,23 @@ namespace TradingCardMod } // Cycle through all spawnable items - Item itemToSpawn = _allSpawnableItems[_debugSpawnIndex % _allSpawnableItems.Count]; + Item prefab = _allSpawnableItems[_debugSpawnIndex % _allSpawnableItems.Count]; _debugSpawnIndex++; try { - // Use game's utility to give item to player - ItemUtilities.SendToPlayer(itemToSpawn); - Debug.Log($"[TradingCardMod] Spawned: {itemToSpawn.DisplayName} (ID: {itemToSpawn.TypeID})"); + // Instantiate a fresh copy of the item (don't send prefab directly) + Item instance = ItemAssetsCollection.InstantiateSync(prefab.TypeID); + if (instance != null) + { + // Use game's utility to give item to player + ItemUtilities.SendToPlayer(instance); + Debug.Log($"[TradingCardMod] Spawned: {instance.DisplayName} (ID: {instance.TypeID})"); + } + else + { + Debug.LogError($"[TradingCardMod] Failed to instantiate {prefab.DisplayName} (ID: {prefab.TypeID})"); + } } catch (Exception ex) { @@ -200,6 +343,12 @@ namespace TradingCardMod string imagesDirectory = Path.Combine(setDirectory, "images"); var cards = CardParser.ParseFile(cardsFile, imagesDirectory); + // Initialize set tracking + if (!_cardsBySet.ContainsKey(setName)) + { + _cardsBySet[setName] = new List(); + } + foreach (var card in cards) { // Validate card @@ -214,6 +363,7 @@ namespace TradingCardMod } _loadedCards.Add(card); + _cardsBySet[setName].Add(card); // Register card as game item RegisterCardWithGame(card); @@ -261,12 +411,14 @@ namespace TradingCardMod item.SetPrivateField("typeID", typeId); item.SetPrivateField("weight", card.Weight); - item.SetPrivateField("value", card.Value); item.SetPrivateField("displayName", locKey); - item.SetPrivateField("quality", card.GetQuality()); item.SetPrivateField("order", 0); item.SetPrivateField("maxStackCount", 1); + // Use public setters for properties that have them + item.Value = card.Value; + item.Quality = card.GetQuality(); + // Set display quality based on rarity SetDisplayQuality(item, card.GetQuality()); @@ -280,6 +432,25 @@ namespace TradingCardMod item.Tags.Add(luxuryTag); } + // ============================================================ + // TODO: REMOVE BEFORE RELEASE - TEST TAGS FOR LOOT SPAWNING + // These tags make cards appear frequently in loot for testing. + // Replace with appropriate tags (Collection, Misc, etc.) or + // implement proper loot table integration before shipping. + // ============================================================ + Tag? medicTag = TagHelper.GetTargetTag("Medic"); + if (medicTag != null) + { + item.Tags.Add(medicTag); + } + + Tag? toolTag = TagHelper.GetTargetTag("Tool"); + if (toolTag != null) + { + item.Tags.Add(toolTag); + } + // ============================================================ + // Add our custom TradingCard tag if (_tradingCardTag != null) { @@ -305,6 +476,7 @@ namespace TradingCardMod if (ItemAssetsCollection.AddDynamicEntry(item)) { _registeredItems.Add(item); + _cardNameToTypeId[card.CardName] = typeId; Debug.Log($"[TradingCardMod] Registered: {card.CardName} (ID: {typeId})"); } else @@ -440,10 +612,17 @@ namespace TradingCardMod // Clean up storage items StorageHelper.Cleanup(); + // Clean up packs + PackHelper.Cleanup(); + // Clean up tags TagHelper.Cleanup(); _loadedCards.Clear(); + _cardsBySet.Clear(); + _cardNameToTypeId.Clear(); + _registeredPacks.Clear(); + _packDefinitions.Clear(); _allSpawnableItems.Clear(); Debug.Log("[TradingCardMod] Cleanup complete."); diff --git a/src/PackHelper.cs b/src/PackHelper.cs new file mode 100644 index 0000000..8ef1e1a --- /dev/null +++ b/src/PackHelper.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using ItemStatsSystem; +using SodaCraft.Localizations; +using Duckov.Utilities; + +namespace TradingCardMod +{ + /// + /// Helper class for creating card pack items. + /// + public static class PackHelper + { + // Base item to clone for packs (same as cards) + private const int BASE_ITEM_ID = 135; + + // Track created packs for cleanup + private static readonly List _createdPacks = new List(); + private static readonly List _createdGameObjects = new List(); + + /// + /// Creates a card pack item with gacha functionality. + /// + public static Item? CreatePackItem( + CardPack pack, + Sprite? icon = null) + { + try + { + // Get base item to clone + Item original = ItemAssetsCollection.GetPrefab(BASE_ITEM_ID); + if (original == null) + { + Debug.LogError($"[TradingCardMod] Base item ID {BASE_ITEM_ID} not found for pack!"); + return null; + } + + // Clone the item + GameObject clone = UnityEngine.Object.Instantiate(original.gameObject); + clone.name = $"CardPack_{pack.SetName}_{pack.PackName}"; + UnityEngine.Object.DontDestroyOnLoad(clone); + _createdGameObjects.Add(clone); + + Item item = clone.GetComponent(); + if (item == null) + { + Debug.LogError("[TradingCardMod] Cloned pack object has no Item component!"); + return null; + } + + // Set item properties + int typeId = pack.GenerateTypeID(); + string locKey = $"TC_Pack_{pack.SetName}_{pack.PackName}".Replace(" ", "_"); + + item.SetPrivateField("typeID", typeId); + item.SetPrivateField("weight", pack.Weight); + item.SetPrivateField("displayName", locKey); + item.SetPrivateField("order", 0); + item.SetPrivateField("maxStackCount", 10); // Packs can stack + + // Use public setters + item.Value = pack.Value; + item.Quality = 3; // Uncommon quality for packs + item.DisplayQuality = (DisplayQuality)3; + + // Set tags + item.Tags.Clear(); + + // Add Misc tag for loot spawning + Tag? miscTag = TagHelper.GetTargetTag("Misc"); + if (miscTag != null) + { + item.Tags.Add(miscTag); + } + + // Set icon if provided + if (icon != null) + { + item.SetPrivateField("icon", icon); + } + + // Set up UsageUtilities for the "Use" context menu + // First, get or create UsageUtilities component + UsageUtilities usageUtils = clone.GetComponent(); + if (usageUtils == null) + { + usageUtils = clone.AddComponent(); + } + + // Clear any existing behaviors from the cloned base item + usageUtils.behaviors.Clear(); + + // Add our custom usage behavior for gacha + var usageBehavior = clone.AddComponent(); + usageBehavior.SetName = pack.SetName; + usageBehavior.PackName = pack.PackName; + // Note: Slots, AvailableCards, and CardTypeIds are looked up at runtime via ModBehaviour + + // Register our behavior with UsageUtilities + usageUtils.behaviors.Add(usageBehavior); + + // Set the Item's usageUtilities field to enable the Use option + item.SetPrivateField("usageUtilities", usageUtils); + + // Set localization + LocalizationManager.SetOverrideText(locKey, pack.PackName); + string slotDesc = pack.Slots.Count == 1 ? "1 card" : $"{pack.Slots.Count} cards"; + LocalizationManager.SetOverrideText($"{locKey}_Desc", + $"Open to receive {slotDesc} from {pack.SetName}"); + + // Register with game + if (ItemAssetsCollection.AddDynamicEntry(item)) + { + _createdPacks.Add(item); + Debug.Log($"[TradingCardMod] Registered pack: {pack.PackName} (ID: {typeId}, Slots: {pack.Slots.Count})"); + return item; + } + else + { + Debug.LogError($"[TradingCardMod] Failed to register pack {pack.PackName}!"); + UnityEngine.Object.Destroy(clone); + _createdGameObjects.Remove(clone); + return null; + } + } + catch (Exception ex) + { + Debug.LogError($"[TradingCardMod] Error creating pack {pack.PackName}: {ex.Message}"); + return null; + } + } + + /// + /// Gets all pack items created by this helper. + /// + public static IReadOnlyList GetCreatedPacks() + { + return _createdPacks.AsReadOnly(); + } + + /// + /// Cleans up all packs created by this helper. + /// + public static void Cleanup() + { + foreach (var item in _createdPacks) + { + if (item != null) + { + ItemAssetsCollection.RemoveDynamicEntry(item); + } + } + _createdPacks.Clear(); + + foreach (var go in _createdGameObjects) + { + if (go != null) + { + UnityEngine.Object.Destroy(go); + } + } + _createdGameObjects.Clear(); + + Debug.Log("[TradingCardMod] PackHelper cleaned up."); + } + } +} diff --git a/src/PackParser.cs b/src/PackParser.cs new file mode 100644 index 0000000..b49b688 --- /dev/null +++ b/src/PackParser.cs @@ -0,0 +1,233 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace TradingCardMod +{ + /// + /// Parses pack definition files (packs.txt) into CardPack objects. + /// + public static class PackParser + { + /// + /// Parses a packs.txt file into a list of CardPack objects. + /// + /// Path to the packs.txt file. + /// The card set name these packs belong to. + /// Directory containing pack images. + /// List of parsed CardPack objects. + public static List ParseFile(string filePath, string setName, string imagesDirectory) + { + var packs = new List(); + + if (!File.Exists(filePath)) + { + return packs; + } + + string[] lines = File.ReadAllLines(filePath); + CardPack? currentPack = null; + + for (int i = 0; i < lines.Length; i++) + { + string line = lines[i]; + string trimmedLine = line.Trim(); + + // Skip empty lines and comments + if (string.IsNullOrWhiteSpace(trimmedLine) || trimmedLine.StartsWith("#")) + { + continue; + } + + // Check if this is a slot definition (indented line) + bool isIndented = line.StartsWith(" ") || line.StartsWith("\t"); + + if (isIndented && currentPack != null) + { + // Parse slot definition + PackSlot? slot = ParseSlotLine(trimmedLine); + if (slot != null) + { + currentPack.Slots.Add(slot); + } + } + else if (!isIndented) + { + // Parse pack header line + CardPack? pack = ParsePackHeader(trimmedLine, setName, imagesDirectory); + if (pack != null) + { + if (currentPack != null) + { + packs.Add(currentPack); + } + currentPack = pack; + } + } + } + + // Don't forget the last pack + if (currentPack != null) + { + packs.Add(currentPack); + } + + return packs; + } + + /// + /// Parses a pack header line. + /// Format: PackName | Image | Value + /// + private static CardPack? ParsePackHeader(string line, string setName, string imagesDirectory) + { + string[] parts = line.Split('|'); + + if (parts.Length < 3) + { + return null; + } + + string packName = parts[0].Trim(); + string imageFile = parts[1].Trim(); + + if (!int.TryParse(parts[2].Trim(), out int value)) + { + value = 100; + } + + float weight = 0.1f; + if (parts.Length >= 4 && float.TryParse(parts[3].Trim(), out float parsedWeight)) + { + weight = parsedWeight; + } + + return new CardPack + { + PackName = packName, + SetName = setName, + ImageFile = imageFile, + ImagePath = Path.Combine(imagesDirectory, imageFile), + Value = value, + Weight = weight, + IsDefault = false + }; + } + + /// + /// Parses a slot definition line. + /// Format: RARITY: Common:100, Uncommon:50, Rare:10 + /// or: CARDS: CardName:100, CardName:50 + /// + private static PackSlot? ParseSlotLine(string line) + { + var slot = new PackSlot(); + + if (line.StartsWith("RARITY:", StringComparison.OrdinalIgnoreCase)) + { + slot.UseRarityWeights = true; + string weightsStr = line.Substring(7).Trim(); + slot.RarityWeights = ParseWeights(weightsStr); + } + else if (line.StartsWith("CARDS:", StringComparison.OrdinalIgnoreCase)) + { + slot.UseRarityWeights = false; + string weightsStr = line.Substring(6).Trim(); + slot.CardWeights = ParseWeights(weightsStr); + } + else + { + // Default to rarity weights if no prefix + slot.UseRarityWeights = true; + slot.RarityWeights = ParseWeights(line); + } + + return slot; + } + + /// + /// Parses a comma-separated list of Name:Weight pairs. + /// + private static Dictionary ParseWeights(string weightsStr) + { + var weights = new Dictionary(); + + string[] pairs = weightsStr.Split(','); + foreach (string pair in pairs) + { + string[] keyValue = pair.Split(':'); + if (keyValue.Length == 2) + { + string key = keyValue[0].Trim(); + if (float.TryParse(keyValue[1].Trim(), out float weight)) + { + weights[key] = weight; + } + } + } + + return weights; + } + + /// + /// Creates a default pack for a card set. + /// + /// The card set name. + /// Directory for pack images. + /// A default CardPack with standard slot weights. + public static CardPack CreateDefaultPack(string setName, string imagesDirectory) + { + string packName = $"{setName} Pack"; + string imageFile = "pack.png"; + + return new CardPack + { + PackName = packName, + SetName = setName, + ImageFile = imageFile, + ImagePath = Path.Combine(imagesDirectory, imageFile), + Value = 100, + Weight = 0.1f, + IsDefault = true, + Slots = DefaultPackSlots.GetDefaultSlots() + }; + } + + /// + /// Validates a CardPack and returns any errors found. + /// + public static List ValidatePack(CardPack pack) + { + var errors = new List(); + + if (string.IsNullOrWhiteSpace(pack.PackName)) + { + errors.Add("Pack name is required"); + } + + if (pack.Slots.Count == 0) + { + errors.Add("Pack must have at least one slot"); + } + + foreach (var slot in pack.Slots) + { + if (slot.UseRarityWeights && slot.RarityWeights.Count == 0) + { + errors.Add("Rarity slot must have at least one weight defined"); + } + else if (!slot.UseRarityWeights && slot.CardWeights.Count == 0) + { + errors.Add("Card slot must have at least one card defined"); + } + } + + if (pack.Value < 0) + { + errors.Add("Pack value must be non-negative"); + } + + return errors; + } + } +} diff --git a/src/PackUsageBehavior.cs b/src/PackUsageBehavior.cs new file mode 100644 index 0000000..23dfca5 --- /dev/null +++ b/src/PackUsageBehavior.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Cysharp.Threading.Tasks; +using ItemStatsSystem; +using UnityEngine; + +namespace TradingCardMod +{ + /// + /// Custom usage behavior for card packs that generates multiple cards based on slot weights. + /// + public class PackUsageBehavior : UsageBehavior + { + /// + /// The card set this pack draws from. + /// + public string SetName = string.Empty; + + /// + /// The pack name within the set (for looking up slot config). + /// + public string PackName = string.Empty; + + private bool _running; + + /// + /// Gets available cards for this set from ModBehaviour. + /// + private List GetAvailableCards() + { + if (ModBehaviour.Instance == null) return new List(); + return ModBehaviour.Instance.GetCardsBySet(SetName); + } + + /// + /// Gets card type IDs from ModBehaviour. + /// + private Dictionary GetCardTypeIds() + { + if (ModBehaviour.Instance == null) return new Dictionary(); + return ModBehaviour.Instance.GetCardTypeIds(); + } + + /// + /// Gets slot configuration from ModBehaviour. + /// + private List GetSlots() + { + if (ModBehaviour.Instance == null) return new List(); + return ModBehaviour.Instance.GetPackSlots(SetName, PackName); + } + + public override DisplaySettingsData DisplaySettings => new DisplaySettingsData + { + display = true, + description = $"Open to receive {GetSlots().Count} trading cards" + }; + + public override bool CanBeUsed(Item item, object user) + { + if (!(user is CharacterMainControl)) + { + return false; + } + return true; + } + + protected override void OnUse(Item item, object user) + { + CharacterMainControl character = user as CharacterMainControl; + var slots = GetSlots(); + if (character != null && slots.Count > 0) + { + GenerateCards(item, character, slots).Forget(); + } + else + { + Debug.LogWarning($"[TradingCardMod] Cannot open pack: character={character != null}, slots={slots.Count}"); + } + } + + private async UniTask GenerateCards(Item packItem, CharacterMainControl character, List slots) + { + if (_running) + { + return; + } + _running = true; + + var generatedCards = new List(); + + try + { + foreach (var slot in slots) + { + int? cardTypeId = RollSlot(slot); + if (cardTypeId.HasValue) + { + Item card = await ItemAssetsCollection.InstantiateAsync(cardTypeId.Value); + if (card != null) + { + string cardName = card.DisplayName; + generatedCards.Add(cardName); + + bool pickedUp = character.PickupItem(card); + if (!pickedUp && card != null) + { + if (card.ActiveAgent != null) + { + card.AgentUtilities.ReleaseActiveAgent(); + } + PlayerStorage.Push(card); + } + } + } + } + + // Show notification + if (generatedCards.Count > 0) + { + string message = $"Received: {string.Join(", ", generatedCards)}"; + Debug.Log($"[TradingCardMod] Pack opened: {message}"); + // NotificationText.Push(message); // Uncomment if NotificationText is accessible + } + + // Consume the pack after successfully generating cards + ConsumeItem(packItem); + } + catch (Exception ex) + { + Debug.LogError($"[TradingCardMod] Error generating cards from pack: {ex.Message}"); + } + + _running = false; + } + + /// + /// Consumes one pack from the stack or detaches if only one left. + /// + private void ConsumeItem(Item item) + { + if (item == null) return; + + if (item.StackCount > 1) + { + item.StackCount -= 1; + } + else + { + item.Detach(); + } + } + + /// + /// Rolls a single slot to determine which card to give. + /// + private int? RollSlot(PackSlot slot) + { + if (slot.UseRarityWeights) + { + return RollByRarity(slot.RarityWeights); + } + else + { + return RollByCardName(slot.CardWeights); + } + } + + /// + /// Selects a card based on rarity weights. + /// + private int? RollByRarity(Dictionary rarityWeights) + { + // Build weighted list of cards based on rarity + var weightedCards = new List<(int typeId, float weight)>(); + var availableCards = GetAvailableCards(); + var cardTypeIds = GetCardTypeIds(); + + foreach (var card in availableCards) + { + if (cardTypeIds.TryGetValue(card.CardName, out int typeId)) + { + // Get the weight for this card's rarity + if (rarityWeights.TryGetValue(card.Rarity, out float weight) && weight > 0) + { + weightedCards.Add((typeId, weight)); + } + } + } + + if (weightedCards.Count == 0) + { + Debug.LogWarning($"[TradingCardMod] No cards available for rarity roll (set: {SetName}, cards: {availableCards.Count})"); + return null; + } + + return WeightedRandom(weightedCards); + } + + /// + /// Selects a card based on specific card name weights. + /// + private int? RollByCardName(Dictionary cardWeights) + { + var weightedCards = new List<(int typeId, float weight)>(); + var cardTypeIds = GetCardTypeIds(); + + foreach (var kvp in cardWeights) + { + if (cardTypeIds.TryGetValue(kvp.Key, out int typeId)) + { + weightedCards.Add((typeId, kvp.Value)); + } + else + { + Debug.LogWarning($"[TradingCardMod] Card '{kvp.Key}' not found for pack slot"); + } + } + + if (weightedCards.Count == 0) + { + Debug.LogWarning("[TradingCardMod] No cards available for card name roll"); + return null; + } + + return WeightedRandom(weightedCards); + } + + /// + /// Performs weighted random selection. + /// + private int WeightedRandom(List<(int typeId, float weight)> items) + { + float totalWeight = items.Sum(x => x.weight); + float roll = UnityEngine.Random.Range(0f, totalWeight); + + float cumulative = 0f; + foreach (var item in items) + { + cumulative += item.weight; + if (roll <= cumulative) + { + return item.typeId; + } + } + + // Fallback to last item + return items[items.Count - 1].typeId; + } + } +} diff --git a/src/Patches.cs b/src/Patches.cs index 9e649fd..ba1061a 100644 --- a/src/Patches.cs +++ b/src/Patches.cs @@ -74,4 +74,7 @@ namespace TradingCardMod } } } + + // Search injection patch disabled - causing index errors + // TODO: Find better approach for loot table integration }