diff --git a/levels/2.json b/levels/2.json deleted file mode 100644 index 50ab131..0000000 --- a/levels/2.json +++ /dev/null @@ -1,178 +0,0 @@ -{ - "level_id": 2, - "name": "The Graveyard", - "description": "The mausoleum led to an ancient graveyard. Watch out for falling skulls!", - "player_start": { - "x": 0, - "y": 0 - }, - "objects": [ - { - "x": 5, - "y": 3, - "sound": "coffin", - "type": "coffin" - }, - { - "x_range": [15, 20], - "y": 3, - "sound": "coin", - "collectible": true, - "static": true - }, - { - "x_range": [21, 29], - "y": 0, - "enemy_type": "goblin", - "health": 2, - "damage": 2, - "attack_range": 1, - "attack_pattern": { - "type": "patrol" - } - }, - { - "x": 35, - "y": 0, - "hazard": true, - "sound": "grave", - "static": true, - "zombie_spawn_chance": 10 - }, - { - "x_range": [33, 38], - "y": 3, - "sound": "coin", - "collectible": true, - "static": true - }, - { - "x_range": [45, 60], - "y": 12, - "type": "skull_storm", - "damage": 3, - "maximum_skulls": 2, - "frequency": { - "min": 3, - "max": 6 - } - }, - { - "x": 55, - "y": 3, - "sound": "coffin", - "type": "coffin", - "item": "guts" - }, - { - "x_range": [65, 70], - "y": 3, - "sound": "coin", - "collectible": true, - "static": true - }, - { - "x_range": [71, 79], - "y": 0, - "enemy_type": "goblin", - "health": 2, - "damage": 2, - "attack_range": 1, - "attack_pattern": { - "type": "patrol" - } - }, - { - "x": 85, - "y": 3, - "sound": "coffin", - "type": "coffin" - }, - { - "x": 95, - "y": 0, - "hazard": true, - "sound": "grave", - "static": true, - "zombie_spawn_chance": 10 - }, - { - "x_range": [105, 108], - "y": 3, - "sound": "coin", - "collectible": true, - "static": true - }, - { - "x": 120, - "y": 0, - "type": "catapult", - "fire_interval": 5000, - "range": 15 - }, - { - "x_range": [130, 165], - "y": 15, - "type": "skull_storm", - "damage": 4, - "maximum_skulls": 3, - "frequency": { - "min": 2, - "max": 5 - } - }, - { - "x_range": [145, 150], - "y": 3, - "sound": "coin", - "collectible": true, - "static": true - }, - { - "x": 160, - "y": 0, - "hazard": true, - "sound": "grave", - "static": true, - "zombie_spawn_chance": 10 - }, - { - "x": 170, - "y": 3, - "sound": "coffin", - "type": "coffin", - "item": "extra_life" - }, - { - "x_range": [175, 180], - "y": 3, - "sound": "coin", - "collectible": true, - "static": true - }, - { - "x": 185, - "y": 0, - "hazard": true, - "sound": "grave", - "static": true, - "zombie_spawn_chance": 10 - }, - { - "x_range": [189, 198], - "y": 0, - "enemy_type": "witch", - "health": 2, - "damage": 2, - "attack_range": 1.5, - "attack_pattern": { - "type": "patrol" - } - } - ], - "boundaries": { - "left": 0, - "right": 200 - }, - "footstep_sound": "footstep_tall_grass" -} diff --git a/levels/3.json b/levels/3.json deleted file mode 100644 index 216f890..0000000 --- a/levels/3.json +++ /dev/null @@ -1,252 +0,0 @@ -{ - "level_id": 3, - "name": "Endless Graves", - "description": "Graves continue in all directions as far as you can see. The dead seem restless.", - "player_start": { - "x": 0, - "y": 0 - }, - "objects": [ - { - "x_range": [1, 4], - "y": 3, - "sound": "coin", - "collectible": true, - "static": true - }, - { - "x": 5, - "y": 3, - "sound": "coffin", - "type": "coffin" - }, - { - "x_range": [6, 10], - "y": 3, - "sound": "coin", - "collectible": true, - "static": true - }, - { - "x": 15, - "y": 0, - "hazard": true, - "sound": "grave", - "static": true, - "zombie_spawn_chance": 25 - }, - { - "x_range": [25, 28], - "y": 3, - "sound": "coin", - "collectible": true, - "static": true - }, - { - "x_range": [21, 31], - "y": 0, - "enemy_type": "goblin", - "health": 3, - "damage": 2, - "attack_range": 1, - "attack_pattern": { - "type": "patrol" - } - }, - { - "x_range": [35, 50], - "y": 12, - "type": "skull_storm", - "damage": 3, - "maximum_skulls": 2, - "frequency": { - "min": 2, - "max": 5 - } - }, - { - "x_range": [40, 44], - "y": 3, - "sound": "coin", - "collectible": true, - "static": true - }, - { - "x": 55, - "y": 0, - "hazard": true, - "sound": "grave", - "static": true, - "zombie_spawn_chance": 25 - }, - { - "x_range": [60, 70], - "y": 0, - "enemy_type": "goblin", - "health": 3, - "damage": 2, - "attack_range": 1, - "attack_pattern": { - "type": "patrol" - } - }, - { - "x_range": [75, 78], - "y": 3, - "sound": "coin", - "collectible": true, - "static": true - }, - { - "x_range": [71, 81], - "y": 0, - "enemy_type": "goblin", - "health": 3, - "damage": 2, - "attack_range": 1, - "attack_pattern": { - "type": "patrol" - } - }, - { - "x": 85, - "y": 0, - "hazard": true, - "sound": "grave", - "static": true, - "zombie_spawn_chance": 28 - }, - { - "x": 85, - "y": 3, - "sound": "coffin", - "type": "coffin" - }, - { - "x_range": [95, 120], - "y": 15, - "type": "skull_storm", - "damage": 3, - "maximum_skulls": 2, - "frequency": { - "min": 2, - "max": 4 - } - }, - { - "x_range": [105, 115], - "y": 3, - "sound": "coin", - "collectible": true, - "static": true - }, - { - "x_range": [101, 111], - "y": 0, - "enemy_type": "witch", - "health": 6, - "damage": 2, - "attack_range": 1, - "attack_pattern": { - "type": "patrol" - } - }, - { - "x": 125, - "y": 0, - "hazard": true, - "sound": "grave", - "static": true, - "zombie_spawn_chance": 28 - }, - { - "x": 135, - "y": 0, - "hazard": true, - "sound": "grave", - "static": true, - "zombie_spawn_chance": 30 - }, - { - "x_range": [140, 150], - "y": 0, - "enemy_type": "goblin", - "health": 3, - "damage": 2, - "attack_range": 1, - "attack_pattern": { - "type": "patrol" - } - }, - { - "x_range": [155, 158], - "y": 3, - "sound": "coin", - "collectible": true, - "static": true - }, - { - "x": 145, - "y": 3, - "item": "hand_of_glory", - "sound": "coffin", - "type": "coffin" - }, - { - "x_range": [146, 166], - "y": 0, - "enemy_type": "ghoul", - "health": 10, - "damage": 3, - "attack_range": 2, - "attack_pattern": { - "type": "hunter", - "turn_threshold": 5 - } - }, - { - "x_range": [165, 190], - "y": 15, - "type": "skull_storm", - "damage": 4, - "maximum_skulls": 3, - "frequency": { - "min": 2, - "max": 4 - } - }, - { - "x": 175, - "y": 0, - "type": "catapult", - "fire_interval": 4500, - "range": 20 - }, - { - "x_range": [173, 176], - "y": 3, - "sound": "coin", - "collectible": true, - "static": true - }, - { - "x": 185, - "y": 3, - "item": "extra_life", - "sound": "coffin", - "type": "coffin" - }, - { - "x_range": [190, 195], - "y": 3, - "sound": "coin", - "collectible": true, - "static": true - } - ], - "boundaries": { - "left": 0, - "right": 200 - }, - "footstep_sound": "footstep_tall_grass" -} diff --git a/levels/1.json b/levels/Wicked Quest/1.json similarity index 100% rename from levels/1.json rename to levels/Wicked Quest/1.json diff --git a/src/game_selection.py b/src/game_selection.py new file mode 100644 index 0000000..779a932 --- /dev/null +++ b/src/game_selection.py @@ -0,0 +1,133 @@ +import os +import time +import pygame +from os.path import isdir, join +from libstormgames import speak + +def get_available_games(): + """Get list of available game directories in levels folder. + + Returns: + list: List of game directory names + """ + try: + return [d for d in os.listdir("levels") if isdir(join("levels", d))] + except FileNotFoundError: + return [] + +def selection_menu(sounds, *options): + """Display level selection menu. + + Args: + sounds (dict): Dictionary of loaded sound effects + *options: Variable number of menu options + + Returns: + str: Selected option or None if cancelled + """ + loop = True + pygame.mixer.stop() + i = 0 + j = -1 + + # Clear any pending events + pygame.event.clear() + + speak("Select an adventure") + time.sleep(1.0) + + while loop: + if i != j: + speak(options[i]) + j = i + + pygame.event.pump() + event = pygame.event.wait() + + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: + return None + + if event.key == pygame.K_DOWN and i < len(options) - 1: + i = i + 1 + try: + sounds['menu-move'].play() + except: + pass + + if event.key == pygame.K_UP and i > 0: + i = i - 1 + try: + sounds['menu-move'].play() + except: + pass + + if event.key == pygame.K_HOME and i != 0: + i = 0 + try: + sounds['menu-move'].play() + except: + pass + + if event.key == pygame.K_END and i != len(options) - 1: + i = len(options) - 1 + try: + sounds['menu-move'].play() + except: + pass + + if event.key == pygame.K_RETURN: + try: + sounds['menu-select'].play() + time.sleep(sounds['menu-select'].get_length()) + except: + pass + return options[i] + elif event.type == pygame.QUIT: + return None + + pygame.event.pump() + event = pygame.event.clear() + time.sleep(0.001) + +def select_game(sounds): + """Display game selection menu and return chosen game. + + Args: + sounds (dict): Dictionary of loaded sound effects + + Returns: + str: Selected game directory name or None if cancelled + """ + availableGames = get_available_games() + + if not availableGames: + speak("No games found in levels directory!") + return None + + # Convert directory names to display names (replace underscores with spaces) + menuOptions = [game.replace("_", " ") for game in availableGames] + + choice = selection_menu(sounds, *menuOptions) + + if choice is None: + return None + + # Convert display name back to directory name if needed + gameDir = choice.replace(" ", "_") + if gameDir not in availableGames: + gameDir = choice # Use original if conversion doesn't match + + return gameDir + +def get_level_path(gameDir, levelNum): + """Get full path to level JSON file. + + Args: + gameDir (str): Game directory name + levelNum (int): Level number + + Returns: + str: Full path to level JSON file + """ + return os.path.join("levels", gameDir, f"{levelNum}.json") diff --git a/wicked_quest.py b/wicked_quest.py index 11cad4c..e640d31 100644 --- a/wicked_quest.py +++ b/wicked_quest.py @@ -5,6 +5,7 @@ from libstormgames import * from src.level import Level from src.object import Object from src.player import Player +from src.game_selection import select_game, get_level_path class WickedQuest: @@ -15,24 +16,29 @@ class WickedQuest: self.gameStartTime = None self.lastThrowTime = 0 self.throwDelay = 250 - self.player = None # Will be initialized when first level loads + self.player = None + self.currentGame = None def load_level(self, levelNumber): """Load a level from its JSON file.""" - levelFile = f"levels/{levelNumber}.json" + levelFile = get_level_path(self.currentGame, levelNumber) + pygame.event.pump() try: with open(levelFile, 'r') as f: levelData = json.load(f) # Create player if this is the first level if self.player is None: - self.player = Player(levelData["player_start"]["x"], levelData["player_start"]["y"], self.sounds) + self.player = Player(levelData["player_start"]["x"], + levelData["player_start"]["y"], + self.sounds) else: # Just update player position for new level self.player.xPos = levelData["player_start"]["x"] self.player.yPos = levelData["player_start"]["y"] # Pass existing player to new level + pygame.event.pump() self.currentLevel = Level(levelData, self.sounds, self.player) # Announce level details @@ -49,6 +55,7 @@ class WickedQuest: keys = pygame.key.get_pressed() player = self.currentLevel.player currentTime = pygame.time.get_ticks() + pygame.event.pump() # Update running and ducking states if keys[pygame.K_s] and not player.isDucking: @@ -162,18 +169,19 @@ class WickedQuest: while True: currentTime = pygame.time.get_ticks() + pygame.event.pump() # Game volume controls for event in pygame.event.get(): if event.type == pygame.KEYDOWN: # Check for Alt modifier mods = pygame.key.get_mods() - alt_pressed = mods & pygame.KMOD_ALT + altPressed = mods & pygame.KMOD_ALT if event.key == pygame.K_ESCAPE: return # Volume controls (require Alt) - elif alt_pressed: + elif altPressed: if event.key == pygame.K_PAGEUP: adjust_master_volume(0.1) elif event.key == pygame.K_PAGEDOWN: @@ -233,15 +241,18 @@ class WickedQuest: def run(self): """Main game loop with menu system.""" while True: - choice = game_menu(self.sounds, "play", "instructions", "learn_sounds", "credits", "donate", "exit") + choice = game_menu(self.sounds, "play", "instructions", "learn_sounds", + "credits", "donate", "exit") if choice == "exit": exit_game() elif choice == "play": - self.player = None # Reset player for new game - self.gameStartTime = pygame.time.get_ticks() # Set game start time here - if self.load_level(1): - self.game_loop() + self.currentGame = select_game(self.sounds) + if self.currentGame: + self.player = None # Reset player for new game + self.gameStartTime = pygame.time.get_ticks() + if self.load_level(1): + self.game_loop() elif choice == "learn_sounds": choice = learn_sounds(self.sounds)