Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions data/inventory.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"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},
"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}
}
10 changes: 10 additions & 0 deletions data/messages.json
Original file line number Diff line number Diff line change
@@ -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."
}
}
70 changes: 70 additions & 0 deletions docs/InventoryGuide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
## 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.

## Running Tests
Tests in this project use `pytest`. To run the tests:
`pytest tests/test_inventory.py`

## 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.
45 changes: 45 additions & 0 deletions docs/UtilsGuide.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 2 additions & 1 deletion requirements_dev.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
ruff>=0.5.3
mypy>=1.11.1
mypy>=1.11.1
pytest>=7.4.4
Empty file added src/GUI/__init__.py
Empty file.
45 changes: 45 additions & 0 deletions src/GUI/gameloop.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from os.path import join
import sys
import json

# import dataclasses and typchecking
from dataclasses import dataclass, field
Expand All @@ -12,6 +13,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:
Expand All @@ -31,6 +36,14 @@ def __post_init__(self):
pygame.display.set_caption("PySeas")
self.clock = pygame.Clock()

# 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")

# Players list currently commented out; keeping for potential future use
# self.players: list[src.sprites.Player] = [src.sprites.Player()]

self.all_sprites = src.sprites.AllSprites()
Expand Down Expand Up @@ -91,6 +104,38 @@ 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
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

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.JSONDecodeError):
print(f"Error: The file at {file_path} does not exist.")

def render(self) -> None:
"""draw sprites to the canvas"""
Expand Down
59 changes: 52 additions & 7 deletions src/GUI/inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
this file contain types of items, like Chest
"""

from src.utils.messaging import get_message


class Chest:
"""contain loot, and worth"""
Expand All @@ -19,28 +21,71 @@ 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) -> 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
)
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) -> 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) -> 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
):
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:
"""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()
Loading