From bcfd344f00a31898e00fb574a66f922c4c73ec84 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Thu, 20 Mar 2025 17:07:18 +0100 Subject: [PATCH 01/18] Edited the document file slightly because of some Pycharm errors, also changed the correct path where the `src/inventory_gui.py` is now located in. --- docs/Guides/InventoryGuide.md | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/docs/Guides/InventoryGuide.md b/docs/Guides/InventoryGuide.md index f890057..bfd3d25 100644 --- a/docs/Guides/InventoryGuide.md +++ b/docs/Guides/InventoryGuide.md @@ -33,9 +33,7 @@ The `data/inventory.json` file controls the data for all items in the inventory. 2. Add, remove, or edit item entries using the following format: ```json { - ... - "Gold Coin": {"type": "currency", "effect": "collect", "quantity": 1}, - ... + "Gold Coin": {"type": "currency", "effect": "collect", "quantity": 1} } ``` @@ -52,17 +50,24 @@ Each item in the inventory has the following properties: ### Adding a New Item Icon To add a new item icon to the inventory: -1. Open `src/GUI/inventory_gui.py`. -2. Locate the initializer method (`self.icons: {}`). -3. Map the item name in `inventory.json` to the icon's location in the spritesheet. Use the following format: + 1. Open `src/inventory_gui.py`. + 2. Locate the initializer method (`self.icons: {}`). + 3. Map the item name in `inventory.json` to the icon's location in the sprite sheet. Use the following format: + +```py +class InventoryGUI: -```python - "Gold Coin": self.extract_icon(0, 0), + def __init__(self, ): + + self.icons = { "Gold Coin": self.extract_icon(0, 0) } + + def extract_icon(self, x, y, size=16): + ... ``` [![icons-inventory-gui.png](https://i.postimg.cc/CMNKKL8T/icons-inventory-gui.png)](https://postimg.cc/231YcYT2) -4. Test the new item in-game to verify functionality and ensure no errors occur. + 4. Test the new item in-game to verify functionality and ensure no errors occur. ## Known Issues From 6928c3ee781be8d5198ad23a28d44342f5ecbb28 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Thu, 20 Mar 2025 17:12:58 +0100 Subject: [PATCH 02/18] - Moved the sprites.py to a new directory called sprites - Also changed the `src/sprites.py` file name to `src/sprites/base.py` since we might want to adapt the sprites classes to inherit from a base Sprite class later on, it also helps to make the file not too big and easier to navigate. - Changed the reference of sprites.py to base.py in game_running.py --- src/{sprites.py => sprites/base.py} | 19 +++++++++---------- src/states/game_running.py | 16 ++++++++-------- 2 files changed, 17 insertions(+), 18 deletions(-) rename src/{sprites.py => sprites/base.py} (94%) diff --git a/src/sprites.py b/src/sprites/base.py similarity index 94% rename from src/sprites.py rename to src/sprites/base.py index 24ba2fd..97af90a 100644 --- a/src/sprites.py +++ b/src/sprites/base.py @@ -17,13 +17,12 @@ class Entity(pygame.sprite.Sprite): def __init__(self, pos, frames, groups): super().__init__(groups) self.z = WORLD_LAYERS["main"] - # graphics self.frame_index, self.frames = 0, frames self.facing_direction = "down" # movement - self.direction = pygame.math.Vector2() + self.direction = pygame.math.Vector3() # self.speed = 250 # sprite setup @@ -109,7 +108,7 @@ def draw(self, player_center): # self.display_surface.blit(scaled_preview, scaled_preview_rect.topleft) - # method for zooming (might be usefull later?) + # method for zooming (might be useful later?) # def set_scale(self, scale): # self.scale = max(scale, 0.1) @@ -141,19 +140,19 @@ def input(self) -> None: # Reset direction self.direction = pygame.math.Vector2(0, 0) - # gost preview + # ghost preview mouse_pos = pygame.mouse.get_pos() # get the relative pos of the player from the mouse - # to know on wich axis the player will move + # to know on which axis the player will move delta_x = abs(self.rect.centerx - mouse_pos[0]) delta_y = abs(self.rect.centery - mouse_pos[1]) - # # move the gost on the x axis + # # move the ghost on the x axis # self.player_preview_rect = self.rect.copy() # if delta_x > delta_y: # if delta_x < (TILE_SIZE / 2): - # # don't move the gost if the mouse is on the player hitbox + # # don't move the ghost if the mouse is on the player hit box # self.player_preview_rect.x = self.rect.x # elif mouse_pos[0] > self.rect.centerx: # # go right @@ -161,7 +160,7 @@ def input(self) -> None: # else: # # go left # self.player_preview_rect.x = self.rect.x - TILE_SIZE - # # move the gost on the y axis + # # move the ghost on the y axis # else: # if delta_y < (TILE_SIZE / 2): # # don't move if the mouse is on the player hitbox @@ -202,7 +201,7 @@ def input(self) -> None: # return None def update(self, dt) -> None: - """blit player image and gost preview to a given surface""" + """blit player image and ghost preview to a given surface""" self.input() self.animate(dt) @@ -229,7 +228,7 @@ def __init__( Note: There is a known typing error related to missing type parameters for the generic type 'Group' in pygame. You may see warnings like: - - src\\sprites.py:107: error: Missing type parameters for generic type 'Group' [type-arg] + - src\\base.py:107: error: Missing type parameters for generic type 'Group' [type-arg] - src\\GUI\\game_manager.py:45: error: Missing type parameters for generic type 'SpriteGroup' [type-arg] These errors can be ignored for now, as they are related to type annotations in pygame's codebase. diff --git a/src/states/game_running.py b/src/states/game_running.py index b4993c6..f3474ad 100644 --- a/src/states/game_running.py +++ b/src/states/game_running.py @@ -9,10 +9,10 @@ from pytmx.util_pygame import load_pygame # type: ignore import src.shop -import src.sprites +import src.sprites.base from src.inventory import Inventory from src.settings import TILE_SIZE, WORLD_LAYERS -from src.sprites import AnimatedSprites +from src.sprites.base import AnimatedSprites from src.states.base_state import BaseState from src.states.paused import Paused from src.support import all_character_import, coast_importer, import_folder @@ -37,7 +37,7 @@ def __init__(self, game_state_manager) -> None: self.player_inventory = Inventory() self.load_inventory_from_json("data/inventory.json") - self.all_sprites = src.sprites.AllSprites() + self.all_sprites = src.sprites.base.AllSprites() # The start positions will be one of the 4 islands in the corners of the board self.setup(player_start_pos="top_left_island") @@ -48,7 +48,7 @@ def __init__(self, game_state_manager) -> None: def setup(self, player_start_pos): """ - setup the map and player from the tiled file + set up the map and player from the tiled file """ self.tmx_map = {"map": load_pygame(os.path.join(".", "data", "new_maps", "100x100_map.tmx"))} @@ -60,7 +60,7 @@ def setup(self, player_start_pos): # Sea for x, y, surface in self.tmx_map["map"].get_layer_by_name("Sea").tiles(): - src.sprites.Sprite( + src.sprites.base.Sprite( (x * TILE_SIZE, y * TILE_SIZE), surface, self.all_sprites, @@ -80,7 +80,7 @@ def setup(self, player_start_pos): # Shallow water for x, y, surface in self.tmx_map["map"].get_layer_by_name("Shallow Sea").tiles(): - src.sprites.Sprite((x * TILE_SIZE, y * TILE_SIZE), surface, self.all_sprites, WORLD_LAYERS["bg"]) + src.sprites.base.Sprite((x * TILE_SIZE, y * TILE_SIZE), surface, self.all_sprites, WORLD_LAYERS["bg"]) # buildings for x, y, surface in self.tmx_map["map"].get_layer_by_name("Shop").tiles(): @@ -91,7 +91,7 @@ def setup(self, player_start_pos): # Islands islands = self.tmx_map["map"].get_layer_by_name("Islands") for x, y, surface in islands.tiles(): - src.sprites.Sprite( + src.sprites.base.Sprite( (x * TILE_SIZE, y * TILE_SIZE), surface, self.all_sprites, @@ -101,7 +101,7 @@ def setup(self, player_start_pos): # Enitites for obj in self.tmx_map["map"].get_layer_by_name("Ships"): if obj.name == "Player" and obj.properties["pos"] == player_start_pos: - self.player = src.sprites.Player( + self.player = src.sprites.base.Player( pos=(obj.x, obj.y), frames=self.world_frames["ships"]["player_test_ship"], groups=self.all_sprites, From 31f935c1c2f23a2aff78ac0814b1e98d6d2ceeb2 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Fri, 21 Mar 2025 14:23:07 +0100 Subject: [PATCH 03/18] Removed redundant code, this is used in the game_running.py file in the states directory --- src/GUI/gameloop.py | 155 -------------------------------------------- 1 file changed, 155 deletions(-) delete mode 100644 src/GUI/gameloop.py diff --git a/src/GUI/gameloop.py b/src/GUI/gameloop.py deleted file mode 100644 index d30f12f..0000000 --- a/src/GUI/gameloop.py +++ /dev/null @@ -1,155 +0,0 @@ -# import sys - -# # import dataclasses and typchecking -# from dataclasses import dataclass, field -# from os.path import join - -# # import pygame related -# import pygame -# from pytmx.util_pygame import load_pygame # type: ignore - -# import src.sprites - -# # import Pygame specific objects, functions and functionality -# from src.settings import SCREEN_HEIGHT, SCREEN_WIDTH, TILE_SIZE, WORLD_LAYERS -# from src.sprites import AnimatedSprites -# from src.support import all_character_import, coast_importer, import_folder - - -# @dataclass -# class GUI: -# """Graphial User Interface vertion of the game, using pygame-ce""" - -# screen_size: tuple[int, int] = (SCREEN_WIDTH, SCREEN_HEIGHT) -# screen: pygame.Surface = field(init=False) - -# # groups -# # all_sprites: pygame.sprite.Group = field( -# # init=False, default_factory=pygame.sprite.Group -# # ) - -# def __post_init__(self): -# pygame.init() -# self.screen = pygame.display.set_mode(self.screen_size) -# pygame.display.set_caption("PySeas") -# self.clock = pygame.Clock() - -# # self.players: list[src.sprites.Player] = [src.sprites.Player()] - -# self.all_sprites = src.sprites.AllSprites() -# self.running = True -# self.import_assets() -# self.setup( -# tmx_maps=self.tmx_map["map"], player_start_pos="top_left_island" -# ) # The start positions will be one of the 4 islands in the corners of the board -# self.camera_mode = "drag" - -# def import_assets(self): -# """load the map""" -# # The map was made as a basic start for the game, it can be changes or altered if it is better for the overall -# flow of the game -# self.tmx_map = { -# "map": load_pygame(join(".", "data", "new_maps", "100x100_map.tmx")) -# } - -# self.world_frames = { -# "water": import_folder(".", "images", "tilesets", "water"), -# "coast": coast_importer(6, 6, ".", "images", "tilesets", "coast"), -# "ships": all_character_import(".", "images", "tilesets", "ships"), -# } -# # print(self.world_frames["ships"]) - -# def setup(self, tmx_maps, player_start_pos): -# """create tiles""" - -# # Sea -# for x, y, surface in tmx_maps.get_layer_by_name("Sea").tiles(): -# src.sprites.Sprite( -# (x * TILE_SIZE, y * TILE_SIZE), -# surface, -# self.all_sprites, -# WORLD_LAYERS["bg"], -# ) - -# # Water animated -# for obj in tmx_maps.get_layer_by_name("Water"): -# for x in range(int(obj.x), int(obj.x + obj.width), TILE_SIZE): -# for y in range(int(obj.y), int(obj.y + obj.height), TILE_SIZE): -# AnimatedSprites( -# (x, y), -# self.world_frames["water"], -# self.all_sprites, -# WORLD_LAYERS["water"], -# ) - -# # Shallow water -# for x, y, surface in tmx_maps.get_layer_by_name("Shallow Sea").tiles(): -# src.sprites.Sprite( -# (x * TILE_SIZE, y * TILE_SIZE), -# surface, -# self.all_sprites, -# WORLD_LAYERS["bg"], -# ) - -# # Islands -# islands = tmx_maps.get_layer_by_name("Islands") -# for x, y, surface in islands.tiles(): -# src.sprites.Sprite( -# (x * TILE_SIZE, y * TILE_SIZE), -# surface, -# self.all_sprites, -# WORLD_LAYERS["bg"], -# ) - -# # Enitites -# for obj in tmx_maps.get_layer_by_name("Ships"): -# if obj.name == "Player" and obj.properties["pos"] == player_start_pos: -# self.player = src.sprites.Player( -# pos=(obj.x, obj.y), -# frames=self.world_frames["ships"]["player_test_ship"], -# groups=self.all_sprites, -# ) - -# # Coast -# for obj in tmx_maps.get_layer_by_name("Coast"): -# terrain = obj.properties["terrain"] -# side = obj.properties["side"] -# AnimatedSprites( -# (obj.x, obj.y), -# self.world_frames["coast"][terrain][side], -# self.all_sprites, -# WORLD_LAYERS["bg"], -# ) - -# def run(self) -> None: -# """main loop of the game""" -# while self.running: -# self.handle_events() -# self.render() - -# def handle_events(self) -> None: -# """get events like keypress or mouse clicks""" -# for event in pygame.event.get(): -# match event.type: -# case pygame.QUIT: -# pygame.quit() -# sys.exit() - -# def render(self) -> None: -# """draw sprites to the canvas""" -# dt = self.clock.tick() / 1000 -# self.screen.fill("#000000") - -# self.all_sprites.update(dt) -# self.all_sprites.draw( -# self.player.rect.center, -# self.player.player_preview, -# self.player.player_preview_rect, -# ) - -# """No need to loop through the players because it is now in the sprite group AllSprites""" -# # draw players on top of the other sprites -# # for player in self.players: -# # player.render(surface=self.screen) - -# pygame.display.update() From bc610b0d6369b98c993b6b66343bf810b5f869b5 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Sat, 22 Mar 2025 17:36:30 +0100 Subject: [PATCH 04/18] Fix type errors --- src/game_manager.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/game_manager.py b/src/game_manager.py index a3fc152..f766588 100644 --- a/src/game_manager.py +++ b/src/game_manager.py @@ -9,7 +9,7 @@ from src.settings import FPS, SCREEN_HEIGHT, SCREEN_WIDTH -# import basestate for typehint +# import base state for typehint from src.states.base_state import BaseState from src.states.game_running import GameRunning @@ -29,14 +29,14 @@ def __init__(self) -> None: # init pygame pygame.init() self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) - pygame.display.set_caption("PySeas") + pygame.display.set_caption("PyCeas") self.clock = pygame.Clock() self.running = True self.events: list[pygame.event.Event] = [] self.states_stack: list[BaseState] = [] - # instanciate the initial state + # instantiate the initial state self.states_stack.append(GameRunning(self)) def __str__(self) -> str: @@ -76,11 +76,11 @@ def run(self) -> None: while self.running: self._handle_events() - # give the pygame events to each states + # give the pygame events to each state # to ensure that pygame.event.get() is only called once per frame self.states_stack[-1].update(self.events) self.states_stack[-1].render(self.screen) - # magic value, use a FPS const in settings or delta time + # magic value, use 'a' FPS const in settings or delta time self.clock.tick(FPS) From dee0c726bb4b88cec36de6a4acfbd4d18fa1eb28 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Sun, 23 Mar 2025 23:53:06 +0100 Subject: [PATCH 05/18] Remove old deprecated 'typing' import, since it is now standard in Python 3.9 --- src/shop.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/shop.py b/src/shop.py index 724e32e..dc88b8d 100644 --- a/src/shop.py +++ b/src/shop.py @@ -1,5 +1,3 @@ -from typing import Dict, Tuple - import pygame from src.inventory import Inventory @@ -9,7 +7,7 @@ class ShowShop(pygame.sprite.Sprite): def __init__(self, pos, surface, groups, z=WORLD_LAYERS["main"]): - super().__init__(groups) + super().__init__(*groups) self.image = pygame.Surface((32, 32)) self.image.fill("white") @@ -27,7 +25,7 @@ def __init__(self, game_state_manager, player, show_shop, inventory: Inventory): self.button_height = 50 self.scroll_offset = 0 - self.button_actions: Dict[str, Tuple[pygame.Rect, pygame.Rect]] = {} + self.button_actions: dict[str, tuple[pygame.Rect, pygame.Rect]] = {} self.inventory = inventory self.show_shop = show_shop self.player = player @@ -177,7 +175,7 @@ def handle_mouse_clicks(self, mouse_pos): def extract_icon(self, x, y, size=16): return self.sprite_sheet.subsurface((x, y, size, size)) - def draw_buttons(self, x: int, y: int, item: str) -> Tuple[pygame.Rect, pygame.Rect]: + def draw_buttons(self, x: int, y: int, item: str) -> tuple[pygame.Rect, pygame.Rect]: use_button = pygame.Rect(x, y, self.button_width, self.button_height) discard_button = pygame.Rect(x + self.button_width + 10, y, self.button_width, self.button_height) From 6d6a859ae16cf972c07b323e46c04f5b7159572b Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Mon, 24 Mar 2025 01:31:43 +0100 Subject: [PATCH 06/18] Remove old deprecated 'typing' import, since it is now standard in Python 3.9, also fixed some typos --- src/states/paused.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/states/paused.py b/src/states/paused.py index f95283d..bc3fab5 100644 --- a/src/states/paused.py +++ b/src/states/paused.py @@ -3,7 +3,7 @@ holding the inventory """ -from typing import Dict, Tuple +# from typing import Dict, Tuple import pygame # type: ignore @@ -35,7 +35,7 @@ def __init__(self, game_state_manager, inventory: Inventory) -> None: # Load sprite sheet and extract the icons (Testing purposes) # To be replaced when: - # 1) Spritesheet has been decide. + # 1) Sprite sheet has been decided. # 2) A 'Buy', 'Found' or 'Add' in-game feature has been implemented self.sprite_sheet = pygame.image.load("images/tilesets/Treasure+.png").convert_alpha() self.icons = { @@ -82,12 +82,12 @@ def __init__(self, game_state_manager, inventory: Inventory) -> None: "Glowing Crystal": self.extract_icon(16, 176), } - # Button dimmentions + # Button dimensions self.button_width = 100 self.button_height = 50 # Initialize button actions - self.button_actions: Dict[str, Tuple[pygame.Rect, pygame.Rect]] = {} + self.button_actions: dict[str, tuple[pygame.Rect, pygame.Rect]] = {} # Action messages self.message = "" @@ -107,7 +107,7 @@ def extract_icon(self, x, y, size=16): """Extract a single icon from the sprite sheet.""" return self.sprite_sheet.subsurface((x, y, size, size)) - def draw_buttons(self, x: int, y: int, item: str) -> Tuple[pygame.Rect, pygame.Rect]: + def draw_buttons(self, x: int, y: int, item: str) -> tuple[pygame.Rect, pygame.Rect]: """Draw Use and Discard buttons for a specific item.""" use_button = pygame.Rect(x, y, self.button_width, self.button_height) discard_button = pygame.Rect(x + self.button_width + 10, y, self.button_width, self.button_height) @@ -173,11 +173,11 @@ def render(self, screen: pygame.Surface) -> None: self.button_actions[item] = (use_button, discard_button) y_offset += 60 # Move down for the next item - # Draw hint + # Draw a hint hint_text = self.font.render("Press 'I' to close inventory", True, (200, 200, 200)) # Light gray text self.screen.blit(hint_text, (50, self.screen.get_height() - 60)) - # Display action message above the hint + # Display an action message above the hint if self.message and pygame.time.get_ticks() < self.message_end_time: # Render the message text message_text = self.font.render(self.message, True, (255, 255, 0)) # Yellow @@ -204,6 +204,6 @@ def render(self, screen: pygame.Surface) -> None: (message_bg_x + 10, message_bg_y + 5), # Position text with padding ) - # blit tmp self.screen to the actual display (screen form the argument) + # blit tmp self.screen to the actual display (screen forms the argument) screen.blit(self.screen, dest=(0, 0)) pygame.display.flip() # Update the display From fc029794a722dec59ffe1da1ff2cdb72b4637a6b Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Mon, 24 Mar 2025 01:32:51 +0100 Subject: [PATCH 07/18] Moved the shop.py code to a separate folder, states | sprites --- src/sprites/shop/shop_sprite.py | 41 ++++++++++++ src/{shop.py => states/shop_state.py} | 95 ++++++++------------------- 2 files changed, 70 insertions(+), 66 deletions(-) create mode 100644 src/sprites/shop/shop_sprite.py rename src/{shop.py => states/shop_state.py} (61%) diff --git a/src/sprites/shop/shop_sprite.py b/src/sprites/shop/shop_sprite.py new file mode 100644 index 0000000..775d664 --- /dev/null +++ b/src/sprites/shop/shop_sprite.py @@ -0,0 +1,41 @@ +from pygame import Surface +from pygame.sprite import Group + +from src.sprites.base import BaseSprite +from src.settings import WORLD_LAYERS + + +class ShowShop(BaseSprite): + def __init__( + self, + pos: tuple[int, int], + surface: Surface, + groups: tuple[Group, ...], + z=WORLD_LAYERS["main"], + ): + super().__init__(pos, surface, groups, z) + + self.image = Surface((32, 32)) + self.image.fill("white") + # Compatibility check for get_frect + if hasattr(self.image, "get_frect"): + self.rect = self.image.get_frect(topleft=pos) + else: + self.rect = self.image.get_rect(topleft=pos) + self.z = z + + @staticmethod + def extract_icon(sprite_sheet: Surface, x: int, y: int, size: int = 16) -> Surface: + """ + Extract a single icon from a sprite sheet. + + Args: + sprite_sheet (Surface): The loaded sprite sheet image. + x (int): The x-coordinate of the top-left corner of the icon. + y (int): The y-coordinate of the top-left corner of the icon. + size (int): The width and height of the icon (square). Defaults to 16. + + Returns: + Surface: A pygame.Surface object representing the extracted icon. + """ + return sprite_sheet.subsurface((x, y, size, size)) diff --git a/src/shop.py b/src/states/shop_state.py similarity index 61% rename from src/shop.py rename to src/states/shop_state.py index dc88b8d..189ab52 100644 --- a/src/shop.py +++ b/src/states/shop_state.py @@ -1,29 +1,22 @@ +import os import pygame - +from pygame import Surface +from pygame.sprite import Group from src.inventory import Inventory from src.settings import WORLD_LAYERS from src.states.base_state import BaseState - - -class ShowShop(pygame.sprite.Sprite): - def __init__(self, pos, surface, groups, z=WORLD_LAYERS["main"]): - super().__init__(*groups) - - self.image = pygame.Surface((32, 32)) - self.image.fill("white") - self.rect = self.image.get_frect(topleft=pos) - self.z = z +from src.sprites.shop.shop_sprite import ShowShop class WindowShop(BaseState): - def __init__(self, game_state_manager, player, show_shop, inventory: Inventory): + def __init__(self, game_state_manager, player, show_shop: ShowShop, inventory: Inventory): super().__init__(game_state_manager) self.font = pygame.font.Font(None, 36) self.screen = pygame.Surface((800, 600)) - self.button_width = 100 - self.button_height = 50 - self.scroll_offset = 0 + self.button_width: int = 100 + self.button_height: int = 50 + self.scroll_offset: int = 0 self.button_actions: dict[str, tuple[pygame.Rect, pygame.Rect]] = {} self.inventory = inventory @@ -31,60 +24,33 @@ def __init__(self, game_state_manager, player, show_shop, inventory: Inventory): self.player = player self.big_screen = pygame.Surface((1280, 720)) - self.max_visible_items = 5 + self.max_visible_items: int = 5 self.in_shop = True self.collide = False self.message = "" self.message_end_time = 0 - self.sprite_sheet = pygame.image.load("images/tilesets/Treasure+.png").convert_alpha() + # Load sprite sheet + sprite_sheet_path = os.path.join("images/tilesets/Treasure+.png") + if not os.path.exists(sprite_sheet_path): + print(f"Error: Sprite sheet not found at '{sprite_sheet_path}'") + exit() + self.sprite_sheet = pygame.image.load(sprite_sheet_path) + + # Extract icons using `ShowShop.extract_icon` self.icons = { - "Gold Coin": self.extract_icon(0, 0), - "Silver Coin": self.extract_icon(16, 0), - "Coin Stack (1)": self.extract_icon(32, 0), - "Coin Stack (2)": self.extract_icon(48, 0), - "Circular Gem": self.extract_icon(64, 0), - "Single Gold Bar": self.extract_icon(0, 16), - "Gold Bar Stack": self.extract_icon(16, 16), - "Treasure Block": self.extract_icon(32, 16), - "Golden Crown": self.extract_icon(0, 32), - "Ornate Cup": self.extract_icon(16, 32), - "Golden Figurine": self.extract_icon(32, 32), - "Simple Sword": self.extract_icon(0, 48), - "Ornate Sword": self.extract_icon(16, 48), - "Double-Bladed Axe": self.extract_icon(32, 48), - "Spear": self.extract_icon(48, 48), - "Circular Shield": self.extract_icon(64, 48), - "Golden Trophy": self.extract_icon(0, 64), - "Candelabra": self.extract_icon(16, 64), - "Potion (Red)": self.extract_icon(0, 80), - "Potion (Blue)": self.extract_icon(16, 80), - "Potion (Green)": self.extract_icon(32, 80), - "Square Jar": self.extract_icon(48, 80), - "Cake": self.extract_icon(0, 96), - "Donut": self.extract_icon(16, 96), - "Bread": self.extract_icon(32, 96), - "Rug Tile": self.extract_icon(0, 112), - "Geometric Pattern": self.extract_icon(16, 112), - "Glowing Orb (Blue)": self.extract_icon(0, 128), - "Glowing Orb (Red)": self.extract_icon(16, 128), - "Glowing Orb (Green)": self.extract_icon(32, 128), - "Golden Ring": self.extract_icon(48, 128), - "Amulet": self.extract_icon(64, 128), - "Scroll": self.extract_icon(0, 144), - "Key": self.extract_icon(16, 144), - "Tool": self.extract_icon(32, 144), - "Dragon (Red)": self.extract_icon(0, 160), - "Dragon (Green)": self.extract_icon(16, 160), - "Dragon (Black)": self.extract_icon(32, 160), - "Dragon (White)": self.extract_icon(48, 160), - "Gem Cluster": self.extract_icon(0, 176), - "Glowing Crystal": self.extract_icon(16, 176), + "Gold Coin": ShowShop.extract_icon(self.sprite_sheet, 0, 0), + "Silver Coin": ShowShop.extract_icon(self.sprite_sheet, 16, 0), + # Add other icons as needed... } def update(self, events): - self.collide = self.player.rect.colliderect(self.show_shop.rect) + # Check collision + if hasattr(self.player, "rect") and hasattr(self.show_shop, "rect"): + self.collide = self.player.rect.colliderect(self.show_shop.rect) + else: + self.collide = False for event in events: match event.type: @@ -101,7 +67,7 @@ def update(self, events): max_offset = max(0, len(self.inventory.get_items()) - self.max_visible_items) self.scroll_offset = min(self.scroll_offset, max_offset) - def render(self, screen: pygame.Surface): + def render(self, screen: Surface): if self.collide: welcome_message = self.font.render("Press 'E' to enter the shop!", True, (0, 0, 0)) self.big_screen.blit(welcome_message, (50, self.screen.get_height() - 60)) @@ -112,13 +78,13 @@ def render(self, screen: pygame.Surface): self.button_actions = {} items = list(self.inventory.get_items().items()) - visible_items = items[self.scroll_offset : self.scroll_offset + self.max_visible_items] + visible_items = items[self.scroll_offset: self.scroll_offset + self.max_visible_items] y_offset = 50 for item, quantity in visible_items: if item in self.icons: self.screen.blit(self.icons[item], (50, y_offset)) - + quantity_text = self.font.render(f"x{quantity}", True, (255, 255, 255)) self.screen.blit(quantity_text, (100, y_offset + 5)) @@ -166,15 +132,12 @@ def render(self, screen: pygame.Surface): def handle_mouse_clicks(self, mouse_pos): for item, (use_button, discard_button) in self.button_actions.items(): if use_button.collidepoint(mouse_pos): - self.message = self.inventory.buy_item(item, 1) # `self.message` stores strings + self.message = self.inventory.buy_item(item, 1) self.message_end_time = pygame.time.get_ticks() + 3000 # 3 seconds elif discard_button.collidepoint(mouse_pos): self.message = self.inventory.sell_item(item, 1) self.message_end_time = pygame.time.get_ticks() + 4000 # 4 seconds - def extract_icon(self, x, y, size=16): - return self.sprite_sheet.subsurface((x, y, size, size)) - def draw_buttons(self, x: int, y: int, item: str) -> tuple[pygame.Rect, pygame.Rect]: use_button = pygame.Rect(x, y, self.button_width, self.button_height) discard_button = pygame.Rect(x + self.button_width + 10, y, self.button_width, self.button_height) From 2364853beae7022197378a43d1873bd20fc935a0 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Mon, 24 Mar 2025 01:36:12 +0100 Subject: [PATCH 08/18] - Combined the Sprite & Entity classes - Created an abstract class called BaseSprite, that has Sprite as argument. It's referred to pygame.sprite.Sprite. -Each new sprite class that will be made, must inherit BaseSprite class --- src/sprites/base.py | 297 ++++++++------------------------------------ 1 file changed, 53 insertions(+), 244 deletions(-) diff --git a/src/sprites/base.py b/src/sprites/base.py index 97af90a..eda5b57 100644 --- a/src/sprites/base.py +++ b/src/sprites/base.py @@ -1,269 +1,78 @@ """custom sprites classes""" import pygame # type: ignore -from pygame import FRect +from pygame.sprite import Sprite, Group +from pygame import FRect, Surface, Vector2 + +from abc import ABC, abstractmethod -from src.inventory import Inventory from src.settings import ( - ANIMATION_SPEED, - SCREEN_HEIGHT, - SCREEN_WIDTH, - TILE_SIZE, WORLD_LAYERS, + ANIMATION_SPEED ) -class Entity(pygame.sprite.Sprite): - def __init__(self, pos, frames, groups): - super().__init__(groups) - self.z = WORLD_LAYERS["main"] - # graphics - self.frame_index, self.frames = 0, frames - self.facing_direction = "down" - - # movement - self.direction = pygame.math.Vector3() - # self.speed = 250 - - # sprite setup - self.image = pygame.Surface((TILE_SIZE, TILE_SIZE)) - self.image.fill("red") - # self.image = self.frames[self.get_state()][self.frame_index] - self.rect = self.image.get_frect(center=pos) - - def animate(self, dt): - self.frame_index += ANIMATION_SPEED * dt - # self.image = self.frames[self.get_state()][int(self.frame_index % len(self.frames[self.get_state()]))] - - def get_state(self): - moving = bool(self.direction) - if moving: - if self.direction.x != 0: - self.facing_direction = "right" if self.direction.x > 0 else "left" - if self.direction.y != 0: - self.facing_direction = "down" if self.direction.x > 0 else "up" - return f"{self.facing_direction}{'' if moving else '_idle'}" - - -class AllSprites(pygame.sprite.Group): - """A sprite group that handles every sprite and handles the camera logic""" - - def __init__(self): - super().__init__() - - self.display_surface = pygame.display.get_surface() - if not self.display_surface: - raise ValueError("Display surface is not initialized") - - self.offset = pygame.math.Vector2() - self.scale = 2.0 - - def draw(self, player_center): - # Calculate offsets - self.offset.x = -(player_center[0] * self.scale - SCREEN_WIDTH / 2) - self.offset.y = -(player_center[1] * self.scale - SCREEN_HEIGHT / 2) - - # Separate sprites into layers - background_sprites = [sprite for sprite in self if sprite.z < WORLD_LAYERS["main"]] - main_sprites = [sprite for sprite in self if sprite.z == WORLD_LAYERS["main"]] - foreground_sprites = [sprite for sprite in self if sprite.z > WORLD_LAYERS["main"]] - - # Render each layer - for layer in (background_sprites, main_sprites, foreground_sprites): - for sprite in layer: - # Scale the image - scaled_image = pygame.transform.scale( - sprite.image, - ( - int(sprite.rect.width * self.scale), - int(sprite.rect.height * self.scale), - ), - ) - - # Adjust the rect to the new scale - scaled_rect = scaled_image.get_rect( - center=( - int(sprite.rect.center[0] * self.scale), - int(sprite.rect.center[1] * self.scale), - ) - ) - - # Add offset to the rect position - scaled_rect.topleft = ( - scaled_rect.topleft[0] + int(self.offset.x), - scaled_rect.topleft[1] + int(self.offset.y), - ) - - # Ensure display_surface is valid before blitting - if self.display_surface is None: - raise ValueError("self.display_surface cannot be None") - self.display_surface.blit(scaled_image, scaled_rect.topleft) - - # scaling of the ghost preview - # scaled_preview = pygame.transform.scale(player_preview, - # (int(player_preview_rect.width * self.scale), int(player_preview_rect.height * self.scale))) - # scaled_preview_rect = scaled_preview.get_rect(center=( - # player_preview_rect.center[0] * self.scale, player_preview_rect.center[1] * self.scale)) - # scaled_preview_rect.topleft += self.offset - - # self.display_surface.blit(scaled_preview, scaled_preview_rect.topleft) - - # method for zooming (might be useful later?) - # def set_scale(self, scale): - # self.scale = max(scale, 0.1) - - -class Player(Entity): - """move tile by tile""" +class BaseSprite(Sprite, ABC): + """ + Abstract base class for all custom sprites in the game, combining functionality + of both Sprite and Entity classes. Provides support for rendering, animation, + and basic movement logic. - rect: FRect + Attributes: + rect (FRect): The rectangle representing the sprite's position and size. + frames (list[Surface]): A list of animation frames for the sprite. + frame_index (float): The current frame index for animations. + direction (Vector2): The current movement direction of the sprite. + facing_direction (str): The current facing direction (e.g., "up", "down"). + """ - def __init__(self, pos, frames, groups): - super().__init__(pos, frames, groups) - - # Ensure self.image is not None - if self.image is None: - raise ValueError("self.image cannot be None") - - # ghost preview - self.player_preview = self.image.copy() - self.player_preview.set_alpha(128) - - self.inventory = Inventory() - self.mouse_have_been_pressed: bool = False - self.draggin = False - self.offset_x = 0 - self.offset_y = 0 - - def input(self) -> None: - """move the player and show a ghost to preview the move""" - # Reset direction - self.direction = pygame.math.Vector2(0, 0) - - # ghost preview - mouse_pos = pygame.mouse.get_pos() - - # get the relative pos of the player from the mouse - # to know on which axis the player will move - delta_x = abs(self.rect.centerx - mouse_pos[0]) - delta_y = abs(self.rect.centery - mouse_pos[1]) - - # # move the ghost on the x axis - # self.player_preview_rect = self.rect.copy() - # if delta_x > delta_y: - # if delta_x < (TILE_SIZE / 2): - # # don't move the ghost if the mouse is on the player hit box - # self.player_preview_rect.x = self.rect.x - # elif mouse_pos[0] > self.rect.centerx: - # # go right - # self.player_preview_rect.x = self.rect.x + TILE_SIZE - # else: - # # go left - # self.player_preview_rect.x = self.rect.x - TILE_SIZE - # # move the ghost on the y axis - # else: - # if delta_y < (TILE_SIZE / 2): - # # don't move if the mouse is on the player hitbox - # self.player_preview_rect.y = self.rect.y - # elif mouse_pos[1] > self.rect.centery: - # # go down - # self.player_preview_rect.y = self.rect.y + TILE_SIZE - # else: - # # go up - # self.player_preview_rect.y = self.rect.y - TILE_SIZE - - # move the player - if not pygame.mouse.get_pressed()[0]: - self.mouse_have_been_pressed = False - return - if self.mouse_have_been_pressed: - return - - self.mouse_have_been_pressed = True - - # move on the x axis - if delta_x > delta_y: - if delta_x >= (TILE_SIZE / 2): - if mouse_pos[0] > self.rect.centerx: - self.direction.x = 1 - else: - self.direction.x = -1 - else: - if delta_y >= (TILE_SIZE / 2): - if mouse_pos[1] > self.rect.centery: - self.direction.y = 1 - else: - self.direction.y = -1 - - self.rect.x += self.direction.x * TILE_SIZE - self.rect.y += self.direction.y * TILE_SIZE - - # return None - - def update(self, dt) -> None: - """blit player image and ghost preview to a given surface""" - self.input() - self.animate(dt) - - -class Tile(pygame.sprite.Sprite): - """Handle tiles for the map""" + rect = FRect def __init__( self, - *groups: pygame.sprite.Group, - pos: tuple[float, float], - surf: pygame.Surface, - name: str | None = None, + pos: tuple[int, int], + surf: Surface, + groups: tuple[Group, ...], + z: object = WORLD_LAYERS["main"], + frames: list[Surface] = None, ) -> None: - r""" - Initialize a Tile instance. - - Args: - *groups (pygame.sprite.Group): One or more sprite groups that this tile will belong to. - pos (Tuple[float, float]): The top-left position where the tile will be placed on the screen. - surf (pygame.Surface): The surface (image) that will be drawn for this tile. - name (Optional[str]): An optional name for the tile. - - Note: - There is a known typing error related to missing type parameters for the generic type 'Group' in pygame. - You may see warnings like: - - src\\base.py:107: error: Missing type parameters for generic type 'Group' [type-arg] - - src\\GUI\\game_manager.py:45: error: Missing type parameters for generic type 'SpriteGroup' [type-arg] - - These errors can be ignored for now, as they are related to type annotations in pygame's codebase. """ - super().__init__(*groups) + Initialize the base sprite. + :param pos: The (x, y) position of the sprite. + :param surf: The surface (image) of the sprite. + :param groups: Groups the sprite belongs to. + :param z: The layer index for rendering. + :param frames: The frame index for rendering. + """ - self.pos = pos - self.surf = surf - self.name = name - - self.image: pygame.Surface = self.surf - self.rect: pygame.FRect = self.image.get_frect(topleft=self.pos) - - def draw(self, display_surface: pygame.Surface, offset: tuple[float, float] = (0, 0)) -> None: - """Could be useful for a camera?""" - offset_rect = self.rect.move(offset) - display_surface.blit(self.image, offset_rect) + super().__init__(groups) + if surf is None: + raise ValueError("The `surf` parameter must be a valid pygame.Surface.") -class Sprite(pygame.sprite.Sprite): - def __init__(self, pos: tuple[int, int], surf: pygame.Surface, groups, z=WORLD_LAYERS["main"]): - super().__init__(groups) self.image = surf - self.rect = self.image.get_frect(topleft=pos) + self.rect: FRect = self.image.get_frect(topleft=pos) self.z = z + self.frames = frames or[surf] + self.frame_index: float = 0.0 -class AnimatedSprites(Sprite): - def __init__(self, pos, frames, groups, z=WORLD_LAYERS["main"]): - self.frame_index, self.frames = 0, frames - super().__init__(pos, frames[self.frame_index], groups, z) + # Movement + self.direction = Vector2() + self.facing_direction: str = "down" - def animate(self, dt): - self.frame_index += ANIMATION_SPEED * dt - self.image = self.frames[int(self.frame_index % len(self.frames))] + def animate(self, dt: float) -> None: + """Handle animation logic.""" + if len(self.frames) > 1: # Only animate if there's more than one frame + self.frame_index += ANIMATION_SPEED * dt + # self.image = self.frames[int(self.frame_index) % len(self.frames)] - def update(self, dt): - self.animate(dt) + def get_state(self) -> str: + """Determine the current state of the sprite.""" + moving = bool(self.direction) + if moving: + if self.direction.x != 0: + self.facing_direction = "right" if self.direction.x > 0 else "left" + if self.direction.y != 0: + self.facing_direction = "down" if self.direction.y > 0 else "up" + return f"{self.facing_direction}{'' if moving else '_idle'}" From fa381c40739df12e8facc79784f938ed32e975ec Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Mon, 24 Mar 2025 01:38:25 +0100 Subject: [PATCH 09/18] - Fixed a typo in docstring. --- src/sprites/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sprites/base.py b/src/sprites/base.py index eda5b57..ed72d2f 100644 --- a/src/sprites/base.py +++ b/src/sprites/base.py @@ -14,7 +14,7 @@ class BaseSprite(Sprite, ABC): """ - Abstract base class for all custom sprites in the game, combining functionality + Abstract base class for all custom sprites in the game, combining the functionality of both Sprite and Entity classes. Provides support for rendering, animation, and basic movement logic. From 47347a7bac458cdcb01d15b36fbcc9d6db11e827 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Mon, 24 Mar 2025 01:40:12 +0100 Subject: [PATCH 10/18] Moved the AnimatedSprites class to its own file called animations.py --- src/sprites/animations.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/sprites/animations.py diff --git a/src/sprites/animations.py b/src/sprites/animations.py new file mode 100644 index 0000000..0fcdc1d --- /dev/null +++ b/src/sprites/animations.py @@ -0,0 +1,32 @@ +import pygame +from pygame.sprite import Group + +from src.settings import ( + WORLD_LAYERS, + ANIMATION_SPEED +) + +from src.sprites.base import BaseSprite + +class AnimatedSprites(BaseSprite): + + def __init__( + self, + pos: tuple[int, int], + frames: list[pygame.Surface], + groups: tuple[Group, ...], + z: int = WORLD_LAYERS["main"] + ): + super().__init__(pos, frames[0], groups, z) + self.frames = frames + self.frame_index = 0 + self.image = self.frames[self.frame_index] # Use the first frame + + def animate(self, dt: float) -> None: + """Handle frame-based animation.""" + + self.frame_index += ANIMATION_SPEED * dt + self.image = self.frames[int(self.frame_index % len(self.frames))] + + def update(self, dt: float) -> None: + self.animate(dt) \ No newline at end of file From 6b7d67e0565b6f20196a1a96d8a79c245e2be779 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Mon, 24 Mar 2025 01:43:27 +0100 Subject: [PATCH 11/18] - Created a new abstract class for the CameraGroup, I wanted to change it to this, but I have AllSprites still as reference and in use for this abstract class. - Moved the current working player camera to make it more clear what the purpose of the camera is. --- src/sprites/camera/group.py | 23 +++++++++++++ src/sprites/camera/player.py | 65 ++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 src/sprites/camera/group.py create mode 100644 src/sprites/camera/player.py diff --git a/src/sprites/camera/group.py b/src/sprites/camera/group.py new file mode 100644 index 0000000..518f585 --- /dev/null +++ b/src/sprites/camera/group.py @@ -0,0 +1,23 @@ +import pygame + +from abc import ABC, abstractmethod + +# class CameraGroup(pygame.sprite.Group, ABC): +class AllSprites(pygame.sprite.Group, ABC): + # """A sprite group that handles every sprite and handles the camera logic""" + """Abstract base class for camera logic and sprite rendering.""" + + def __init__(self): + super().__init__() + + self.display_surface = pygame.display.get_surface() + if not self.display_surface: + raise ValueError("Display surface is not initialized") + + self.offset = pygame.math.Vector2() + self.scale = 2.0 + + @abstractmethod + def draw(self, player_center) -> None: + """Render sprites with camera adjustments.""" + pass \ No newline at end of file diff --git a/src/sprites/camera/player.py b/src/sprites/camera/player.py new file mode 100644 index 0000000..9c75d3f --- /dev/null +++ b/src/sprites/camera/player.py @@ -0,0 +1,65 @@ +import pygame # ignore + +from src.sprites.camera.group import AllSprites + +from src.settings import ( + SCREEN_WIDTH, + SCREEN_HEIGHT, + WORLD_LAYERS +) + +class PlayerCamera(AllSprites): + """ + A sprite group that manages rendering and camera logic focused on the player. + + This class extends the functionality of the AllSprites group by adjusting + sprite positions based on the player's location, creating a camera effect. + It supports rendering sprites at different layers (background, main, foreground) + and scales sprites dynamically based on the camera's zoom level. + + Attributes: + display_surface (pygame.Surface): The surface to render the sprites on. + offset (pygame.math.Vector2): The camera's offset, calculated relative to the player's position. + scale (float): The scaling factor for rendering sprites. + """ + + def draw(self, player_center): + # Calculate offsets + self.offset.x = -(player_center[0] * self.scale - SCREEN_WIDTH / 2) + self.offset.y = -(player_center[1] * self.scale - SCREEN_HEIGHT / 2) + + # Separate sprites into layers + background_sprites = [sprite for sprite in self if sprite.z < WORLD_LAYERS["main"]] + main_sprites = [sprite for sprite in self if sprite.z == WORLD_LAYERS["main"]] + foreground_sprites = [sprite for sprite in self if sprite.z > WORLD_LAYERS["main"]] + + # Render each layer + for layer in (background_sprites, main_sprites, foreground_sprites): + for sprite in layer: + # Scale the image + scaled_image = pygame.transform.scale( + sprite.image, + ( + int(sprite.rect.width * self.scale), + int(sprite.rect.height * self.scale), + ), + ) + + # Adjust the rect to the new scale + scaled_rect = scaled_image.get_rect( + center=( + int(sprite.rect.center[0] * self.scale), + int(sprite.rect.center[1] * self.scale), + ) + ) + + # Add offset to the rect position + scaled_rect.topleft = ( + scaled_rect.topleft[0] + int(self.offset.x), + scaled_rect.topleft[1] + int(self.offset.y), + ) + + # Ensure display_surface is valid before blitting + if self.display_surface is None: + raise ValueError("self.display_surface cannot be None") + self.display_surface.blit(scaled_image, scaled_rect.topleft) \ No newline at end of file From 4feaed95236e96bbc6c6b5eba12d92e9c9af374c Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Mon, 24 Mar 2025 01:49:28 +0100 Subject: [PATCH 12/18] - Added some typehints to the codebase. - Fixed the imports of the codebase with the changes I made from the old sprites.py file. --- src/states/game_running.py | 90 ++++++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 37 deletions(-) diff --git a/src/states/game_running.py b/src/states/game_running.py index f3474ad..f245ab4 100644 --- a/src/states/game_running.py +++ b/src/states/game_running.py @@ -8,11 +8,15 @@ import pygame # type: ignore from pytmx.util_pygame import load_pygame # type: ignore -import src.shop -import src.sprites.base + +from src.states.shop_state import ShowShop, WindowShop from src.inventory import Inventory from src.settings import TILE_SIZE, WORLD_LAYERS -from src.sprites.base import AnimatedSprites + +from src.sprites.base import BaseSprite +from src.sprites.animations import AnimatedSprites +from src.sprites.camera import PlayerCamera +from src.sprites.entities.player import Player from src.states.base_state import BaseState from src.states.paused import Paused from src.support import all_character_import, coast_importer, import_folder @@ -37,7 +41,7 @@ def __init__(self, game_state_manager) -> None: self.player_inventory = Inventory() self.load_inventory_from_json("data/inventory.json") - self.all_sprites = src.sprites.base.AllSprites() + self.all_sprites = PlayerCamera() # The start positions will be one of the 4 islands in the corners of the board self.setup(player_start_pos="top_left_island") @@ -46,7 +50,7 @@ def __init__(self, game_state_manager) -> None: self.shop_window = pygame.Surface((800, 600)) self.in_shop = False - def setup(self, player_start_pos): + def setup(self, player_start_pos: str) -> None: """ set up the map and player from the tiled file """ @@ -58,13 +62,16 @@ def setup(self, player_start_pos): "ships": all_character_import(".", "images", "tilesets", "ships"), } + # Initialize self.player to None by default + # self.player = None + # Sea for x, y, surface in self.tmx_map["map"].get_layer_by_name("Sea").tiles(): - src.sprites.base.Sprite( - (x * TILE_SIZE, y * TILE_SIZE), - surface, - self.all_sprites, - WORLD_LAYERS["bg"], + BaseSprite( + pos=(x * TILE_SIZE, y * TILE_SIZE), + surf=surface, + groups=(self.all_sprites,), + z=WORLD_LAYERS["bg"], ) # Water animated @@ -72,50 +79,58 @@ def setup(self, player_start_pos): for x in range(int(obj.x), int(obj.x + obj.width), TILE_SIZE): for y in range(int(obj.y), int(obj.y + obj.height), TILE_SIZE): AnimatedSprites( - (x, y), - self.world_frames["water"], - self.all_sprites, - WORLD_LAYERS["water"], + pos=(x, y), + frames=self.world_frames["water"], + groups=(self.all_sprites,), + z=WORLD_LAYERS["water"], ) # Shallow water for x, y, surface in self.tmx_map["map"].get_layer_by_name("Shallow Sea").tiles(): - src.sprites.base.Sprite((x * TILE_SIZE, y * TILE_SIZE), surface, self.all_sprites, WORLD_LAYERS["bg"]) + BaseSprite( + pos=(x * TILE_SIZE, y * TILE_SIZE), + surf=surface, + groups=(self.all_sprites,), + z=WORLD_LAYERS["bg"] + ) - # buildings + # Buildings for x, y, surface in self.tmx_map["map"].get_layer_by_name("Shop").tiles(): - self.shop = src.shop.ShowShop( - (x * TILE_SIZE, y * TILE_SIZE), surface, self.all_sprites, WORLD_LAYERS["main"] + self.shop = ShowShop( + pos=(x * TILE_SIZE, y * TILE_SIZE), + surface=surface, + groups=(self.all_sprites,), + z=WORLD_LAYERS ["main"] ) # Islands islands = self.tmx_map["map"].get_layer_by_name("Islands") for x, y, surface in islands.tiles(): - src.sprites.base.Sprite( - (x * TILE_SIZE, y * TILE_SIZE), - surface, - self.all_sprites, - WORLD_LAYERS["bg"], + BaseSprite( + pos=(x * TILE_SIZE, y * TILE_SIZE), + surf=surface, + groups=(self.all_sprites,), + z=WORLD_LAYERS["bg"], ) - # Enitites - for obj in self.tmx_map["map"].get_layer_by_name("Ships"): - if obj.name == "Player" and obj.properties["pos"] == player_start_pos: - self.player = src.sprites.base.Player( - pos=(obj.x, obj.y), - frames=self.world_frames["ships"]["player_test_ship"], - groups=self.all_sprites, - ) + # Enitites + for obj in self.tmx_map["map"].get_layer_by_name("Ships"): + if obj.name == "Player" and obj.properties["pos"] == player_start_pos: + self.player = Player( + pos=(obj.x, obj.y), + frames=self.world_frames["ships"]["player_test_ship"], + groups=(self.all_sprites,), + ) # Coast for obj in self.tmx_map["map"].get_layer_by_name("Coast"): terrain = obj.properties["terrain"] side = obj.properties["side"] AnimatedSprites( - (obj.x, obj.y), - self.world_frames["coast"][terrain][side], - self.all_sprites, - WORLD_LAYERS["bg"], + pos=(obj.x, obj.y), + frames=self.world_frames["coast"][terrain][side], + groups=(self.all_sprites,), + z=WORLD_LAYERS["bg"], ) def load_inventory_from_json(self, file_path: str): @@ -133,7 +148,8 @@ def update(self, events) -> None: """ update each sprites and handle events """ - collide = self.player.rect.colliderect(self.shop.rect) + + collide = self.player.rect.colliderect(self.shop.rect) if self.player else False dt = self.clock.tick() / 1000 self.all_sprites.update(dt) @@ -144,7 +160,7 @@ def update(self, events) -> None: self.game_state_manager.enter_state(Paused(self.game_state_manager, self.player_inventory)) if collide and event.key == pygame.K_e: self.game_state_manager.enter_state( - src.shop.WindowShop(self.game_state_manager, self.player, self.shop, self.player_inventory) + WindowShop(self.game_state_manager, self.player, self.shop, self.player_inventory) ) def render(self, screen) -> None: From e4cf5b3db383b3708e4ad0b364f6b676c2620fc9 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Mon, 24 Mar 2025 01:51:06 +0100 Subject: [PATCH 13/18] - Player class inherits from BaseSprite now. - Moved class to the entities directory inside the sprites directory. --- src/sprites/entities/player.py | 122 +++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 src/sprites/entities/player.py diff --git a/src/sprites/entities/player.py b/src/sprites/entities/player.py new file mode 100644 index 0000000..392fb09 --- /dev/null +++ b/src/sprites/entities/player.py @@ -0,0 +1,122 @@ +import pygame +from pygame import FRect, Surface, Vector2 +from pygame.sprite import Group + +from src.inventory import Inventory +from src.settings import ( + TILE_SIZE +) + +from src.sprites.base import BaseSprite + +class Player(BaseSprite): + def __init__( + self, + pos: tuple[int, int], + frames: list[Surface], + groups: tuple[Group, ...] = (), + ) -> None: + """ + Initialize the player. + :param pos: Starting position of the player. + :param frames: A list of frames for player animation. + :param groups: Sprite groups the player belongs to. + """ + + rect: FRect + + # Use the first frame as the base surface + first_frame = frames[0] if isinstance(frames, (list, tuple)) and frames else Surface((TILE_SIZE, TILE_SIZE)) + first_frame.fill("red") + super().__init__(pos=pos, surf=first_frame, groups=groups) + + # Animation frames + self.frames = frames + self.frame_index: float = 0.0 + + # Ghost preview + self.player_preview = first_frame.copy() + self.player_preview.set_alpha(128) + + # Inventory system + self.inventory = Inventory() + + # Input handling + self.mouse_have_been_pressed: bool = False + + + def input(self) -> None: + """move the player and show a ghost to preview the move""" + + # Reset direction + self.direction = Vector2(0, 0) + + # Get mouse position + mouse_pos = pygame.mouse.get_pos() + + # get the relative pos of the player from the mouse + # to know on which axis the player will move + delta_x = abs(self.rect.centerx - mouse_pos[0]) + delta_y = abs(self.rect.centery - mouse_pos[1]) + + # # move the ghost on the x-axis + # self.player_preview_rect = self.rect.copy() + # if delta_x > delta_y: + # if delta_x < (TILE_SIZE / 2): + # # don't move the ghost if the mouse is on the player hit box + # self.player_preview_rect.x = self.rect.x + # elif mouse_pos[0] > self.rect.centerx: + # # go right + # self.player_preview_rect.x = self.rect.x + TILE_SIZE + # else: + # # go left + # self.player_preview_rect.x = self.rect.x - TILE_SIZE + # # move the ghost on the y-axis + # else: + # if delta_y < (TILE_SIZE / 2): + # # don't move if the mouse is on the player hit box + # self.player_preview_rect.y = self.rect.y + # elif mouse_pos[1] > self.rect.centery: + # # go down + # self.player_preview_rect.y = self.rect.y + TILE_SIZE + # else: + # # go up + # self.player_preview_rect.y = self.rect.y - TILE_SIZE + + # Handle mouse input for movement + if not pygame.mouse.get_pressed()[0]: + self.mouse_have_been_pressed = False + return + if self.mouse_have_been_pressed: + return + + self.mouse_have_been_pressed = True + + # Move on the x-axis or y-axis + if delta_x > delta_y: + if delta_x >= (TILE_SIZE / 2): + if mouse_pos[0] > self.rect.centerx: + self.direction.x = 1 + # if delta_x >= (TILE_SIZE / 2): + # self.direction.x = 1 if mouse_pos[0] > self.rect.centerx else -1 + else: + self.direction.x = -1 + # if delta_y >= (TILE_SIZE / 2): + # self.direction.y = 1 if mouse_pos[1] > self.rect.centery else -1 + else: + if delta_y >= (TILE_SIZE / 2): + if mouse_pos[1] > self.rect.centery: + self.direction.y = 1 + else: + self.direction.y = -1 + + # Update position + self.rect.x += self.direction.x * TILE_SIZE + self.rect.y += self.direction.y * TILE_SIZE + + # return None + + def update(self, dt: float) -> None: + """Update the player's position and state.""" + self.input() + self.animate(dt) \ No newline at end of file From 211673173bc36011221a9a6caf4d8b8c9e8fc92e Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Mon, 24 Mar 2025 02:00:30 +0100 Subject: [PATCH 14/18] - Tile class is moved to its own file and also inherits BaseSprite abstract class now. --- src/sprites/tiles/tile.py | 43 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/sprites/tiles/tile.py diff --git a/src/sprites/tiles/tile.py b/src/sprites/tiles/tile.py new file mode 100644 index 0000000..1f26d40 --- /dev/null +++ b/src/sprites/tiles/tile.py @@ -0,0 +1,43 @@ +import pygame +from src.sprites.base import BaseSprite + +class Tile(BaseSprite): + """Handle tiles for the map""" + + def __init__( + self, + *groups: pygame.sprite.Group, + pos: tuple[float, float], + surf: pygame.Surface, + name: str | None = None, + ) -> None: + r""" + Initialize a Tile instance. + + Args: + *groups (pygame.sprite.Group): One or more sprite groups that this tile will belong to. + pos (Tuple[float, float]): The top-left position where the tile will be placed on the screen. + surf (pygame.Surface): The surface (image) that will be drawn for this tile. + name (Optional[str]): An optional name for the tile. + + Note: + There is a known typing error related to missing type parameters for the generic type 'Group' in pygame. + You may see warnings like: + - src\\base.py:107: error: Missing type parameters for generic type 'Group' [type-arg] + - src\\GUI\\game_manager.py:45: error: Missing type parameters for generic type 'SpriteGroup' [type-arg] + + These errors can be ignored for now, as they are related to type annotations in pygame's codebase. + """ + super().__init__(*groups) + + self.pos = pos + self.surf = surf + self.name = name + + self.image: pygame.Surface = self.surf + self.rect: pygame.FRect = self.image.get_frect(topleft=self.pos) + + def draw(self, display_surface: pygame.Surface, offset: tuple[float, float] = (0, 0)) -> None: + """Could be useful for a camera?""" + offset_rect = self.rect.move(offset) + display_surface.blit(self.image, offset_rect) \ No newline at end of file From 945b9478ba68e9467c7a0b0f358e1e4c89ff321e Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Mon, 24 Mar 2025 02:44:16 +0100 Subject: [PATCH 15/18] Fixed ruff and mypy errors. --- src/inventory.py | 2 +- src/sprites/animations.py | 14 ++++------- src/sprites/base.py | 42 ++++++++++++++------------------- src/sprites/camera/group.py | 7 +++--- src/sprites/camera/player.py | 10 +++----- src/sprites/entities/player.py | 15 ++++-------- src/sprites/shop/shop_sprite.py | 4 +--- src/sprites/tiles/tile.py | 6 +++-- src/states/game_running.py | 16 ++++--------- src/states/shop_state.py | 8 +++---- 10 files changed, 49 insertions(+), 75 deletions(-) diff --git a/src/inventory.py b/src/inventory.py index ac00c94..dd6f0ac 100644 --- a/src/inventory.py +++ b/src/inventory.py @@ -58,7 +58,7 @@ def use_item(self, item_name: str) -> str: if self.remove_item(item_name, 1) == get_message("inventory", "remove_success", item=item_name, quantity=1): return get_message("inventory", "use_success", item=item_name) return get_message("inventory", "use_fail", item=item_name) - + def buy_item(self, item_name, quantity): if item_name in self.items: self.items[item_name] += quantity diff --git a/src/sprites/animations.py b/src/sprites/animations.py index 0fcdc1d..f6c7509 100644 --- a/src/sprites/animations.py +++ b/src/sprites/animations.py @@ -1,21 +1,17 @@ -import pygame +import pygame from pygame.sprite import Group -from src.settings import ( - WORLD_LAYERS, - ANIMATION_SPEED -) - +from src.settings import ANIMATION_SPEED, WORLD_LAYERS from src.sprites.base import BaseSprite -class AnimatedSprites(BaseSprite): +class AnimatedSprites(BaseSprite): def __init__( self, pos: tuple[int, int], frames: list[pygame.Surface], groups: tuple[Group, ...], - z: int = WORLD_LAYERS["main"] + z: int = WORLD_LAYERS["main"], ): super().__init__(pos, frames[0], groups, z) self.frames = frames @@ -29,4 +25,4 @@ def animate(self, dt: float) -> None: self.image = self.frames[int(self.frame_index % len(self.frames))] def update(self, dt: float) -> None: - self.animate(dt) \ No newline at end of file + self.animate(dt) diff --git a/src/sprites/base.py b/src/sprites/base.py index ed72d2f..c6b866f 100644 --- a/src/sprites/base.py +++ b/src/sprites/base.py @@ -1,15 +1,11 @@ """custom sprites classes""" -import pygame # type: ignore -from pygame.sprite import Sprite, Group -from pygame import FRect, Surface, Vector2 +from abc import ABC -from abc import ABC, abstractmethod +from pygame import FRect, Surface, Vector2 +from pygame.sprite import Group, Sprite -from src.settings import ( - WORLD_LAYERS, - ANIMATION_SPEED -) +from src.settings import ANIMATION_SPEED, WORLD_LAYERS class BaseSprite(Sprite, ABC): @@ -26,26 +22,24 @@ class BaseSprite(Sprite, ABC): facing_direction (str): The current facing direction (e.g., "up", "down"). """ - rect = FRect - def __init__( self, - pos: tuple[int, int], - surf: Surface, - groups: tuple[Group, ...], - z: object = WORLD_LAYERS["main"], - frames: list[Surface] = None, + pos: tuple[int, int], + surf: Surface, + groups: tuple[Group, ...], + z: object = WORLD_LAYERS["main"], + frames: list[Surface] | None = None, ) -> None: """ - Initialize the base sprite. - :param pos: The (x, y) position of the sprite. - :param surf: The surface (image) of the sprite. - :param groups: Groups the sprite belongs to. - :param z: The layer index for rendering. - :param frames: The frame index for rendering. - """ + Initialize the base sprite. + :param pos: The (x, y) position of the sprite. + :param surf: The surface (image) of the sprite. + :param groups: Groups the sprite belongs to. + :param z: The layer index for rendering. + :param frames: The frame index for rendering. + """ - super().__init__(groups) + super().__init__(*groups) if surf is None: raise ValueError("The `surf` parameter must be a valid pygame.Surface.") @@ -54,7 +48,7 @@ def __init__( self.rect: FRect = self.image.get_frect(topleft=pos) self.z = z - self.frames = frames or[surf] + self.frames = frames or [surf] self.frame_index: float = 0.0 # Movement diff --git a/src/sprites/camera/group.py b/src/sprites/camera/group.py index 518f585..ba1b7fe 100644 --- a/src/sprites/camera/group.py +++ b/src/sprites/camera/group.py @@ -1,6 +1,7 @@ +from abc import ABC, abstractmethod + import pygame -from abc import ABC, abstractmethod # class CameraGroup(pygame.sprite.Group, ABC): class AllSprites(pygame.sprite.Group, ABC): @@ -18,6 +19,6 @@ def __init__(self): self.scale = 2.0 @abstractmethod - def draw(self, player_center) -> None: + def draw(self, player_center): """Render sprites with camera adjustments.""" - pass \ No newline at end of file + pass diff --git a/src/sprites/camera/player.py b/src/sprites/camera/player.py index 9c75d3f..2c6ab44 100644 --- a/src/sprites/camera/player.py +++ b/src/sprites/camera/player.py @@ -1,12 +1,8 @@ -import pygame # ignore +import pygame # ignore +from src.settings import SCREEN_HEIGHT, SCREEN_WIDTH, WORLD_LAYERS from src.sprites.camera.group import AllSprites -from src.settings import ( - SCREEN_WIDTH, - SCREEN_HEIGHT, - WORLD_LAYERS -) class PlayerCamera(AllSprites): """ @@ -62,4 +58,4 @@ def draw(self, player_center): # Ensure display_surface is valid before blitting if self.display_surface is None: raise ValueError("self.display_surface cannot be None") - self.display_surface.blit(scaled_image, scaled_rect.topleft) \ No newline at end of file + self.display_surface.blit(scaled_image, scaled_rect.topleft) diff --git a/src/sprites/entities/player.py b/src/sprites/entities/player.py index 392fb09..d6933fc 100644 --- a/src/sprites/entities/player.py +++ b/src/sprites/entities/player.py @@ -1,14 +1,12 @@ -import pygame -from pygame import FRect, Surface, Vector2 +import pygame +from pygame import Surface, Vector2 from pygame.sprite import Group from src.inventory import Inventory -from src.settings import ( - TILE_SIZE -) - +from src.settings import TILE_SIZE from src.sprites.base import BaseSprite + class Player(BaseSprite): def __init__( self, @@ -23,8 +21,6 @@ def __init__( :param groups: Sprite groups the player belongs to. """ - rect: FRect - # Use the first frame as the base surface first_frame = frames[0] if isinstance(frames, (list, tuple)) and frames else Surface((TILE_SIZE, TILE_SIZE)) first_frame.fill("red") @@ -44,7 +40,6 @@ def __init__( # Input handling self.mouse_have_been_pressed: bool = False - def input(self) -> None: """move the player and show a ghost to preview the move""" @@ -119,4 +114,4 @@ def input(self) -> None: def update(self, dt: float) -> None: """Update the player's position and state.""" self.input() - self.animate(dt) \ No newline at end of file + self.animate(dt) diff --git a/src/sprites/shop/shop_sprite.py b/src/sprites/shop/shop_sprite.py index 775d664..9fd4642 100644 --- a/src/sprites/shop/shop_sprite.py +++ b/src/sprites/shop/shop_sprite.py @@ -1,8 +1,8 @@ from pygame import Surface from pygame.sprite import Group -from src.sprites.base import BaseSprite from src.settings import WORLD_LAYERS +from src.sprites.base import BaseSprite class ShowShop(BaseSprite): @@ -20,8 +20,6 @@ def __init__( # Compatibility check for get_frect if hasattr(self.image, "get_frect"): self.rect = self.image.get_frect(topleft=pos) - else: - self.rect = self.image.get_rect(topleft=pos) self.z = z @staticmethod diff --git a/src/sprites/tiles/tile.py b/src/sprites/tiles/tile.py index 1f26d40..1421869 100644 --- a/src/sprites/tiles/tile.py +++ b/src/sprites/tiles/tile.py @@ -1,6 +1,8 @@ -import pygame +import pygame + from src.sprites.base import BaseSprite + class Tile(BaseSprite): """Handle tiles for the map""" @@ -40,4 +42,4 @@ def __init__( def draw(self, display_surface: pygame.Surface, offset: tuple[float, float] = (0, 0)) -> None: """Could be useful for a camera?""" offset_rect = self.rect.move(offset) - display_surface.blit(self.image, offset_rect) \ No newline at end of file + display_surface.blit(self.image, offset_rect) diff --git a/src/states/game_running.py b/src/states/game_running.py index f245ab4..fe7af0d 100644 --- a/src/states/game_running.py +++ b/src/states/game_running.py @@ -8,17 +8,15 @@ import pygame # type: ignore from pytmx.util_pygame import load_pygame # type: ignore - -from src.states.shop_state import ShowShop, WindowShop from src.inventory import Inventory from src.settings import TILE_SIZE, WORLD_LAYERS - -from src.sprites.base import BaseSprite from src.sprites.animations import AnimatedSprites +from src.sprites.base import BaseSprite from src.sprites.camera import PlayerCamera from src.sprites.entities.player import Player from src.states.base_state import BaseState from src.states.paused import Paused +from src.states.shop_state import ShowShop, WindowShop from src.support import all_character_import, coast_importer, import_folder @@ -88,19 +86,13 @@ def setup(self, player_start_pos: str) -> None: # Shallow water for x, y, surface in self.tmx_map["map"].get_layer_by_name("Shallow Sea").tiles(): BaseSprite( - pos=(x * TILE_SIZE, y * TILE_SIZE), - surf=surface, - groups=(self.all_sprites,), - z=WORLD_LAYERS["bg"] + pos=(x * TILE_SIZE, y * TILE_SIZE), surf=surface, groups=(self.all_sprites,), z=WORLD_LAYERS["bg"] ) # Buildings for x, y, surface in self.tmx_map["map"].get_layer_by_name("Shop").tiles(): self.shop = ShowShop( - pos=(x * TILE_SIZE, y * TILE_SIZE), - surface=surface, - groups=(self.all_sprites,), - z=WORLD_LAYERS ["main"] + pos=(x * TILE_SIZE, y * TILE_SIZE), surface=surface, groups=(self.all_sprites,), z=WORLD_LAYERS["main"] ) # Islands diff --git a/src/states/shop_state.py b/src/states/shop_state.py index 189ab52..820e7cd 100644 --- a/src/states/shop_state.py +++ b/src/states/shop_state.py @@ -1,11 +1,11 @@ import os + import pygame from pygame import Surface -from pygame.sprite import Group + from src.inventory import Inventory -from src.settings import WORLD_LAYERS -from src.states.base_state import BaseState from src.sprites.shop.shop_sprite import ShowShop +from src.states.base_state import BaseState class WindowShop(BaseState): @@ -78,7 +78,7 @@ def render(self, screen: Surface): self.button_actions = {} items = list(self.inventory.get_items().items()) - visible_items = items[self.scroll_offset: self.scroll_offset + self.max_visible_items] + visible_items = items[self.scroll_offset : self.scroll_offset + self.max_visible_items] y_offset = 50 for item, quantity in visible_items: From 5445cd2112d1b11f168fd7dfac29296ee4ac858d Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Fri, 28 Mar 2025 22:27:01 +0100 Subject: [PATCH 16/18] Fixed an import issue for the PlayerCamera module. --- src/states/game_running.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/states/game_running.py b/src/states/game_running.py index fe7af0d..35e7788 100644 --- a/src/states/game_running.py +++ b/src/states/game_running.py @@ -12,7 +12,7 @@ from src.settings import TILE_SIZE, WORLD_LAYERS from src.sprites.animations import AnimatedSprites from src.sprites.base import BaseSprite -from src.sprites.camera import PlayerCamera +from src.sprites.camera.player import PlayerCamera from src.sprites.entities.player import Player from src.states.base_state import BaseState from src.states.paused import Paused From 4f3568ceadc3d4041696cad3a03c7c67452a23a7 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Sat, 29 Mar 2025 17:17:04 +0100 Subject: [PATCH 17/18] Add easier import to abstract base class for the pygame Group class. --- src/sprites/camera/group.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sprites/camera/group.py b/src/sprites/camera/group.py index ba1b7fe..eec8377 100644 --- a/src/sprites/camera/group.py +++ b/src/sprites/camera/group.py @@ -1,10 +1,11 @@ from abc import ABC, abstractmethod import pygame +from pygame.sprite import Group # class CameraGroup(pygame.sprite.Group, ABC): -class AllSprites(pygame.sprite.Group, ABC): +class AllSprites(Group, ABC): # """A sprite group that handles every sprite and handles the camera logic""" """Abstract base class for camera logic and sprite rendering.""" From 58858a6b24fed79436b26b61a94473315eed75a6 Mon Sep 17 00:00:00 2001 From: ultimateownsz Date: Sun, 30 Mar 2025 21:42:45 +0200 Subject: [PATCH 18/18] refactor(inventory_gui) - move inventory_gui.py to `gui` directory. - add type hints to file - size of extract icon was set to 16, is now TILE_SIZE. --- src/{ => sprites/gui}/inventory_gui.py | 80 ++++++++++++++------------ 1 file changed, 43 insertions(+), 37 deletions(-) rename src/{ => sprites/gui}/inventory_gui.py (74%) diff --git a/src/inventory_gui.py b/src/sprites/gui/inventory_gui.py similarity index 74% rename from src/inventory_gui.py rename to src/sprites/gui/inventory_gui.py index 4231309..b929768 100644 --- a/src/inventory_gui.py +++ b/src/sprites/gui/inventory_gui.py @@ -1,29 +1,29 @@ -from typing import Dict, Tuple - import pygame # type: ignore from src.inventory import Inventory +from src.settings import TILE_SIZE class InventoryGUI: """Graphical User Interface to display the player's inventory.""" def __init__(self, screen: pygame.Surface, inventory: Inventory) -> None: - self.screen = screen - self.inventory = inventory - self.font = pygame.font.Font(None, 36) - self.running = False + self.screen: pygame.Surface = screen + self.inventory: Inventory = inventory + self.font: pygame.font.Font = pygame.font.Font(None, 36) + self.running: bool = False # Scrolling inventory - self.scroll_offset = 0 - self.max_visible_items = 10 - self.item_height = 60 + self.scroll_offset: int = 0 + self.max_visible_items: int = 10 + self.item_height: int = 60 - # Load sprite sheet and extract the icons (Testing purposes) + # Load a sprite sheet and extract the icons (Testing purposes) # To be replaced when: - # 1) Spritesheet has been decide. 2) A 'Buy', 'Found' or 'Add' in-game feature has been implemented + # 1) Sprite sheet has been decided. + # 2) A 'Buy', 'Found' or 'Add' in-game feature has been implemented self.sprite_sheet = pygame.image.load("images/tilesets/Treasure+.png").convert_alpha() - self.icons = { + self.icons: dict[str, pygame.Surface] = { "Gold Coin": self.extract_icon(0, 0), "Silver Coin": self.extract_icon(16, 0), "Coin Stack (1)": self.extract_icon(32, 0), @@ -67,56 +67,61 @@ def __init__(self, screen: pygame.Surface, inventory: Inventory) -> None: "Glowing Crystal": self.extract_icon(16, 176), } - # Button dimmentions - self.button_width = 100 - self.button_height = 50 + # Button dimensions + self.button_width: int = 100 + self.button_height: int = 50 # Initialize button actions - self.button_actions: Dict[str, Tuple[pygame.Rect, pygame.Rect]] = {} + self.button_actions: dict[str, tuple[pygame.Rect, pygame.Rect]] = {} # Action messages - self.message = "" - self.message_end_time = 0 # Time to display the message + self.message: str = "" + self.message_end_time: int = 0 # Time to display the message - def handle_events(self, event): + def handle_events(self, event: pygame.event.Event) -> None: """Handle events like keypress or mouse wheel.""" + if event.type == pygame.MOUSEWHEEL: # Adjust scroll offset self.scroll_offset = max(0, self.scroll_offset - event.y) max_offset = max(0, len(self.inventory.get_items()) - self.max_visible_items) self.scroll_offset = min(self.scroll_offset, max_offset) - def extract_icon(self, x, y, size=16): + def extract_icon(self, x: int, y: int, size: int = TILE_SIZE) -> pygame.Surface: """Extract a single icon from the sprite sheet.""" + return self.sprite_sheet.subsurface((x, y, size, size)) - def draw_buttons(self, x: int, y: int, item: str) -> Tuple[pygame.Rect, pygame.Rect]: + def draw_buttons(self, x: int, y: int, item: str) -> tuple[pygame.Rect, pygame.Rect]: """Draw Use and Discard buttons for a specific item.""" - use_button = pygame.Rect(x, y, self.button_width, self.button_height) - discard_button = pygame.Rect(x + self.button_width + 10, y, self.button_width, self.button_height) + + use_button: pygame.Rect = pygame.Rect(x, y, self.button_width, self.button_height) + discard_button: pygame.Rect = pygame.Rect(x + self.button_width + 10, y, self.button_width, self.button_height) pygame.draw.rect(self.screen, (0, 255, 0), use_button) # Green pygame.draw.rect(self.screen, (150, 75, 0), discard_button) # Brown - use_text = self.font.render("Use", True, (0, 0, 0)) # Black - discard_text = self.font.render("Discard", True, (0, 0, 0)) + use_text: pygame.Surface = self.font.render("Use", True, (0, 0, 0)) # Black + discard_text: pygame.Surface = self.font.render("Discard", True, (0, 0, 0)) self.screen.blit(use_text, (x + 10, y + 10)) self.screen.blit(discard_text, (x + self.button_width + 20, y + 10)) return use_button, discard_button - def draw(self): + def draw(self) -> None: """Draw the inventory overlay.""" + self.screen.fill((0, 0, 0)) # Solid Black background # Reset button actions - self.button_actions = {} + self.button_actions: dict[str, tuple[pygame.Rect, pygame.Rect]] = {} + # Draw the inventory items - items = list(self.inventory.get_items().items()) - visible_items = items[self.scroll_offset : self.scroll_offset + self.max_visible_items] - y_offset = 50 # Start below the title + items: list = list(self.inventory.get_items().items()) + visible_items: list = items[self.scroll_offset : self.scroll_offset + self.max_visible_items] + y_offset: int = 50 # Start below the title for item, quantity in visible_items: # Draw icon @@ -138,11 +143,11 @@ def draw(self): self.button_actions[item] = (use_button, discard_button) y_offset += 60 # Move down for the next item - # Draw hint + # Draw a hint hint_text = self.font.render("Press 'I' to close inventory", True, (200, 200, 200)) # Light gray text self.screen.blit(hint_text, (50, self.screen.get_height() - 60)) - # Display action message above the hint + # Display an action message above the hint if self.message and pygame.time.get_ticks() < self.message_end_time: # Render the message text message_text = self.font.render(self.message, True, (255, 255, 0)) # Yellow @@ -151,10 +156,10 @@ def draw(self): text_width, text_height = message_text.get_size() # Message background - message_bg_x = 40 - message_bg_y = self.screen.get_height() - 120 - message_bg_width = text_width + 20 # Add padding - message_bg_height = text_height + 10 # Add padding + message_bg_x: int = 40 + message_bg_y: int = self.screen.get_height() - 120 + message_bg_width: int = text_width + 20 # Add padding + message_bg_height: int = text_height + 10 # Add padding # Draw background rectangle for the message pygame.draw.rect( @@ -169,8 +174,9 @@ def draw(self): (message_bg_x + 10, message_bg_y + 5), # Position text with padding ) - def handle_mouse_click(self, mouse_pos) -> None: + def handle_mouse_click(self, mouse_pos: tuple[int, int]) -> None: """Handle mouse clicks on buttons.""" + for item, (use_button, discard_button) in self.button_actions.items(): if use_button.collidepoint(mouse_pos): self.message = self.inventory.use_item(item) # `self.message` stores strings