Mi2dRPGamEng/engine/inventory.py

73 lines
2.7 KiB
Python

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)