diff --git a/menu.py b/menu.py index c2c0183..1877e8f 100644 --- a/menu.py +++ b/menu.py @@ -13,32 +13,41 @@ Provides functionality for: import pygame import time import webbrowser +import os from sys import exit from os.path import isfile from os import listdir from os.path import join from inspect import isfunction -from .speech import Speech +from .speech import messagebox, Speech from .sound import adjust_master_volume, adjust_bgm_volume, adjust_sfx_volume, play_bgm from .display import display_text from .services import PathService -def game_menu(sounds, *options): - """Display and handle the main game menu. +def game_menu(sounds, playCallback=None, *customOptions): + """Display and handle the main game menu with standard and custom options. - Provides menu navigation with: + Standard menu structure: + 1. Play (always first) + 2. Custom options (high scores, etc.) + 3. Learn Sounds + 4. Instructions (if available) + 5. Credits (if available) + 6. Donate + 7. Exit + + Handles navigation with: - Up/Down arrows for selection - Home/End for first/last option - Enter to select - Escape to exit - - Volume controls (with Alt modifier): - - Alt+PageUp/PageDown: Master volume up/down - - Alt+Home/End: Background music volume up/down - - Alt+Insert/Delete: Sound effects volume up/down + - Volume controls (with Alt modifier) Args: sounds (dict): Dictionary of sound objects - *options: Variable list of menu option names (strings) + playCallback (function, optional): Callback function for the "play" option. + If None, "play" is returned as a string like other options. + *customOptions: Additional custom options to include after play but before standard ones Returns: str: Selected menu option or "exit" if user pressed escape @@ -46,24 +55,45 @@ def game_menu(sounds, *options): # Get speech instance speech = Speech.get_instance() - loop = True - pygame.mixer.stop() + # Start with Play option + allOptions = ["play"] - if pygame.mixer.music.get_busy(): - pygame.mixer.music.unpause() - else: + # Add custom options (high scores, etc.) + allOptions.extend(customOptions) + + # Add standard options in preferred order + allOptions.append("learn_sounds") + + # Check for instructions file + if os.path.isfile('files/instructions.txt'): + allOptions.append("instructions") + + # Check for credits file + if os.path.isfile('files/credits.txt'): + allOptions.append("credits") + + # Final options + allOptions.extend(["donate", "exit_game"]) + + # Track if music was previously playing + musicWasPlaying = pygame.mixer.music.get_busy() + + # Only start menu music if no music is currently playing + if not musicWasPlaying: try: from .sound import play_bgm play_bgm("sounds/music_menu.ogg") except: pass + loop = True + pygame.mixer.stop() currentIndex = 0 lastSpoken = -1 # Track last spoken index while loop: if currentIndex != lastSpoken: - speech.speak(options[currentIndex]) + speech.speak(allOptions[currentIndex]) lastSpoken = currentIndex event = pygame.event.wait() @@ -98,24 +128,24 @@ def game_menu(sounds, *options): sounds['menu-move'].play() except: pass - if options[currentIndex] != "donate": + if allOptions[currentIndex] != "donate": pygame.mixer.music.unpause() elif event.key == pygame.K_END: - if currentIndex != len(options) - 1: - currentIndex = len(options) - 1 + if currentIndex != len(allOptions) - 1: + currentIndex = len(allOptions) - 1 try: sounds['menu-move'].play() except: pass - if options[currentIndex] != "donate": + if allOptions[currentIndex] != "donate": pygame.mixer.music.unpause() - elif event.key in [pygame.K_DOWN, pygame.K_s] and currentIndex < len(options) - 1: + elif event.key in [pygame.K_DOWN, pygame.K_s] and currentIndex < len(allOptions) - 1: currentIndex += 1 try: sounds['menu-move'].play() except: pass - if options[currentIndex] != "donate": + if allOptions[currentIndex] != "donate": pygame.mixer.music.unpause() elif event.key in [pygame.K_UP, pygame.K_w] and currentIndex > 0: currentIndex -= 1 @@ -123,7 +153,7 @@ def game_menu(sounds, *options): sounds['menu-move'].play() except: pass - if options[currentIndex] != "donate": + if allOptions[currentIndex] != "donate": pygame.mixer.music.unpause() elif event.key == pygame.K_RETURN: try: @@ -134,20 +164,74 @@ def game_menu(sounds, *options): except: pass + selectedOption = allOptions[currentIndex] + # Special case for exit_game with fade - if options[currentIndex] == "exit_game": + if selectedOption == "exit_game": exit_game(500 if pygame.mixer.music.get_busy() else 0) + # Special case for play option + elif selectedOption == "play": + if playCallback: + # If a play callback is provided, call it directly + try: + pygame.mixer.music.fadeout(500) + time.sleep(0.5) + except: + pass + playCallback() + else: + # Otherwise return "play" to the caller + return "play" + # Handle standard options directly + elif selectedOption in ["instructions", "credits", "learn_sounds", "donate"]: + # Pause music before calling the selected function + try: + pygame.mixer.music.pause() + except: + pass + + # Handle standard options + if selectedOption == "instructions": + instructions() + elif selectedOption == "credits": + credits() + elif selectedOption == "learn_sounds": + learn_sounds(sounds) + elif selectedOption == "donate": + donate() + + # Unpause music after function returns + try: + # Check if music is actually paused before trying to unpause + if not pygame.mixer.music.get_busy(): + pygame.mixer.music.unpause() + # If music is already playing, don't try to restart it + except: + # Only start fresh music if no music is playing at all + if not pygame.mixer.music.get_busy(): + try: + from .sound import play_bgm + play_bgm("sounds/music_menu.ogg") + except: + pass + # Return custom options to the calling function else: - eval(options[currentIndex] + "()") - except: + lastSpoken = -1 + try: + pygame.mixer.music.fadeout(500) + time.sleep(0.5) + except: + pass + return selectedOption + except Exception as e: + print(f"Error handling menu selection: {e}") lastSpoken = -1 try: pygame.mixer.music.fadeout(500) time.sleep(0.5) except: pass - - return options[currentIndex] + return allOptions[currentIndex] event = pygame.event.clear() time.sleep(0.001) @@ -169,12 +253,6 @@ def learn_sounds(sounds): # Get speech instance speech = Speech.get_instance() - loop = True - try: - pygame.mixer.music.pause() - except: - pass - currentIndex = 0 # Get list of available sounds, excluding special sounds @@ -187,7 +265,10 @@ def learn_sounds(sounds): # Track last spoken index to avoid repetition lastSpoken = -1 - while loop: + # Flag to track when to exit the loop + returnToMenu = False + + while not returnToMenu: if currentIndex != lastSpoken: speech.speak(soundFiles[currentIndex][:-4]) lastSpoken = currentIndex @@ -195,12 +276,7 @@ def learn_sounds(sounds): event = pygame.event.wait() if event.type == pygame.KEYDOWN: if event.key == pygame.K_ESCAPE: - try: - pygame.mixer.music.unpause() - except: - pass - - return "menu" + returnToMenu = True if event.key in [pygame.K_DOWN, pygame.K_s] and currentIndex < len(soundFiles) - 1: pygame.mixer.stop() @@ -221,6 +297,8 @@ def learn_sounds(sounds): event = pygame.event.clear() time.sleep(0.001) + + return "menu" def instructions(): """Display game instructions from file. @@ -228,11 +306,6 @@ def instructions(): Reads and displays instructions from 'files/instructions.txt'. If file is missing, displays an error message. """ - try: - pygame.mixer.music.pause() - except: - pass - try: with open('files/instructions.txt', 'r') as f: info = f.readlines() @@ -240,11 +313,6 @@ def instructions(): info = ["Instructions file is missing."] display_text(info) - try: - pygame.mixer.music.unpause() - except: - pass - def credits(): """Display game credits from file. @@ -252,11 +320,6 @@ def credits(): Adds game name header before displaying. If file is missing, displays an error message. """ - try: - pygame.mixer.music.pause() - except: - pass - try: with open('files/credits.txt', 'r') as f: info = f.readlines() @@ -269,18 +332,13 @@ def credits(): display_text(info) - try: - pygame.mixer.music.unpause() - except: - pass - def donate(): """Open the donation webpage. - Pauses background music and opens the Ko-fi donation page. + Opens the Ko-fi donation page. """ - pygame.mixer.music.pause() webbrowser.open('https://ko-fi.com/stormux') + messagebox("The donation page has been opened in your browser.") def exit_game(fade=0): """Clean up and exit the game properly.