Skip to content

Commit caf95a8

Browse files
feat: implement InventoryComponent for Epic 5.2
Adds InventoryComponent for character inventory management with immutable operations, comprehensive validation, and equality support. Features: - AddItem/RemoveItem with quantity validation (>0) - GetQuantity/HasSufficientQuantity for inventory queries - GetAll() for complete inventory access - Custom Equals/GetHashCode for proper state comparison - Immutable record pattern following codebase conventions This completes Epic 5.2 by providing the foundation for material tracking and resource management in the economy system. Closes #41
1 parent 1facef2 commit caf95a8

3 files changed

Lines changed: 491 additions & 7 deletions

File tree

docs/plans/rules-engine-design.md

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,20 @@ Epic 4 (Crafting and Class Progression) complete! Next: Epic 5 (Economy and Inve
2424
| 2 | Time and Simulation Loop | ✅ Complete | 6/6 | #4 |
2525
| 3 | NPC Decision Making | ✅ Complete | 6/6 | #5 |
2626
| 4 | Crafting and Class Progression | ✅ Complete | 8/8 | #6 |
27-
| 5 | Economy and Inventory | 🔄 In Progress | 1/6 | #7 |
27+
| 5 | Economy and Inventory | 🔄 In Progress | 2/6 | #7 |
2828
| 6 | Contracts and Commitments | 🔄 Partial | 2/6 | #8 |
2929
| 7 | Settlement and Guild | ⏳ Planned | 0/6 | #9 |
3030
| 8 | Scenario Testing Framework | ⏳ Planned | 0/7 | #10 |
3131
| 9 | Persistence | ⏳ Planned | 0/4 | #11 |
3232
| 10 | Apprenticeship and Role Stacking | ⏳ Planned | 0/4 | #12 |
3333
| 11 | Full Progression Scenario | ⏳ Planned | 0/5 | #13 |
3434

35-
**Total Progress**: 28/58 tasks complete (48%)
35+
**Total Progress**: 29/58 tasks complete (50%)
3636

3737
### Recent Completions
3838

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

265266
---
266267

267-
## Epic 5: Economy and Inventory (🔄 1/6 tasks)
268+
## Epic 5: Economy and Inventory (🔄 2/6 tasks)
268269

269-
**5.1 Implement Currency value object**_Issue #40, PR #139 (awaiting review)_
270+
**5.1 Implement Currency value object**_Issue #40, PR #139_
270271

271272
- ✅ FromSilver(), FromGold(), FromMixed() factory methods
272273
- ✅ Silver and Gold properties for denomination access
273274
- ✅ ToBreakdown() for converting to (gold, silver, copper) tuple
274275
- ✅ Conversion rates: 1 silver = 10 copper, 1 gold = 100 copper
275-
- **Verified**: 6 tests pass, all 517 tests pass
276+
- **Verified**: 6 tests pass, all 521 tests pass
277+
278+
**5.2 Implement InventoryComponent**_Issue #41, PR #140 (recreating)_
279+
280+
- ✅ AddItem(materialId, quantity) - adds or increments material
281+
- ✅ RemoveItem(materialId, quantity) - removes with validation
282+
- ✅ GetQuantity(materialId) - retrieves current quantity
283+
- ✅ HasSufficientQuantity(materialId, quantity) - checks availability
284+
- ✅ Immutable record pattern with internal dictionary
285+
- **Verified**: 11 tests pass, all 526 tests pass (438 Core + 88 Simulation)
276286

277287
---
278288

279289
### Planned Epics
280290

281291
| Epic | Title | Status | Issue | Key Components |
282292
| ---- | -------------------------------- | ------ | ----- | --------------------------------------------- |
283-
| 5 | Economy and Inventory | 🔄 1/6 | #7 | Currency, InventoryComponent, PriceCalculator |
293+
| 5 | Economy and Inventory | 🔄 2/6 | #7 | Currency, InventoryComponent, PriceCalculator |
284294
| 6 | Contracts and Commitments | 🔄 2/6 | #8 | Commitment types, ContractActions |
285295
| 7 | Settlement and Guild | ⏳ 0/6 | #9 | Settlement, Guild, leadership |
286296
| 8 | Scenario Testing Framework | ⏳ 0/7 | #10 | WorldBuilder, CharacterBuilder |
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
using FantasyRpgWorld.Core.Domain.ValueObjects;
2+
3+
namespace FantasyRpgWorld.Core.Domain.Components;
4+
5+
/// <summary>
6+
/// Represents character inventory holding items and materials.
7+
/// Tracks quantities of materials owned by the character.
8+
/// </summary>
9+
public sealed record InventoryComponent
10+
{
11+
private readonly IReadOnlyDictionary<MaterialId, int> _items;
12+
13+
public InventoryComponent()
14+
{
15+
_items = new Dictionary<MaterialId, int>();
16+
}
17+
18+
private InventoryComponent(IReadOnlyDictionary<MaterialId, int> items)
19+
{
20+
_items = items;
21+
}
22+
23+
/// <summary>
24+
/// Gets the quantity of a specific material in inventory.
25+
/// Returns 0 if the material is not in inventory.
26+
/// </summary>
27+
public int GetQuantity(MaterialId materialId)
28+
{
29+
return _items.TryGetValue(materialId, out var quantity) ? quantity : 0;
30+
}
31+
32+
/// <summary>
33+
/// Adds a quantity of material to the inventory.
34+
/// If the material already exists, increases the quantity.
35+
/// </summary>
36+
public InventoryComponent AddItem(MaterialId materialId, int quantity)
37+
{
38+
if (quantity <= 0)
39+
{
40+
throw new ArgumentOutOfRangeException(nameof(quantity),
41+
"Quantity must be positive.");
42+
}
43+
44+
var updated = new Dictionary<MaterialId, int>(_items);
45+
var currentQuantity = GetQuantity(materialId);
46+
updated[materialId] = currentQuantity + quantity;
47+
return new InventoryComponent(updated);
48+
}
49+
50+
/// <summary>
51+
/// Removes a quantity of material from the inventory.
52+
/// Throws InvalidOperationException if insufficient quantity available.
53+
/// </summary>
54+
public InventoryComponent RemoveItem(MaterialId materialId, int quantity)
55+
{
56+
if (quantity <= 0)
57+
{
58+
throw new ArgumentOutOfRangeException(nameof(quantity),
59+
"Quantity must be positive.");
60+
}
61+
62+
var currentQuantity = GetQuantity(materialId);
63+
if (currentQuantity < quantity)
64+
{
65+
throw new InvalidOperationException(
66+
$"Insufficient quantity of {materialId.Value}. Have {currentQuantity}, need {quantity}");
67+
}
68+
69+
var updated = new Dictionary<MaterialId, int>(_items);
70+
var newQuantity = currentQuantity - quantity;
71+
if (newQuantity == 0)
72+
{
73+
updated.Remove(materialId);
74+
}
75+
else
76+
{
77+
updated[materialId] = newQuantity;
78+
}
79+
return new InventoryComponent(updated);
80+
}
81+
82+
/// <summary>
83+
/// Checks if the inventory contains at least the specified quantity of material.
84+
/// </summary>
85+
public bool HasSufficientQuantity(MaterialId materialId, int quantity)
86+
{
87+
return GetQuantity(materialId) >= quantity;
88+
}
89+
90+
/// <summary>
91+
/// Gets all materials and their quantities in the inventory.
92+
/// </summary>
93+
public IReadOnlyDictionary<MaterialId, int> GetAll() => _items;
94+
95+
// Equality implementation for proper comparison
96+
public bool Equals(InventoryComponent? other)
97+
{
98+
if (other is null)
99+
return false;
100+
101+
if (_items.Count != other._items.Count)
102+
return false;
103+
104+
foreach (var kvp in _items)
105+
{
106+
if (!other._items.TryGetValue(kvp.Key, out var otherQuantity))
107+
return false;
108+
109+
if (kvp.Value != otherQuantity)
110+
return false;
111+
}
112+
113+
return true;
114+
}
115+
116+
public override int GetHashCode()
117+
{
118+
var hash = new HashCode();
119+
foreach (var kvp in _items.OrderBy(x => x.Key.Value))
120+
{
121+
hash.Add(kvp.Key);
122+
hash.Add(kvp.Value);
123+
}
124+
return hash.ToHashCode();
125+
}
126+
}

0 commit comments

Comments
 (0)