Implement Phase 2: Core card framework with clone + reflection
- Add ItemExtensions.cs with reflection helpers for private field access - Add TagHelper.cs for game tag operations and custom TradingCard tag - Rewrite ModBehaviour.cs with complete item creation pipeline: - Clone base game item (ID 135) as template - Set properties via reflection (typeID, weight, value, quality) - Load PNG sprites from CardSets/*/images/ - Register items with ItemAssetsCollection - Proper cleanup on mod unload - Add F9 debug key to spawn cards for testing - Add deploy.sh and remove.sh scripts for quick mod installation - Add placeholder card images for ExampleSet - Add Unity module references (ImageConversion, InputLegacy) Cards now appear in-game with custom icons and can be spawned to inventory. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
8d23f152eb
commit
d05ba64700
BIN
CardSets/ExampleSet/images/bread_seeker.png
Normal file
BIN
CardSets/ExampleSet/images/bread_seeker.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
BIN
CardSets/ExampleSet/images/duck_hero.png
Normal file
BIN
CardSets/ExampleSet/images/duck_hero.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
BIN
CardSets/ExampleSet/images/feathered_fury.png
Normal file
BIN
CardSets/ExampleSet/images/feathered_fury.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
BIN
CardSets/ExampleSet/images/golden_quacker.png
Normal file
BIN
CardSets/ExampleSet/images/golden_quacker.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
BIN
CardSets/ExampleSet/images/pond_guardian.png
Normal file
BIN
CardSets/ExampleSet/images/pond_guardian.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
@ -44,6 +44,12 @@
|
|||||||
<Reference Include="UnityEngine.CoreModule">
|
<Reference Include="UnityEngine.CoreModule">
|
||||||
<HintPath>$(DuckovPath)$(SubPath)UnityEngine.CoreModule.dll</HintPath>
|
<HintPath>$(DuckovPath)$(SubPath)UnityEngine.CoreModule.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
|
<Reference Include="UnityEngine.ImageConversionModule">
|
||||||
|
<HintPath>$(DuckovPath)$(SubPath)UnityEngine.ImageConversionModule.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="UnityEngine.InputLegacyModule">
|
||||||
|
<HintPath>$(DuckovPath)$(SubPath)UnityEngine.InputLegacyModule.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@ -57,4 +63,9 @@
|
|||||||
<None Include="info.ini" CopyToOutputDirectory="PreserveNewest" />
|
<None Include="info.ini" CopyToOutputDirectory="PreserveNewest" />
|
||||||
<None Include="preview.png" CopyToOutputDirectory="PreserveNewest" Condition="Exists('preview.png')" />
|
<None Include="preview.png" CopyToOutputDirectory="PreserveNewest" Condition="Exists('preview.png')" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- Exclude test files from main build (they're in the test project) -->
|
||||||
|
<Compile Remove="tests/**/*.cs" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
61
deploy.sh
Executable file
61
deploy.sh
Executable file
@ -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)"
|
||||||
46
remove.sh
Executable file
46
remove.sh
Executable file
@ -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
|
||||||
128
src/ItemExtensions.cs
Normal file
128
src/ItemExtensions.cs
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace TradingCardMod
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public static class ItemExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sets a private field value on an object using reflection.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the value to set.</typeparam>
|
||||||
|
/// <param name="obj">The object containing the field.</param>
|
||||||
|
/// <param name="fieldName">The name of the private field.</param>
|
||||||
|
/// <param name="value">The value to set.</param>
|
||||||
|
/// <exception cref="ArgumentNullException">Thrown when obj is null.</exception>
|
||||||
|
/// <exception cref="ArgumentException">Thrown when field is not found.</exception>
|
||||||
|
public static void SetPrivateField<T>(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a private field value from an object using reflection.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The expected type of the field value.</typeparam>
|
||||||
|
/// <param name="obj">The object containing the field.</param>
|
||||||
|
/// <param name="fieldName">The name of the private field.</param>
|
||||||
|
/// <returns>The field value cast to type T.</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">Thrown when obj is null.</exception>
|
||||||
|
/// <exception cref="ArgumentException">Thrown when field is not found.</exception>
|
||||||
|
/// <exception cref="InvalidCastException">Thrown when field value cannot be cast to T.</exception>
|
||||||
|
public static T GetPrivateField<T>(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to set a private field value, logging errors instead of throwing.
|
||||||
|
/// Useful for non-critical field assignments where failure shouldn't halt execution.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the value to set.</typeparam>
|
||||||
|
/// <param name="obj">The object containing the field.</param>
|
||||||
|
/// <param name="fieldName">The name of the private field.</param>
|
||||||
|
/// <param name="value">The value to set.</param>
|
||||||
|
/// <returns>True if successful, false otherwise.</returns>
|
||||||
|
public static bool TrySetPrivateField<T>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to get a private field value, returning default on failure.
|
||||||
|
/// Useful for optional field access where failure shouldn't halt execution.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The expected type of the field value.</typeparam>
|
||||||
|
/// <param name="obj">The object containing the field.</param>
|
||||||
|
/// <param name="fieldName">The name of the private field.</param>
|
||||||
|
/// <param name="defaultValue">The default value to return on failure.</param>
|
||||||
|
/// <returns>The field value if successful, otherwise defaultValue.</returns>
|
||||||
|
public static T TryGetPrivateField<T>(this object obj, string fieldName, T defaultValue = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return obj.GetPrivateField<T>(fieldName);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[TradingCardMod] Failed to get field '{fieldName}': {ex.Message}");
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ using System.Collections.Generic;
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using ItemStatsSystem;
|
using ItemStatsSystem;
|
||||||
using SodaCraft.Localizations;
|
using SodaCraft.Localizations;
|
||||||
|
using Duckov.Utilities;
|
||||||
|
|
||||||
namespace TradingCardMod
|
namespace TradingCardMod
|
||||||
{
|
{
|
||||||
@ -16,8 +17,17 @@ namespace TradingCardMod
|
|||||||
private static ModBehaviour? _instance;
|
private static ModBehaviour? _instance;
|
||||||
public static ModBehaviour Instance => _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 string _modPath = string.Empty;
|
||||||
private List<TradingCard> _loadedCards = new List<TradingCard>();
|
private List<TradingCard> _loadedCards = new List<TradingCard>();
|
||||||
|
private List<Item> _registeredItems = new List<Item>();
|
||||||
|
private List<GameObject> _createdGameObjects = new List<GameObject>();
|
||||||
|
private Tag? _tradingCardTag;
|
||||||
|
|
||||||
|
// Debug: track if we've spawned test items
|
||||||
|
private int _debugSpawnIndex = 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Called when the mod is loaded. Initialize the card system here.
|
/// Called when the mod is loaded. Initialize the card system here.
|
||||||
@ -37,6 +47,10 @@ namespace TradingCardMod
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Create our custom tag first
|
||||||
|
_tradingCardTag = TagHelper.GetOrCreateTradingCardTag();
|
||||||
|
|
||||||
|
// Load and register cards
|
||||||
LoadCardSets();
|
LoadCardSets();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -69,13 +83,53 @@ namespace TradingCardMod
|
|||||||
}
|
}
|
||||||
|
|
||||||
Debug.Log($"[TradingCardMod] Total cards loaded: {_loadedCards.Count}");
|
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!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update is called every frame. Used for debug input handling.
|
||||||
|
/// </summary>
|
||||||
|
void Update()
|
||||||
|
{
|
||||||
|
// Debug: Press F9 to spawn a card
|
||||||
|
if (Input.GetKeyDown(KeyCode.F9))
|
||||||
|
{
|
||||||
|
SpawnDebugCard();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spawns a trading card for testing purposes.
|
||||||
|
/// </summary>
|
||||||
|
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}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loads a single card set from a directory.
|
/// Loads a single card set from a directory.
|
||||||
/// Expects a cards.txt file with pipe-separated values.
|
/// Expects a cards.txt file with pipe-separated values.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="setDirectory">Path to the card set directory</param>
|
|
||||||
private void LoadCardSet(string setDirectory)
|
private void LoadCardSet(string setDirectory)
|
||||||
{
|
{
|
||||||
string setName = Path.GetFileName(setDirectory);
|
string setName = Path.GetFileName(setDirectory);
|
||||||
@ -91,26 +145,30 @@ namespace TradingCardMod
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string[] lines = File.ReadAllLines(cardsFile);
|
// Use CardParser to load cards
|
||||||
int cardCount = 0;
|
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
|
// Validate card
|
||||||
if (string.IsNullOrWhiteSpace(line) || line.TrimStart().StartsWith("#"))
|
var errors = CardParser.ValidateCard(card);
|
||||||
continue;
|
if (errors.Count > 0)
|
||||||
|
|
||||||
TradingCard? card = ParseCardLine(line, setDirectory);
|
|
||||||
if (card != null)
|
|
||||||
{
|
{
|
||||||
_loadedCards.Add(card);
|
foreach (var error in errors)
|
||||||
cardCount++;
|
{
|
||||||
// TODO: Register card with game's item system
|
Debug.LogWarning($"[TradingCardMod] {card.CardName}: {error}");
|
||||||
// RegisterCardWithGame(card);
|
}
|
||||||
|
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -119,35 +177,181 @@ namespace TradingCardMod
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parses a single line from cards.txt into a TradingCard object.
|
/// Creates a game item for a trading card using clone + reflection.
|
||||||
/// Format: CardName | SetName | SetNumber | ImageFile | Rarity | Weight | Value
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
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
|
try
|
||||||
{
|
{
|
||||||
return new TradingCard
|
// Get base item to clone
|
||||||
|
Item original = ItemAssetsCollection.GetPrefab(BASE_ITEM_ID);
|
||||||
|
if (original == null)
|
||||||
{
|
{
|
||||||
CardName = parts[0].Trim(),
|
Debug.LogError($"[TradingCardMod] Base item ID {BASE_ITEM_ID} not found!");
|
||||||
SetName = parts[1].Trim(),
|
return;
|
||||||
SetNumber = int.Parse(parts[2].Trim()),
|
}
|
||||||
ImagePath = Path.Combine(setDirectory, "images", parts[3].Trim()),
|
|
||||||
Rarity = parts[4].Trim(),
|
// Clone the item
|
||||||
Weight = float.Parse(parts[5].Trim()),
|
GameObject clone = UnityEngine.Object.Instantiate(original.gameObject);
|
||||||
Value = int.Parse(parts[6].Trim())
|
clone.name = $"TradingCard_{card.SetName}_{card.CardName}";
|
||||||
};
|
UnityEngine.Object.DontDestroyOnLoad(clone);
|
||||||
|
_createdGameObjects.Add(clone);
|
||||||
|
|
||||||
|
Item item = clone.GetComponent<Item>();
|
||||||
|
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Debug.LogWarning($"[TradingCardMod] Failed to parse card line: {line} - {ex.Message}");
|
Debug.LogError($"[TradingCardMod] Error registering {card.CardName}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the DisplayQuality enum based on quality level.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads a sprite from an image file (PNG/JPG).
|
||||||
|
/// </summary>
|
||||||
|
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<CardResourceHolder>();
|
||||||
|
resourceHolder.Texture = texture;
|
||||||
|
resourceHolder.Sprite = sprite;
|
||||||
|
|
||||||
|
return sprite;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogError($"[TradingCardMod] Error loading sprite: {ex.Message}");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -162,42 +366,41 @@ namespace TradingCardMod
|
|||||||
// Remove Harmony patches
|
// Remove Harmony patches
|
||||||
Patches.RemovePatches();
|
Patches.RemovePatches();
|
||||||
|
|
||||||
// TODO: Remove registered items from game
|
// Remove registered items from game
|
||||||
// foreach (var card in _loadedCards)
|
foreach (var item in _registeredItems)
|
||||||
// {
|
{
|
||||||
// ItemAssetsCollection.RemoveDynamicEntry(card.ItemPrefab);
|
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();
|
_loadedCards.Clear();
|
||||||
|
|
||||||
|
Debug.Log("[TradingCardMod] Cleanup complete.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a trading card's data loaded from a card set file.
|
/// Component to hold texture and sprite references to prevent garbage collection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class TradingCard
|
public class CardResourceHolder : MonoBehaviour
|
||||||
{
|
{
|
||||||
public string CardName { get; set; } = string.Empty;
|
public Texture2D? Texture;
|
||||||
public string SetName { get; set; } = string.Empty;
|
public Sprite? Sprite;
|
||||||
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; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generates a unique TypeID for this card to avoid conflicts.
|
|
||||||
/// Uses hash of set name + card name for uniqueness.
|
|
||||||
/// </summary>
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
124
src/TagHelper.cs
Normal file
124
src/TagHelper.cs
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Duckov.Utilities;
|
||||||
|
using SodaCraft.Localizations;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace TradingCardMod
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Helper class for working with the game's Tag system.
|
||||||
|
/// Tags are ScriptableObjects used for filtering items in slots.
|
||||||
|
/// </summary>
|
||||||
|
public static class TagHelper
|
||||||
|
{
|
||||||
|
// Track tags we create for cleanup
|
||||||
|
private static readonly List<Tag> _createdTags = new List<Tag>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an existing game tag by name.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tagName">The internal name of the tag (e.g., "Luxury", "Food").</param>
|
||||||
|
/// <returns>The Tag if found, null otherwise.</returns>
|
||||||
|
public static Tag GetTargetTag(string tagName)
|
||||||
|
{
|
||||||
|
return Resources.FindObjectsOfTypeAll<Tag>()
|
||||||
|
.FirstOrDefault(t => t.name == tagName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new custom tag by cloning an existing game tag.
|
||||||
|
/// If a tag with the same name already exists, returns that tag instead.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tagName">The internal name for the new tag.</param>
|
||||||
|
/// <param name="displayName">The localized display name shown to players.</param>
|
||||||
|
/// <param name="templateTagName">The name of an existing tag to clone (default: "Luxury").</param>
|
||||||
|
/// <returns>The created or existing tag, or null if template not found.</returns>
|
||||||
|
public static Tag CreateOrCloneTag(string tagName, string displayName, string templateTagName = "Luxury")
|
||||||
|
{
|
||||||
|
// Check if tag already exists
|
||||||
|
Tag existing = Resources.FindObjectsOfTypeAll<Tag>()
|
||||||
|
.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<Tag>()
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the "TradingCard" tag, creating it if it doesn't exist.
|
||||||
|
/// This is the primary tag used to identify and filter trading cards.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The TradingCard tag.</returns>
|
||||||
|
public static Tag GetOrCreateTradingCardTag()
|
||||||
|
{
|
||||||
|
return CreateOrCloneTag("TradingCard", "Trading Card");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all tags created by this mod.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>List of tags created by the mod.</returns>
|
||||||
|
public static IReadOnlyList<Tag> GetCreatedTags()
|
||||||
|
{
|
||||||
|
return _createdTags.AsReadOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cleans up all tags created by the mod.
|
||||||
|
/// Should be called when the mod is unloaded.
|
||||||
|
/// </summary>
|
||||||
|
public static void Cleanup()
|
||||||
|
{
|
||||||
|
foreach (var tag in _createdTags)
|
||||||
|
{
|
||||||
|
if (tag != null)
|
||||||
|
{
|
||||||
|
Object.Destroy(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_createdTags.Clear();
|
||||||
|
Debug.Log("[TradingCardMod] TagHelper cleaned up.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs all available tags in the game (for debugging).
|
||||||
|
/// </summary>
|
||||||
|
public static void LogAvailableTags()
|
||||||
|
{
|
||||||
|
var tags = Resources.FindObjectsOfTypeAll<Tag>();
|
||||||
|
Debug.Log($"[TradingCardMod] Available tags ({tags.Length}):");
|
||||||
|
foreach (var tag in tags.OrderBy(t => t.name))
|
||||||
|
{
|
||||||
|
Debug.Log($" - {tag.name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user