Compare commits
10 Commits
80da308d17
...
d3d3b998c3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3d3b998c3 | ||
|
|
8c7a131869 | ||
|
|
30285644d6 | ||
|
|
aea9f81d1f | ||
|
|
8a45d26515 | ||
|
|
d0663d569a | ||
|
|
1050d4f018 | ||
|
|
58b435028e | ||
|
|
42b71e3447 | ||
|
|
b10ecbf733 |
@ -1,10 +1,10 @@
|
||||
{
|
||||
"$schema": "https://claude.ai/code/settings-schema.json",
|
||||
"$schema": "https://json.schemastore.org/claude-code-settings.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
@ -10,6 +10,7 @@ obj/
|
||||
|
||||
# Claude Code private notes
|
||||
.claude/scratchpad/
|
||||
.claude/skills/
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
@ -22,9 +23,14 @@ packages/
|
||||
# Debug logs
|
||||
*.log
|
||||
|
||||
# Card set images (user-generated content)
|
||||
# Uncomment if you don't want to track example images
|
||||
# CardSets/*/images/
|
||||
# Card sets - only ExampleSet is tracked in git
|
||||
CardSets/*
|
||||
!CardSets/ExampleSet/
|
||||
|
||||
# Preview image (generate your own)
|
||||
# preview.png
|
||||
|
||||
# Steam Workshop (local config)
|
||||
workshop-staging/
|
||||
workshop.vdf
|
||||
workshop-upload.sh
|
||||
|
||||
106
CLAUDE.md
@ -36,6 +36,15 @@ 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
|
||||
@ -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.
|
||||
|
||||
- **`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
|
||||
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/`.
|
||||
|
||||
**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:
|
||||
@ -114,19 +172,38 @@ Key namespaces and APIs from the game:
|
||||
|
||||
## Current Project Status
|
||||
|
||||
**Phase:** 2 Complete - Core Card Framework ✅
|
||||
**Next Phase:** 3 - Storage System (Binders)
|
||||
**Phase:** 3 - Storage System ✅
|
||||
**Status:** Ready for first release
|
||||
**Project Plan:** `.claude/scratchpad/PROJECT_PLAN.md`
|
||||
**Technical Analysis:** `.claude/scratchpad/item-system-analysis.md`
|
||||
|
||||
### 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
|
||||
- Cards register as game items with proper TypeIDs
|
||||
- Custom "TradingCard" tag for filtering
|
||||
- Debug spawn with F9 key (for testing)
|
||||
- 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)
|
||||
- Deploy/remove scripts for quick iteration
|
||||
- Unit tests for parsing logic
|
||||
|
||||
### 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)
|
||||
2. **Use reflection** to set private fields (typeID, weight, value, etc.)
|
||||
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:
|
||||
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
|
||||
- **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
|
||||
|
||||
### Log File Location
|
||||
|
||||
|
||||
@ -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
|
||||
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
61
README.md
@ -2,6 +2,18 @@
|
||||
|
||||
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:**
|
||||
@ -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.
|
||||
|
||||
**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. 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
|
||||
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)
|
||||
|
||||
## 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:
|
||||
|
||||
```
|
||||
CardName | SetName | SetNumber | ImageFile | Rarity | Weight | Value
|
||||
CardName | SetName | SetNumber | ImageFile | Rarity | Weight | Value | Description (optional)
|
||||
```
|
||||
|
||||
Example:
|
||||
```
|
||||
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
|
||||
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
|
||||
```
|
||||
|
||||
### 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 |
|
||||
| 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.01 |
|
||||
| Weight | Physical weight in game units | 0.05 |
|
||||
| Value | In-game currency value | 500 |
|
||||
| Description | Optional flavor text for the card | "A majestic dragon..." |
|
||||
|
||||
### Image Requirements
|
||||
|
||||
- Place images in your card set's `images/` subfolder
|
||||
- Place images in your cardset's `images/` subfolder
|
||||
- Recommended format: PNG
|
||||
- Recommended size: 256x256 or similar aspect ratio
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
```
|
||||
@ -133,4 +174,4 @@ This mod is provided as-is for personal use. Do not distribute copyrighted card
|
||||
|
||||
## 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.
|
||||
|
||||
@ -28,6 +28,8 @@
|
||||
<!-- 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>
|
||||
|
||||
@ -50,6 +50,9 @@
|
||||
<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>
|
||||
|
||||
BIN
assets/binder_sheet.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
assets/card_binder.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
44
deploy.sh
@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# Deploy Trading Card Mod to Escape from Duckov
|
||||
# Usage: ./deploy.sh [--release]
|
||||
# Usage: ./deploy.sh [--release] [--no-example]
|
||||
|
||||
set -e
|
||||
|
||||
@ -9,11 +9,20 @@ GAME_PATH="/mnt/NV2/SteamLibrary/steamapps/common/Escape from Duckov"
|
||||
MOD_NAME="TradingCardMod"
|
||||
MOD_DIR="$GAME_PATH/Duckov_Data/Mods/$MOD_NAME"
|
||||
|
||||
# Build configuration
|
||||
# Parse arguments
|
||||
BUILD_CONFIG="Debug"
|
||||
if [[ "$1" == "--release" ]]; then
|
||||
BUILD_CONFIG="Release"
|
||||
fi
|
||||
EXCLUDE_EXAMPLE=false
|
||||
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
--release)
|
||||
BUILD_CONFIG="Release"
|
||||
;;
|
||||
--no-example)
|
||||
EXCLUDE_EXAMPLE=true
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "=== Trading Card Mod Deployment ==="
|
||||
echo "Build config: $BUILD_CONFIG"
|
||||
@ -35,7 +44,7 @@ mkdir -p "$MOD_DIR"
|
||||
mkdir -p "$MOD_DIR/CardSets"
|
||||
|
||||
# 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 "info.ini" "$MOD_DIR/"
|
||||
|
||||
@ -44,10 +53,29 @@ 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 "[4/4] Copying card sets..."
|
||||
echo "[5/5] Copying card sets..."
|
||||
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
|
||||
|
||||
echo ""
|
||||
|
||||
BIN
preview.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
293
src/DisassemblyHelper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using ItemStatsSystem;
|
||||
using SodaCraft.Localizations;
|
||||
@ -63,14 +64,16 @@ 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 Tag? _cardBinderContentTag;
|
||||
private Item? _binderItem;
|
||||
private Item? _cardBoxItem;
|
||||
|
||||
@ -78,13 +81,15 @@ 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: track spawn cycling
|
||||
private int _debugSpawnIndex = 0;
|
||||
private List<Item> _allSpawnableItems = new List<Item>();
|
||||
// Debug spawn window
|
||||
private bool _showSpawnWindow = false;
|
||||
private Rect _spawnWindowRect = new Rect(20, 250, 350, 450);
|
||||
private System.Random _random = new System.Random();
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
TagHelper.LogAvailableTags();
|
||||
|
||||
// Create our custom tag first
|
||||
// Create our custom tags first
|
||||
_tradingCardTag = TagHelper.GetOrCreateTradingCardTag();
|
||||
_binderSheetTag = TagHelper.GetOrCreateBinderSheetTag();
|
||||
_cardBinderContentTag = TagHelper.GetOrCreateCardBinderContentTag();
|
||||
|
||||
// Load and register cards - do this early so saves can load them
|
||||
LoadCardSets();
|
||||
@ -116,14 +123,12 @@ namespace TradingCardMod
|
||||
// Create storage items
|
||||
CreateStorageItems();
|
||||
|
||||
// Create card packs
|
||||
CreateCardPacks();
|
||||
// Card packs disabled - feature not working correctly
|
||||
// TODO: Fix pack opening mechanics before re-enabling
|
||||
// CreateCardPacks();
|
||||
|
||||
// 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);
|
||||
// Set up disassembly formulas for all items
|
||||
SetupDisassembly();
|
||||
|
||||
Debug.Log("[TradingCardMod] Mod initialized successfully!");
|
||||
}
|
||||
@ -220,6 +225,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,14 +278,30 @@ 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)");
|
||||
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
|
||||
ClearSearchCache();
|
||||
@ -298,36 +333,67 @@ 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 || _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;
|
||||
}
|
||||
|
||||
// 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(
|
||||
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,
|
||||
0.5f, // weight
|
||||
500, // value
|
||||
_tradingCardTag
|
||||
0.1f, // weight
|
||||
7500, // value
|
||||
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(
|
||||
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
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
@ -386,51 +452,197 @@ 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 F9 to spawn an item
|
||||
if (Input.GetKeyDown(KeyCode.F9))
|
||||
// Debug: Press F10 to toggle spawn menu
|
||||
if (Input.GetKeyDown(KeyCode.F10))
|
||||
{
|
||||
SpawnDebugItem();
|
||||
_showSpawnWindow = !_showSpawnWindow;
|
||||
Debug.Log($"[TradingCardMod] Spawn window {(_showSpawnWindow ? "opened" : "closed")}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns items for testing - cycles through cards, then storage items.
|
||||
/// OnGUI is called for rendering and handling GUI events.
|
||||
/// </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;
|
||||
}
|
||||
|
||||
// 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: {instance.DisplayName} (ID: {instance.TypeID})");
|
||||
Debug.Log($"[TradingCardMod] Spawned: {itemName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"[TradingCardMod] Failed to instantiate {prefab.DisplayName} (ID: {prefab.TypeID})");
|
||||
Debug.LogError($"[TradingCardMod] Failed to instantiate {itemName}");
|
||||
}
|
||||
}
|
||||
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
|
||||
SetDisplayQuality(item, card.GetQuality());
|
||||
|
||||
// Set tags
|
||||
// Set tags - Cards are collectibles
|
||||
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");
|
||||
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
|
||||
@ -571,6 +770,12 @@ 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)
|
||||
@ -666,10 +871,33 @@ 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(
|
||||
texture,
|
||||
new Rect(0f, 0f, texture.width, texture.height),
|
||||
finalTexture,
|
||||
new Rect(0f, 0f, finalTexture.width, finalTexture.height),
|
||||
new Vector2(0.5f, 0.5f),
|
||||
100f
|
||||
);
|
||||
@ -681,7 +909,7 @@ namespace TradingCardMod
|
||||
|
||||
// Store references on the holder to prevent GC
|
||||
var resourceHolder = holder.AddComponent<CardResourceHolder>();
|
||||
resourceHolder.Texture = texture;
|
||||
resourceHolder.Texture = finalTexture;
|
||||
resourceHolder.Sprite = sprite;
|
||||
|
||||
return sprite;
|
||||
@ -729,6 +957,9 @@ namespace TradingCardMod
|
||||
// Clean up packs
|
||||
PackHelper.Cleanup();
|
||||
|
||||
// Clean up disassembly formulas
|
||||
DisassemblyHelper.Cleanup();
|
||||
|
||||
// Clean up tags
|
||||
TagHelper.Cleanup();
|
||||
|
||||
@ -737,7 +968,6 @@ namespace TradingCardMod
|
||||
_cardNameToTypeId.Clear();
|
||||
_registeredPacks.Clear();
|
||||
_packDefinitions.Clear();
|
||||
_allSpawnableItems.Clear();
|
||||
|
||||
Debug.Log("[TradingCardMod] Cleanup complete.");
|
||||
}
|
||||
|
||||
@ -64,10 +64,15 @@ namespace TradingCardMod
|
||||
item.Quality = 3; // Uncommon quality for packs
|
||||
item.DisplayQuality = (DisplayQuality)3;
|
||||
|
||||
// Set tags
|
||||
// Set tags - Packs are collectibles found in misc loot
|
||||
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");
|
||||
if (miscTag != null)
|
||||
{
|
||||
|
||||
@ -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
|
||||
@ -79,16 +80,23 @@ namespace TradingCardMod
|
||||
// Set display quality
|
||||
item.DisplayQuality = (DisplayQuality)3;
|
||||
|
||||
// Set tags - storage items should be tools
|
||||
// Set tags - storage items are luxury collectibles
|
||||
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
|
||||
ConfigureCardSlots(item, tradingCardTag, slotCount);
|
||||
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);
|
||||
|
||||
// Set icon if provided
|
||||
if (icon != null)
|
||||
@ -123,9 +131,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 +155,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>
|
||||
|
||||
@ -82,6 +82,26 @@ 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>
|
||||
|
||||
463
tests/PackParserTests.cs
Normal 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
|
||||
}
|
||||
}
|
||||