Compare commits

..

No commits in common. "d3d3b998c3ac3d4afff16c7e9afb724de85ba9c4" and "80da308d1754203b6eb0a270bd233ad11b72600a" have entirely different histories.

24 changed files with 147 additions and 1310 deletions

View File

@ -1,10 +1,10 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"$schema": "https://claude.ai/code/settings-schema.json",
"permissions": {
"allow": [
"Bash(dotnet build:*)",
"Bash(dotnet clean:*)",
"Bash(dotnet restore:*)",
"Bash(dotnet build*)",
"Bash(dotnet clean*)",
"Bash(dotnet restore*)",
"WebFetch(domain:code.claude.com)"
]
}

12
.gitignore vendored
View File

@ -10,7 +10,6 @@ obj/
# Claude Code private notes
.claude/scratchpad/
.claude/skills/
# OS files
.DS_Store
@ -23,14 +22,9 @@ packages/
# Debug logs
*.log
# Card sets - only ExampleSet is tracked in git
CardSets/*
!CardSets/ExampleSet/
# Card set images (user-generated content)
# Uncomment if you don't want to track example images
# CardSets/*/images/
# Preview image (generate your own)
# preview.png
# Steam Workshop (local config)
workshop-staging/
workshop.vdf
workshop-upload.sh

106
CLAUDE.md
View File

@ -36,15 +36,6 @@ dotnet build TradingCardMod.csproj -c Release
./remove.sh --backup
```
## Project Skills
Two workflow skills are available in this project:
- `/build` - Build and deploy locally for testing (runs `dotnet build` + `./deploy.sh`)
- `/deploy` - Build Release and stage for Steam Workshop upload (uses `workshop-upload.sh`)
Skills are defined in `.claude/skills/`.
## Testing
```bash
@ -86,68 +77,19 @@ The game loads mods from `Duckov_Data/Mods/`. Each mod requires:
- **`TagHelper`** (`src/TagHelper.cs`): Utilities for working with game tags, including creating custom tags.
- **`PackUsageBehavior`** (`src/PackUsageBehavior.cs`): Handles card pack opening mechanics. Implements gacha-style random card distribution based on rarity weights.
- **`ModConfigApi`** (`src/ModConfigApi.cs`): Optional integration with ModConfig mod. Adds card set information (set name, card number, rarity) to item descriptions in inventory. Also displays mod statistics including disabled card sets count.
- **`StorageHelper`** (`src/StorageHelper.cs`): Helper class for creating storage items with multi-tag slot filtering, enabling hierarchical storage systems.
- **`DisassemblyHelper`** (`src/DisassemblyHelper.cs`): Helper class for adding disassembly (deconstruction) formulas to custom items at runtime.
### Disassembly System Pattern
The game's disassembly system uses `DecomposeDatabase` (singleton in `Duckov.Economy` namespace) to store formulas. Key implementation details:
**Types:**
- `DecomposeFormula`: Contains `item` (TypeID), `valid` (bool), and `result` (Cost)
- `Cost`: A **struct** with `money` (long) and `items` (ItemEntry[])
- `Cost+ItemEntry`: **Nested struct** inside Cost with `id` (int) and `amount` (long)
**Critical Reflection Pattern (Cost is a struct):**
```csharp
// Cost is a VALUE TYPE - must box before reflection, then unbox
Cost result = new Cost { money = 0L };
object boxedResult = result; // Box the struct
itemsField.SetValue(boxedResult, itemEntries); // Modify boxed copy
result = (Cost)boxedResult; // Unbox back
formula.result = result;
```
**Adding a Formula:**
1. Get `DecomposeDatabase.Instance`
2. Access private `entries` field via reflection
3. Create `DecomposeFormula` with item TypeID and `valid = true`
4. Create `Cost` with money and/or items array
5. For items: Create `Cost+ItemEntry[]` via reflection (type found from `Cost.items` field type)
6. Add formula to list, write back via reflection
7. Call private `RebuildDictionary()` method
**Known Material TypeIDs:**
- Polyethylene Sheet: `764`
**Cleanup:** Track added item IDs and remove formulas in `OnDestroy()`.
See `DisassemblyHelper.cs` for complete implementation.
### Dependencies
- **HarmonyLoadMod** (Workshop ID: 3589088839): Required mod dependency providing Harmony 2.4.1. Referenced at build time but not bundled to avoid version conflicts.
- **ModConfig** (Workshop ID: 3592433938): Optional mod dependency. When installed, enhances card descriptions with set information in the inventory UI.
### Card Definition Format
Cards are defined in `CardSets/{SetName}/cards.txt` using pipe-separated values:
```
CardName | SetName | SetNumber | ImageFile | Rarity | Weight | Value | Description (optional)
CardName | SetName | SetNumber | ImageFile | Rarity | Weight | Value
```
The Description field is optional. If provided, it will be displayed in the item's in-game description/tooltip.
Images go in `CardSets/{SetName}/images/`.
**Disabling Card Sets:** Prefix a card set folder name with `_` (underscore) to exclude it from loading. For example, `_TestSet/` will be skipped. This is useful for work-in-progress sets or seasonal content. The count of disabled sets is displayed in ModConfig.
## Game API Reference
Key namespaces and APIs from the game:
@ -172,38 +114,19 @@ Key namespaces and APIs from the game:
## Current Project Status
**Phase:** 3 - Storage System
**Status:** Ready for first release
**Phase:** 2 Complete - Core Card Framework
**Next Phase:** 3 - Storage System (Binders)
**Project Plan:** `.claude/scratchpad/PROJECT_PLAN.md`
**Technical Analysis:** `.claude/scratchpad/item-system-analysis.md`
### Completed Features
- Cards load from `CardSets/*/cards.txt` files with optional descriptions
- Cards load from `CardSets/*/cards.txt` files
- Custom PNG images display as item icons
- Cards register as game items with proper TypeIDs
- Custom tags for slot filtering:
- "TradingCard": Identifies trading cards
- "BinderSheet": Identifies binder sheet items
- "CardBinderContent": Parent tag for both cards and binder sheets (enables hierarchical storage)
- Hierarchical storage system:
- **Binder Sheet** (9 slots, weight 0.1): Holds trading cards only
- **Card Binder** (12 slots, weight 1.5): Holds trading cards OR binder sheets
- Nested storage: Cards → Binder Sheets → Card Binders
- Card set exclusion: Prefix folders with `_` to disable loading
- ModConfig integration:
- Enhanced card info display (set name, number, rarity)
- Mod statistics display (total cards, disabled sets)
- Debug spawn window with F10 key (for testing):
- Spawn storage items (Card Binder, Binder Sheet)
- Spawn random cards by rarity
- Draggable OnGUI window interface
- Disassembly support for storage items:
- Binder Sheet → 2x Polyethylene Sheet
- Card Binder → 4x Polyethylene Sheet
- Cards intentionally have no disassembly (collectibles)
- Custom "TradingCard" tag for filtering
- Debug spawn with F9 key (for testing)
- Deploy/remove scripts for quick iteration
- Unit tests for parsing logic
### Implementation Approach: Clone + Reflection
@ -212,18 +135,15 @@ Based on analysis of the AdditionalCollectibles mod:
1. **Clone existing game items** as templates (base item ID 135)
2. **Use reflection** to set private fields (typeID, weight, value, etc.)
3. **Create custom tags** by cloning existing ScriptableObject tags
4. **Parent tag pattern** for slot filtering (Unity's requireTags uses AND logic, so parent tags enable OR-like behavior)
5. **Load sprites** from user files in `CardSets/*/images/`
6. **Attach custom behaviors** for pack opening mechanics
4. **Load sprites** from user files in `CardSets/*/images/`
### Upcoming Features
### Next Implementation Steps
- **Card packs** with gacha-style mechanics (weighted random distribution) - disabled pending fix
- Additional storage variants or customization options (e.g., larger binders, themed storage boxes)
### Future Considerations
- Investigate new ItemBuilder API (added in recent game update) as potential replacement for reflection-based approach
Phase 3 - Storage System:
1. Research existing storage items in game
2. Create binder item with Inventory component
3. Implement slot-based filtering for "TradingCard" tag
4. Create card box variant with higher capacity
### Log File Location

View File

@ -0,0 +1,21 @@
# Example Card Set - Trading Card Mod for Escape from Duckov
# Format: CardName | SetName | SetNumber | ImageFile | Rarity | Weight | Value | Description (optional)
#
# Fields:
# CardName - Display name of the card
# SetName - Name of the card collection
# SetNumber - Number within the set (for sorting)
# ImageFile - Filename of the card image (must be in images/ subfolder)
# Rarity - Card rarity. Valid values: Common, Uncommon, Rare, Very Rare, Ultra Rare, Legendary
# Weight - Physical weight in game units
# Value - In-game currency value
# Description - (Optional) Custom tooltip text. If omitted, auto-generates as "SetName #SetNumber - Rarity"
#
# Add your own cards below! Just follow the format above.
# Place corresponding images in the images/ subfolder.
Duck Hero | Example Set | 001 | duck_hero.png | Rare | 0.01 | 100 | The brave defender of all ponds
Golden Quacker | Example Set | 002 | golden_quacker.png | Ultra Rare | 0.01 | 500 | A legendary duck made of pure gold
Pond Guardian | Example Set | 003 | pond_guardian.png | Uncommon | 0.01 | 25
Bread Seeker | Example Set | 004 | bread_seeker.png | Common | 0.01 | 10
Feathered Fury | Example Set | 005 | feathered_fury.png | Rare | 0.01 | 75 | Known for its fierce battle cry

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -2,18 +2,6 @@
A customizable trading card system that lets you add your own card sets to the game.
## Features
- **Custom Card Sets** - Create your own trading cards with custom artwork and stats
- **Card Packs** - Open randomized card packs with gacha-style rarity distribution
- **Hierarchical Storage System** - Organize your collection with:
- **Binder Sheets** (9 slots, lightweight) - Hold individual cards
- **Card Binders** (12 slots) - Hold cards OR binder sheets for nested storage
- **Card Set Management** - Disable card sets by prefixing folder names with `_`
- **User-Friendly Format** - Define cards using simple pipe-separated text files
- **ModConfig Integration** - Enhanced card info display and mod statistics (optional)
- **No Programming Required** - Add new card sets without writing any code
## Requirements
**Required Mod Dependency:**
@ -21,20 +9,12 @@ A customizable trading card system that lets you add your own card sets to the g
This mod requires the HarmonyLoadMod to be installed. It provides the Harmony library that many mods share to avoid version conflicts.
**Optional Mod Dependency:**
- [ModConfig](https://steamcommunity.com/sharedfiles/filedetails/?id=3592433938) - Subscribe on Steam Workshop
ModConfig is optional but recommended. When installed, it:
- Adds card set information (set name, card number, rarity) to item descriptions in your inventory
- Displays mod statistics including total cards loaded, packs available, and disabled card sets
## Installation
1. Subscribe to [HarmonyLib](https://steamcommunity.com/sharedfiles/filedetails/?id=3589088839) on Steam Workshop
2. (Optional) Subscribe to [ModConfig](https://steamcommunity.com/sharedfiles/filedetails/?id=3592433938) for enhanced card descriptions
3. Build the mod (see Development section)
4. Copy the `TradingCardMod` folder to your game's `Duckov_Data/Mods` directory
5. Launch the game and enable the mods in the Mods menu (HarmonyLib is required, ModConfig is optional)
2. Build the mod (see Development section)
3. Copy the `TradingCardMod` folder to your game's `Duckov_Data/Mods` directory
4. Launch the game and enable both HarmonyLib and this mod in the Mods menu
## Adding Card Sets
@ -49,13 +29,13 @@ ModConfig is optional but recommended. When installed, it:
Cards are defined in `cards.txt` using pipe-separated values:
```
CardName | SetName | SetNumber | ImageFile | Rarity | Weight | Value | Description (optional)
CardName | SetName | SetNumber | ImageFile | Rarity | Weight | Value
```
Example:
```
Blue Dragon | Fantasy Set | 001 | blue_dragon.png | Ultra Rare | 0.05 | 500| A majestic dragon with scales of sapphire blue.
Fire Sprite | Fantasy Set | 002 | fire_sprite.png | Rare | 0.05 | 100
Blue Dragon | Fantasy Set | 001 | blue_dragon.png | Ultra Rare | 0.01 | 500
Fire Sprite | Fantasy Set | 002 | fire_sprite.png | Rare | 0.01 | 100
```
### Field Descriptions
@ -67,14 +47,14 @@ Fire Sprite | Fantasy Set | 002 | fire_sprite.png | Rare | 0.05 | 100
| SetNumber | Number for sorting (as integer) | 001 |
| ImageFile | Image filename in images/ folder | "blue_dragon.png" |
| Rarity | Card rarity tier | Common, Uncommon, Rare, Ultra Rare |
| Weight | Physical weight in game units | 0.05 |
| Weight | Physical weight in game units | 0.01 |
| Value | In-game currency value | 500 |
| Description | Optional flavor text for the card | "A majestic dragon..." |
### Image Requirements
- Place images in your cardset's `images/` subfolder
- Place images in your card set's `images/` subfolder
- Recommended format: PNG
- Recommended size: 256x256 or similar aspect ratio
### Comments
@ -86,27 +66,6 @@ Lines starting with `#` are treated as comments and ignored:
Blue Dragon | Fantasy Set | 001 | blue_dragon.png | Ultra Rare | 0.01 | 500
```
## Disabling Card Sets
To temporarily disable a card set without deleting it, prefix the folder name with an underscore `_`:
```
CardSets/
├── MyActiveSet/ # This set will load
├── _MyTestSet/ # This set will be skipped
└── _SeasonalCards/ # This set will be skipped
```
Disabled sets are:
- Skipped during mod initialization
- Logged in the game console for reference
- Counted and displayed in ModConfig (if installed)
This is useful for:
- Work-in-progress card sets
- Seasonal or event-specific content
- Testing different configurations
## Folder Structure
```
@ -174,4 +133,4 @@ This mod is provided as-is for personal use. Do not distribute copyrighted card
## Credits
Built using the official Duckov modding framework and building on the awesome work of the AdditionalCollectibles mod.
Built using the official Duckov modding framework.

View File

@ -28,8 +28,6 @@
<!-- Reference the main project's testable code -->
<Compile Include="src/CardParser.cs" Link="CardParser.cs" />
<Compile Include="src/TradingCard.cs" Link="TradingCard.cs" />
<Compile Include="src/CardPack.cs" Link="CardPack.cs" />
<Compile Include="src/PackParser.cs" Link="PackParser.cs" />
</ItemGroup>
</Project>

View File

@ -50,9 +50,6 @@
<Reference Include="UnityEngine.InputLegacyModule">
<HintPath>$(DuckovPath)$(SubPath)UnityEngine.InputLegacyModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.IMGUIModule">
<HintPath>$(DuckovPath)$(SubPath)UnityEngine.IMGUIModule.dll</HintPath>
</Reference>
<Reference Include="UniTask">
<HintPath>$(DuckovPath)$(SubPath)UniTask.dll</HintPath>
</Reference>

View File

@ -1 +0,0 @@
1.0.0-rc.1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

View File

@ -1,6 +1,6 @@
#!/bin/bash
# Deploy Trading Card Mod to Escape from Duckov
# Usage: ./deploy.sh [--release] [--no-example]
# Usage: ./deploy.sh [--release]
set -e
@ -9,20 +9,11 @@ GAME_PATH="/mnt/NV2/SteamLibrary/steamapps/common/Escape from Duckov"
MOD_NAME="TradingCardMod"
MOD_DIR="$GAME_PATH/Duckov_Data/Mods/$MOD_NAME"
# Parse arguments
# Build configuration
BUILD_CONFIG="Debug"
EXCLUDE_EXAMPLE=false
for arg in "$@"; do
case $arg in
--release)
BUILD_CONFIG="Release"
;;
--no-example)
EXCLUDE_EXAMPLE=true
;;
esac
done
if [[ "$1" == "--release" ]]; then
BUILD_CONFIG="Release"
fi
echo "=== Trading Card Mod Deployment ==="
echo "Build config: $BUILD_CONFIG"
@ -44,7 +35,7 @@ mkdir -p "$MOD_DIR"
mkdir -p "$MOD_DIR/CardSets"
# Copy mod files
echo "[3/5] Copying mod files..."
echo "[3/4] Copying mod files..."
cp "bin/$BUILD_CONFIG/netstandard2.1/$MOD_NAME.dll" "$MOD_DIR/"
cp "info.ini" "$MOD_DIR/"
@ -53,29 +44,10 @@ if [[ -f "preview.png" ]]; then
cp "preview.png" "$MOD_DIR/"
fi
# Copy assets (storage item icons, etc.)
echo "[4/5] Copying assets..."
if [[ -d "assets" ]]; then
mkdir -p "$MOD_DIR/assets"
cp -r assets/* "$MOD_DIR/assets/"
echo " Copied assets folder"
fi
# Copy card sets
echo "[5/5] Copying card sets..."
echo "[4/4] Copying card sets..."
if [[ -d "CardSets" ]]; then
for set_dir in CardSets/*/; do
set_name=$(basename "$set_dir")
# Skip ExampleSet if --no-example flag is set
if [[ "$EXCLUDE_EXAMPLE" == true && "$set_name" == "ExampleSet" ]]; then
echo " Skipping: $set_name (--no-example)"
continue
fi
cp -r "$set_dir" "$MOD_DIR/CardSets/"
echo " Copied: $set_name"
done
cp -r CardSets/* "$MOD_DIR/CardSets/" 2>/dev/null || true
fi
echo ""

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

View File

@ -1,293 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
using Duckov.Economy;
namespace TradingCardMod
{
/// <summary>
/// Helper class for adding disassembly formulas to custom items.
/// Uses reflection to modify the game's DecomposeDatabase at runtime.
/// Based on the approach from AdditionalCollectibles mod.
/// </summary>
public static class DisassemblyHelper
{
// Common material IDs from the game
public static readonly int Metal01 = 91043;
public static readonly int Metal02 = 91044;
public static readonly int Metal03 = 91045;
public static readonly int Plastic01 = 91046;
public static readonly int Plastic02 = 91047;
public static readonly int Plastic03 = 91048;
// Track added formulas for cleanup
private static readonly List<int> _addedItemIds = new List<int>();
// Cached reflection info for ItemEntry type
private static Type? _itemEntryType;
private static FieldInfo? _itemEntryIdField;
private static FieldInfo? _itemEntryAmountField;
/// <summary>
/// Initializes the ItemEntry type info via reflection.
/// Gets the type from Cost.items field to find the correct ItemEntry type.
/// </summary>
private static bool InitializeItemEntryType()
{
if (_itemEntryType != null)
return true;
try
{
// Get ItemEntry type from Cost.items field (more reliable than searching by name)
FieldInfo? itemsField = typeof(Cost).GetField("items", BindingFlags.Instance | BindingFlags.Public);
if (itemsField == null)
{
Debug.LogError("[TradingCardMod] Could not find Cost.items field!");
return false;
}
// items is ItemEntry[], so get the element type
Type itemsType = itemsField.FieldType;
if (!itemsType.IsArray)
{
Debug.LogError("[TradingCardMod] Cost.items is not an array!");
return false;
}
_itemEntryType = itemsType.GetElementType();
if (_itemEntryType == null)
{
Debug.LogError("[TradingCardMod] Could not get ItemEntry element type!");
return false;
}
Debug.Log($"[TradingCardMod] Found ItemEntry type: {_itemEntryType.FullName}");
_itemEntryIdField = _itemEntryType.GetField("id", BindingFlags.Instance | BindingFlags.Public);
_itemEntryAmountField = _itemEntryType.GetField("amount", BindingFlags.Instance | BindingFlags.Public);
if (_itemEntryIdField == null || _itemEntryAmountField == null)
{
// Try non-public fields
_itemEntryIdField = _itemEntryType.GetField("id", BindingFlags.Instance | BindingFlags.NonPublic);
_itemEntryAmountField = _itemEntryType.GetField("amount", BindingFlags.Instance | BindingFlags.NonPublic);
}
if (_itemEntryIdField == null || _itemEntryAmountField == null)
{
// Log available fields for debugging
var fields = _itemEntryType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
Debug.LogError($"[TradingCardMod] Could not find ItemEntry id/amount fields! Available: {string.Join(", ", fields.Select(f => f.Name))}");
return false;
}
Debug.Log($"[TradingCardMod] ItemEntry fields: id={_itemEntryIdField.Name}, amount={_itemEntryAmountField.Name}");
return true;
}
catch (Exception ex)
{
Debug.LogError($"[TradingCardMod] Error initializing ItemEntry type: {ex.Message}");
return false;
}
}
/// <summary>
/// Creates an ItemEntry array using reflection.
/// </summary>
private static Array? CreateItemEntryArray((int id, long amount)[] items)
{
if (!InitializeItemEntryType() || _itemEntryType == null)
return null;
Array array = Array.CreateInstance(_itemEntryType, items.Length);
for (int i = 0; i < items.Length; i++)
{
object entry = Activator.CreateInstance(_itemEntryType)!;
_itemEntryIdField!.SetValue(entry, items[i].id);
_itemEntryAmountField!.SetValue(entry, items[i].amount);
array.SetValue(entry, i);
}
return array;
}
/// <summary>
/// Adds a disassembly formula for an item.
/// </summary>
/// <param name="itemId">The TypeID of the item that can be disassembled</param>
/// <param name="money">Money reward when disassembled (0 for none)</param>
/// <param name="resultItems">Array of (itemId, amount) tuples for materials received</param>
/// <returns>True if formula was added successfully</returns>
public static bool AddFormula(int itemId, long money, params (int id, long amount)[] resultItems)
{
try
{
DecomposeDatabase instance = DecomposeDatabase.Instance;
if (instance == null)
{
Debug.LogError("[TradingCardMod] DecomposeDatabase.Instance is null!");
return false;
}
// Get the private entries field via reflection
FieldInfo? entriesField = typeof(DecomposeDatabase).GetField("entries",
BindingFlags.Instance | BindingFlags.NonPublic);
if (entriesField == null)
{
Debug.LogError("[TradingCardMod] Could not find 'entries' field on DecomposeDatabase!");
return false;
}
DecomposeFormula[] currentEntries = (DecomposeFormula[])entriesField.GetValue(instance);
List<DecomposeFormula> formulaList = new List<DecomposeFormula>(currentEntries);
// Check if formula already exists for this item
foreach (DecomposeFormula existing in formulaList)
{
if (existing.item == itemId)
{
Debug.LogWarning($"[TradingCardMod] Disassembly formula already exists for item {itemId}, skipping");
return false;
}
}
// Create new formula
DecomposeFormula newFormula = new DecomposeFormula
{
item = itemId,
valid = true
};
// Create cost (result items)
// Cost is a struct, so we need to box it for reflection to work
Cost result = new Cost
{
money = money
};
// Build item entries array using reflection
if (resultItems.Length > 0)
{
Array? itemEntries = CreateItemEntryArray(resultItems);
if (itemEntries == null)
{
Debug.LogError("[TradingCardMod] Failed to create ItemEntry array!");
return false;
}
// Set items field on Cost using reflection (it expects Cost+ItemEntry[])
// Must box the struct first, modify, then unbox
FieldInfo? itemsField = typeof(Cost).GetField("items", BindingFlags.Instance | BindingFlags.Public);
if (itemsField != null)
{
object boxedResult = result; // Box the struct
itemsField.SetValue(boxedResult, itemEntries);
result = (Cost)boxedResult; // Unbox back
Debug.Log($"[TradingCardMod] Set {itemEntries.Length} items on Cost for item {itemId}");
}
}
newFormula.result = result;
// Add to list and write back
formulaList.Add(newFormula);
entriesField.SetValue(instance, formulaList.ToArray());
// Track for cleanup
if (!_addedItemIds.Contains(itemId))
{
_addedItemIds.Add(itemId);
}
// Rebuild internal dictionary for lookup performance
MethodInfo? rebuildMethod = typeof(DecomposeDatabase).GetMethod("RebuildDictionary",
BindingFlags.Instance | BindingFlags.NonPublic);
rebuildMethod?.Invoke(instance, null);
Debug.Log($"[TradingCardMod] Added disassembly formula for item {itemId}");
return true;
}
catch (Exception ex)
{
Debug.LogError($"[TradingCardMod] Error adding disassembly formula: {ex.Message}");
return false;
}
}
/// <summary>
/// Adds a simple money-only disassembly formula.
/// </summary>
/// <param name="itemId">The TypeID of the item</param>
/// <param name="money">Money reward when disassembled</param>
/// <returns>True if formula was added successfully</returns>
public static bool AddMoneyOnlyFormula(int itemId, long money)
{
return AddFormula(itemId, money);
}
/// <summary>
/// Removes all disassembly formulas added by this mod.
/// Should be called during mod cleanup/unload.
/// </summary>
public static void Cleanup()
{
try
{
DecomposeDatabase instance = DecomposeDatabase.Instance;
if (instance == null)
{
Debug.LogWarning("[TradingCardMod] DecomposeDatabase.Instance is null during cleanup");
_addedItemIds.Clear();
return;
}
FieldInfo entriesField = typeof(DecomposeDatabase).GetField("entries",
BindingFlags.Instance | BindingFlags.NonPublic);
if (entriesField == null)
{
Debug.LogWarning("[TradingCardMod] Could not find 'entries' field during cleanup");
_addedItemIds.Clear();
return;
}
DecomposeFormula[] currentEntries = (DecomposeFormula[])entriesField.GetValue(instance);
List<DecomposeFormula> formulaList = new List<DecomposeFormula>(currentEntries);
int removedCount = 0;
for (int i = formulaList.Count - 1; i >= 0; i--)
{
if (_addedItemIds.Contains(formulaList[i].item))
{
formulaList.RemoveAt(i);
removedCount++;
}
}
entriesField.SetValue(instance, formulaList.ToArray());
// Rebuild dictionary
MethodInfo rebuildMethod = typeof(DecomposeDatabase).GetMethod("RebuildDictionary",
BindingFlags.Instance | BindingFlags.NonPublic);
rebuildMethod?.Invoke(instance, null);
Debug.Log($"[TradingCardMod] Removed {removedCount} disassembly formulas");
_addedItemIds.Clear();
}
catch (Exception ex)
{
Debug.LogError($"[TradingCardMod] Error during disassembly cleanup: {ex.Message}");
_addedItemIds.Clear();
}
}
/// <summary>
/// Gets the count of disassembly formulas added by this mod.
/// </summary>
public static int GetAddedFormulaCount()
{
return _addedItemIds.Count;
}
}
}

View File

@ -1,7 +1,6 @@
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using ItemStatsSystem;
using SodaCraft.Localizations;
@ -64,16 +63,14 @@ namespace TradingCardMod
private const int BASE_ITEM_ID = 135;
// Storage item IDs (high range to avoid conflicts)
private const int BINDER_SHEET_ITEM_ID = 200001;
private const int CARD_BINDER_ITEM_ID = 200002;
private const int BINDER_ITEM_ID = 200001;
private const int CARD_BOX_ITEM_ID = 200002;
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;
private Tag? _binderSheetTag;
private Tag? _cardBinderContentTag;
private Item? _binderItem;
private Item? _cardBoxItem;
@ -81,15 +78,13 @@ namespace TradingCardMod
private Dictionary<string, List<TradingCard>> _cardsBySet = new Dictionary<string, List<TradingCard>>();
private Dictionary<string, int> _cardNameToTypeId = new Dictionary<string, int>();
private List<Item> _registeredPacks = new List<Item>();
private int _skippedSetsCount = 0;
// Store pack definitions for runtime lookup (key = "SetName|PackName")
private Dictionary<string, CardPack> _packDefinitions = new Dictionary<string, CardPack>();
// Debug spawn window
private bool _showSpawnWindow = false;
private Rect _spawnWindowRect = new Rect(20, 250, 350, 450);
private System.Random _random = new System.Random();
// Debug: track spawn cycling
private int _debugSpawnIndex = 0;
private List<Item> _allSpawnableItems = new List<Item>();
/// <summary>
/// Called when the GameObject is created. Initialize early to register items before saves load.
@ -112,10 +107,8 @@ namespace TradingCardMod
// Log all available tags for reference
TagHelper.LogAvailableTags();
// Create our custom tags first
// Create our custom tag first
_tradingCardTag = TagHelper.GetOrCreateTradingCardTag();
_binderSheetTag = TagHelper.GetOrCreateBinderSheetTag();
_cardBinderContentTag = TagHelper.GetOrCreateCardBinderContentTag();
// Load and register cards - do this early so saves can load them
LoadCardSets();
@ -123,12 +116,14 @@ namespace TradingCardMod
// Create storage items
CreateStorageItems();
// Card packs disabled - feature not working correctly
// TODO: Fix pack opening mechanics before re-enabling
// CreateCardPacks();
// Create card packs
CreateCardPacks();
// Set up disassembly formulas for all items
SetupDisassembly();
// 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!");
}
@ -225,20 +220,6 @@ namespace TradingCardMod
_registeredPacks.Count
);
// Skipped sets display
var skippedOption = new System.Collections.Generic.SortedDictionary<string, object>
{
{ $"{_skippedSetsCount} sets", _skippedSetsCount }
};
ModConfigAPI.SafeAddDropdownList(
MOD_NAME,
"SkippedSets",
"Disabled Card Sets",
skippedOption,
typeof(int),
_skippedSetsCount
);
// Card sets info - one entry per set for clarity
int setIndex = 0;
foreach (var setEntry in _cardsBySet)
@ -278,30 +259,14 @@ namespace TradingCardMod
string[] setDirectories = Directory.GetDirectories(cardSetsPath);
Debug.Log($"[TradingCardMod] Found {setDirectories.Length} card set directories");
_skippedSetsCount = 0;
foreach (string setDir in setDirectories)
{
string setName = Path.GetFileName(setDir);
// Skip directories that start with underscore (disabled sets)
if (setName.StartsWith("_"))
{
Debug.Log($"[TradingCardMod] Skipping disabled card set: {setName}");
_skippedSetsCount++;
continue;
}
LoadCardSet(setDir);
}
if (_skippedSetsCount > 0)
{
Debug.Log($"[TradingCardMod] Skipped {_skippedSetsCount} disabled card set(s)");
}
Debug.Log($"[TradingCardMod] Total cards loaded: {_loadedCards.Count}");
Debug.Log($"[TradingCardMod] Total items registered: {_registeredItems.Count}");
Debug.Log("[TradingCardMod] DEBUG: Press F10 to open spawn menu (storage items & random cards by rarity)");
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();
@ -333,67 +298,36 @@ namespace TradingCardMod
}
/// <summary>
/// Creates storage items (binder sheet and card binder) for holding trading cards.
/// Creates storage items (binder and card box) for holding trading cards.
/// </summary>
private void CreateStorageItems()
{
if (_tradingCardTag == null || _binderSheetTag == null || _cardBinderContentTag == null)
if (_tradingCardTag == null)
{
Debug.LogError("[TradingCardMod] Cannot create storage items - Required tags not created!");
Debug.LogError("[TradingCardMod] Cannot create storage items - TradingCard tag not created!");
return;
}
// Load custom icons for storage items
string assetsPath = Path.Combine(_modPath, "assets");
Sprite? binderSheetIcon = null;
Sprite? cardBinderIcon = null;
string binderSheetIconPath = Path.Combine(assetsPath, "binder_sheet.png");
if (File.Exists(binderSheetIconPath))
{
binderSheetIcon = LoadSpriteFromFile(binderSheetIconPath, BINDER_SHEET_ITEM_ID);
Debug.Log($"[TradingCardMod] Loaded binder sheet icon from {binderSheetIconPath}");
}
string cardBinderIconPath = Path.Combine(assetsPath, "card_binder.png");
if (File.Exists(cardBinderIconPath))
{
cardBinderIcon = LoadSpriteFromFile(cardBinderIconPath, CARD_BINDER_ITEM_ID);
Debug.Log($"[TradingCardMod] Loaded card binder icon from {cardBinderIconPath}");
}
// Create Binder Sheet (9 slots, stores cards only)
// Create Card Binder (9 slots = 3x3 grid)
_binderItem = StorageHelper.CreateCardStorage(
BINDER_SHEET_ITEM_ID,
"Binder Sheet",
"A sheet for storing and organizing trading cards. Holds 9 cards.",
BINDER_ITEM_ID,
"Card Binder",
"A binder for storing and organizing trading cards. Holds 9 cards.",
9,
0.1f, // weight
7500, // value
new List<Tag> { _tradingCardTag }, // Only trading cards allowed
binderSheetIcon
0.5f, // weight
500, // value
_tradingCardTag
);
// Add BinderSheet and CardBinderContent tags to the binder sheet item itself
// so it can be stored in Card Binders
if (_binderItem != null)
{
_binderItem.Tags.Add(_binderSheetTag);
_binderItem.Tags.Add(_cardBinderContentTag);
Debug.Log("[TradingCardMod] Added BinderSheet and CardBinderContent tags to Binder Sheet item");
}
// Create Card Binder (12 slots, stores items with CardBinderContent tag)
// This allows both cards and binder sheets (which both have CardBinderContent tag)
// Create Card Box (36 slots = bulk storage)
_cardBoxItem = StorageHelper.CreateCardStorage(
CARD_BINDER_ITEM_ID,
"Card Binder",
"A binder for storing and organizing trading cards. Can hold cards or binder sheets. Holds 12 items.",
12,
1.5f, // weight
12500, // value
new List<Tag> { _cardBinderContentTag }, // Only CardBinderContent tag required
cardBinderIcon
CARD_BOX_ITEM_ID,
"Card Box",
"A large box for bulk storage of trading cards. Holds 36 cards.",
36,
2.0f, // weight
1500, // value
_tradingCardTag
);
}
@ -452,197 +386,51 @@ namespace TradingCardMod
Debug.Log($"[TradingCardMod] Created {_registeredPacks.Count} card packs");
}
/// <summary>
/// Sets up disassembly formulas for storage items only.
/// Cards intentionally have no disassembly (they're collectibles).
/// </summary>
private void SetupDisassembly()
{
int formulasAdded = 0;
// Polyethylene Sheet TypeID (base game material)
const int POLYETHYLENE_SHEET_ID = 764;
// Binder Sheet: 2x polyethylene sheet
if (_binderItem != null)
{
if (DisassemblyHelper.AddFormula(BINDER_SHEET_ITEM_ID, 0L, (POLYETHYLENE_SHEET_ID, 2L)))
{
formulasAdded++;
}
}
// Card Binder: 4x polyethylene sheet
if (_cardBoxItem != null)
{
if (DisassemblyHelper.AddFormula(CARD_BINDER_ITEM_ID, 0L, (POLYETHYLENE_SHEET_ID, 4L)))
{
formulasAdded++;
}
}
// No disassembly for cards or packs (collectibles)
Debug.Log($"[TradingCardMod] Added {formulasAdded} disassembly formulas");
}
/// <summary>
/// Update is called every frame. Used for debug input handling.
/// </summary>
void Update()
{
// Debug: Press F10 to toggle spawn menu
if (Input.GetKeyDown(KeyCode.F10))
// Debug: Press F9 to spawn an item
if (Input.GetKeyDown(KeyCode.F9))
{
_showSpawnWindow = !_showSpawnWindow;
Debug.Log($"[TradingCardMod] Spawn window {(_showSpawnWindow ? "opened" : "closed")}");
SpawnDebugItem();
}
}
/// <summary>
/// OnGUI is called for rendering and handling GUI events.
/// Spawns items for testing - cycles through cards, then storage items.
/// </summary>
void OnGUI()
private void SpawnDebugItem()
{
if (!_showSpawnWindow)
return;
// Set depth to ensure window is on top
UnityEngine.GUI.depth = -1000;
// Draw window and bring to front
_spawnWindowRect = UnityEngine.GUI.Window(12345, _spawnWindowRect, DrawSpawnWindow, "Spawn Items (F10 to close)");
UnityEngine.GUI.BringWindowToFront(12345);
}
/// <summary>
/// Draws the spawn window contents.
/// </summary>
private void DrawSpawnWindow(int windowID)
{
UnityEngine.GUILayout.BeginVertical();
// Storage items
UnityEngine.GUILayout.Label("Storage Items:", UnityEngine.GUI.skin.box);
if (UnityEngine.GUILayout.Button("Card Binder"))
if (_allSpawnableItems.Count == 0)
{
SpawnStorageItem(_cardBoxItem, "Card Binder");
}
if (UnityEngine.GUILayout.Button("Binder Sheet"))
{
SpawnStorageItem(_binderItem, "Binder Sheet");
}
UnityEngine.GUILayout.Space(10);
// Random cards by rarity
UnityEngine.GUILayout.Label("Spawn Random Card:", UnityEngine.GUI.skin.box);
if (UnityEngine.GUILayout.Button("Random Common Card"))
{
SpawnRandomCardByRarity("Common");
}
if (UnityEngine.GUILayout.Button("Random Uncommon Card"))
{
SpawnRandomCardByRarity("Uncommon");
}
if (UnityEngine.GUILayout.Button("Random Rare Card"))
{
SpawnRandomCardByRarity("Rare");
}
if (UnityEngine.GUILayout.Button("Random Very Rare Card"))
{
SpawnRandomCardByRarity("Very Rare");
}
if (UnityEngine.GUILayout.Button("Random Ultra Rare Card"))
{
SpawnRandomCardByRarity("Ultra Rare");
}
UnityEngine.GUILayout.EndVertical();
// Make window draggable
UnityEngine.GUI.DragWindow();
}
/// <summary>
/// Spawns a storage item (binder or binder sheet).
/// </summary>
private void SpawnStorageItem(Item? prefab, string itemName)
{
if (prefab == null)
{
Debug.LogWarning($"[TradingCardMod] {itemName} not available!");
Debug.LogWarning("[TradingCardMod] No items registered to spawn!");
return;
}
// Cycle through all spawnable items
Item prefab = _allSpawnableItems[_debugSpawnIndex % _allSpawnableItems.Count];
_debugSpawnIndex++;
try
{
// 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: {itemName}");
Debug.Log($"[TradingCardMod] Spawned: {instance.DisplayName} (ID: {instance.TypeID})");
}
else
{
Debug.LogError($"[TradingCardMod] Failed to instantiate {itemName}");
Debug.LogError($"[TradingCardMod] Failed to instantiate {prefab.DisplayName} (ID: {prefab.TypeID})");
}
}
catch (Exception ex)
{
Debug.LogError($"[TradingCardMod] Failed to spawn {itemName}: {ex.Message}");
}
}
/// <summary>
/// Spawns a random card of the specified rarity.
/// </summary>
private void SpawnRandomCardByRarity(string rarity)
{
// Find all cards of this rarity
var matchingCards = _loadedCards
.Where(c => c.Rarity.Equals(rarity, StringComparison.OrdinalIgnoreCase))
.ToList();
if (matchingCards.Count == 0)
{
Debug.LogWarning($"[TradingCardMod] No {rarity} cards found!");
return;
}
// Pick a random card
TradingCard randomCard = matchingCards[_random.Next(matchingCards.Count)];
// Get the item ID for this card
if (!_cardNameToTypeId.TryGetValue(randomCard.CardName, out int typeId))
{
Debug.LogError($"[TradingCardMod] Card '{randomCard.CardName}' not registered!");
return;
}
try
{
Item instance = ItemAssetsCollection.InstantiateSync(typeId);
if (instance != null)
{
ItemUtilities.SendToPlayer(instance);
Debug.Log($"[TradingCardMod] Spawned random {rarity}: {randomCard.CardName}");
}
else
{
Debug.LogError($"[TradingCardMod] Failed to instantiate card: {randomCard.CardName}");
}
}
catch (Exception ex)
{
Debug.LogError($"[TradingCardMod] Failed to spawn card: {ex.Message}");
Debug.LogError($"[TradingCardMod] Failed to spawn item: {ex.Message}");
}
}
@ -748,20 +536,33 @@ namespace TradingCardMod
// Set display quality based on rarity
SetDisplayQuality(item, card.GetQuality());
// Set tags - Cards are collectibles
// Set tags
item.Tags.Clear();
Tag? collTag = TagHelper.GetTargetTag("Collection");
if (collTag != null)
{
item.Tags.Add(collTag);
}
// Add Luxury tag (for selling at shops)
Tag? luxuryTag = TagHelper.GetTargetTag("Luxury");
if (luxuryTag != null)
{
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
@ -770,12 +571,6 @@ namespace TradingCardMod
item.Tags.Add(_tradingCardTag);
}
// Add CardBinderContent tag so cards can be stored in Card Binders
if (_cardBinderContentTag != null)
{
item.Tags.Add(_cardBinderContentTag);
}
// Load and set icon
Sprite? cardSprite = LoadSpriteFromFile(card.ImagePath, typeId);
if (cardSprite != null)
@ -871,33 +666,10 @@ namespace TradingCardMod
texture.filterMode = FilterMode.Bilinear;
texture.Apply();
// Pad to square if rectangular (game preview expects square icons)
Texture2D finalTexture = texture;
if (texture.width != texture.height)
{
int size = Mathf.Max(texture.width, texture.height);
finalTexture = new Texture2D(size, size, TextureFormat.RGBA32, false);
// Fill with transparent pixels
Color[] clearPixels = new Color[size * size];
for (int i = 0; i < clearPixels.Length; i++)
{
clearPixels[i] = Color.clear;
}
finalTexture.SetPixels(clearPixels);
// Center the original image
int offsetX = (size - texture.width) / 2;
int offsetY = (size - texture.height) / 2;
finalTexture.SetPixels(offsetX, offsetY, texture.width, texture.height, texture.GetPixels());
finalTexture.filterMode = FilterMode.Bilinear;
finalTexture.Apply();
}
// Create sprite from texture
Sprite sprite = Sprite.Create(
finalTexture,
new Rect(0f, 0f, finalTexture.width, finalTexture.height),
texture,
new Rect(0f, 0f, texture.width, texture.height),
new Vector2(0.5f, 0.5f),
100f
);
@ -909,7 +681,7 @@ namespace TradingCardMod
// Store references on the holder to prevent GC
var resourceHolder = holder.AddComponent<CardResourceHolder>();
resourceHolder.Texture = finalTexture;
resourceHolder.Texture = texture;
resourceHolder.Sprite = sprite;
return sprite;
@ -957,9 +729,6 @@ namespace TradingCardMod
// Clean up packs
PackHelper.Cleanup();
// Clean up disassembly formulas
DisassemblyHelper.Cleanup();
// Clean up tags
TagHelper.Cleanup();
@ -968,6 +737,7 @@ namespace TradingCardMod
_cardNameToTypeId.Clear();
_registeredPacks.Clear();
_packDefinitions.Clear();
_allSpawnableItems.Clear();
Debug.Log("[TradingCardMod] Cleanup complete.");
}

View File

@ -64,15 +64,10 @@ namespace TradingCardMod
item.Quality = 3; // Uncommon quality for packs
item.DisplayQuality = (DisplayQuality)3;
// Set tags - Packs are collectibles found in misc loot
// Set tags
item.Tags.Clear();
Tag? collTag = TagHelper.GetTargetTag("Collection");
if (collTag != null)
{
item.Tags.Add(collTag);
}
// Add Misc tag for loot spawning
Tag? miscTag = TagHelper.GetTargetTag("Misc");
if (miscTag != null)
{

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
using ItemStatsSystem;
@ -23,7 +22,7 @@ namespace TradingCardMod
private static readonly List<GameObject> _createdGameObjects = new List<GameObject>();
/// <summary>
/// Creates a card binder item with slots that only accept items with specific tags.
/// Creates a card binder item with slots that only accept TradingCard tagged items.
/// </summary>
/// <param name="itemId">Unique ID for this storage item</param>
/// <param name="displayName">Display name shown to player</param>
@ -31,7 +30,7 @@ namespace TradingCardMod
/// <param name="slotCount">Number of card slots</param>
/// <param name="weight">Item weight</param>
/// <param name="value">Item value</param>
/// <param name="allowedTags">List of tags that can be stored in this container</param>
/// <param name="tradingCardTag">The TradingCard tag for filtering</param>
/// <param name="icon">Optional custom icon sprite</param>
/// <returns>The created Item, or null on failure</returns>
public static Item? CreateCardStorage(
@ -41,7 +40,7 @@ namespace TradingCardMod
int slotCount,
float weight,
int value,
List<Tag> allowedTags,
Tag tradingCardTag,
Sprite? icon = null)
{
try
@ -80,23 +79,16 @@ namespace TradingCardMod
// Set display quality
item.DisplayQuality = (DisplayQuality)3;
// Set tags - storage items are luxury collectibles
// Set tags - storage items should be tools
item.Tags.Clear();
Tag? collTag = TagHelper.GetTargetTag("Collection");
if (collTag != null)
Tag? toolTag = TagHelper.GetTargetTag("Tool");
if (toolTag != null)
{
item.Tags.Add(collTag);
item.Tags.Add(toolTag);
}
Tag? luxuryTag = TagHelper.GetTargetTag("Luxury");
if (luxuryTag != null)
{
item.Tags.Add(luxuryTag);
}
// Configure slots to only accept items with allowed tags
ConfigureCardSlots(item, allowedTags, slotCount);
// Configure slots to only accept TradingCard tagged items
ConfigureCardSlots(item, tradingCardTag, slotCount);
// Set icon if provided
if (icon != null)
@ -131,9 +123,9 @@ namespace TradingCardMod
}
/// <summary>
/// Configures an item's slots to only accept items with specific tags.
/// Configures an item's slots to only accept items with a specific tag.
/// </summary>
private static void ConfigureCardSlots(Item item, List<Tag> requiredTags, int slotCount)
private static void ConfigureCardSlots(Item item, Tag requiredTag, int slotCount)
{
// Get template slot info if available
Slot templateSlot = new Slot();
@ -155,17 +147,13 @@ namespace TradingCardMod
typeof(Slot).GetField("key", BindingFlags.Instance | BindingFlags.NonPublic)
?.SetValue(newSlot, $"CardSlot{i}");
// Add all tag requirements - only items with these tags can go in
foreach (var tag in requiredTags)
{
newSlot.requireTags.Add(tag);
}
// Add tag requirement - only TradingCard items can go in
newSlot.requireTags.Add(requiredTag);
item.Slots.Add(newSlot);
}
string tagNames = string.Join(", ", requiredTags.Select(t => t.name));
Debug.Log($"[TradingCardMod] Configured {slotCount} slots with filter: {tagNames}");
Debug.Log($"[TradingCardMod] Configured {slotCount} slots with TradingCard filter");
}
/// <summary>

View File

@ -82,26 +82,6 @@ namespace TradingCardMod
return CreateOrCloneTag("TradingCard", "Trading Card");
}
/// <summary>
/// Gets the "BinderSheet" tag, creating it if it doesn't exist.
/// This tag identifies binder sheets that can be stored in card binders.
/// </summary>
/// <returns>The BinderSheet tag.</returns>
public static Tag GetOrCreateBinderSheetTag()
{
return CreateOrCloneTag("BinderSheet", "Binder Sheet");
}
/// <summary>
/// Gets the "CardBinderContent" tag, creating it if it doesn't exist.
/// This is a parent tag shared by both cards and binder sheets, allowing both to be stored in card binders.
/// </summary>
/// <returns>The CardBinderContent tag.</returns>
public static Tag GetOrCreateCardBinderContentTag()
{
return CreateOrCloneTag("CardBinderContent", "Card Binder Content");
}
/// <summary>
/// Gets all tags created by this mod.
/// </summary>

View File

@ -1,463 +0,0 @@
using System.Collections.Generic;
using System.IO;
using Xunit;
using TradingCardMod;
namespace TradingCardMod.Tests
{
/// <summary>
/// Unit tests for the CardPack, PackSlot, and PackParser classes.
/// Tests cover pack data structures, default slot configurations, and parsing functionality.
/// </summary>
public class PackParserTests
{
#region CardPack Tests
[Fact]
public void CardPack_GenerateTypeID_ReturnsConsistentValue()
{
// Arrange
var pack = new CardPack
{
PackName = "Booster Pack",
SetName = "Example Set"
};
// Act
int id1 = pack.GenerateTypeID();
int id2 = pack.GenerateTypeID();
// Assert - same pack should always generate same ID
Assert.Equal(id1, id2);
Assert.True(id1 >= 300000, "Pack TypeID should be in 300000+ range");
Assert.True(id1 < 400000, "Pack TypeID should be below 400000");
}
[Fact]
public void CardPack_GenerateTypeID_DifferentPacksGetDifferentIDs()
{
// Arrange
var pack1 = new CardPack { PackName = "Booster Pack", SetName = "Set A" };
var pack2 = new CardPack { PackName = "Premium Pack", SetName = "Set A" };
// Act
int id1 = pack1.GenerateTypeID();
int id2 = pack2.GenerateTypeID();
// Assert
Assert.NotEqual(id1, id2);
}
[Fact]
public void CardPack_GenerateTypeID_SameNameDifferentSetGetsDifferentIDs()
{
// Arrange
var pack1 = new CardPack { PackName = "Booster Pack", SetName = "Set A" };
var pack2 = new CardPack { PackName = "Booster Pack", SetName = "Set B" };
// Act
int id1 = pack1.GenerateTypeID();
int id2 = pack2.GenerateTypeID();
// Assert
Assert.NotEqual(id1, id2);
}
[Fact]
public void CardPack_DefaultValues_AreCorrect()
{
// Arrange & Act
var pack = new CardPack();
// Assert
Assert.Equal(string.Empty, pack.PackName);
Assert.Equal(string.Empty, pack.SetName);
Assert.Equal(0, pack.Value);
Assert.Equal(0.1f, pack.Weight);
Assert.False(pack.IsDefault);
Assert.NotNull(pack.Slots);
Assert.Empty(pack.Slots);
}
#endregion
#region PackSlot Tests
[Fact]
public void PackSlot_DefaultValues_AreCorrect()
{
// Arrange & Act
var slot = new PackSlot();
// Assert
Assert.True(slot.UseRarityWeights);
Assert.NotNull(slot.RarityWeights);
Assert.Empty(slot.RarityWeights);
Assert.NotNull(slot.CardWeights);
Assert.Empty(slot.CardWeights);
}
[Fact]
public void PackSlot_RarityWeights_CanBePopulated()
{
// Arrange
var slot = new PackSlot
{
UseRarityWeights = true,
RarityWeights = new Dictionary<string, float>
{
{ "Common", 100f },
{ "Rare", 10f }
}
};
// Assert
Assert.Equal(2, slot.RarityWeights.Count);
Assert.Equal(100f, slot.RarityWeights["Common"]);
Assert.Equal(10f, slot.RarityWeights["Rare"]);
}
[Fact]
public void PackSlot_CardWeights_CanBePopulated()
{
// Arrange
var slot = new PackSlot
{
UseRarityWeights = false,
CardWeights = new Dictionary<string, float>
{
{ "Duck Hero", 50f },
{ "Golden Quacker", 10f }
}
};
// Assert
Assert.False(slot.UseRarityWeights);
Assert.Equal(2, slot.CardWeights.Count);
Assert.Equal(50f, slot.CardWeights["Duck Hero"]);
}
#endregion
#region DefaultPackSlots Tests
[Fact]
public void DefaultPackSlots_CommonSlot_HasCorrectWeights()
{
// Act
var slot = DefaultPackSlots.CommonSlot;
// Assert
Assert.True(slot.UseRarityWeights);
Assert.Equal(100f, slot.RarityWeights["Common"]);
Assert.Equal(30f, slot.RarityWeights["Uncommon"]);
Assert.Equal(5f, slot.RarityWeights["Rare"]);
Assert.Equal(1f, slot.RarityWeights["Very Rare"]);
Assert.Equal(0f, slot.RarityWeights["Ultra Rare"]);
Assert.Equal(0f, slot.RarityWeights["Legendary"]);
}
[Fact]
public void DefaultPackSlots_UncommonSlot_HasCorrectWeights()
{
// Act
var slot = DefaultPackSlots.UncommonSlot;
// Assert
Assert.True(slot.UseRarityWeights);
Assert.Equal(60f, slot.RarityWeights["Common"]);
Assert.Equal(80f, slot.RarityWeights["Uncommon"]);
Assert.Equal(20f, slot.RarityWeights["Rare"]);
}
[Fact]
public void DefaultPackSlots_RareSlot_HasBetterOdds()
{
// Act
var slot = DefaultPackSlots.RareSlot;
// Assert
Assert.True(slot.UseRarityWeights);
// Rare slot should have better odds for rare+ cards
Assert.True(slot.RarityWeights["Rare"] > slot.RarityWeights["Common"]);
Assert.True(slot.RarityWeights["Legendary"] > 0f);
}
[Fact]
public void DefaultPackSlots_GetDefaultSlots_ReturnsThreeSlots()
{
// Act
var slots = DefaultPackSlots.GetDefaultSlots();
// Assert
Assert.Equal(3, slots.Count);
Assert.All(slots, s => Assert.True(s.UseRarityWeights));
}
[Fact]
public void DefaultPackSlots_GetDefaultSlots_ReturnsNewInstances()
{
// Act
var slots1 = DefaultPackSlots.GetDefaultSlots();
var slots2 = DefaultPackSlots.GetDefaultSlots();
// Assert - should be different list instances
Assert.NotSame(slots1, slots2);
}
#endregion
#region PackParser.CreateDefaultPack Tests
[Fact]
public void CreateDefaultPack_ReturnsPackWithCorrectSetName()
{
// Arrange
string setName = "Example Set";
string imagesDir = "/fake/path/images";
// Act
var pack = PackParser.CreateDefaultPack(setName, imagesDir);
// Assert
Assert.Equal(setName, pack.SetName);
Assert.Equal($"{setName} Pack", pack.PackName);
Assert.True(pack.IsDefault);
}
[Fact]
public void CreateDefaultPack_HasThreeSlots()
{
// Arrange
string setName = "Example Set";
string imagesDir = "/fake/path/images";
// Act
var pack = PackParser.CreateDefaultPack(setName, imagesDir);
// Assert
Assert.Equal(3, pack.Slots.Count);
}
[Fact]
public void CreateDefaultPack_HasCorrectImagePath()
{
// Arrange
string setName = "Example Set";
string imagesDir = "/path/to/images";
// Act
var pack = PackParser.CreateDefaultPack(setName, imagesDir);
// Assert
Assert.Equal("pack.png", pack.ImageFile);
Assert.Contains("pack.png", pack.ImagePath);
}
[Fact]
public void CreateDefaultPack_HasDefaultValues()
{
// Arrange
string setName = "Example Set";
string imagesDir = "/fake/path";
// Act
var pack = PackParser.CreateDefaultPack(setName, imagesDir);
// Assert
Assert.Equal(100, pack.Value);
Assert.Equal(0.1f, pack.Weight);
}
#endregion
#region PackParser.ParseFile Tests
[Fact]
public void ParseFile_NonExistentFile_ReturnsEmptyList()
{
// Arrange
string fakePath = "/nonexistent/packs.txt";
string imagesDir = "/nonexistent/images";
// Act
var packs = PackParser.ParseFile(fakePath, "TestSet", imagesDir);
// Assert
Assert.Empty(packs);
}
[Fact]
public void ParseFile_EmptyFile_ReturnsEmptyList()
{
// Arrange
string tempFile = Path.GetTempFileName();
string imagesDir = Path.GetDirectoryName(tempFile) ?? "";
try
{
File.WriteAllText(tempFile, "");
// Act
var packs = PackParser.ParseFile(tempFile, "TestSet", imagesDir);
// Assert
Assert.Empty(packs);
}
finally
{
File.Delete(tempFile);
}
}
[Fact]
public void ParseFile_CommentOnlyFile_ReturnsEmptyList()
{
// Arrange
string tempFile = Path.GetTempFileName();
string imagesDir = Path.GetDirectoryName(tempFile) ?? "";
try
{
File.WriteAllText(tempFile, "# This is a comment\n# Another comment");
// Act
var packs = PackParser.ParseFile(tempFile, "TestSet", imagesDir);
// Assert
Assert.Empty(packs);
}
finally
{
File.Delete(tempFile);
}
}
[Fact]
public void ParseFile_ValidPackDefinition_ReturnsPack()
{
// Arrange
string tempFile = Path.GetTempFileName();
string imagesDir = Path.GetDirectoryName(tempFile) ?? "";
try
{
// Format: PackName | Image | Value | Weight
// Slots are indented with RARITY: or CARDS: prefix
string content = @"# Test pack file
Premium Pack | premium.png | 500 | 0.05
RARITY: Common:50, Rare:50
";
File.WriteAllText(tempFile, content);
// Act
var packs = PackParser.ParseFile(tempFile, "TestSet", imagesDir);
// Assert
Assert.Single(packs);
Assert.Equal("Premium Pack", packs[0].PackName);
Assert.Equal("TestSet", packs[0].SetName);
Assert.Equal(500, packs[0].Value);
Assert.Equal(0.05f, packs[0].Weight);
Assert.Single(packs[0].Slots);
}
finally
{
File.Delete(tempFile);
}
}
[Fact]
public void ParseFile_MultipleSlots_ParsesAllSlots()
{
// Arrange
string tempFile = Path.GetTempFileName();
string imagesDir = Path.GetDirectoryName(tempFile) ?? "";
try
{
string content = @"Test Pack | test.png | 100
RARITY: Common:100
RARITY: Uncommon:100
RARITY: Rare:100
";
File.WriteAllText(tempFile, content);
// Act
var packs = PackParser.ParseFile(tempFile, "TestSet", imagesDir);
// Assert
Assert.Single(packs);
Assert.Equal(3, packs[0].Slots.Count);
}
finally
{
File.Delete(tempFile);
}
}
[Fact]
public void ParseFile_MultiplePacks_ParsesAll()
{
// Arrange
string tempFile = Path.GetTempFileName();
string imagesDir = Path.GetDirectoryName(tempFile) ?? "";
try
{
string content = @"Pack One | one.png | 100
RARITY: Common:100
Pack Two | two.png | 200
RARITY: Rare:100
";
File.WriteAllText(tempFile, content);
// Act
var packs = PackParser.ParseFile(tempFile, "TestSet", imagesDir);
// Assert
Assert.Equal(2, packs.Count);
Assert.Equal("Pack One", packs[0].PackName);
Assert.Equal("Pack Two", packs[1].PackName);
}
finally
{
File.Delete(tempFile);
}
}
[Fact]
public void ParseFile_CardSpecificWeights_ParsesCorrectly()
{
// Arrange
string tempFile = Path.GetTempFileName();
string imagesDir = Path.GetDirectoryName(tempFile) ?? "";
try
{
string content = @"Special Pack | special.png | 1000
CARDS: Duck Hero:50, Golden Quacker:10
";
File.WriteAllText(tempFile, content);
// Act
var packs = PackParser.ParseFile(tempFile, "TestSet", imagesDir);
// Assert
Assert.Single(packs);
var slot = packs[0].Slots[0];
Assert.False(slot.UseRarityWeights);
Assert.Equal(2, slot.CardWeights.Count);
Assert.Equal(50f, slot.CardWeights["Duck Hero"]);
Assert.Equal(10f, slot.CardWeights["Golden Quacker"]);
}
finally
{
File.Delete(tempFile);
}
}
#endregion
}
}