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": {
|
"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
@ -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
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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.
|
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.
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
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
|
#!/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
|
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;
|
||||||
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.");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||