diff --git a/game/common/avatar.py b/game/common/avatar.py index bce4db0..47a61fd 100644 --- a/game/common/avatar.py +++ b/game/common/avatar.py @@ -6,12 +6,100 @@ class Avatar(GameObject): - def __init__(self, item: Item | None = None, position: Vector | None = None): + """ + Notes for the inventory: + + The avatar's inventory is a list of items. Each item has a quantity and a stack_size (the max amount of an + item that can be held in a stack. Think of the Minecraft inventory). + + This upcoming example is just to facilitate understanding the concept. The Dispensing Station concept that will + be mentioned is completely optional to implement if you desire. The Dispensing Station is used to help with the + explanation. + + ---------------------------------------------------------------------------------------------------------------- + + Items: + Every Item has a quantity and a stack_size. The quantity is how much of the Item the player *currently* has. + The stack_size is the max of that Item that can be in a stack. For example, if the quantity is 5, and the + stack_size is 5 (5/5), the item cannot be added to that stack + + Picking up items: + Example 1: + When you pick up an item (which will now be referred to as picked_up_item), picked_up_item has a given + quantity. In this case, let's say the quantity of picked_up_item is 2. + + Imagine you already have this item in your inventory (which will now be referred to as inventory_item), + and inventory_item has a quantity of 1 and a stack_size of 10 (think of this as a fraction: 1/10). + + When you pick up picked_up_item, inventory_item will be checked. + If picked_up_item's quantity + inventory_item < stack_size, it'll be added without issue. + Remember, for this example: picked_up_item quantity is 2, and inventory_item quantity is 1, and stack_size + is 10. + Inventory_item quantity before picking up: 1/10 + 2 + 1 < 10 --> True + Inventory_item quantity after picking up: 3/10 + + ---------------------------------------------------------------------------------------------------------------- + + Example 2: + For the next two examples, the total inventory size will be considered. + + Let's say inventory_item has quantity 4 and a stack_size of 5. Now say that picked_up_item has quantity 3. + Recall: if picked_up_item's quantity + inventory_item < stack_size, it will be added without issue + Inventory_item quantity before picking up: 4/5 + 3 + 4 < 5 --> False + + What do we do in this situation? If you want to add picked_up_item to inventory_item and there is an + overflow of quantity, that is handled for you. + + Let's say that your inventory size (which will now be referred to as max_inventory_size) is 5. You already + have inventory_item in there that has a quantity of 4 and a stack_size of 5. An image of the inventory is + below. 'None' is used to help show the max_inventory_size. Inventory_item quantity and stack_size will be + listed in parentheses as a fraction. + Inventory: + [inventory_item (4/5), None, None, None, None] + + Now we will add picked_up_item and its quantity of 3: + Inventory before: + [inventory_item (4/5), None, None, None, None] + + 3 + 4 < 5 --> False + inventory_item (4/5) will now be inventory_item (5/5) + picked_up_item now has a quantity of 2 + Since we have a surplus, we will append the same item with a quantity of 2 in the inventory. + + The result is: + [inventory_item (5/5), inventory_item (2/5), None, None, None] + + ---------------------------------------------------------------------------------------------------------------- + Example 3: + + For this last example, assume your inventory looks like this: + [inventory_item (5/5), inventory_item (5/5) inventory_item (5/5) inventory_item (5/5), inventory_item (4/5)] + + You can only fit one more inventory_item into the last stack before the inventory is full. + Let's say that picked_up_item has quantity of 3 again. + + Inventory before: + [inventory_item (5/5), inventory_item (5/5) inventory_item (5/5) inventory_item (5/5), inventory_item (4/5)] + 3 + 4 < 5 --> False + inventory_item (4/5) will now be inventory_item (5/5) + picked_up_item now has a quantity of 2 + However, despite the surplus, we cannot add it into our inventory, so the remaining quantity of + picked_up_item is left where it was first found. + Inventory after: + [inventory_item (5/5), inventory_item (5/5) inventory_item (5/5) inventory_item (5/5), inventory_item (5/5)] + """ + + def __init__(self, item: Item | None = None, position: Vector | None = None, inventory: list[Item] = [], + max_inventory_size: int = 10): super().__init__() self.object_type: ObjectType = ObjectType.AVATAR self.held_item: Item | None = item self.score: int = 0 self.position: Vector | None = position + self.max_inventory_size: int = max_inventory_size + self.inventory: list[Item] = inventory @property def held_item(self) -> Item | None: @@ -25,45 +113,82 @@ def score(self) -> int: def position(self) -> Vector | None: return self.__position + @property + def inventory(self) -> list[Item]: + return self.__inventory + + @property + def max_inventory_size(self) -> int: + return self.__max_inventory_size + @held_item.setter def held_item(self, item: Item | None) -> None: # If it's not an item, and it's not None, raise the error if item is not None and not isinstance(item, Item): raise ValueError(f"{self.__class__.__name__}.held_item must be an Item or None.") - self.__held_item = item + self.__held_item: Item = item @score.setter def score(self, score: int) -> None: if score is None or not isinstance(score, int): raise ValueError(f"{self.__class__.__name__}.score must be an int.") - self.__score = score + self.__score: int = score @position.setter def position(self, position: Vector | None) -> None: if position is not None and not isinstance(position, Vector): raise ValueError(f"{self.__class__.__name__}.position must be a Vector or None.") - self.__position = position + self.__position: Vector | None = position + + @inventory.setter + def inventory(self, inventory: list[Item]) -> None: + if inventory is None or not isinstance(inventory, list) \ + or (len(inventory) > 0 and any(map(lambda item: not isinstance(item, Item), inventory))): + raise ValueError(f"{self.__class__.__name__}.inventory must be a list of Items.") + if len(inventory) > self.max_inventory_size: + raise ValueError(f"{self.__class__.__name__}.inventory size must be less than max_inventory_size") + self.__inventory: list[Item] = inventory + + @max_inventory_size.setter + def max_inventory_size(self, size: int) -> None: + if size is None or not isinstance(size, int): + raise ValueError(f"{self.__class__.__name__}.max_inventory_size must be an int.") + self.__max_inventory_size: int = size + + def pick_up(self, item: Item) -> Item | None: + t = item + [t := i.pick_up(t) for i in self.inventory] + + if t is not None and len(self.inventory) < self.max_inventory_size: + self.inventory.append(t) + return None + + return t def to_json(self) -> dict: data: dict = super().to_json() data['held_item'] = self.held_item.to_json() if self.held_item is not None else None data['score'] = self.score data['position'] = self.position + data['inventory'] = self.inventory + data['max_inventory_size'] = self.max_inventory_size return data def from_json(self, data: dict) -> Self: super().from_json(data) self.score: int = data['score'] self.position: Vector | None = data['position'] + self.inventory: list[Item] = data['inventory'] + self.max_inventory_size: int = data['max_inventory_size'] held_item: Item | None = data['held_item'] if held_item is None: self.held_item = None return self - + match held_item['object_type']: case ObjectType.ITEM: self.held_item = Item().from_json(data['held_item']) case _: raise ValueError(f"{self.__class__.__name__}.held_item needs to be an item.") - + return self diff --git a/game/common/enums.py b/game/common/enums.py index 19771b8..3e934be 100644 --- a/game/common/enums.py +++ b/game/common/enums.py @@ -33,4 +33,18 @@ class ActionType(Enum): INTERACT_DOWN = auto() INTERACT_LEFT = auto() INTERACT_RIGHT = auto() - INTERACT_CENTER = auto() \ No newline at end of file + INTERACT_CENTER = auto() + SELECT_SLOT_0 = auto() + SELECT_SLOT_1 = auto() + SELECT_SLOT_2 = auto() + SELECT_SLOT_3 = auto() + SELECT_SLOT_4 = auto() + SELECT_SLOT_5 = auto() + SELECT_SLOT_6 = auto() + SELECT_SLOT_7 = auto() + SELECT_SLOT_8 = auto() + SELECT_SLOT_9 = auto() + """ + These last 10 enums are for selecting a slot from the Avatar class' inventory. + You can add/remove these as needed for the purposes of your game. + """ diff --git a/game/common/items/item.py b/game/common/items/item.py index a617ead..c600c21 100644 --- a/game/common/items/item.py +++ b/game/common/items/item.py @@ -4,12 +4,15 @@ class Item(GameObject): - def __init__(self, value: int = 1, durability: int | None = 100): + def __init__(self, value: int = 1, durability: int | None = 100, quantity: int = 1, stack_size: int = 1): super().__init__() + self.__quantity = None # This is here to prevent an error self.object_type: ObjectType = ObjectType.ITEM - self.value: int = value - self.durability: int | None = durability # durability can be None if infinite durability - + self.value: int = value # Value can more specified based on purpose (e.g., the sell price) + self.stack_size: int = stack_size # the max quantity this item can contain + self.durability: int | None = durability # durability can be None if infinite durability + self.quantity: int = quantity # the current amount of this item + @property def durability(self) -> int | None: return self.__durability @@ -18,26 +21,74 @@ def durability(self) -> int | None: def value(self) -> int: return self.__value + @property + def quantity(self) -> int: + return self.__quantity + + @property + def stack_size(self) -> int: + return self.__stack_size + @durability.setter - def durability(self, durability: int | None) -> None: - if durability is not None and not isinstance(durability, int): - raise ValueError(f'{self.__class__.__name__}.durability must be an int or None.') + def durability(self, durability: int | None): + if durability is not None and not isinstance(durability, int) or self.stack_size > 1: + raise ValueError(f'{self.__class__.__name__}.durability must be an int or None, and stack_size must be 1.') self.__durability = durability @value.setter def value(self, value: int) -> None: if value is None or not isinstance(value, int): raise ValueError(f'{self.__class__.__name__}.value must be an int.') - self.__value = value + self.__value: int = value + + @quantity.setter + def quantity(self, quantity: int) -> None: + if quantity is None or not isinstance(quantity, int): + raise ValueError(f'{self.__class__.__name__}.quantity must be an int.') + if quantity < 0: + raise ValueError(f'{self.__class__.__name__}.quantity must be greater than 0.') + + # The self.quantity is set to the lower value between stack_size and the given quantity + # The remaining given quantity is returned if it's larger than self.quantity + if quantity > self.stack_size: + raise ValueError(f'{self.__class__.__name__}.quantity cannot be greater than ' + f'{self.__class__.__name__}.stack_size') + self.__quantity: int = quantity + + @stack_size.setter + def stack_size(self, stack_size: int) -> None: + if stack_size is None or not isinstance(stack_size, int): + raise ValueError(f'{self.__class__.__name__}.stack_size must be an int.') + if self.__quantity is not None and stack_size < self.__quantity: + raise ValueError(f'{self.__class__.__name__}.stack_size must be greater than or equal to the quantity.') + self.__stack_size: int = stack_size + + def pick_up(self, item: Self) -> Self | None: + # If the items don't match, return the given item without modifications + if self.object_type != item.object_type: + return item + + # If the picked up quantity goes over the stack_size, add to make the quantity equal the stack_size + if self.quantity + item.quantity > self.stack_size: + item.quantity -= self.stack_size - self.quantity + self.quantity: int = self.stack_size + return item + + # Add the given item's quantity to the self item + self.quantity += item.quantity def to_json(self) -> dict: data: dict = super().to_json() data['durability'] = self.durability data['value'] = self.value + data['quantity'] = self.quantity + data['stack_size'] = self.stack_size return data def from_json(self, data: dict) -> Self: super().from_json(data) self.durability: int | None = data['durability'] self.value: int = data['value'] + self.quantity: int = data['quantity'] + self.stack_size: int = data['stack_size'] return self diff --git a/game/controllers/inventory_controller.py b/game/controllers/inventory_controller.py new file mode 100644 index 0000000..2aa426f --- /dev/null +++ b/game/controllers/inventory_controller.py @@ -0,0 +1,28 @@ +from game.common.enums import * +from game.common.avatar import Avatar +from game.common.player import Player +from game.controllers.controller import Controller +from game.common.map.game_board import GameBoard + + +class InventoryController(Controller): + def __init__(self): + super().__init__() + + def handle_actions(self, action: ActionType, client: Player, world: GameBoard) -> None: + # If a larger inventory is created, create more enums and add them here as needed + avatar: Avatar = client.avatar + + # If there are more than 10 slots in the inventory, change "ActionType.SELECT_SLOT_9" + # This checks if the given action isn't one of the select slot enums + if action.value < ActionType.SELECT_SLOT_0.value or action.value > ActionType.SELECT_SLOT_9.value: + return + + index: int = action.value - ActionType.SELECT_SLOT_0.value + + try: + avatar.held_item = avatar.inventory[index] + except IndexError: + raise IndexError(f'The given action type, {action}, is not within bounds of the given inventory of ' + f'size {len(avatar.inventory)}. Select an ActionType enum that will be within the ' + f'inventory\'s bounds.')