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 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() 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) 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.py b/src/sprites.py deleted file mode 100644 index 24ba2fd..0000000 --- a/src/sprites.py +++ /dev/null @@ -1,270 +0,0 @@ -"""custom sprites classes""" - -import pygame # type: ignore -from pygame import FRect - -from src.inventory import Inventory -from src.settings import ( - ANIMATION_SPEED, - SCREEN_HEIGHT, - SCREEN_WIDTH, - TILE_SIZE, - WORLD_LAYERS, -) - - -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.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 usefull later?) - # def set_scale(self, scale): - # self.scale = max(scale, 0.1) - - -class Player(Entity): - """move tile by tile""" - - rect: FRect - - 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) - - # gost 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 - 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 - # 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 - # 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 gost 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 gost preview to a given surface""" - self.input() - self.animate(dt) - - -class Tile(pygame.sprite.Sprite): - """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\\sprites.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) - - -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.z = z - - -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) - - def animate(self, dt): - self.frame_index += ANIMATION_SPEED * dt - self.image = self.frames[int(self.frame_index % len(self.frames))] - - def update(self, dt): - self.animate(dt) diff --git a/src/sprites/animations.py b/src/sprites/animations.py new file mode 100644 index 0000000..f6c7509 --- /dev/null +++ b/src/sprites/animations.py @@ -0,0 +1,28 @@ +import pygame +from pygame.sprite import Group + +from src.settings import ANIMATION_SPEED, WORLD_LAYERS +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) diff --git a/src/sprites/base.py b/src/sprites/base.py new file mode 100644 index 0000000..c6b866f --- /dev/null +++ b/src/sprites/base.py @@ -0,0 +1,72 @@ +"""custom sprites classes""" + +from abc import ABC + +from pygame import FRect, Surface, Vector2 +from pygame.sprite import Group, Sprite + +from src.settings import ANIMATION_SPEED, WORLD_LAYERS + + +class BaseSprite(Sprite, ABC): + """ + 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. + + 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: 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. + """ + + super().__init__(*groups) + + if surf is None: + raise ValueError("The `surf` parameter must be a valid pygame.Surface.") + + self.image = surf + self.rect: FRect = self.image.get_frect(topleft=pos) + self.z = z + + self.frames = frames or [surf] + self.frame_index: float = 0.0 + + # Movement + self.direction = Vector2() + self.facing_direction: str = "down" + + 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 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'}" diff --git a/src/sprites/camera/group.py b/src/sprites/camera/group.py new file mode 100644 index 0000000..eec8377 --- /dev/null +++ b/src/sprites/camera/group.py @@ -0,0 +1,25 @@ +from abc import ABC, abstractmethod + +import pygame +from pygame.sprite import Group + + +# class CameraGroup(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.""" + + 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): + """Render sprites with camera adjustments.""" + pass diff --git a/src/sprites/camera/player.py b/src/sprites/camera/player.py new file mode 100644 index 0000000..2c6ab44 --- /dev/null +++ b/src/sprites/camera/player.py @@ -0,0 +1,61 @@ +import pygame # ignore + +from src.settings import SCREEN_HEIGHT, SCREEN_WIDTH, WORLD_LAYERS +from src.sprites.camera.group import AllSprites + + +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) diff --git a/src/sprites/entities/player.py b/src/sprites/entities/player.py new file mode 100644 index 0000000..d6933fc --- /dev/null +++ b/src/sprites/entities/player.py @@ -0,0 +1,117 @@ +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.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. + """ + + # 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) 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 diff --git a/src/sprites/shop/shop_sprite.py b/src/sprites/shop/shop_sprite.py new file mode 100644 index 0000000..9fd4642 --- /dev/null +++ b/src/sprites/shop/shop_sprite.py @@ -0,0 +1,39 @@ +from pygame import Surface +from pygame.sprite import Group + +from src.settings import WORLD_LAYERS +from src.sprites.base import BaseSprite + + +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) + 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/sprites/tiles/tile.py b/src/sprites/tiles/tile.py new file mode 100644 index 0000000..1421869 --- /dev/null +++ b/src/sprites/tiles/tile.py @@ -0,0 +1,45 @@ +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) diff --git a/src/states/game_running.py b/src/states/game_running.py index b4993c6..35e7788 100644 --- a/src/states/game_running.py +++ b/src/states/game_running.py @@ -8,13 +8,15 @@ import pygame # type: ignore from pytmx.util_pygame import load_pygame # type: ignore -import src.shop -import src.sprites from src.inventory import Inventory from src.settings import TILE_SIZE, WORLD_LAYERS -from src.sprites import AnimatedSprites +from src.sprites.animations import AnimatedSprites +from src.sprites.base import BaseSprite +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 src.states.shop_state import ShowShop, WindowShop from src.support import all_character_import, coast_importer, import_folder @@ -37,7 +39,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 = 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,9 +48,9 @@ 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: """ - 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"))} @@ -58,13 +60,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.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 +77,52 @@ 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.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.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.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 +140,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 +152,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: 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 diff --git a/src/shop.py b/src/states/shop_state.py similarity index 60% rename from src/shop.py rename to src/states/shop_state.py index 724e32e..820e7cd 100644 --- a/src/shop.py +++ b/src/states/shop_state.py @@ -1,92 +1,56 @@ -from typing import Dict, Tuple +import os import pygame +from pygame import Surface from src.inventory import Inventory -from src.settings import WORLD_LAYERS +from src.sprites.shop.shop_sprite import ShowShop 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 - - 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.button_actions: dict[str, tuple[pygame.Rect, pygame.Rect]] = {} self.inventory = inventory self.show_shop = show_shop 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: @@ -103,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)) @@ -120,7 +84,7 @@ def render(self, screen: pygame.Surface): 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)) @@ -168,16 +132,13 @@ 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]: + 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)