Compare commits

..

10 Commits

Author SHA1 Message Date
Cal Corum
d3d3b998c3 Disable card packs, add project skills, update workshop config
- Disable card pack creation (feature not working, moved to upcoming)
- Add /build and /deploy project skills for workflow automation
- Add workshop files to .gitignore
- Update CLAUDE.md with project skills section and status

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 13:24:05 -06:00
Cal Corum
8c7a131869 Add disassembly support for storage items
- Add DisassemblyHelper class for runtime disassembly formula management
- Binder Sheet disassembles into 2x Polyethylene Sheet
- Card Binder disassembles into 4x Polyethylene Sheet
- Cards and packs intentionally have no disassembly (collectibles)
- Document disassembly pattern in CLAUDE.md (struct boxing for reflection)
- Fix .claude/settings.json permission patterns

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 08:58:07 -06:00
Cal Corum
30285644d6 Optimize item tags for proper loot spawning
- Cards: Collection + Luxury (removed Misc for rarer spawns)
- Packs: Collection + Misc (findable in misc loot)
- Binder/Sheet: Collection + Luxury (removed Tool placeholder)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-06 23:41:38 -06:00
Cal Corum
aea9f81d1f Release Candidate 2 for v1.0.0
- Add custom icons for storage items (Card Binder, Binder Sheet)
- Add mod preview image for in-game mod manager
- Update deploy script to copy assets folder
- Icons generated with Z-Image using vinyl toy style to match game aesthetic

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-06 23:23:59 -06:00
Cal Corum
8a45d26515 Update documentation for F10 spawn window and tag system
- Update debug spawn reference from F9 to F10
- Document F10 spawn window features (storage items, random cards by rarity)
- Add CardBinderContent parent tag to custom tags documentation
- Add parent tag pattern to implementation approach (explains AND vs OR logic)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 01:30:31 -06:00
Cal Corum
d0663d569a Fix storage slot filtering with parent tag system and add F10 spawn window
Storage Fix:
- Implemented CardBinderContent parent tag shared by cards and binder sheets
- Fixed Card Binder slots to use single tag requirement (CardBinderContent)
- Unity's requireTags uses AND logic, not OR - previous approach required items to have ALL tags
- Storage hierarchy now works correctly:
  * Cards can be stored in Binder Sheets (requires TradingCard tag)
  * Cards can be stored in Card Binders (requires CardBinderContent tag)
  * Binder Sheets can be stored in Card Binders (requires CardBinderContent tag)
  * Binder Sheets cannot be stored in other Binder Sheets (lacks TradingCard tag)

F10 Spawn Window:
- Replaced F9 key cycling with OnGUI floating window (changed to F10 to avoid mod conflicts)
- Added buttons for spawning Card Binder and Binder Sheet
- Added buttons for spawning random cards by rarity (Common, Uncommon, Rare, Very Rare, Ultra Rare)
- Window is draggable and positioned to avoid UI overlap

Cleanup:
- Removed ExampleSet placeholder card data

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 00:03:18 -06:00
Cal Corum
1050d4f018 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>
2025-11-22 23:22:43 -06:00
Cal Corum
58b435028e Release Candidate 1 for v1.0.0
Release candidate for first stable release of Trading Card Mod.

Changes in this RC:
- Added VERSION file tracking release versions
- Standardized card weight to 0.05 across all card sets
- Updated documentation (README.md, CLAUDE.md) for ModConfig integration
- Updated card format to include optional Description field
- Documented Collection and Misc tags for cards
- All features complete: cards, packs, storage, ModConfig integration

Features:
- Custom card sets with user-provided artwork
- Card packs with gacha-style mechanics
- Storage system (9-slot binders, 36-slot boxes)
- ModConfig integration for enhanced card info
- 93 cards across 2 example sets (ExampleSet, PokemonGO)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-21 00:41:03 -06:00
Cal Corum
42b71e3447 Add sprite padding, updated values, and deploy improvements
Sprite fixes:
- Pad rectangular card images to squares with transparent pixels
- Centers image to maintain aspect ratio in game preview UI

Card values updated to new scale:
- Common: 25, Uncommon: 100, Rare: 500
- Very Rare: 2500, Ultra Rare: 12500, Legendary: 62500

Deploy script improvements:
- Add --no-example flag to exclude ExampleSet during testing
- Show which sets are copied/skipped during deployment

Git configuration:
- Only track ExampleSet in CardSets/, ignore user content

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 23:02:17 -06:00
Cal Corum
b10ecbf733 Add unit tests for pack system
Add comprehensive test coverage for CardPack, PackSlot, DefaultPackSlots,
and PackParser classes. Tests verify TypeID generation, slot weight
configurations, file parsing with various formats, and default pack creation.

Test coverage:
- CardPack: GenerateTypeID consistency and uniqueness
- PackSlot: Default values, rarity/card weight population
- DefaultPackSlots: Slot weight distributions, GetDefaultSlots
- PackParser: File parsing, multiple packs/slots, card-specific weights

Total: 60 tests (29 existing + 31 new)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 22:12:54 -06:00
24 changed files with 1310 additions and 147 deletions

View File

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

12
.gitignore vendored
View File

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

106
CLAUDE.md
View File

@ -36,6 +36,15 @@ dotnet build TradingCardMod.csproj -c Release
./remove.sh --backup ./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 ## Testing
```bash ```bash
@ -77,19 +86,68 @@ 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. - **`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 ### Dependencies
- **HarmonyLoadMod** (Workshop ID: 3589088839): Required mod dependency providing Harmony 2.4.1. Referenced at build time but not bundled to avoid version conflicts. - **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 ### Card Definition Format
Cards are defined in `CardSets/{SetName}/cards.txt` using pipe-separated values: Cards are defined in `CardSets/{SetName}/cards.txt` using pipe-separated values:
``` ```
CardName | SetName | SetNumber | ImageFile | Rarity | Weight | Value CardName | SetName | SetNumber | ImageFile | Rarity | Weight | Value | Description (optional)
``` ```
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/`. 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 ## Game API Reference
Key namespaces and APIs from the game: Key namespaces and APIs from the game:
@ -114,19 +172,38 @@ Key namespaces and APIs from the game:
## Current Project Status ## Current Project Status
**Phase:** 2 Complete - Core Card Framework **Phase:** 3 - Storage System
**Next Phase:** 3 - Storage System (Binders) **Status:** Ready for first release
**Project Plan:** `.claude/scratchpad/PROJECT_PLAN.md` **Project Plan:** `.claude/scratchpad/PROJECT_PLAN.md`
**Technical Analysis:** `.claude/scratchpad/item-system-analysis.md` **Technical Analysis:** `.claude/scratchpad/item-system-analysis.md`
### Completed Features ### Completed Features
- Cards load from `CardSets/*/cards.txt` files - Cards load from `CardSets/*/cards.txt` files with optional descriptions
- Custom PNG images display as item icons - Custom PNG images display as item icons
- Cards register as game items with proper TypeIDs - Cards register as game items with proper TypeIDs
- Custom "TradingCard" tag for filtering - Custom tags for slot filtering:
- Debug spawn with F9 key (for testing) - "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)
- Deploy/remove scripts for quick iteration - Deploy/remove scripts for quick iteration
- Unit tests for parsing logic
### Implementation Approach: Clone + Reflection ### Implementation Approach: Clone + Reflection
@ -135,15 +212,18 @@ Based on analysis of the AdditionalCollectibles mod:
1. **Clone existing game items** as templates (base item ID 135) 1. **Clone existing game items** as templates (base item ID 135)
2. **Use reflection** to set private fields (typeID, weight, value, etc.) 2. **Use reflection** to set private fields (typeID, weight, value, etc.)
3. **Create custom tags** by cloning existing ScriptableObject tags 3. **Create custom tags** by cloning existing ScriptableObject tags
4. **Load sprites** from user files in `CardSets/*/images/` 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
### Next Implementation Steps ### Upcoming Features
Phase 3 - Storage System: - **Card packs** with gacha-style mechanics (weighted random distribution) - disabled pending fix
1. Research existing storage items in game - Additional storage variants or customization options (e.g., larger binders, themed storage boxes)
2. Create binder item with Inventory component
3. Implement slot-based filtering for "TradingCard" tag ### Future Considerations
4. Create card box variant with higher capacity
- Investigate new ItemBuilder API (added in recent game update) as potential replacement for reflection-based approach
### Log File Location ### Log File Location

View File

@ -1,21 +0,0 @@
# 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.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -2,6 +2,18 @@
A customizable trading card system that lets you add your own card sets to the game. 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 ## Requirements
**Required Mod Dependency:** **Required Mod Dependency:**
@ -9,12 +21,20 @@ 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. 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 ## Installation
1. Subscribe to [HarmonyLib](https://steamcommunity.com/sharedfiles/filedetails/?id=3589088839) on Steam Workshop 1. Subscribe to [HarmonyLib](https://steamcommunity.com/sharedfiles/filedetails/?id=3589088839) on Steam Workshop
2. Build the mod (see Development section) 2. (Optional) Subscribe to [ModConfig](https://steamcommunity.com/sharedfiles/filedetails/?id=3592433938) for enhanced card descriptions
3. Copy the `TradingCardMod` folder to your game's `Duckov_Data/Mods` directory 3. Build the mod (see Development section)
4. Launch the game and enable both HarmonyLib and this mod in the Mods menu 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)
## Adding Card Sets ## Adding Card Sets
@ -29,13 +49,13 @@ This mod requires the HarmonyLoadMod to be installed. It provides the Harmony li
Cards are defined in `cards.txt` using pipe-separated values: Cards are defined in `cards.txt` using pipe-separated values:
``` ```
CardName | SetName | SetNumber | ImageFile | Rarity | Weight | Value CardName | SetName | SetNumber | ImageFile | Rarity | Weight | Value | Description (optional)
``` ```
Example: Example:
``` ```
Blue Dragon | Fantasy Set | 001 | blue_dragon.png | Ultra Rare | 0.01 | 500 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.01 | 100 Fire Sprite | Fantasy Set | 002 | fire_sprite.png | Rare | 0.05 | 100
``` ```
### Field Descriptions ### Field Descriptions
@ -47,14 +67,14 @@ Fire Sprite | Fantasy Set | 002 | fire_sprite.png | Rare | 0.01 | 100
| SetNumber | Number for sorting (as integer) | 001 | | SetNumber | Number for sorting (as integer) | 001 |
| ImageFile | Image filename in images/ folder | "blue_dragon.png" | | ImageFile | Image filename in images/ folder | "blue_dragon.png" |
| Rarity | Card rarity tier | Common, Uncommon, Rare, Ultra Rare | | Rarity | Card rarity tier | Common, Uncommon, Rare, Ultra Rare |
| Weight | Physical weight in game units | 0.01 | | Weight | Physical weight in game units | 0.05 |
| Value | In-game currency value | 500 | | Value | In-game currency value | 500 |
| Description | Optional flavor text for the card | "A majestic dragon..." |
### Image Requirements ### Image Requirements
- Place images in your card set's `images/` subfolder - Place images in your cardset's `images/` subfolder
- Recommended format: PNG - Recommended format: PNG
- Recommended size: 256x256 or similar aspect ratio
### Comments ### Comments
@ -66,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 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 ## Folder Structure
``` ```
@ -133,4 +174,4 @@ This mod is provided as-is for personal use. Do not distribute copyrighted card
## Credits ## Credits
Built using the official Duckov modding framework. Built using the official Duckov modding framework and building on the awesome work of the AdditionalCollectibles mod.

View File

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

View File

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

1
VERSION Normal file
View File

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

BIN
assets/binder_sheet.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
assets/card_binder.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

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

BIN
preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

293
src/DisassemblyHelper.cs Normal file
View File

@ -0,0 +1,293 @@
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,6 +1,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using UnityEngine; using UnityEngine;
using ItemStatsSystem; using ItemStatsSystem;
using SodaCraft.Localizations; using SodaCraft.Localizations;
@ -63,14 +64,16 @@ namespace TradingCardMod
private const int BASE_ITEM_ID = 135; private const int BASE_ITEM_ID = 135;
// Storage item IDs (high range to avoid conflicts) // Storage item IDs (high range to avoid conflicts)
private const int BINDER_ITEM_ID = 200001; private const int BINDER_SHEET_ITEM_ID = 200001;
private const int CARD_BOX_ITEM_ID = 200002; private const int CARD_BINDER_ITEM_ID = 200002;
private string _modPath = string.Empty; private string _modPath = string.Empty;
private List<TradingCard> _loadedCards = new List<TradingCard>(); private List<TradingCard> _loadedCards = new List<TradingCard>();
private List<Item> _registeredItems = new List<Item>(); private List<Item> _registeredItems = new List<Item>();
private List<GameObject> _createdGameObjects = new List<GameObject>(); private List<GameObject> _createdGameObjects = new List<GameObject>();
private Tag? _tradingCardTag; private Tag? _tradingCardTag;
private Tag? _binderSheetTag;
private Tag? _cardBinderContentTag;
private Item? _binderItem; private Item? _binderItem;
private Item? _cardBoxItem; private Item? _cardBoxItem;
@ -78,13 +81,15 @@ namespace TradingCardMod
private Dictionary<string, List<TradingCard>> _cardsBySet = new Dictionary<string, List<TradingCard>>(); private Dictionary<string, List<TradingCard>> _cardsBySet = new Dictionary<string, List<TradingCard>>();
private Dictionary<string, int> _cardNameToTypeId = new Dictionary<string, int>(); private Dictionary<string, int> _cardNameToTypeId = new Dictionary<string, int>();
private List<Item> _registeredPacks = new List<Item>(); private List<Item> _registeredPacks = new List<Item>();
private int _skippedSetsCount = 0;
// Store pack definitions for runtime lookup (key = "SetName|PackName") // Store pack definitions for runtime lookup (key = "SetName|PackName")
private Dictionary<string, CardPack> _packDefinitions = new Dictionary<string, CardPack>(); private Dictionary<string, CardPack> _packDefinitions = new Dictionary<string, CardPack>();
// Debug: track spawn cycling // Debug spawn window
private int _debugSpawnIndex = 0; private bool _showSpawnWindow = false;
private List<Item> _allSpawnableItems = new List<Item>(); private Rect _spawnWindowRect = new Rect(20, 250, 350, 450);
private System.Random _random = new System.Random();
/// <summary> /// <summary>
/// Called when the GameObject is created. Initialize early to register items before saves load. /// Called when the GameObject is created. Initialize early to register items before saves load.
@ -107,8 +112,10 @@ namespace TradingCardMod
// Log all available tags for reference // Log all available tags for reference
TagHelper.LogAvailableTags(); TagHelper.LogAvailableTags();
// Create our custom tag first // Create our custom tags first
_tradingCardTag = TagHelper.GetOrCreateTradingCardTag(); _tradingCardTag = TagHelper.GetOrCreateTradingCardTag();
_binderSheetTag = TagHelper.GetOrCreateBinderSheetTag();
_cardBinderContentTag = TagHelper.GetOrCreateCardBinderContentTag();
// Load and register cards - do this early so saves can load them // Load and register cards - do this early so saves can load them
LoadCardSets(); LoadCardSets();
@ -116,14 +123,12 @@ namespace TradingCardMod
// Create storage items // Create storage items
CreateStorageItems(); CreateStorageItems();
// Create card packs // Card packs disabled - feature not working correctly
CreateCardPacks(); // TODO: Fix pack opening mechanics before re-enabling
// CreateCardPacks();
// Build spawnable items list (cards + storage + packs) // Set up disassembly formulas for all items
_allSpawnableItems.AddRange(_registeredItems); SetupDisassembly();
if (_binderItem != null) _allSpawnableItems.Add(_binderItem);
if (_cardBoxItem != null) _allSpawnableItems.Add(_cardBoxItem);
_allSpawnableItems.AddRange(_registeredPacks);
Debug.Log("[TradingCardMod] Mod initialized successfully!"); Debug.Log("[TradingCardMod] Mod initialized successfully!");
} }
@ -220,6 +225,20 @@ namespace TradingCardMod
_registeredPacks.Count _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 // Card sets info - one entry per set for clarity
int setIndex = 0; int setIndex = 0;
foreach (var setEntry in _cardsBySet) foreach (var setEntry in _cardsBySet)
@ -259,14 +278,30 @@ namespace TradingCardMod
string[] setDirectories = Directory.GetDirectories(cardSetsPath); string[] setDirectories = Directory.GetDirectories(cardSetsPath);
Debug.Log($"[TradingCardMod] Found {setDirectories.Length} card set directories"); Debug.Log($"[TradingCardMod] Found {setDirectories.Length} card set directories");
_skippedSetsCount = 0;
foreach (string setDir in setDirectories) 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); 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 cards loaded: {_loadedCards.Count}");
Debug.Log($"[TradingCardMod] Total items registered: {_registeredItems.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)"); Debug.Log("[TradingCardMod] DEBUG: Press F10 to open spawn menu (storage items & random cards by rarity)");
// Clear the search cache so our items can be found // Clear the search cache so our items can be found
ClearSearchCache(); ClearSearchCache();
@ -298,36 +333,67 @@ namespace TradingCardMod
} }
/// <summary> /// <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> /// </summary>
private void CreateStorageItems() private void CreateStorageItems()
{ {
if (_tradingCardTag == null) if (_tradingCardTag == null || _binderSheetTag == null || _cardBinderContentTag == null)
{ {
Debug.LogError("[TradingCardMod] Cannot create storage items - TradingCard tag not created!"); Debug.LogError("[TradingCardMod] Cannot create storage items - Required tags not created!");
return; return;
} }
// Create Card Binder (9 slots = 3x3 grid) // 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)
_binderItem = StorageHelper.CreateCardStorage( _binderItem = StorageHelper.CreateCardStorage(
BINDER_ITEM_ID, BINDER_SHEET_ITEM_ID,
"Card Binder", "Binder Sheet",
"A binder for storing and organizing trading cards. Holds 9 cards.", "A sheet for storing and organizing trading cards. Holds 9 cards.",
9, 9,
0.5f, // weight 0.1f, // weight
500, // value 7500, // value
_tradingCardTag new List<Tag> { _tradingCardTag }, // Only trading cards allowed
binderSheetIcon
); );
// Create Card Box (36 slots = bulk storage) // 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)
_cardBoxItem = StorageHelper.CreateCardStorage( _cardBoxItem = StorageHelper.CreateCardStorage(
CARD_BOX_ITEM_ID, CARD_BINDER_ITEM_ID,
"Card Box", "Card Binder",
"A large box for bulk storage of trading cards. Holds 36 cards.", "A binder for storing and organizing trading cards. Can hold cards or binder sheets. Holds 12 items.",
36, 12,
2.0f, // weight 1.5f, // weight
1500, // value 12500, // value
_tradingCardTag new List<Tag> { _cardBinderContentTag }, // Only CardBinderContent tag required
cardBinderIcon
); );
} }
@ -386,51 +452,197 @@ namespace TradingCardMod
Debug.Log($"[TradingCardMod] Created {_registeredPacks.Count} card packs"); 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> /// <summary>
/// Update is called every frame. Used for debug input handling. /// Update is called every frame. Used for debug input handling.
/// </summary> /// </summary>
void Update() void Update()
{ {
// Debug: Press F9 to spawn an item // Debug: Press F10 to toggle spawn menu
if (Input.GetKeyDown(KeyCode.F9)) if (Input.GetKeyDown(KeyCode.F10))
{ {
SpawnDebugItem(); _showSpawnWindow = !_showSpawnWindow;
Debug.Log($"[TradingCardMod] Spawn window {(_showSpawnWindow ? "opened" : "closed")}");
} }
} }
/// <summary> /// <summary>
/// Spawns items for testing - cycles through cards, then storage items. /// OnGUI is called for rendering and handling GUI events.
/// </summary> /// </summary>
private void SpawnDebugItem() void OnGUI()
{ {
if (_allSpawnableItems.Count == 0) 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"))
{ {
Debug.LogWarning("[TradingCardMod] No items registered to spawn!"); 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!");
return; return;
} }
// Cycle through all spawnable items
Item prefab = _allSpawnableItems[_debugSpawnIndex % _allSpawnableItems.Count];
_debugSpawnIndex++;
try try
{ {
// Instantiate a fresh copy of the item (don't send prefab directly)
Item instance = ItemAssetsCollection.InstantiateSync(prefab.TypeID); Item instance = ItemAssetsCollection.InstantiateSync(prefab.TypeID);
if (instance != null) if (instance != null)
{ {
// Use game's utility to give item to player
ItemUtilities.SendToPlayer(instance); ItemUtilities.SendToPlayer(instance);
Debug.Log($"[TradingCardMod] Spawned: {instance.DisplayName} (ID: {instance.TypeID})"); Debug.Log($"[TradingCardMod] Spawned: {itemName}");
} }
else else
{ {
Debug.LogError($"[TradingCardMod] Failed to instantiate {prefab.DisplayName} (ID: {prefab.TypeID})"); Debug.LogError($"[TradingCardMod] Failed to instantiate {itemName}");
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.LogError($"[TradingCardMod] Failed to spawn item: {ex.Message}"); 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}");
} }
} }
@ -536,33 +748,20 @@ namespace TradingCardMod
// Set display quality based on rarity // Set display quality based on rarity
SetDisplayQuality(item, card.GetQuality()); SetDisplayQuality(item, card.GetQuality());
// Set tags // Set tags - Cards are collectibles
item.Tags.Clear(); item.Tags.Clear();
// Add Luxury tag (for selling at shops) Tag? collTag = TagHelper.GetTargetTag("Collection");
if (collTag != null)
{
item.Tags.Add(collTag);
}
Tag? luxuryTag = TagHelper.GetTargetTag("Luxury"); Tag? luxuryTag = TagHelper.GetTargetTag("Luxury");
if (luxuryTag != null) if (luxuryTag != null)
{ {
item.Tags.Add(luxuryTag); 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 // Add our custom TradingCard tag
@ -571,6 +770,12 @@ namespace TradingCardMod
item.Tags.Add(_tradingCardTag); 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 // Load and set icon
Sprite? cardSprite = LoadSpriteFromFile(card.ImagePath, typeId); Sprite? cardSprite = LoadSpriteFromFile(card.ImagePath, typeId);
if (cardSprite != null) if (cardSprite != null)
@ -666,10 +871,33 @@ namespace TradingCardMod
texture.filterMode = FilterMode.Bilinear; texture.filterMode = FilterMode.Bilinear;
texture.Apply(); 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 // Create sprite from texture
Sprite sprite = Sprite.Create( Sprite sprite = Sprite.Create(
texture, finalTexture,
new Rect(0f, 0f, texture.width, texture.height), new Rect(0f, 0f, finalTexture.width, finalTexture.height),
new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f),
100f 100f
); );
@ -681,7 +909,7 @@ namespace TradingCardMod
// Store references on the holder to prevent GC // Store references on the holder to prevent GC
var resourceHolder = holder.AddComponent<CardResourceHolder>(); var resourceHolder = holder.AddComponent<CardResourceHolder>();
resourceHolder.Texture = texture; resourceHolder.Texture = finalTexture;
resourceHolder.Sprite = sprite; resourceHolder.Sprite = sprite;
return sprite; return sprite;
@ -729,6 +957,9 @@ namespace TradingCardMod
// Clean up packs // Clean up packs
PackHelper.Cleanup(); PackHelper.Cleanup();
// Clean up disassembly formulas
DisassemblyHelper.Cleanup();
// Clean up tags // Clean up tags
TagHelper.Cleanup(); TagHelper.Cleanup();
@ -737,7 +968,6 @@ namespace TradingCardMod
_cardNameToTypeId.Clear(); _cardNameToTypeId.Clear();
_registeredPacks.Clear(); _registeredPacks.Clear();
_packDefinitions.Clear(); _packDefinitions.Clear();
_allSpawnableItems.Clear();
Debug.Log("[TradingCardMod] Cleanup complete."); Debug.Log("[TradingCardMod] Cleanup complete.");
} }

View File

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

View File

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

View File

@ -82,6 +82,26 @@ namespace TradingCardMod
return CreateOrCloneTag("TradingCard", "Trading Card"); 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> /// <summary>
/// Gets all tags created by this mod. /// Gets all tags created by this mod.
/// </summary> /// </summary>

463
tests/PackParserTests.cs Normal file
View File

@ -0,0 +1,463 @@
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
}
}