Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions docs/plans/rules-engine-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,20 @@ Epic 4 (Crafting and Class Progression) complete! Next: Epic 5 (Economy and Inve
| 2 | Time and Simulation Loop | ✅ Complete | 6/6 | #4 |
| 3 | NPC Decision Making | ✅ Complete | 6/6 | #5 |
| 4 | Crafting and Class Progression | ✅ Complete | 8/8 | #6 |
| 5 | Economy and Inventory | 🔄 In Progress | 1/6 | #7 |
| 5 | Economy and Inventory | 🔄 In Progress | 2/6 | #7 |
| 6 | Contracts and Commitments | 🔄 Partial | 2/6 | #8 |
| 7 | Settlement and Guild | ⏳ Planned | 0/6 | #9 |
| 8 | Scenario Testing Framework | ⏳ Planned | 0/7 | #10 |
| 9 | Persistence | ⏳ Planned | 0/4 | #11 |
| 10 | Apprenticeship and Role Stacking | ⏳ Planned | 0/4 | #12 |
| 11 | Full Progression Scenario | ⏳ Planned | 0/5 | #13 |

**Total Progress**: 28/58 tasks complete (48%)
**Total Progress**: 29/58 tasks complete (50%)

### Recent Completions

- ✅ **2026-01-19**: Epic 5.1 - Currency value object (#40, PR #139 - awaiting review)
- ✅ **2026-01-19**: Epic 5.2 - InventoryComponent (#41, PR #140 - recreating)
- ✅ **2026-01-19**: Epic 5.1 - Currency value object (#40, PR #139)
- ✅ **2026-01-19**: Epic 4.8 - Masterwork → Master rank trigger (#39, PR #136)
- ✅ **2026-01-19**: Epic 4.7 - CraftingSystem (#38, PR #135)
- ✅ **2026-01-19**: Epic 4.6 - CraftingOutcomeRule (#37, PR #134)
Expand Down Expand Up @@ -264,23 +265,32 @@ _For implementation patterns, examine referenced code paths._

---

## Epic 5: Economy and Inventory (🔄 1/6 tasks)
## Epic 5: Economy and Inventory (🔄 2/6 tasks)

**5.1 Implement Currency value object** ✅ _Issue #40, PR #139 (awaiting review)_
**5.1 Implement Currency value object** ✅ _Issue #40, PR #139_

- ✅ FromSilver(), FromGold(), FromMixed() factory methods
- ✅ Silver and Gold properties for denomination access
- ✅ ToBreakdown() for converting to (gold, silver, copper) tuple
- ✅ Conversion rates: 1 silver = 10 copper, 1 gold = 100 copper
- **Verified**: 6 tests pass, all 517 tests pass
- **Verified**: 6 tests pass, all 521 tests pass

**5.2 Implement InventoryComponent** ✅ _Issue #41, PR #140 (recreating)_

- ✅ AddItem(materialId, quantity) - adds or increments material
- ✅ RemoveItem(materialId, quantity) - removes with validation
- ✅ GetQuantity(materialId) - retrieves current quantity
- ✅ HasSufficientQuantity(materialId, quantity) - checks availability
- ✅ Immutable record pattern with internal dictionary
- **Verified**: 11 tests pass, all 526 tests pass (438 Core + 88 Simulation)

---

### Planned Epics

| Epic | Title | Status | Issue | Key Components |
| ---- | -------------------------------- | ------ | ----- | --------------------------------------------- |
| 5 | Economy and Inventory | 🔄 1/6 | #7 | Currency, InventoryComponent, PriceCalculator |
| 5 | Economy and Inventory | 🔄 2/6 | #7 | Currency, InventoryComponent, PriceCalculator |
| 6 | Contracts and Commitments | 🔄 2/6 | #8 | Commitment types, ContractActions |
| 7 | Settlement and Guild | ⏳ 0/6 | #9 | Settlement, Guild, leadership |
| 8 | Scenario Testing Framework | ⏳ 0/7 | #10 | WorldBuilder, CharacterBuilder |
Expand Down
126 changes: 126 additions & 0 deletions src/FantasyRpgWorld.Core/Domain/Components/InventoryComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
using FantasyRpgWorld.Core.Domain.ValueObjects;

namespace FantasyRpgWorld.Core.Domain.Components;

/// <summary>
/// Represents character inventory holding items and materials.
/// Tracks quantities of materials owned by the character.
/// </summary>
public sealed record InventoryComponent
{
private readonly IReadOnlyDictionary<MaterialId, int> _items;

public InventoryComponent()
{
_items = new Dictionary<MaterialId, int>();
}

private InventoryComponent(IReadOnlyDictionary<MaterialId, int> items)
{
_items = items;
}

/// <summary>
/// Gets the quantity of a specific material in inventory.
/// Returns 0 if the material is not in inventory.
/// </summary>
public int GetQuantity(MaterialId materialId)
{
return _items.TryGetValue(materialId, out var quantity) ? quantity : 0;
}

/// <summary>
/// Adds a quantity of material to the inventory.
/// If the material already exists, increases the quantity.
/// </summary>
public InventoryComponent AddItem(MaterialId materialId, int quantity)
{
if (quantity <= 0)
{
throw new ArgumentOutOfRangeException(nameof(quantity),
"Quantity must be positive.");
}

var updated = new Dictionary<MaterialId, int>(_items);
var currentQuantity = GetQuantity(materialId);
updated[materialId] = currentQuantity + quantity;
return new InventoryComponent(updated);
}

/// <summary>
/// Removes a quantity of material from the inventory.
/// Throws InvalidOperationException if insufficient quantity available.
/// </summary>
public InventoryComponent RemoveItem(MaterialId materialId, int quantity)
{
if (quantity <= 0)
{
throw new ArgumentOutOfRangeException(nameof(quantity),
"Quantity must be positive.");
}

var currentQuantity = GetQuantity(materialId);
if (currentQuantity < quantity)
{
throw new InvalidOperationException(
$"Insufficient quantity of {materialId.Value}. Have {currentQuantity}, need {quantity}");
}

var updated = new Dictionary<MaterialId, int>(_items);
var newQuantity = currentQuantity - quantity;
if (newQuantity == 0)
{
updated.Remove(materialId);
}
else
{
updated[materialId] = newQuantity;
}
return new InventoryComponent(updated);
}

/// <summary>
/// Checks if the inventory contains at least the specified quantity of material.
/// </summary>
public bool HasSufficientQuantity(MaterialId materialId, int quantity)
{
return GetQuantity(materialId) >= quantity;
}

/// <summary>
/// Gets all materials and their quantities in the inventory.
/// </summary>
public IReadOnlyDictionary<MaterialId, int> GetAll() => _items;

// Equality implementation for proper comparison
public bool Equals(InventoryComponent? other)
{
if (other is null)
return false;

if (_items.Count != other._items.Count)
return false;

foreach (var kvp in _items)
{
if (!other._items.TryGetValue(kvp.Key, out var otherQuantity))
return false;

if (kvp.Value != otherQuantity)
return false;
}

return true;
}

public override int GetHashCode()
{
var hash = new HashCode();
foreach (var kvp in _items.OrderBy(x => x.Key.Value))
{
hash.Add(kvp.Key);
hash.Add(kvp.Value);
}
return hash.ToHashCode();
}
}
Loading