Add hierarchical storage system, card set exclusion, and weight adjustments

**Storage System Improvements:**
- Renamed "Card Binder" → "Binder Sheet" (9 slots, weight 0.1)
- Renamed "Card Box" → "Card Binder" (12 slots, weight 1.5)
- Implemented nested storage: Binder Sheets can be stored in Card Binders
- Created new "BinderSheet" tag for hierarchical filtering
- Updated StorageHelper to support multi-tag slot filtering

**Card Set Management:**
- Added folder prefix exclusion: folders starting with `_` are skipped
- Skipped sets are logged and counted
- ModConfig displays disabled card sets count

**Technical Changes:**
- TagHelper: Added GetOrCreateBinderSheetTag() method
- StorageHelper: Modified CreateCardStorage() to accept List<Tag>
- ModBehaviour: Added _skippedSetsCount field and exclusion logic
- Updated documentation (CLAUDE.md, README.md)

**Why:**
- Hierarchical storage provides better organization for large collections
- Lighter weights make storage items more practical
- Folder exclusion enables easy management of WIP/seasonal content
- Multi-tag filtering creates flexible storage systems

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2025-11-22 23:22:43 -06:00
parent 58b435028e
commit 1050d4f018
5 changed files with 129 additions and 39 deletions

View File

@ -79,7 +79,9 @@ The game loads mods from `Duckov_Data/Mods/`. Each mod requires:
- **`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.
- **`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.
### Dependencies
@ -98,6 +100,8 @@ The Description field is optional. If provided, it will be displayed in the item
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:
@ -132,10 +136,16 @@ Key namespaces and APIs from the game:
- Cards load from `CardSets/*/cards.txt` files with optional descriptions
- Custom PNG images display as item icons
- Cards register as game items with proper TypeIDs
- Custom "TradingCard" tag for filtering
- Custom tags: "TradingCard" and "BinderSheet" for filtering
- Card packs with gacha-style mechanics (weighted random distribution)
- Storage system with slot-based filtering (9-slot binders, 18-slot boxes)
- ModConfig integration for enhanced card info display (set name, number, rarity)
- 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, packs, disabled sets)
- Debug spawn with F9 key (for testing)
- Deploy/remove scripts for quick iteration
- Unit tests for parsing logic and pack system
@ -153,8 +163,7 @@ Based on analysis of the AdditionalCollectibles mod:
### Future Considerations
- Investigate new ItemBuilder API (added in recent game update) as potential replacement for reflection-based approach
- Additional storage variants or customization options
- Binder sheets which hold cards are are held by binders
- Additional storage variants or customization options (e.g., larger binders, themed storage boxes)
### Log File Location

View File

@ -6,9 +6,12 @@ A customizable trading card system that lets you add your own card sets to the g
- **Custom Card Sets** - Create your own trading cards with custom artwork and stats
- **Card Packs** - Open randomized card packs with gacha-style rarity distribution
- **Storage System** - Organize your collection with 9-slot binders and 18-slot card boxes
- **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 when ModConfig is installed (optional)
- **ModConfig Integration** - Enhanced card info display and mod statistics (optional)
- **No Programming Required** - Add new card sets without writing any code
## Requirements
@ -21,7 +24,9 @@ This mod requires the HarmonyLoadMod to be installed. It provides the Harmony li
**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 the item description in your inventory, making it easier to identify and organize your cards.
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
@ -81,6 +86,27 @@ 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
```

View File

@ -63,14 +63,15 @@ namespace TradingCardMod
private const int BASE_ITEM_ID = 135;
// Storage item IDs (high range to avoid conflicts)
private const int BINDER_ITEM_ID = 200001;
private const int CARD_BOX_ITEM_ID = 200002;
private const int BINDER_SHEET_ITEM_ID = 200001;
private const int CARD_BINDER_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 Item? _binderItem;
private Item? _cardBoxItem;
@ -78,6 +79,7 @@ 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>();
@ -107,8 +109,9 @@ namespace TradingCardMod
// Log all available tags for reference
TagHelper.LogAvailableTags();
// Create our custom tag first
// Create our custom tags first
_tradingCardTag = TagHelper.GetOrCreateTradingCardTag();
_binderSheetTag = TagHelper.GetOrCreateBinderSheetTag();
// Load and register cards - do this early so saves can load them
LoadCardSets();
@ -220,6 +223,20 @@ 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)
@ -259,11 +276,27 @@ 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 F9 to spawn items (cycles through cards, then binder, then box)");
@ -298,36 +331,43 @@ namespace TradingCardMod
}
/// <summary>
/// Creates storage items (binder and card box) for holding trading cards.
/// Creates storage items (binder sheet and card binder) for holding trading cards.
/// </summary>
private void CreateStorageItems()
{
if (_tradingCardTag == null)
if (_tradingCardTag == null || _binderSheetTag == null)
{
Debug.LogError("[TradingCardMod] Cannot create storage items - TradingCard tag not created!");
Debug.LogError("[TradingCardMod] Cannot create storage items - Required tags not created!");
return;
}
// Create Card Binder (9 slots = 3x3 grid)
// Create Binder Sheet (9 slots, stores cards only)
_binderItem = StorageHelper.CreateCardStorage(
BINDER_ITEM_ID,
"Card Binder",
"A binder for storing and organizing trading cards. Holds 9 cards.",
BINDER_SHEET_ITEM_ID,
"Binder Sheet",
"A sheet for storing and organizing trading cards. Holds 9 cards.",
9,
1.5f, // weight
0.1f, // weight
7500, // value
_tradingCardTag
new List<Tag> { _tradingCardTag } // Only trading cards allowed
);
// Create Card Box (36 slots = bulk storage)
// Add BinderSheet tag to the binder sheet item itself so it can be stored in Card Binders
if (_binderItem != null)
{
_binderItem.Tags.Add(_binderSheetTag);
Debug.Log("[TradingCardMod] Added BinderSheet tag to Binder Sheet item");
}
// Create Card Binder (12 slots, stores cards AND binder sheets)
_cardBoxItem = StorageHelper.CreateCardStorage(
CARD_BOX_ITEM_ID,
"Card Box",
"A large box for bulk storage of trading cards. Holds 36 cards.",
36,
2.0f, // weight
37500, // value
_tradingCardTag
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> { _tradingCardTag, _binderSheetTag } // Cards and binder sheets allowed
);
}

View File

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

View File

@ -82,6 +82,16 @@ 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 all tags created by this mod.
/// </summary>