scripts/inventory.pyΒΆ
Part of Dungeon Explorer.
1"""Inventory grid logic: add/remove/equip/stack items."""
2
3from .item import EQUIPMENT_SLOTS, Item
4
5
6class Inventory:
7 """Grid-based inventory with equipment slots.
8
9 Args:
10 rows: Number of inventory rows.
11 cols: Number of inventory columns.
12 """
13
14 def __init__(self, rows: int = 5, cols: int = 8):
15 self.rows = rows
16 self.cols = cols
17 self._grid: list[Item | None] = [None] * (rows * cols)
18 self._equipment: dict[str, Item | None] = dict.fromkeys(EQUIPMENT_SLOTS)
19
20 @property
21 def capacity(self) -> int:
22 return self.rows * self.cols
23
24 @property
25 def item_count(self) -> int:
26 return sum(1 for item in self._grid if item is not None)
27
28 @property
29 def is_full(self) -> bool:
30 return all(slot is not None for slot in self._grid)
31
32 def get(self, index: int) -> Item | None:
33 """Get item at grid index."""
34 if 0 <= index < len(self._grid):
35 return self._grid[index]
36 return None
37
38 def set(self, index: int, item: Item | None) -> None:
39 """Set item at grid index."""
40 if 0 <= index < len(self._grid):
41 self._grid[index] = item
42
43 def add(self, item: Item) -> bool:
44 """Add item to first available slot. Returns True if successful.
45
46 Stackable items are stacked first, then placed in empty slots.
47 """
48 if item.is_stackable:
49 # Try to stack with existing
50 for existing in self._grid:
51 if (
52 existing is not None
53 and existing.template_key == item.template_key
54 and existing.stack_count < existing.stack_size
55 ):
56 space = existing.stack_size - existing.stack_count
57 transfer = min(item.stack_count, space)
58 existing.stack_count += transfer
59 item.stack_count -= transfer
60 if item.stack_count <= 0:
61 return True
62
63 # Place in empty slot
64 for i, slot in enumerate(self._grid):
65 if slot is None:
66 self._grid[i] = item
67 return True
68 return False
69
70 def remove(self, index: int) -> Item | None:
71 """Remove and return item at index."""
72 if 0 <= index < len(self._grid):
73 item = self._grid[index]
74 self._grid[index] = None
75 return item
76 return None
77
78 def equip(self, item: Item) -> Item | None:
79 """Equip an item, returning the previously equipped item (or None)."""
80 if not item.is_equipment:
81 return None
82 slot = item.slot
83 previous = self._equipment.get(slot)
84 self._equipment[slot] = item
85 return previous
86
87 def unequip(self, slot: str) -> Item | None:
88 """Unequip item from a slot. Returns the item, or None if slot was empty."""
89 item = self._equipment.get(slot)
90 if item is not None:
91 self._equipment[slot] = None
92 return item
93
94 def get_equipped(self, slot: str) -> Item | None:
95 """Get the currently equipped item in a slot."""
96 return self._equipment.get(slot)
97
98 def all_equipped(self) -> dict[str, Item | None]:
99 """Return dict of all equipment slots."""
100 return dict(self._equipment)
101
102 def equipment_stats(self) -> dict[str, float]:
103 """Compute total stats from all equipped items."""
104 stats: dict[str, float] = {}
105 for item in self._equipment.values():
106 if item is None:
107 continue
108 for key, val in item.total_stats().items():
109 stats[key] = stats.get(key, 0) + val
110 return stats
111
112 def items(self) -> list[tuple[int, Item]]:
113 """Return list of (index, item) for non-empty slots."""
114 return [(i, item) for i, item in enumerate(self._grid) if item is not None]
115
116 def find_by_template(self, template_key: str) -> list[tuple[int, Item]]:
117 """Find all items matching a template key."""
118 return [
119 (i, item) for i, item in enumerate(self._grid) if item is not None and item.template_key == template_key
120 ]
121
122 def swap(self, idx_a: int, idx_b: int) -> None:
123 """Swap items at two indices."""
124 self._grid[idx_a], self._grid[idx_b] = self._grid[idx_b], self._grid[idx_a]
125
126 def to_dict(self) -> dict:
127 """Serialise inventory to dict."""
128 return {
129 "rows": self.rows,
130 "cols": self.cols,
131 "grid": [item.to_dict() if item else None for item in self._grid],
132 "equipment": {slot: (item.to_dict() if item else None) for slot, item in self._equipment.items()},
133 }
134
135 @classmethod
136 def from_dict(cls, d: dict) -> Inventory:
137 """Deserialise from dict."""
138 inv = cls(rows=d.get("rows", 5), cols=d.get("cols", 8))
139 for i, item_data in enumerate(d.get("grid", [])):
140 if item_data and i < len(inv._grid):
141 inv._grid[i] = Item.from_dict(item_data)
142 for slot, item_data in d.get("equipment", {}).items():
143 if item_data and slot in inv._equipment:
144 inv._equipment[slot] = Item.from_dict(item_data)
145 return inv