diff --git a/CardSets/ExampleSet/images/bread_seeker.png b/CardSets/ExampleSet/images/bread_seeker.png
new file mode 100644
index 0000000..e2548c5
Binary files /dev/null and b/CardSets/ExampleSet/images/bread_seeker.png differ
diff --git a/CardSets/ExampleSet/images/duck_hero.png b/CardSets/ExampleSet/images/duck_hero.png
new file mode 100644
index 0000000..2b05e33
Binary files /dev/null and b/CardSets/ExampleSet/images/duck_hero.png differ
diff --git a/CardSets/ExampleSet/images/feathered_fury.png b/CardSets/ExampleSet/images/feathered_fury.png
new file mode 100644
index 0000000..9fc5e6d
Binary files /dev/null and b/CardSets/ExampleSet/images/feathered_fury.png differ
diff --git a/CardSets/ExampleSet/images/golden_quacker.png b/CardSets/ExampleSet/images/golden_quacker.png
new file mode 100644
index 0000000..daf13f6
Binary files /dev/null and b/CardSets/ExampleSet/images/golden_quacker.png differ
diff --git a/CardSets/ExampleSet/images/pond_guardian.png b/CardSets/ExampleSet/images/pond_guardian.png
new file mode 100644
index 0000000..082ac9f
Binary files /dev/null and b/CardSets/ExampleSet/images/pond_guardian.png differ
diff --git a/TradingCardMod.csproj b/TradingCardMod.csproj
index bbbed72..76f4029 100644
--- a/TradingCardMod.csproj
+++ b/TradingCardMod.csproj
@@ -44,6 +44,12 @@
$(DuckovPath)$(SubPath)UnityEngine.CoreModule.dll
+
+ $(DuckovPath)$(SubPath)UnityEngine.ImageConversionModule.dll
+
+
+ $(DuckovPath)$(SubPath)UnityEngine.InputLegacyModule.dll
+
@@ -57,4 +63,9 @@
+
+
+
+
+
diff --git a/deploy.sh b/deploy.sh
new file mode 100755
index 0000000..e4247c2
--- /dev/null
+++ b/deploy.sh
@@ -0,0 +1,61 @@
+#!/bin/bash
+# Deploy Trading Card Mod to Escape from Duckov
+# Usage: ./deploy.sh [--release]
+
+set -e
+
+# Configuration
+GAME_PATH="/mnt/NV2/SteamLibrary/steamapps/common/Escape from Duckov"
+MOD_NAME="TradingCardMod"
+MOD_DIR="$GAME_PATH/Duckov_Data/Mods/$MOD_NAME"
+
+# Build configuration
+BUILD_CONFIG="Debug"
+if [[ "$1" == "--release" ]]; then
+ BUILD_CONFIG="Release"
+fi
+
+echo "=== Trading Card Mod Deployment ==="
+echo "Build config: $BUILD_CONFIG"
+echo "Target: $MOD_DIR"
+echo ""
+
+# Build the project
+echo "[1/4] Building project..."
+dotnet build TradingCardMod.csproj -c "$BUILD_CONFIG" --verbosity quiet
+if [[ $? -ne 0 ]]; then
+ echo "ERROR: Build failed!"
+ exit 1
+fi
+echo " Build successful"
+
+# Create mod directory if it doesn't exist
+echo "[2/4] Creating mod directory..."
+mkdir -p "$MOD_DIR"
+mkdir -p "$MOD_DIR/CardSets"
+
+# Copy mod files
+echo "[3/4] Copying mod files..."
+cp "bin/$BUILD_CONFIG/netstandard2.1/$MOD_NAME.dll" "$MOD_DIR/"
+cp "info.ini" "$MOD_DIR/"
+
+# Copy preview if it exists
+if [[ -f "preview.png" ]]; then
+ cp "preview.png" "$MOD_DIR/"
+fi
+
+# Copy card sets
+echo "[4/4] Copying card sets..."
+if [[ -d "CardSets" ]]; then
+ cp -r CardSets/* "$MOD_DIR/CardSets/" 2>/dev/null || true
+fi
+
+echo ""
+echo "=== Deployment Complete ==="
+echo "Mod installed to: $MOD_DIR"
+echo ""
+echo "Contents:"
+ls -la "$MOD_DIR/"
+echo ""
+echo "Card sets:"
+ls -la "$MOD_DIR/CardSets/" 2>/dev/null || echo " (none)"
diff --git a/remove.sh b/remove.sh
new file mode 100755
index 0000000..4ceac6d
--- /dev/null
+++ b/remove.sh
@@ -0,0 +1,46 @@
+#!/bin/bash
+# Remove Trading Card Mod from Escape from Duckov
+# Usage: ./remove.sh [--backup]
+
+set -e
+
+# Configuration
+GAME_PATH="/mnt/NV2/SteamLibrary/steamapps/common/Escape from Duckov"
+MOD_NAME="TradingCardMod"
+MOD_DIR="$GAME_PATH/Duckov_Data/Mods/$MOD_NAME"
+BACKUP_DIR="$HOME/.local/share/TradingCardMod_backup"
+
+echo "=== Trading Card Mod Removal ==="
+echo "Target: $MOD_DIR"
+echo ""
+
+# Check if mod exists
+if [[ ! -d "$MOD_DIR" ]]; then
+ echo "Mod not installed at: $MOD_DIR"
+ exit 0
+fi
+
+# Backup option
+if [[ "$1" == "--backup" ]]; then
+ echo "[1/2] Creating backup..."
+ mkdir -p "$BACKUP_DIR"
+ TIMESTAMP=$(date +%Y%m%d_%H%M%S)
+ BACKUP_PATH="$BACKUP_DIR/${MOD_NAME}_$TIMESTAMP"
+ cp -r "$MOD_DIR" "$BACKUP_PATH"
+ echo " Backup saved to: $BACKUP_PATH"
+ echo "[2/2] Removing mod..."
+else
+ echo "[1/1] Removing mod..."
+fi
+
+# Remove the mod
+rm -rf "$MOD_DIR"
+
+echo ""
+echo "=== Removal Complete ==="
+echo "Mod removed from: $MOD_DIR"
+
+# Show backup location if created
+if [[ "$1" == "--backup" ]]; then
+ echo "Backup location: $BACKUP_PATH"
+fi
diff --git a/src/ItemExtensions.cs b/src/ItemExtensions.cs
new file mode 100644
index 0000000..f436bba
--- /dev/null
+++ b/src/ItemExtensions.cs
@@ -0,0 +1,128 @@
+using System;
+using System.Reflection;
+using UnityEngine;
+
+namespace TradingCardMod
+{
+ ///
+ /// Extension methods for reflection-based field access on Unity objects.
+ /// Used to set private fields on cloned game items since we can't use
+ /// constructors or public setters for internal game types.
+ ///
+ public static class ItemExtensions
+ {
+ ///
+ /// Sets a private field value on an object using reflection.
+ ///
+ /// The type of the value to set.
+ /// The object containing the field.
+ /// The name of the private field.
+ /// The value to set.
+ /// Thrown when obj is null.
+ /// Thrown when field is not found.
+ public static void SetPrivateField(this object obj, string fieldName, T value)
+ {
+ if (obj == null)
+ throw new ArgumentNullException(nameof(obj));
+
+ Type type = obj.GetType();
+ FieldInfo field = null;
+
+ // Search up the inheritance hierarchy for the field
+ while (type != null && field == null)
+ {
+ field = type.GetField(fieldName,
+ BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
+ type = type.BaseType;
+ }
+
+ if (field == null)
+ {
+ throw new ArgumentException(
+ $"Field '{fieldName}' not found on type '{obj.GetType().Name}' or its base types.");
+ }
+
+ field.SetValue(obj, value);
+ }
+
+ ///
+ /// Gets a private field value from an object using reflection.
+ ///
+ /// The expected type of the field value.
+ /// The object containing the field.
+ /// The name of the private field.
+ /// The field value cast to type T.
+ /// Thrown when obj is null.
+ /// Thrown when field is not found.
+ /// Thrown when field value cannot be cast to T.
+ public static T GetPrivateField(this object obj, string fieldName)
+ {
+ if (obj == null)
+ throw new ArgumentNullException(nameof(obj));
+
+ Type type = obj.GetType();
+ FieldInfo field = null;
+
+ // Search up the inheritance hierarchy for the field
+ while (type != null && field == null)
+ {
+ field = type.GetField(fieldName,
+ BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
+ type = type.BaseType;
+ }
+
+ if (field == null)
+ {
+ throw new ArgumentException(
+ $"Field '{fieldName}' not found on type '{obj.GetType().Name}' or its base types.");
+ }
+
+ return (T)field.GetValue(obj);
+ }
+
+ ///
+ /// Attempts to set a private field value, logging errors instead of throwing.
+ /// Useful for non-critical field assignments where failure shouldn't halt execution.
+ ///
+ /// The type of the value to set.
+ /// The object containing the field.
+ /// The name of the private field.
+ /// The value to set.
+ /// True if successful, false otherwise.
+ public static bool TrySetPrivateField(this object obj, string fieldName, T value)
+ {
+ try
+ {
+ obj.SetPrivateField(fieldName, value);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"[TradingCardMod] Failed to set field '{fieldName}': {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// Attempts to get a private field value, returning default on failure.
+ /// Useful for optional field access where failure shouldn't halt execution.
+ ///
+ /// The expected type of the field value.
+ /// The object containing the field.
+ /// The name of the private field.
+ /// The default value to return on failure.
+ /// The field value if successful, otherwise defaultValue.
+ public static T TryGetPrivateField(this object obj, string fieldName, T defaultValue = default)
+ {
+ try
+ {
+ return obj.GetPrivateField(fieldName);
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"[TradingCardMod] Failed to get field '{fieldName}': {ex.Message}");
+ return defaultValue;
+ }
+ }
+ }
+}
diff --git a/src/ModBehaviour.cs b/src/ModBehaviour.cs
index aa81de8..d183470 100644
--- a/src/ModBehaviour.cs
+++ b/src/ModBehaviour.cs
@@ -4,6 +4,7 @@ using System.Collections.Generic;
using UnityEngine;
using ItemStatsSystem;
using SodaCraft.Localizations;
+using Duckov.Utilities;
namespace TradingCardMod
{
@@ -16,8 +17,17 @@ namespace TradingCardMod
private static ModBehaviour? _instance;
public static ModBehaviour Instance => _instance!;
+ // Base game item ID to clone (135 is commonly used for collectibles)
+ private const int BASE_ITEM_ID = 135;
+
private string _modPath = string.Empty;
private List _loadedCards = new List();
+ private List- _registeredItems = new List
- ();
+ private List _createdGameObjects = new List();
+ private Tag? _tradingCardTag;
+
+ // Debug: track if we've spawned test items
+ private int _debugSpawnIndex = 0;
///
/// Called when the mod is loaded. Initialize the card system here.
@@ -37,6 +47,10 @@ namespace TradingCardMod
try
{
+ // Create our custom tag first
+ _tradingCardTag = TagHelper.GetOrCreateTradingCardTag();
+
+ // Load and register cards
LoadCardSets();
}
catch (Exception ex)
@@ -69,13 +83,53 @@ 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 a trading card!");
+ }
+
+ ///
+ /// Update is called every frame. Used for debug input handling.
+ ///
+ void Update()
+ {
+ // Debug: Press F9 to spawn a card
+ if (Input.GetKeyDown(KeyCode.F9))
+ {
+ SpawnDebugCard();
+ }
+ }
+
+ ///
+ /// Spawns a trading card for testing purposes.
+ ///
+ private void SpawnDebugCard()
+ {
+ if (_registeredItems.Count == 0)
+ {
+ Debug.LogWarning("[TradingCardMod] No cards registered to spawn!");
+ return;
+ }
+
+ // Cycle through registered cards
+ Item cardToSpawn = _registeredItems[_debugSpawnIndex % _registeredItems.Count];
+ _debugSpawnIndex++;
+
+ try
+ {
+ // Use game's utility to give item to player
+ ItemUtilities.SendToPlayer(cardToSpawn);
+ Debug.Log($"[TradingCardMod] Spawned card: {cardToSpawn.DisplayName} (ID: {cardToSpawn.TypeID})");
+ }
+ catch (Exception ex)
+ {
+ Debug.LogError($"[TradingCardMod] Failed to spawn card: {ex.Message}");
+ }
}
///
/// Loads a single card set from a directory.
/// Expects a cards.txt file with pipe-separated values.
///
- /// Path to the card set directory
private void LoadCardSet(string setDirectory)
{
string setName = Path.GetFileName(setDirectory);
@@ -91,26 +145,30 @@ namespace TradingCardMod
try
{
- string[] lines = File.ReadAllLines(cardsFile);
- int cardCount = 0;
+ // Use CardParser to load cards
+ string imagesDirectory = Path.Combine(setDirectory, "images");
+ var cards = CardParser.ParseFile(cardsFile, imagesDirectory);
- foreach (string line in lines)
+ foreach (var card in cards)
{
- // Skip empty lines and comments
- if (string.IsNullOrWhiteSpace(line) || line.TrimStart().StartsWith("#"))
- continue;
-
- TradingCard? card = ParseCardLine(line, setDirectory);
- if (card != null)
+ // Validate card
+ var errors = CardParser.ValidateCard(card);
+ if (errors.Count > 0)
{
- _loadedCards.Add(card);
- cardCount++;
- // TODO: Register card with game's item system
- // RegisterCardWithGame(card);
+ foreach (var error in errors)
+ {
+ Debug.LogWarning($"[TradingCardMod] {card.CardName}: {error}");
+ }
+ continue;
}
+
+ _loadedCards.Add(card);
+
+ // Register card as game item
+ RegisterCardWithGame(card);
}
- Debug.Log($"[TradingCardMod] Loaded {cardCount} cards from {setName}");
+ Debug.Log($"[TradingCardMod] Loaded {cards.Count} cards from {setName}");
}
catch (Exception ex)
{
@@ -119,35 +177,181 @@ namespace TradingCardMod
}
///
- /// Parses a single line from cards.txt into a TradingCard object.
- /// Format: CardName | SetName | SetNumber | ImageFile | Rarity | Weight | Value
+ /// Creates a game item for a trading card using clone + reflection.
///
- private TradingCard? ParseCardLine(string line, string setDirectory)
+ private void RegisterCardWithGame(TradingCard card)
{
- string[] parts = line.Split('|');
-
- if (parts.Length < 7)
- {
- Debug.LogWarning($"[TradingCardMod] Invalid card line (expected 7 fields): {line}");
- return null;
- }
-
try
{
- return new TradingCard
+ // Get base item to clone
+ Item original = ItemAssetsCollection.GetPrefab(BASE_ITEM_ID);
+ if (original == null)
{
- CardName = parts[0].Trim(),
- SetName = parts[1].Trim(),
- SetNumber = int.Parse(parts[2].Trim()),
- ImagePath = Path.Combine(setDirectory, "images", parts[3].Trim()),
- Rarity = parts[4].Trim(),
- Weight = float.Parse(parts[5].Trim()),
- Value = int.Parse(parts[6].Trim())
- };
+ Debug.LogError($"[TradingCardMod] Base item ID {BASE_ITEM_ID} not found!");
+ return;
+ }
+
+ // Clone the item
+ GameObject clone = UnityEngine.Object.Instantiate(original.gameObject);
+ clone.name = $"TradingCard_{card.SetName}_{card.CardName}";
+ UnityEngine.Object.DontDestroyOnLoad(clone);
+ _createdGameObjects.Add(clone);
+
+ Item item = clone.GetComponent
- ();
+ if (item == null)
+ {
+ Debug.LogError($"[TradingCardMod] Cloned object has no Item component!");
+ return;
+ }
+
+ // Set item properties via reflection
+ int typeId = card.GenerateTypeID();
+ string locKey = $"TC_{card.SetName}_{card.CardName}".Replace(" ", "_");
+
+ 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);
+
+ // Set display quality based on rarity
+ SetDisplayQuality(item, card.GetQuality());
+
+ // Set tags
+ item.Tags.Clear();
+
+ // Add Luxury tag (for selling at shops)
+ Tag? luxuryTag = TagHelper.GetTargetTag("Luxury");
+ if (luxuryTag != null)
+ {
+ item.Tags.Add(luxuryTag);
+ }
+
+ // Add our custom TradingCard tag
+ if (_tradingCardTag != null)
+ {
+ item.Tags.Add(_tradingCardTag);
+ }
+
+ // Load and set icon
+ Sprite? cardSprite = LoadSpriteFromFile(card.ImagePath, typeId);
+ if (cardSprite != null)
+ {
+ item.SetPrivateField("icon", cardSprite);
+ }
+ else
+ {
+ Debug.LogWarning($"[TradingCardMod] Using default icon for {card.CardName}");
+ }
+
+ // Set localization
+ LocalizationManager.SetOverrideText(locKey, card.CardName);
+ LocalizationManager.SetOverrideText($"{locKey}_Desc", card.GetDescription());
+
+ // Register with game's item system
+ if (ItemAssetsCollection.AddDynamicEntry(item))
+ {
+ _registeredItems.Add(item);
+ Debug.Log($"[TradingCardMod] Registered: {card.CardName} (ID: {typeId})");
+ }
+ else
+ {
+ Debug.LogError($"[TradingCardMod] Failed to register {card.CardName}!");
+ UnityEngine.Object.Destroy(clone);
+ _createdGameObjects.Remove(clone);
+ }
}
catch (Exception ex)
{
- Debug.LogWarning($"[TradingCardMod] Failed to parse card line: {line} - {ex.Message}");
+ Debug.LogError($"[TradingCardMod] Error registering {card.CardName}: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Sets the DisplayQuality enum based on quality level.
+ ///
+ private void SetDisplayQuality(Item item, int quality)
+ {
+ // DisplayQuality is cast from int - matching AdditionalCollectibles pattern
+ // Values: 0=Common, 2=Uncommon, 3=Rare, 4=Epic, 5=Legendary, 6=Mythic
+ int displayValue;
+ switch (quality)
+ {
+ case 2:
+ displayValue = 2;
+ break;
+ case 3:
+ displayValue = 3;
+ break;
+ case 4:
+ displayValue = 4;
+ break;
+ case 5:
+ displayValue = 5;
+ break;
+ case 6:
+ case 7:
+ displayValue = 6;
+ break;
+ default:
+ displayValue = 0;
+ break;
+ }
+ item.DisplayQuality = (DisplayQuality)displayValue;
+ }
+
+ ///
+ /// Loads a sprite from an image file (PNG/JPG).
+ ///
+ private Sprite? LoadSpriteFromFile(string imagePath, int itemId)
+ {
+ try
+ {
+ if (!File.Exists(imagePath))
+ {
+ Debug.LogWarning($"[TradingCardMod] Image not found: {imagePath}");
+ return null;
+ }
+
+ // Read image bytes
+ byte[] imageData = File.ReadAllBytes(imagePath);
+
+ // Create texture and load image
+ Texture2D texture = new Texture2D(2, 2, TextureFormat.RGBA32, false);
+ if (!ImageConversion.LoadImage(texture, imageData))
+ {
+ Debug.LogError($"[TradingCardMod] Failed to load image (not PNG/JPG?): {imagePath}");
+ return null;
+ }
+
+ texture.filterMode = FilterMode.Bilinear;
+ texture.Apply();
+
+ // Create sprite from texture
+ Sprite sprite = Sprite.Create(
+ texture,
+ new Rect(0f, 0f, texture.width, texture.height),
+ new Vector2(0.5f, 0.5f),
+ 100f
+ );
+
+ // Create holder to keep texture/sprite alive
+ GameObject holder = new GameObject($"CardIcon_{itemId}");
+ UnityEngine.Object.DontDestroyOnLoad(holder);
+ _createdGameObjects.Add(holder);
+
+ // Store references on the holder to prevent GC
+ var resourceHolder = holder.AddComponent();
+ resourceHolder.Texture = texture;
+ resourceHolder.Sprite = sprite;
+
+ return sprite;
+ }
+ catch (Exception ex)
+ {
+ Debug.LogError($"[TradingCardMod] Error loading sprite: {ex.Message}");
return null;
}
}
@@ -162,42 +366,41 @@ namespace TradingCardMod
// Remove Harmony patches
Patches.RemovePatches();
- // TODO: Remove registered items from game
- // foreach (var card in _loadedCards)
- // {
- // ItemAssetsCollection.RemoveDynamicEntry(card.ItemPrefab);
- // }
+ // Remove registered items from game
+ foreach (var item in _registeredItems)
+ {
+ if (item != null)
+ {
+ ItemAssetsCollection.RemoveDynamicEntry(item);
+ }
+ }
+ _registeredItems.Clear();
+
+ // Destroy created GameObjects (including icon holders)
+ foreach (var go in _createdGameObjects)
+ {
+ if (go != null)
+ {
+ UnityEngine.Object.Destroy(go);
+ }
+ }
+ _createdGameObjects.Clear();
+
+ // Clean up tags
+ TagHelper.Cleanup();
_loadedCards.Clear();
+
+ Debug.Log("[TradingCardMod] Cleanup complete.");
}
}
///
- /// Represents a trading card's data loaded from a card set file.
+ /// Component to hold texture and sprite references to prevent garbage collection.
///
- public class TradingCard
+ public class CardResourceHolder : MonoBehaviour
{
- public string CardName { get; set; } = string.Empty;
- public string SetName { get; set; } = string.Empty;
- public int SetNumber { get; set; }
- public string ImagePath { get; set; } = string.Empty;
- public string Rarity { get; set; } = string.Empty;
- public float Weight { get; set; }
- public int Value { get; set; }
-
- // TODO: Add Unity prefab reference once we understand the item system better
- // public Item? ItemPrefab { get; set; }
-
- ///
- /// Generates a unique TypeID for this card to avoid conflicts.
- /// Uses hash of set name + card name for uniqueness.
- ///
- public int GenerateTypeID()
- {
- // Start from a high number to avoid conflicts with base game items
- // Use hash to ensure consistency across loads
- string uniqueKey = $"TradingCard_{SetName}_{CardName}";
- return 100000 + Math.Abs(uniqueKey.GetHashCode() % 900000);
- }
+ public Texture2D? Texture;
+ public Sprite? Sprite;
}
}
diff --git a/src/TagHelper.cs b/src/TagHelper.cs
new file mode 100644
index 0000000..8efbb10
--- /dev/null
+++ b/src/TagHelper.cs
@@ -0,0 +1,124 @@
+using System.Collections.Generic;
+using System.Linq;
+using Duckov.Utilities;
+using SodaCraft.Localizations;
+using UnityEngine;
+
+namespace TradingCardMod
+{
+ ///
+ /// Helper class for working with the game's Tag system.
+ /// Tags are ScriptableObjects used for filtering items in slots.
+ ///
+ public static class TagHelper
+ {
+ // Track tags we create for cleanup
+ private static readonly List _createdTags = new List();
+
+ ///
+ /// Gets an existing game tag by name.
+ ///
+ /// The internal name of the tag (e.g., "Luxury", "Food").
+ /// The Tag if found, null otherwise.
+ public static Tag GetTargetTag(string tagName)
+ {
+ return Resources.FindObjectsOfTypeAll()
+ .FirstOrDefault(t => t.name == tagName);
+ }
+
+ ///
+ /// Creates a new custom tag by cloning an existing game tag.
+ /// If a tag with the same name already exists, returns that tag instead.
+ ///
+ /// The internal name for the new tag.
+ /// The localized display name shown to players.
+ /// The name of an existing tag to clone (default: "Luxury").
+ /// The created or existing tag, or null if template not found.
+ public static Tag CreateOrCloneTag(string tagName, string displayName, string templateTagName = "Luxury")
+ {
+ // Check if tag already exists
+ Tag existing = Resources.FindObjectsOfTypeAll()
+ .FirstOrDefault(t => t.name == tagName);
+
+ if (existing != null)
+ {
+ Debug.Log($"[TradingCardMod] Tag '{tagName}' already exists, reusing.");
+ return existing;
+ }
+
+ // Find template tag to clone
+ Tag template = Resources.FindObjectsOfTypeAll()
+ .FirstOrDefault(t => t.name == templateTagName);
+
+ if (template == null)
+ {
+ Debug.LogError($"[TradingCardMod] Template tag '{templateTagName}' not found. Cannot create '{tagName}'.");
+ return null;
+ }
+
+ // Clone the template
+ Tag newTag = Object.Instantiate(template);
+ newTag.name = tagName;
+ Object.DontDestroyOnLoad(newTag);
+
+ // Track for cleanup
+ _createdTags.Add(newTag);
+
+ // Set localization for display
+ LocalizationManager.SetOverrideText($"Tag_{tagName}", displayName);
+ LocalizationManager.SetOverrideText($"Tag_{tagName}_Desc", "");
+
+ Debug.Log($"[TradingCardMod] Created custom tag '{tagName}' (display: '{displayName}').");
+ return newTag;
+ }
+
+ ///
+ /// Gets the "TradingCard" tag, creating it if it doesn't exist.
+ /// This is the primary tag used to identify and filter trading cards.
+ ///
+ /// The TradingCard tag.
+ public static Tag GetOrCreateTradingCardTag()
+ {
+ return CreateOrCloneTag("TradingCard", "Trading Card");
+ }
+
+ ///
+ /// Gets all tags created by this mod.
+ ///
+ /// List of tags created by the mod.
+ public static IReadOnlyList GetCreatedTags()
+ {
+ return _createdTags.AsReadOnly();
+ }
+
+ ///
+ /// Cleans up all tags created by the mod.
+ /// Should be called when the mod is unloaded.
+ ///
+ public static void Cleanup()
+ {
+ foreach (var tag in _createdTags)
+ {
+ if (tag != null)
+ {
+ Object.Destroy(tag);
+ }
+ }
+ _createdTags.Clear();
+ Debug.Log("[TradingCardMod] TagHelper cleaned up.");
+ }
+
+ ///
+ /// Logs all available tags in the game (for debugging).
+ ///
+ public static void LogAvailableTags()
+ {
+ var tags = Resources.FindObjectsOfTypeAll();
+ Debug.Log($"[TradingCardMod] Available tags ({tags.Length}):");
+ foreach (var tag in tags.OrderBy(t => t.name))
+ {
+ Debug.Log($" - {tag.name}");
+ }
+ }
+ }
+}