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">
|
||||
<HintPath>$(DuckovPath)$(SubPath)UnityEngine.CoreModule.dll</HintPath>
|
||||
</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>
|
||||
@ -57,4 +63,9 @@
|
||||
<None Include="info.ini" CopyToOutputDirectory="PreserveNewest" />
|
||||
<None Include="preview.png" CopyToOutputDirectory="PreserveNewest" Condition="Exists('preview.png')" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Exclude test files from main build (they're in the test project) -->
|
||||
<Compile Remove="tests/**/*.cs" />
|
||||
</ItemGroup>
|
||||
</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 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<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>
|
||||
/// 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!");
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Loads a single card set from a directory.
|
||||
/// Expects a cards.txt file with pipe-separated values.
|
||||
/// </summary>
|
||||
/// <param name="setDirectory">Path to the card set directory</param>
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </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
|
||||
{
|
||||
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<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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
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; }
|
||||
|
||||
/// <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);
|
||||
}
|
||||
public Texture2D? Texture;
|
||||
public Sprite? Sprite;
|
||||
}
|
||||
}
|
||||
|
||||
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