From e279bb5ce68d298fc09307c746c83279795ac3f4 Mon Sep 17 00:00:00 2001 From: AnSiChen Date: Thu, 21 Nov 2024 12:20:37 +0800 Subject: [PATCH 01/14] feat: extend Inventory class with dynamic item management. --- src/GUI/inventory.py | 45 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/src/GUI/inventory.py b/src/GUI/inventory.py index 6533de1..d3c1f1a 100644 --- a/src/GUI/inventory.py +++ b/src/GUI/inventory.py @@ -19,28 +19,59 @@ def __init__(self) -> None: class Inventory: - """contain money, chests, quests""" + """Manage player's inventory, including money, item, chests, and quests""" def __init__(self) -> None: - # special attribute + # Currency self.money: int = 0 - # items : + # Item management + self.items: dict[str, int] = {} # name: quantity + + # Special attributes: self.chests: list[Chest] = [] self.quests: list[Quest] = [] + # General item management + def add_item(self, item_name: str, quantity: int) -> None: + """Add an item to the inventory""" + if item_name in self.items: + self.items[item_name] += quantity + else: + self.items[item_name] = quantity + + def remove_item(self, item_name: str, quantity: int) -> None: + """Remove an item from the inventory. Return True if successful.""" + if item_name in self.items and self.items[item_name] >= quantity: + self.items[item_name] -= quantity + if self.items[item_name] == 0: + del self.items[item_name] + return True + return False + + def use_item(self, item_name: str) -> None: + """Use an item, applying its effect. Return a message.""" + if self.remove_item(item_name, 1): + return f"{item_name} used." + return f"{item_name} not found." + + def get_items(self) -> dict[str, int]: + """Return a copy of the items dictionary.""" + return self.items.copy() + + # Methods for Chest and Quest def add_chest(self, chest: Chest) -> None: - """append a chest to the inventory""" + """Add a chest to the inventory.""" self.chests.append(chest) def get_chests(self) -> list[Chest]: - """Return a copy of the chests list""" + """Return a copy of the chests list.""" return self.chests.copy() def add_quest(self, quest: Quest) -> None: - """append a quest to the inventory""" + """Add a quest to the inventory.""" self.quests.append(quest) def get_quests(self) -> list[Quest]: - """Return a copy of the quests list""" + """Return a copy of the quests list.""" return self.quests.copy() From 75cf575ecd10bfbced99124e81db147cf813596c Mon Sep 17 00:00:00 2001 From: AnSiChen Date: Thu, 21 Nov 2024 12:40:02 +0800 Subject: [PATCH 02/14] feat: add JSON-based dynamic messaging for inventory actions. --- data/messages.json | 10 ++++++++++ src/GUI/inventory.py | 14 ++++++++------ src/utils/messaging.py | 11 +++++++++++ 3 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 data/messages.json create mode 100644 src/utils/messaging.py diff --git a/data/messages.json b/data/messages.json new file mode 100644 index 0000000..2dcf311 --- /dev/null +++ b/data/messages.json @@ -0,0 +1,10 @@ +{ + "inventory": { + "add_success": "Successfully added {quantity} {item}(s) to your inventory.", + "add_fail": "Failed to add {item} to your inventory.", + "remove_success": "Successfully removed {quantity} {item}(s) from your inventory.", + "remove_fail": "Cannot remove {quantity} {item}(s), insufficient quantity.", + "use_success": "You used {item}.", + "use_fail": "You dont' have {item} in your inventory." + } +} \ No newline at end of file diff --git a/src/GUI/inventory.py b/src/GUI/inventory.py index d3c1f1a..25ec24f 100644 --- a/src/GUI/inventory.py +++ b/src/GUI/inventory.py @@ -2,7 +2,7 @@ inventory class for the players this file contain types of items, like Chest """ - +from src.utils.messaging import get_message class Chest: """contain loot, and worth""" @@ -37,8 +37,10 @@ def add_item(self, item_name: str, quantity: int) -> None: """Add an item to the inventory""" if item_name in self.items: self.items[item_name] += quantity + return get_message("inventory", "add_success", item=item_name, quantity=quantity) else: self.items[item_name] = quantity + return get_message("inventory", "add_success", item=item_name, quantity=quantity) def remove_item(self, item_name: str, quantity: int) -> None: """Remove an item from the inventory. Return True if successful.""" @@ -46,14 +48,14 @@ def remove_item(self, item_name: str, quantity: int) -> None: self.items[item_name] -= quantity if self.items[item_name] == 0: del self.items[item_name] - return True - return False + return get_message("inventory", "remove_success", item=item_name, quantity=quantity) + return get_message("inventory", "remove_fail", item=item_name, quantity=quantity) def use_item(self, item_name: str) -> None: """Use an item, applying its effect. Return a message.""" - if self.remove_item(item_name, 1): - return f"{item_name} used." - return f"{item_name} not found." + 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 get_items(self) -> dict[str, int]: """Return a copy of the items dictionary.""" diff --git a/src/utils/messaging.py b/src/utils/messaging.py new file mode 100644 index 0000000..157c23f --- /dev/null +++ b/src/utils/messaging.py @@ -0,0 +1,11 @@ +import json + +def get_message(category: str, key: str, **kwargs) -> str: + """Retrieve and format a message from the JSON file.""" + try: + with open("data/messages.json", "r") as f: + messages = json.load(f) + message = messages[category][key] + return message.format(**kwargs) + except (KeyError, FileNotFoundError): + return "An error occurred while retrieving the message." \ No newline at end of file From 4e0132077d2d61f9bfb6c1df0f6aff79a696029c Mon Sep 17 00:00:00 2001 From: AnSiChen Date: Thu, 21 Nov 2024 13:42:59 +0800 Subject: [PATCH 03/14] feat: implement inventory overlay with solid background --- src/GUI/gameloop.py | 23 +++++++++++++++++++++++ src/GUI/inventory_gui.py | 26 ++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 src/GUI/inventory_gui.py diff --git a/src/GUI/gameloop.py b/src/GUI/gameloop.py index 8a23bb9..b695d49 100644 --- a/src/GUI/gameloop.py +++ b/src/GUI/gameloop.py @@ -12,6 +12,10 @@ from src.settings import SCREEN_WIDTH, SCREEN_HEIGHT, TILE_SIZE import src.sprites +# import inventory related classes +from src.GUI.inventory_gui import InventoryGUI +from src.GUI.inventory import Inventory + @dataclass class GUI: @@ -31,6 +35,10 @@ def __post_init__(self): self.screen = pygame.display.set_mode(self.screen_size) pygame.display.set_caption("PySeas") + # Initialize player inventory + self.player_inventory = Inventory() + self.inventory_gui = InventoryGUI(self.screen, self.player_inventory) + self.players: list[src.sprites.Player] = [src.sprites.Player()] self.running = True @@ -84,6 +92,21 @@ def handle_events(self) -> None: case pygame.QUIT: pygame.quit() sys.exit() + case pygame.KEYDOWN: + if event.key == pygame.K_i: # Toggle inventory with "I" key + self.toggle_inventory() + + def toggle_inventory(self): + """Toggle the inventory overlay.""" + self.inventory_gui.running = not self.inventory_gui.running + + while self.inventory_gui.running: + for event in pygame.event.get(): + if event.type == pygame.KEYDOWN and event.key == pygame.K_i: + self.inventory_gui.running = False # Close the inventory + + self.inventory_gui.draw() + pygame.display.flip() # Update the display def update(self) -> None: """update the player""" diff --git a/src/GUI/inventory_gui.py b/src/GUI/inventory_gui.py new file mode 100644 index 0000000..712ff17 --- /dev/null +++ b/src/GUI/inventory_gui.py @@ -0,0 +1,26 @@ +import pygame +from src.GUI.inventory import Inventory + +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 + + def draw(self): + """Draw the inventory overlay.""" + self.screen.fill((0, 0, 0)) + + # Draw the inventory items + y_offset = 50 # Start below the title + for item, quantity in self.inventory.get_items().items(): + text = self.font.render(f"{item}: {quantity}", True, (255, 255, 255)) # White text + self.screen.blit(text, (50, y_offset)) + y_offset += 40 # Move down for the next item + + # Draw hint + hint_text = self.font.render("Press 'I' to close inventory", True, (200, 200, 200)) # Light gray text + self.screen.blit(hint_text, (50, y_offset + 20)) \ No newline at end of file From e3cf14cc38ac30026bd9d4a7bf59dc336ccd9889 Mon Sep 17 00:00:00 2001 From: AnSiChen Date: Thu, 21 Nov 2024 14:22:51 +0800 Subject: [PATCH 04/14] feat: add interactive buttons for the inventory item management --- src/GUI/gameloop.py | 4 +++- src/GUI/inventory_gui.py | 49 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/GUI/gameloop.py b/src/GUI/gameloop.py index b695d49..f7f1588 100644 --- a/src/GUI/gameloop.py +++ b/src/GUI/gameloop.py @@ -38,7 +38,7 @@ def __post_init__(self): # Initialize player inventory self.player_inventory = Inventory() self.inventory_gui = InventoryGUI(self.screen, self.player_inventory) - + self.players: list[src.sprites.Player] = [src.sprites.Player()] self.running = True @@ -104,6 +104,8 @@ def toggle_inventory(self): for event in pygame.event.get(): if event.type == pygame.KEYDOWN and event.key == pygame.K_i: self.inventory_gui.running = False # Close the inventory + elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: # Left click + self.inventory_gui.handle_mouse_click(event.pos) self.inventory_gui.draw() pygame.display.flip() # Update the display diff --git a/src/GUI/inventory_gui.py b/src/GUI/inventory_gui.py index 712ff17..934c321 100644 --- a/src/GUI/inventory_gui.py +++ b/src/GUI/inventory_gui.py @@ -10,17 +10,58 @@ def __init__(self, screen: pygame.Surface, inventory: Inventory) -> None: self.font = pygame.font.Font(None, 36) self.running = False + # Button dimmentions + self.button_width = 100 + self.button_height = 50 + + # Initialize button actions + self.button_actions = {} + + def draw_buttons(self, x: int, y: int, item: str) -> None: + """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) + + 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)) + + 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): """Draw the inventory overlay.""" - self.screen.fill((0, 0, 0)) + self.screen.fill((0, 0, 0)) # Solid Black background + + # Reset button actions + self.button_actions = {} # Draw the inventory items y_offset = 50 # Start below the title for item, quantity in self.inventory.get_items().items(): - text = self.font.render(f"{item}: {quantity}", True, (255, 255, 255)) # White text + # Display item and quantity + text = self.font.render(f"{item}: {quantity}", True, (255, 255, 255)) # White self.screen.blit(text, (50, y_offset)) - y_offset += 40 # Move down for the next item + + # Draw buttons + use_button, discard_button = self.draw_buttons(300, y_offset, item) + + # Store buttonr references for event handling + self.button_actions[item] = (use_button, discard_button) + y_offset += 60 # Move down for the next item # Draw hint hint_text = self.font.render("Press 'I' to close inventory", True, (200, 200, 200)) # Light gray text - self.screen.blit(hint_text, (50, y_offset + 20)) \ No newline at end of file + self.screen.blit(hint_text, (50, y_offset + 20)) + + def handle_mouse_click(self, mouse_pos): + """handle mouse clicks on buttons.""" + for item, (use_button, discard_button) in self.button_actions.items(): + if use_button.collidepoint(mouse_pos): + print(self.inventory.use_item(item)) # Example: Use the item + elif discard_button.collidepoint(mouse_pos): + print(self.inventory.remove_item(item, 1)) # Example: Discard the item \ No newline at end of file From 4135fd8c30295103732b0475b59acb15f712c3a7 Mon Sep 17 00:00:00 2001 From: AnSiChen Date: Thu, 21 Nov 2024 15:06:40 +0800 Subject: [PATCH 05/14] feat: initialize inventory items from JSON --- data/inventory.json | 12 ++++++++++++ src/GUI/gameloop.py | 15 +++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 data/inventory.json diff --git a/data/inventory.json b/data/inventory.json new file mode 100644 index 0000000..a2f9aaa --- /dev/null +++ b/data/inventory.json @@ -0,0 +1,12 @@ +{ + "Cutlass": {"type": "weapon", "effect": "melee_attack", "quantity": 1}, + "Flintlock Pistol": {"type": "weapon", "effect": "ranged_attack", "quantity": 1}, + "Rum Flask": {"type": "consumable", "effect": "restore_stamina", "quantity": 3}, + "Treasure Map": {"type": "tool", "effect": "reveal_hidden_treasure", "quantity": 1}, + "Spyglass": {"type": "tool", "effect": "enhance_vision", "quantity": 1}, + "Wooden Planks": {"type": "material", "effect": "repair_ship", "quantity": 10}, + "Cannonball": {"type": "ammunition", "effect": "ship_damage", "quantity": 20}, + "Rope": {"type": "tool", "effect": "secure_ship", "quantity": 5}, + "Black Powder": {"type": "material", "effect": "load_cannon", "quantity": 15}, + "Pirate Hat": {"type": "gear", "effect": "boost_charisma", "quantity": 1} +} \ No newline at end of file diff --git a/src/GUI/gameloop.py b/src/GUI/gameloop.py index f7f1588..86c69f6 100644 --- a/src/GUI/gameloop.py +++ b/src/GUI/gameloop.py @@ -1,5 +1,6 @@ from os.path import join import sys +import json # import dataclasses and typchecking from dataclasses import dataclass, field @@ -38,6 +39,9 @@ def __post_init__(self): # Initialize player inventory self.player_inventory = Inventory() self.inventory_gui = InventoryGUI(self.screen, self.player_inventory) + + # Load initial inventory items from JSON file + self.load_inventory_from_json("data/inventory.json") self.players: list[src.sprites.Player] = [src.sprites.Player()] @@ -110,6 +114,17 @@ def toggle_inventory(self): self.inventory_gui.draw() pygame.display.flip() # Update the display + def load_inventory_from_json(self, file_path: str): + """Load initial inventory items from JSON file.""" + try: + with open(file_path, "r") as f: + items = json.load(f) + for item_name, properties in items.items(): + quantity = properties.get("quantity", 1) # Default to 1 if missing + self.player_inventory.add_item(item_name, quantity) + except (FileNotFoundError, json.JSONecodeError) as e: + print(f"Error: The file at {file_path} does not exist.") + def update(self) -> None: """update the player""" for player in self.players: From f481cffe8ad6d1fd66b23cefe84fa4505d2f3b38 Mon Sep 17 00:00:00 2001 From: AnSiChen Date: Thu, 21 Nov 2024 15:31:14 +0800 Subject: [PATCH 06/14] feat: add in-game action messages for inventory interactions. --- src/GUI/inventory_gui.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/GUI/inventory_gui.py b/src/GUI/inventory_gui.py index 934c321..26f090f 100644 --- a/src/GUI/inventory_gui.py +++ b/src/GUI/inventory_gui.py @@ -17,6 +17,10 @@ def __init__(self, screen: pygame.Surface, inventory: Inventory) -> None: # Initialize button actions self.button_actions = {} + # Action messages + self.message = "" + self.message_end_time = 0 # Time to display the message + def draw_buttons(self, x: int, y: int, item: str) -> None: """Draw Use and Discard buttons for a specific item.""" use_button = pygame.Rect(x, y, self.button_width, self.button_height) @@ -58,10 +62,17 @@ def draw(self): hint_text = self.font.render("Press 'I' to close inventory", True, (200, 200, 200)) # Light gray text self.screen.blit(hint_text, (50, y_offset + 20)) + # Display action message at the bottom + if self.message and pygame.time.get_ticks() < self.message_end_time: + message_text = self.font.render(self.message, True, (255, 255, 0)) # Yellow + self.screen.blit(message_text, (50, y_offset + 50)) + def handle_mouse_click(self, mouse_pos): - """handle mouse clicks on buttons.""" + """Handle mouse clicks on buttons.""" for item, (use_button, discard_button) in self.button_actions.items(): if use_button.collidepoint(mouse_pos): - print(self.inventory.use_item(item)) # Example: Use the item + self.message = self.inventory.use_item(item) + self.message_end_time = pygame.time.get_ticks() + 3000 # 3 seconds elif discard_button.collidepoint(mouse_pos): - print(self.inventory.remove_item(item, 1)) # Example: Discard the item \ No newline at end of file + self.message = self.inventory.remove_item(item, 1) + self.message_end_time = pygame.time.get_ticks() + 4000 # 4 seconds \ No newline at end of file From 13904aa49580cb41ad085bad4e4d50bb5909592f Mon Sep 17 00:00:00 2001 From: AnSiChen Date: Thu, 21 Nov 2024 16:40:57 +0800 Subject: [PATCH 07/14] feat: integrate icons for inventory --- data/inventory.json | 3 +++ src/GUI/inventory_gui.py | 27 +++++++++++++++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/data/inventory.json b/data/inventory.json index a2f9aaa..38a9901 100644 --- a/data/inventory.json +++ b/data/inventory.json @@ -1,4 +1,7 @@ { + "Gold Coin": {"type": "currency", "effect": "collect", "quantity": 1}, + "Silver Coin": {"type": "currency", "effect": "collect", "quantity": 1}, + "Coin Stack (1)": {"type": "currency", "effect": "collect", "quantity": 3}, "Cutlass": {"type": "weapon", "effect": "melee_attack", "quantity": 1}, "Flintlock Pistol": {"type": "weapon", "effect": "ranged_attack", "quantity": 1}, "Rum Flask": {"type": "consumable", "effect": "restore_stamina", "quantity": 3}, diff --git a/src/GUI/inventory_gui.py b/src/GUI/inventory_gui.py index 26f090f..083e9fe 100644 --- a/src/GUI/inventory_gui.py +++ b/src/GUI/inventory_gui.py @@ -10,6 +10,13 @@ def __init__(self, screen: pygame.Surface, inventory: Inventory) -> None: self.font = pygame.font.Font(None, 36) self.running = False + # Load sprite sheet and extract the icons + self.sprite_sheet = pygame.image.load("images/tilesets/Treasure+.png").convert_alpha() + self.icons = { + "Gold Coin": self.extract_icon(0, 0), + "Silver Coin": self.extract_icon(16, 0), + "Coin Stack (1)": self.extract_icon(32, 0),} + # Button dimmentions self.button_width = 100 self.button_height = 50 @@ -21,6 +28,10 @@ def __init__(self, screen: pygame.Surface, inventory: Inventory) -> None: self.message = "" self.message_end_time = 0 # Time to display the message + 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) -> None: """Draw Use and Discard buttons for a specific item.""" use_button = pygame.Rect(x, y, self.button_width, self.button_height) @@ -47,12 +58,20 @@ def draw(self): # Draw the inventory items y_offset = 50 # Start below the title for item, quantity in self.inventory.get_items().items(): - # Display item and quantity - text = self.font.render(f"{item}: {quantity}", True, (255, 255, 255)) # White - self.screen.blit(text, (50, y_offset)) + # Draw icon + if item in self.icons: + self.screen.blit(self.icons[item], (50, y_offset)) + + # Draw quantity next to the icon + quantity_text = self.font.render(f"x{quantity}", True, (255, 255, 255)) + self.screen.blit(quantity_text, (100, y_offset + 5)) + + # Draw item name (move text to the right) + text = self.font.render(item, True, (255, 255, 255)) + self.screen.blit(text, (150, y_offset)) # Draw buttons - use_button, discard_button = self.draw_buttons(300, y_offset, item) + use_button, discard_button = self.draw_buttons(400, y_offset, item) # Store buttonr references for event handling self.button_actions[item] = (use_button, discard_button) From 11ecc7e4b0aea1b597593c0c7bcd1cb5c4e8f0d6 Mon Sep 17 00:00:00 2001 From: AnSiChen Date: Thu, 21 Nov 2024 17:52:21 +0800 Subject: [PATCH 08/14] feat: Add scrolling and messsage background to inventory --- src/GUI/gameloop.py | 4 ++- src/GUI/inventory_gui.py | 62 +++++++++++++++++++++++++++++++--------- 2 files changed, 52 insertions(+), 14 deletions(-) diff --git a/src/GUI/gameloop.py b/src/GUI/gameloop.py index 86c69f6..737903f 100644 --- a/src/GUI/gameloop.py +++ b/src/GUI/gameloop.py @@ -99,7 +99,7 @@ def handle_events(self) -> None: case pygame.KEYDOWN: if event.key == pygame.K_i: # Toggle inventory with "I" key self.toggle_inventory() - + def toggle_inventory(self): """Toggle the inventory overlay.""" self.inventory_gui.running = not self.inventory_gui.running @@ -110,6 +110,8 @@ def toggle_inventory(self): self.inventory_gui.running = False # Close the inventory elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: # Left click self.inventory_gui.handle_mouse_click(event.pos) + elif event.type == pygame.MOUSEWHEEL: + self.inventory_gui.handle_events(event) self.inventory_gui.draw() pygame.display.flip() # Update the display diff --git a/src/GUI/inventory_gui.py b/src/GUI/inventory_gui.py index 083e9fe..b3b8b17 100644 --- a/src/GUI/inventory_gui.py +++ b/src/GUI/inventory_gui.py @@ -10,6 +10,11 @@ def __init__(self, screen: pygame.Surface, inventory: Inventory) -> None: self.font = pygame.font.Font(None, 36) self.running = False + # Scrolling inventory + self.scroll_offset = 0 + self.max_visible_items = 10 + self.item_height = 60 + # Load sprite sheet and extract the icons self.sprite_sheet = pygame.image.load("images/tilesets/Treasure+.png").convert_alpha() self.icons = { @@ -28,6 +33,14 @@ def __init__(self, screen: pygame.Surface, inventory: Inventory) -> None: self.message = "" self.message_end_time = 0 # Time to display the message + def handle_events(self, event): + """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): """Extract a single icon from the sprite sheet.""" return self.sprite_sheet.subsurface((x, y, size, size)) @@ -50,15 +63,18 @@ def draw_buttons(self, x: int, y: int, item: str) -> None: def draw(self): """Draw the inventory overlay.""" - self.screen.fill((0, 0, 0)) # Solid Black background + self.screen.fill((0, 0, 0)) # Solid Black background # Reset button actions self.button_actions = {} # Draw the inventory items - y_offset = 50 # Start below the title - for item, quantity in self.inventory.get_items().items(): - # Draw icon + 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 + + for item, quantity in visible_items: + # Draw icon if item in self.icons: self.screen.blit(self.icons[item], (50, y_offset)) @@ -73,18 +89,38 @@ def draw(self): # Draw buttons use_button, discard_button = self.draw_buttons(400, y_offset, item) - # Store buttonr references for event handling + # Store button references for event handling self.button_actions[item] = (use_button, discard_button) - y_offset += 60 # Move down for the next item + y_offset += 60 # Move down for the next item - # Draw hint - hint_text = self.font.render("Press 'I' to close inventory", True, (200, 200, 200)) # Light gray text - self.screen.blit(hint_text, (50, y_offset + 20)) + # Draw 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 at the bottom + # Display action message above the hint if self.message and pygame.time.get_ticks() < self.message_end_time: - message_text = self.font.render(self.message, True, (255, 255, 0)) # Yellow - self.screen.blit(message_text, (50, y_offset + 50)) + # Render the message text + message_text = self.font.render(self.message, True, (255, 255, 0)) # Yellow + + # Measure the message text size + 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 + + # Draw background rectangle for the message + pygame.draw.rect( + self.screen, (0, 0, 0), # Black background + (message_bg_x, message_bg_y, message_bg_width, message_bg_height) + ) + + # Draw the message text on top of the background + self.screen.blit( + message_text, (message_bg_x + 10, message_bg_y + 5) # Position text with padding + ) def handle_mouse_click(self, mouse_pos): """Handle mouse clicks on buttons.""" @@ -94,4 +130,4 @@ def handle_mouse_click(self, mouse_pos): self.message_end_time = pygame.time.get_ticks() + 3000 # 3 seconds elif discard_button.collidepoint(mouse_pos): self.message = self.inventory.remove_item(item, 1) - self.message_end_time = pygame.time.get_ticks() + 4000 # 4 seconds \ No newline at end of file + self.message_end_time = pygame.time.get_ticks() + 4000 # 4 seconds From 69943c78910273dd6f73e672c807337bf0344e7f Mon Sep 17 00:00:00 2001 From: AnSiChen Date: Thu, 21 Nov 2024 18:27:57 +0800 Subject: [PATCH 09/14] feat: expand inventory.json and implement new sample icons in InventoryGUI class --- data/inventory.json | 50 +++++++++++++++++++++++++++++++--------- src/GUI/inventory_gui.py | 46 +++++++++++++++++++++++++++++++++--- 2 files changed, 82 insertions(+), 14 deletions(-) diff --git a/data/inventory.json b/data/inventory.json index 38a9901..ae04b5d 100644 --- a/data/inventory.json +++ b/data/inventory.json @@ -2,14 +2,42 @@ "Gold Coin": {"type": "currency", "effect": "collect", "quantity": 1}, "Silver Coin": {"type": "currency", "effect": "collect", "quantity": 1}, "Coin Stack (1)": {"type": "currency", "effect": "collect", "quantity": 3}, - "Cutlass": {"type": "weapon", "effect": "melee_attack", "quantity": 1}, - "Flintlock Pistol": {"type": "weapon", "effect": "ranged_attack", "quantity": 1}, - "Rum Flask": {"type": "consumable", "effect": "restore_stamina", "quantity": 3}, - "Treasure Map": {"type": "tool", "effect": "reveal_hidden_treasure", "quantity": 1}, - "Spyglass": {"type": "tool", "effect": "enhance_vision", "quantity": 1}, - "Wooden Planks": {"type": "material", "effect": "repair_ship", "quantity": 10}, - "Cannonball": {"type": "ammunition", "effect": "ship_damage", "quantity": 20}, - "Rope": {"type": "tool", "effect": "secure_ship", "quantity": 5}, - "Black Powder": {"type": "material", "effect": "load_cannon", "quantity": 15}, - "Pirate Hat": {"type": "gear", "effect": "boost_charisma", "quantity": 1} -} \ No newline at end of file + "Coin Stack (2)": {"type": "currency", "effect": "collect", "quantity": 5}, + "Circular Gem": {"type": "gem", "effect": "trade", "quantity": 1}, + "Single Gold Bar": {"type": "treasure", "effect": "trade", "quantity": 1}, + "Gold Bar Stack": {"type": "treasure", "effect": "trade", "quantity": 3}, + "Treasure Block": {"type": "treasure", "effect": "open_for_reward", "quantity": 1}, + "Golden Crown": {"type": "artifact", "effect": "boost_status", "quantity": 1}, + "Ornate Cup": {"type": "artifact", "effect": "boost_status", "quantity": 1}, + "Golden Figurine": {"type": "artifact", "effect": "boost_status", "quantity": 1}, + "Simple Sword": {"type": "weapon", "effect": "melee_attack", "quantity": 1}, + "Ornate Sword": {"type": "weapon", "effect": "melee_attack", "quantity": 1}, + "Double-Bladed Axe": {"type": "weapon", "effect": "melee_attack", "quantity": 1}, + "Spear": {"type": "weapon", "effect": "melee_attack", "quantity": 1}, + "Circular Shield": {"type": "armor", "effect": "defense_boost", "quantity": 1}, + "Golden Trophy": {"type": "reward", "effect": "achievement", "quantity": 1}, + "Candelabra": {"type": "decorative", "effect": "none", "quantity": 1}, + "Potion (Red)": {"type": "consumable", "effect": "restore_health", "quantity": 1}, + "Potion (Blue)": {"type": "consumable", "effect": "restore_mana", "quantity": 1}, + "Potion (Green)": {"type": "consumable", "effect": "poison_resistance", "quantity": 1}, + "Square Jar": {"type": "consumable", "effect": "unknown", "quantity": 1}, + "Cake": {"type": "food", "effect": "restore_health", "quantity": 1}, + "Donut": {"type": "food", "effect": "restore_health", "quantity": 1}, + "Bread": {"type": "food", "effect": "restore_health", "quantity": 1}, + "Rug Tile": {"type": "decorative", "effect": "none", "quantity": 1}, + "Geometric Pattern": {"type": "decorative", "effect": "none", "quantity": 1}, + "Glowing Orb (Blue)": {"type": "artifact", "effect": "magic_boost", "quantity": 1}, + "Glowing Orb (Red)": {"type": "artifact", "effect": "fire_boost", "quantity": 1}, + "Glowing Orb (Green)": {"type": "artifact", "effect": "nature_boost", "quantity": 1}, + "Golden Ring": {"type": "artifact", "effect": "magic_resistance", "quantity": 1}, + "Amulet": {"type": "artifact", "effect": "protection", "quantity": 1}, + "Scroll": {"type": "scroll", "effect": "learn_spell", "quantity": 1}, + "Key": {"type": "tool", "effect": "unlock", "quantity": 1}, + "Tool": {"type": "tool", "effect": "repair", "quantity": 1}, + "Dragon (Red)": {"type": "creature", "effect": "fire_attack", "quantity": 1}, + "Dragon (Green)": {"type": "creature", "effect": "nature_attack", "quantity": 1}, + "Dragon (Black)": {"type": "creature", "effect": "dark_attack", "quantity": 1}, + "Dragon (White)": {"type": "creature", "effect": "light_attack", "quantity": 1}, + "Gem Cluster": {"type": "treasure", "effect": "trade", "quantity": 1}, + "Glowing Crystal": {"type": "treasure", "effect": "magic_boost", "quantity": 1} +} diff --git a/src/GUI/inventory_gui.py b/src/GUI/inventory_gui.py index b3b8b17..8c41a45 100644 --- a/src/GUI/inventory_gui.py +++ b/src/GUI/inventory_gui.py @@ -18,9 +18,49 @@ def __init__(self, screen: pygame.Surface, inventory: Inventory) -> None: # Load sprite sheet and extract the icons self.sprite_sheet = pygame.image.load("images/tilesets/Treasure+.png").convert_alpha() self.icons = { - "Gold Coin": self.extract_icon(0, 0), - "Silver Coin": self.extract_icon(16, 0), - "Coin Stack (1)": self.extract_icon(32, 0),} + "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) + } + # Button dimmentions self.button_width = 100 From 6e98cd6cab55cb3c8bde93a789e407d46b92a3d2 Mon Sep 17 00:00:00 2001 From: AnSiChen Date: Thu, 21 Nov 2024 19:50:48 +0800 Subject: [PATCH 10/14] docs: Add documentation for Inventory and Utils(related functionality) --- docs/InventoryGuide.md | 66 ++++++++++++++++++++++++++++++++++++++++++ docs/UtilsGuide.md | 45 ++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 docs/InventoryGuide.md create mode 100644 docs/UtilsGuide.md diff --git a/docs/InventoryGuide.md b/docs/InventoryGuide.md new file mode 100644 index 0000000..b4550f8 --- /dev/null +++ b/docs/InventoryGuide.md @@ -0,0 +1,66 @@ +## Inventory Guide (GUI Version) + +### Description of the Inventory GUI +The Inventory GUI serves as a user-friendly interface for managing in-game items. It allows players to view, interact with, and organize their inventory. + +[![pyseas-inventory.png](https://i.postimg.cc/G3ZXHdnK/pyseas-inventory.png)](https://postimg.cc/14rGdxtV) + +### Key Features +- **Visual Display**: Items are displayed as icon, also providing name and quantity. +- **Responsive Interaction**: Interactive buttons for using, or discarding items. +- **Dynamic Updates**: Changes are reflected in real time. +- **Real Time Message Feedback**: Display message actions for better user experience. *(Refer to [utils](./UtilsGuide) for more details.)* + +[![use-item.png](https://i.postimg.cc/9fK7JXpN/use-item.png)](https://postimg.cc/Y4N0SHT1) +[![remove-item.png](https://i.postimg.cc/QCkK8QrR/remove-item.png)](https://postimg.cc/H8nk37n2) + +## Controls Documentation + +### Controls Summary +- **Keyboard**: Press `I` to toggle the inventory screen on and off. +- **Mouse**: Click buttons to perform actions. + +## Testing Items + +### Modifying `inventory.json` +The `data/inventory.json` file controls the data for all items in the inventory. It can be modified for testing purposes as follows: + +1. Open the `inventory.json` file. +2. Add, remove, or edit item entries using the following format: + ```json + { + ... + "Gold Coin": {"type": "currency", "effect": "collect", "quantity": 1}, + ... + } + ``` + +## Key Properties + +Each item in the inventory has the following properties: + +- **`type`**: The classification of the item (e.g., `weapon`, `potion`, `material`). +- **`effect`**: The functional impact of the item (e.g., `damage`, `healing`, `crafting material`). +- **`quantity`**: The number of instances available for the item. + +## Working with icons + +### 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: + +```python + "Gold Coin": self.extract_icon(0, 0), +``` + +[![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. + +## Known Issues + +1. Icons Mapping: Placeholder icons are used for certain items as not all of them +accurately represent the icon they are linked to. diff --git a/docs/UtilsGuide.md b/docs/UtilsGuide.md new file mode 100644 index 0000000..f6252a0 --- /dev/null +++ b/docs/UtilsGuide.md @@ -0,0 +1,45 @@ +# UtilsGuide.md + +## Utility: `messaging.py` + +This utility module provides functionality for retrieving and formatting messages stored in a centralized JSON file. It simplifies message management and allows for better modularity, multi-language support, and scalability across the game. + +--- + +### File: `src/utils/messaging.py` + +#### Function: `get_message(category: str, key: str, **kwargs) -> str` + +Retrieve and format a message from a JSON file located at `data/messages.json`. + +**Parameters:** +- `category` (`str`): The category of the message (e.g., `"inventory"`). +- `key` (`str`): The specific key for the desired message (e.g., `"add_success"`). +- `**kwargs`: Dynamic keyword arguments for formatting placeholders in the message. + +**Returns:** +- The formatted message string from the JSON file. +- If the message is not found or the file is missing, a default error message is returned: `"An error occurred while retrieving the message."` + +### Usage Examples + +[![utils-usage.png](https://i.postimg.cc/MHvbCbWH/utils-usage.png)](https://postimg.cc/WqckrZqc) + +## Possible Expansion +The messaging.py utility can be expanded to other areas of the game, including: + +- Multi-language support by replacing or extending data/messages.json with localized versions. +- General feedback and logging, ensuring consistency across UI and gameplay mechanics. +- Integration with game state management for dynamic message generation. + +## Best Practices +### Error Handling: + +- Ensure data/messages.json is correctly formatted and accessible. +- Validate category and key inputs to avoid KeyError. + +- Message Consistency: + - Keep all game-related messages in the JSON file for easier updates and consistency. + +- Localization: + - Prepare data/messages.json to support multi-language keys for localization, e.g., en, es, ko. \ No newline at end of file From 824f8ad9fbb3f2d85852bb2f06fab393b0c74acc Mon Sep 17 00:00:00 2001 From: AnSiChen Date: Thu, 21 Nov 2024 20:01:16 +0800 Subject: [PATCH 11/14] chore: Include pytest to requirements_dev.txt for tests --- requirements_dev.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index ebd9fe3..6317e4c 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,2 +1,3 @@ ruff>=0.5.3 -mypy>=1.11.1 \ No newline at end of file +mypy>=1.11.1 +pytest>=7.4.4 \ No newline at end of file From 7d9157255b1f7c37254956b4ae8e39c4d82edc45 Mon Sep 17 00:00:00 2001 From: AnSiChen Date: Thu, 21 Nov 2024 20:15:27 +0800 Subject: [PATCH 12/14] tests: Add inventory.py tests --- tests/test_inventory.py | 94 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 tests/test_inventory.py diff --git a/tests/test_inventory.py b/tests/test_inventory.py new file mode 100644 index 0000000..474e35a --- /dev/null +++ b/tests/test_inventory.py @@ -0,0 +1,94 @@ +import unittest +from src.GUI.inventory import Inventory, Chest, Quest + + +class TestInventory(unittest.TestCase): + def setUp(self): + """Set up a new Inventory object before each test.""" + self.inventory = Inventory() + + # Test add_item + def test_add_item_new(self): + """Test adding a new item.""" + result = self.inventory.add_item("Sword", 1) + self.assertEqual(self.inventory.items, {"Sword": 1}) + self.assertEqual(result, "Successfully added 1 Sword(s) to your inventory.") + + def test_add_item_existing(self): + """Test adding to an existing item.""" + self.inventory.add_item("Potion", 1) + result = self.inventory.add_item("Potion", 2) + self.assertEqual(self.inventory.items, {"Potion": 3}) + self.assertEqual(result, "Successfully added 2 Potion(s) to your inventory.") + + # Test remove_item + def test_remove_item_success(self): + """Test successfully removing an item.""" + self.inventory.add_item("Potion", 3) + result = self.inventory.remove_item("Potion", 2) + self.assertEqual(self.inventory.items, {"Potion": 1}) + self.assertEqual(result, "Successfully removed 2 Potion(s) from your inventory.") + + def test_remove_item_fail(self): + """Test failing to remove an item not in inventory or insufficient quantity.""" + result = self.inventory.remove_item("Sword", 1) + self.assertEqual(self.inventory.items, {}) + self.assertEqual(result, "Cannot remove 1 Sword(s), insufficient quantity.") + + # Test use_item + def test_use_item_success(self): + """Test using an item.""" + self.inventory.add_item("Potion", 1) + result = self.inventory.use_item("Potion") + self.assertEqual(self.inventory.items, {}) + self.assertEqual(result, "You used Potion.") + + def test_use_item_fail(self): + """Test failing to use an item.""" + result = self.inventory.use_item("Potion") + self.assertEqual(self.inventory.items, {}) + self.assertEqual(result, "You dont' have Potion in your inventory.") + + # Test add_chest + def test_add_chest(self): + """Test adding a chest.""" + chest = Chest("Gold Chest") + self.inventory.add_chest(chest) + self.assertEqual(len(self.inventory.chests), 1) + self.assertEqual(self.inventory.chests[0].name, "Gold Chest") + + # Test add_quest + def test_add_quest(self): + """Test adding a quest.""" + quest = Quest() + self.inventory.add_quest(quest) + self.assertEqual(len(self.inventory.quests), 1) + self.assertFalse(self.inventory.quests[0].completed) + + # Test get_items + def test_get_items(self): + """Test getting a copy of items.""" + self.inventory.add_item("Sword", 1) + items = self.inventory.get_items() + self.assertEqual(items, {"Sword": 1}) + self.assertIsNot(items, self.inventory.items) # Copy of items + + # Test get_chests + def test_get_chests(self): + """Test getting a copy of chests.""" + chest = Chest("Gold Chest") + self.inventory.add_chest(chest) + chests = self.inventory.get_chests() + self.assertEqual(len(chests), 1) + self.assertIsNot(chests, self.inventory.chests) # Copy of items + # Test get_quests + def test_get_quests(self): + """Test getting a copy of quests.""" + quest = Quest() + self.inventory.add_quest(quest) + quests = self.inventory.get_quests() + self.assertEqual(len(quests), 1) + self.assertIsNot(quests, self.inventory.quests) # Copy of items + +if __name__ == "__main__": + unittest.main() From 25e1c06b7960283dbd8d56882950469097734602 Mon Sep 17 00:00:00 2001 From: AnSiChen Date: Thu, 21 Nov 2024 20:50:47 +0800 Subject: [PATCH 13/14] fix: resolve type issues, formatting, and update style --- src/GUI/gameloop.py | 20 +++--- src/GUI/inventory.py | 36 ++++++---- src/GUI/inventory_gui.py | 149 +++++++++++++++++++++------------------ src/utils/messaging.py | 3 +- tests/test_inventory.py | 6 +- 5 files changed, 123 insertions(+), 91 deletions(-) diff --git a/src/GUI/gameloop.py b/src/GUI/gameloop.py index 737903f..2c5efed 100644 --- a/src/GUI/gameloop.py +++ b/src/GUI/gameloop.py @@ -41,8 +41,8 @@ def __post_init__(self): self.inventory_gui = InventoryGUI(self.screen, self.player_inventory) # Load initial inventory items from JSON file - self.load_inventory_from_json("data/inventory.json") - + self.load_inventory_from_json("data/inventory.json") + self.players: list[src.sprites.Player] = [src.sprites.Player()] self.running = True @@ -97,9 +97,9 @@ def handle_events(self) -> None: pygame.quit() sys.exit() case pygame.KEYDOWN: - if event.key == pygame.K_i: # Toggle inventory with "I" key + if event.key == pygame.K_i: # Toggle inventory with "I" key self.toggle_inventory() - + def toggle_inventory(self): """Toggle the inventory overlay.""" self.inventory_gui.running = not self.inventory_gui.running @@ -107,14 +107,16 @@ def toggle_inventory(self): while self.inventory_gui.running: for event in pygame.event.get(): if event.type == pygame.KEYDOWN and event.key == pygame.K_i: - self.inventory_gui.running = False # Close the inventory - elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: # Left click + self.inventory_gui.running = False # Close the inventory + elif ( + event.type == pygame.MOUSEBUTTONDOWN and event.button == 1 + ): # Left click self.inventory_gui.handle_mouse_click(event.pos) elif event.type == pygame.MOUSEWHEEL: self.inventory_gui.handle_events(event) self.inventory_gui.draw() - pygame.display.flip() # Update the display + pygame.display.flip() # Update the display def load_inventory_from_json(self, file_path: str): """Load initial inventory items from JSON file.""" @@ -122,9 +124,9 @@ def load_inventory_from_json(self, file_path: str): with open(file_path, "r") as f: items = json.load(f) for item_name, properties in items.items(): - quantity = properties.get("quantity", 1) # Default to 1 if missing + quantity = properties.get("quantity", 1) # Default to 1 if missing self.player_inventory.add_item(item_name, quantity) - except (FileNotFoundError, json.JSONecodeError) as e: + except (FileNotFoundError, json.JSONDecodeError): print(f"Error: The file at {file_path} does not exist.") def update(self) -> None: diff --git a/src/GUI/inventory.py b/src/GUI/inventory.py index 25ec24f..e1591c1 100644 --- a/src/GUI/inventory.py +++ b/src/GUI/inventory.py @@ -2,8 +2,10 @@ inventory class for the players this file contain types of items, like Chest """ + from src.utils.messaging import get_message + class Chest: """contain loot, and worth""" @@ -26,41 +28,51 @@ def __init__(self) -> None: self.money: int = 0 # Item management - self.items: dict[str, int] = {} # name: quantity + self.items: dict[str, int] = {} # name: quantity # Special attributes: self.chests: list[Chest] = [] self.quests: list[Quest] = [] # General item management - def add_item(self, item_name: str, quantity: int) -> None: + def add_item(self, item_name: str, quantity: int) -> str: """Add an item to the inventory""" if item_name in self.items: self.items[item_name] += quantity - return get_message("inventory", "add_success", item=item_name, quantity=quantity) + return get_message( + "inventory", "add_success", item=item_name, quantity=quantity + ) else: self.items[item_name] = quantity - return get_message("inventory", "add_success", item=item_name, quantity=quantity) + return get_message( + "inventory", "add_success", item=item_name, quantity=quantity + ) - def remove_item(self, item_name: str, quantity: int) -> None: + def remove_item(self, item_name: str, quantity: int) -> str: """Remove an item from the inventory. Return True if successful.""" if item_name in self.items and self.items[item_name] >= quantity: self.items[item_name] -= quantity if self.items[item_name] == 0: del self.items[item_name] - return get_message("inventory", "remove_success", item=item_name, quantity=quantity) - return get_message("inventory", "remove_fail", item=item_name, quantity=quantity) - - def use_item(self, item_name: str) -> None: + return get_message( + "inventory", "remove_success", item=item_name, quantity=quantity + ) + return get_message( + "inventory", "remove_fail", item=item_name, quantity=quantity + ) + + def use_item(self, item_name: str) -> str: """Use an item, applying its effect. Return a message.""" - if self.remove_item(item_name, 1) == get_message("inventory", "remove_success", item=item_name, quantity=1): + 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 get_items(self) -> dict[str, int]: """Return a copy of the items dictionary.""" return self.items.copy() - + # Methods for Chest and Quest def add_chest(self, chest: Chest) -> None: """Add a chest to the inventory.""" diff --git a/src/GUI/inventory_gui.py b/src/GUI/inventory_gui.py index 8c41a45..5f20dfa 100644 --- a/src/GUI/inventory_gui.py +++ b/src/GUI/inventory_gui.py @@ -1,106 +1,113 @@ +from typing import Dict, Tuple import pygame from src.GUI.inventory import Inventory + 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.inventory = inventory self.font = pygame.font.Font(None, 36) self.running = False # Scrolling inventory - self.scroll_offset = 0 + self.scroll_offset = 0 self.max_visible_items = 10 self.item_height = 60 - # Load sprite sheet and extract the icons - self.sprite_sheet = pygame.image.load("images/tilesets/Treasure+.png").convert_alpha() + # Load sprite sheet and extract the icons + self.sprite_sheet = pygame.image.load( + "images/tilesets/Treasure+.png" + ).convert_alpha() 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": 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), + } # Button dimmentions self.button_width = 100 self.button_height = 50 - # Initialize button actions - self.button_actions = {} + # Initialize button actions + 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_end_time = 0 # Time to display the message def handle_events(self, event): """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) + 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, 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) -> None: + 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) + discard_button = 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 + 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 = self.font.render("Use", True, (0, 0, 0)) # Black + discard_text = 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): """Draw the inventory overlay.""" self.screen.fill((0, 0, 0)) # Solid Black background @@ -110,7 +117,9 @@ def draw(self): # Draw the inventory items 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 # Start below the title for item, quantity in visible_items: @@ -134,7 +143,9 @@ def draw(self): y_offset += 60 # Move down for the next item # Draw hint - hint_text = self.font.render("Press 'I' to close inventory", True, (200, 200, 200)) # Light gray text + 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 @@ -153,21 +164,23 @@ def draw(self): # Draw background rectangle for the message pygame.draw.rect( - self.screen, (0, 0, 0), # Black background - (message_bg_x, message_bg_y, message_bg_width, message_bg_height) + self.screen, + (0, 0, 0), # Black background + (message_bg_x, message_bg_y, message_bg_width, message_bg_height), ) # Draw the message text on top of the background self.screen.blit( - message_text, (message_bg_x + 10, message_bg_y + 5) # Position text with padding + message_text, + (message_bg_x + 10, message_bg_y + 5), # Position text with padding ) - def handle_mouse_click(self, mouse_pos): + def handle_mouse_click(self, mouse_pos) -> 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_end_time = pygame.time.get_ticks() + 3000 # 3 seconds + self.message = self.inventory.use_item(item) # `self.message` stores strings + self.message_end_time = pygame.time.get_ticks() + 3000 # 3 seconds elif discard_button.collidepoint(mouse_pos): self.message = self.inventory.remove_item(item, 1) - self.message_end_time = pygame.time.get_ticks() + 4000 # 4 seconds + self.message_end_time = pygame.time.get_ticks() + 4000 # 4 seconds diff --git a/src/utils/messaging.py b/src/utils/messaging.py index 157c23f..bd7baa2 100644 --- a/src/utils/messaging.py +++ b/src/utils/messaging.py @@ -1,5 +1,6 @@ import json + def get_message(category: str, key: str, **kwargs) -> str: """Retrieve and format a message from the JSON file.""" try: @@ -8,4 +9,4 @@ def get_message(category: str, key: str, **kwargs) -> str: message = messages[category][key] return message.format(**kwargs) except (KeyError, FileNotFoundError): - return "An error occurred while retrieving the message." \ No newline at end of file + return "An error occurred while retrieving the message." diff --git a/tests/test_inventory.py b/tests/test_inventory.py index 474e35a..e7d7994 100644 --- a/tests/test_inventory.py +++ b/tests/test_inventory.py @@ -27,7 +27,9 @@ def test_remove_item_success(self): self.inventory.add_item("Potion", 3) result = self.inventory.remove_item("Potion", 2) self.assertEqual(self.inventory.items, {"Potion": 1}) - self.assertEqual(result, "Successfully removed 2 Potion(s) from your inventory.") + self.assertEqual( + result, "Successfully removed 2 Potion(s) from your inventory." + ) def test_remove_item_fail(self): """Test failing to remove an item not in inventory or insufficient quantity.""" @@ -81,6 +83,7 @@ def test_get_chests(self): chests = self.inventory.get_chests() self.assertEqual(len(chests), 1) self.assertIsNot(chests, self.inventory.chests) # Copy of items + # Test get_quests def test_get_quests(self): """Test getting a copy of quests.""" @@ -90,5 +93,6 @@ def test_get_quests(self): self.assertEqual(len(quests), 1) self.assertIsNot(quests, self.inventory.quests) # Copy of items + if __name__ == "__main__": unittest.main() From 59daae84d84543c20c290cb1675921246fcfbf92 Mon Sep 17 00:00:00 2001 From: AnSiChen Date: Mon, 9 Dec 2024 10:59:05 +0800 Subject: [PATCH 14/14] refactor: add __init__.py, update tests and docs - Added __init__.py files for package structure - Updated tests/test_inventory.py with sys.path fix and improvements - Revised docs/InventoryGuide.md to include test instructions - Added comments to inventory_gui.py for sprite loading logic --- docs/InventoryGuide.md | 4 ++++ src/GUI/__init__.py | 0 src/GUI/inventory_gui.py | 4 +++- src/__init__.py | 0 tests/__init__.py | 0 tests/test_inventory.py | 7 +++++++ 6 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 src/GUI/__init__.py create mode 100644 src/__init__.py create mode 100644 tests/__init__.py diff --git a/docs/InventoryGuide.md b/docs/InventoryGuide.md index b4550f8..4eb76f7 100644 --- a/docs/InventoryGuide.md +++ b/docs/InventoryGuide.md @@ -20,6 +20,10 @@ The Inventory GUI serves as a user-friendly interface for managing in-game items - **Keyboard**: Press `I` to toggle the inventory screen on and off. - **Mouse**: Click buttons to perform actions. +## Running Tests +Tests in this project use `pytest`. To run the tests: +`pytest tests/test_inventory.py` + ## Testing Items ### Modifying `inventory.json` diff --git a/src/GUI/__init__.py b/src/GUI/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/GUI/inventory_gui.py b/src/GUI/inventory_gui.py index 5f20dfa..2b39118 100644 --- a/src/GUI/inventory_gui.py +++ b/src/GUI/inventory_gui.py @@ -17,7 +17,9 @@ def __init__(self, screen: pygame.Surface, inventory: Inventory) -> None: self.max_visible_items = 10 self.item_height = 60 - # Load sprite sheet and extract the icons + # Load 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 self.sprite_sheet = pygame.image.load( "images/tilesets/Treasure+.png" ).convert_alpha() diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_inventory.py b/tests/test_inventory.py index e7d7994..8e5f3fd 100644 --- a/tests/test_inventory.py +++ b/tests/test_inventory.py @@ -1,7 +1,14 @@ +import sys +import os + +# Add the project root to sys.path to allow imports to work when running tests directly with `python`. +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + import unittest from src.GUI.inventory import Inventory, Chest, Quest + class TestInventory(unittest.TestCase): def setUp(self): """Set up a new Inventory object before each test."""