from __future__ import annotations from dataclasses import dataclass from engine.item import ItemDef, ItemInstance @dataclass class PlacedItem: item: ItemInstance x: int y: int class Inventory: """A grid of cells that items occupy according to their w x h footprint.""" def __init__(self, width: int, height: int): self.width = width self.height = height self._cells: list[str | None] = [None] * (width * height) self._items: dict[str, PlacedItem] = {} def _footprint(self, item_def: ItemDef, x: int, y: int) -> list[tuple[int, int]]: return [(x + dx, y + dy) for dy in range(item_def.height) for dx in range(item_def.width)] def can_place(self, item_def: ItemDef, x: int, y: int) -> bool: for cx, cy in self._footprint(item_def, x, y): if not (0 <= cx < self.width and 0 <= cy < self.height): return False if self._cells[cy * self.width + cx] is not None: return False return True def place(self, item: ItemInstance, item_def: ItemDef, x: int, y: int) -> bool: if not self.can_place(item_def, x, y): return False for cx, cy in self._footprint(item_def, x, y): self._cells[cy * self.width + cx] = item.item_id self._items[item.item_id] = PlacedItem(item, x, y) return True def remove(self, item_id: str, item_def: ItemDef) -> ItemInstance | None: placed = self._items.pop(item_id, None) if placed is None: return None for cx, cy in self._footprint(item_def, placed.x, placed.y): self._cells[cy * self.width + cx] = None return placed.item def find_first_fit(self, item_def: ItemDef) -> tuple[int, int] | None: for y in range(self.height - item_def.height + 1): for x in range(self.width - item_def.width + 1): if self.can_place(item_def, x, y): return (x, y) return None def items(self) -> list[PlacedItem]: return list(self._items.values()) def item_at(self, x: int, y: int) -> PlacedItem | None: """The PlacedItem occupying cell (x, y), if any - checks the item's whole footprint, not just its top-left corner, so a 2x2 item is found from any of its 4 cells. Used for UI hit-testing (see engine/render/menu_draw.py's inventory_cell_at) as well as anything else that needs "what's under this specific cell". """ if not (0 <= x < self.width and 0 <= y < self.height): return None item_id = self._cells[y * self.width + x] if item_id is None: return None return self._items.get(item_id)