2025-02-04 05:12:31 -05:00
|
|
|
#!/usr/bin/env python3
|
2020-09-15 19:35:59 -04:00
|
|
|
# -*- coding: utf-8 -*-
|
2025-02-04 05:12:31 -05:00
|
|
|
"""Standard initializations and functions shared by all Storm Games.
|
|
|
|
|
|
|
|
This module provides core functionality for Storm Games including:
|
|
|
|
- Sound and speech handling
|
|
|
|
- Volume controls
|
|
|
|
- Configuration management
|
|
|
|
- Score tracking
|
|
|
|
- GUI initialization
|
|
|
|
- Game menu systems
|
|
|
|
"""
|
2020-09-15 19:35:59 -04:00
|
|
|
|
|
|
|
from sys import exit
|
|
|
|
import configparser
|
|
|
|
import os
|
|
|
|
from os import listdir
|
|
|
|
from os.path import isfile, join
|
|
|
|
from inspect import isfunction
|
|
|
|
from xdg import BaseDirectory
|
|
|
|
from setproctitle import setproctitle
|
|
|
|
import pygame
|
|
|
|
import pyperclip
|
|
|
|
import random
|
|
|
|
import re
|
|
|
|
import requests
|
2024-07-22 16:08:42 -04:00
|
|
|
import textwrap
|
2020-09-15 19:35:59 -04:00
|
|
|
import webbrowser
|
2025-02-04 05:12:31 -05:00
|
|
|
import math
|
|
|
|
import numpy as np
|
|
|
|
import time
|
|
|
|
import wx
|
|
|
|
|
2020-09-15 19:35:59 -04:00
|
|
|
# Global variable for speech provider
|
|
|
|
try:
|
|
|
|
import speechd
|
|
|
|
spd = speechd.Client()
|
|
|
|
speechProvider = "speechd"
|
|
|
|
except ImportError:
|
|
|
|
import accessible_output2.outputs.auto
|
|
|
|
s = accessible_output2.outputs.auto.Auto()
|
|
|
|
speechProvider = "accessible_output2"
|
|
|
|
except ImportError:
|
|
|
|
print("No other speech providers found.")
|
|
|
|
exit()
|
|
|
|
|
2025-02-04 05:12:31 -05:00
|
|
|
# Configuration objects
|
2020-09-15 19:35:59 -04:00
|
|
|
localConfig = configparser.ConfigParser()
|
|
|
|
globalConfig = configparser.ConfigParser()
|
|
|
|
|
2025-02-04 05:12:31 -05:00
|
|
|
# Volume control globals
|
|
|
|
bgmVolume = 0.75 # Default background music volume
|
|
|
|
sfxVolume = 1.0 # Default sound effects volume
|
|
|
|
masterVolume = 1.0 # Default master volume
|
2020-09-15 19:35:59 -04:00
|
|
|
|
2025-02-14 23:58:26 -05:00
|
|
|
# Handle speech delays so we don't get stuttering
|
|
|
|
_lastSpoken = {"text": None, "time": 0}
|
|
|
|
_speechDelay = 250 # ms
|
|
|
|
|
2025-02-04 05:12:31 -05:00
|
|
|
class Scoreboard:
|
2025-02-14 16:45:28 -05:00
|
|
|
"""Handles high score tracking with player names."""
|
2025-02-04 05:12:31 -05:00
|
|
|
|
2025-02-14 16:45:28 -05:00
|
|
|
def __init__(self, score=0):
|
|
|
|
"""Initialize scoreboard with optional starting score."""
|
2020-09-15 19:35:59 -04:00
|
|
|
read_config()
|
2025-02-14 16:45:28 -05:00
|
|
|
self.currentScore = score
|
|
|
|
self.highScores = []
|
2025-02-14 20:46:38 -05:00
|
|
|
|
2020-09-15 19:35:59 -04:00
|
|
|
try:
|
|
|
|
localConfig.add_section("scoreboard")
|
|
|
|
except:
|
|
|
|
pass
|
2025-02-14 20:46:38 -05:00
|
|
|
|
2025-02-14 16:45:28 -05:00
|
|
|
# Load existing high scores
|
2020-09-15 19:35:59 -04:00
|
|
|
for i in range(1, 11):
|
|
|
|
try:
|
2025-02-14 16:45:28 -05:00
|
|
|
score = localConfig.getint("scoreboard", f"score_{i}")
|
|
|
|
name = localConfig.get("scoreboard", f"name_{i}")
|
|
|
|
self.highScores.append({
|
|
|
|
'name': name,
|
|
|
|
'score': score
|
|
|
|
})
|
2020-09-15 19:35:59 -04:00
|
|
|
except:
|
2025-02-14 16:45:28 -05:00
|
|
|
self.highScores.append({
|
|
|
|
'name': "Player",
|
|
|
|
'score': 0
|
|
|
|
})
|
2025-02-14 20:46:38 -05:00
|
|
|
|
|
|
|
# Sort high scores by score value in descending order
|
|
|
|
self.highScores.sort(key=lambda x: x['score'], reverse=True)
|
2025-02-14 16:45:28 -05:00
|
|
|
|
|
|
|
def get_score(self):
|
|
|
|
"""Get current score."""
|
|
|
|
return self.currentScore
|
2025-02-04 05:12:31 -05:00
|
|
|
|
2025-02-14 16:45:28 -05:00
|
|
|
def get_high_scores(self):
|
|
|
|
"""Get list of high scores."""
|
|
|
|
return self.highScores
|
2025-02-04 05:12:31 -05:00
|
|
|
|
2025-02-14 16:45:28 -05:00
|
|
|
def decrease_score(self, points=1):
|
|
|
|
"""Decrease the current score."""
|
2025-02-14 21:37:20 -05:00
|
|
|
self.currentScore -= int(points)
|
2025-02-04 05:12:31 -05:00
|
|
|
|
|
|
|
def increase_score(self, points=1):
|
2025-02-14 16:45:28 -05:00
|
|
|
"""Increase the current score."""
|
2025-02-14 21:37:20 -05:00
|
|
|
self.currentScore += int(points)
|
2025-02-04 05:12:31 -05:00
|
|
|
|
2025-02-14 16:45:28 -05:00
|
|
|
def check_high_score(self):
|
|
|
|
"""Check if current score qualifies as a high score.
|
2025-02-04 05:12:31 -05:00
|
|
|
|
|
|
|
Returns:
|
2025-02-14 16:45:28 -05:00
|
|
|
int: Position (1-10) if high score, None if not
|
2025-02-04 05:12:31 -05:00
|
|
|
"""
|
2025-02-14 16:45:28 -05:00
|
|
|
for i, entry in enumerate(self.highScores):
|
|
|
|
if self.currentScore > entry['score']:
|
2025-02-04 05:12:31 -05:00
|
|
|
return i + 1
|
2020-09-15 19:35:59 -04:00
|
|
|
return None
|
2025-02-14 16:45:28 -05:00
|
|
|
|
2025-02-14 16:50:04 -05:00
|
|
|
def add_high_score(self):
|
2025-02-14 16:45:28 -05:00
|
|
|
"""Add current score to high scores if it qualifies.
|
2025-02-14 16:50:04 -05:00
|
|
|
|
2025-02-14 16:45:28 -05:00
|
|
|
Returns:
|
|
|
|
bool: True if score was added, False if not
|
|
|
|
"""
|
|
|
|
position = self.check_high_score()
|
|
|
|
if position is None:
|
|
|
|
return False
|
2025-02-14 16:50:04 -05:00
|
|
|
|
|
|
|
# Prompt for name using get_input
|
|
|
|
name = get_input("New high score! Enter your name:", "Player")
|
|
|
|
if name is None: # User cancelled
|
|
|
|
name = "Player"
|
|
|
|
|
2025-02-14 16:45:28 -05:00
|
|
|
# Insert new score at correct position
|
|
|
|
self.highScores.insert(position - 1, {
|
|
|
|
'name': name,
|
|
|
|
'score': self.currentScore
|
|
|
|
})
|
2025-02-14 16:50:04 -05:00
|
|
|
|
2025-02-14 16:45:28 -05:00
|
|
|
# Keep only top 10
|
|
|
|
self.highScores = self.highScores[:10]
|
2025-02-14 16:50:04 -05:00
|
|
|
|
2025-02-14 16:45:28 -05:00
|
|
|
# Save to config
|
|
|
|
for i, entry in enumerate(self.highScores):
|
|
|
|
localConfig.set("scoreboard", f"score_{i+1}", str(entry['score']))
|
|
|
|
localConfig.set("scoreboard", f"name_{i+1}", entry['name'])
|
2025-02-14 16:50:04 -05:00
|
|
|
|
2025-02-14 16:45:28 -05:00
|
|
|
write_config()
|
2025-02-14 19:41:10 -05:00
|
|
|
speak(f"Congratulations {name}! You got position {position} on the scoreboard!")
|
2025-02-15 12:32:08 -05:00
|
|
|
time.sleep(1)
|
2025-02-14 16:45:28 -05:00
|
|
|
return True
|
2020-09-15 19:35:59 -04:00
|
|
|
|
2025-02-04 05:12:31 -05:00
|
|
|
|
|
|
|
def write_config(write_global=False):
|
|
|
|
"""Write configuration to file.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
write_global (bool): If True, write to global config, otherwise local (default: False)
|
|
|
|
"""
|
|
|
|
if not write_global:
|
2020-09-15 19:35:59 -04:00
|
|
|
with open(gamePath + "/config.ini", 'w') as configfile:
|
|
|
|
localConfig.write(configfile)
|
|
|
|
else:
|
|
|
|
with open(globalPath + "/config.ini", 'w') as configfile:
|
|
|
|
globalConfig.write(configfile)
|
|
|
|
|
2025-02-04 05:12:31 -05:00
|
|
|
def read_config(read_global=False):
|
|
|
|
"""Read configuration from file.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
read_global (bool): If True, read global config, otherwise local (default: False)
|
|
|
|
"""
|
|
|
|
if not read_global:
|
2020-09-15 19:35:59 -04:00
|
|
|
try:
|
|
|
|
with open(gamePath + "/config.ini", 'r') as configfile:
|
|
|
|
localConfig.read_file(configfile)
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
with open(globalPath + "/config.ini", 'r') as configfile:
|
|
|
|
globalConfig.read_file(configfile)
|
|
|
|
except:
|
|
|
|
pass
|
2024-07-22 16:08:42 -04:00
|
|
|
|
2025-02-04 05:12:31 -05:00
|
|
|
def initialize_gui(gameTitle):
|
|
|
|
"""Initialize the game GUI and sound system.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
gameTitle (str): Title of the game
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
dict: Dictionary of loaded sound objects
|
|
|
|
"""
|
|
|
|
# Check for, and possibly create, storm-games path
|
|
|
|
global globalPath
|
|
|
|
global gamePath
|
|
|
|
global gameName
|
|
|
|
|
|
|
|
globalPath = BaseDirectory.xdg_config_home + "/storm-games"
|
|
|
|
gamePath = globalPath + "/" + str.lower(str.replace(gameTitle, " ", "-"))
|
|
|
|
if not os.path.exists(gamePath):
|
|
|
|
os.makedirs(gamePath)
|
|
|
|
|
|
|
|
# Seed the random generator to the clock
|
|
|
|
random.seed()
|
|
|
|
|
|
|
|
# Set game's name
|
|
|
|
gameName = gameTitle
|
|
|
|
setproctitle(str.lower(str.replace(gameTitle, " ", "")))
|
|
|
|
|
|
|
|
# Initialize pygame
|
|
|
|
pygame.init()
|
|
|
|
pygame.display.set_mode((800, 600))
|
|
|
|
pygame.display.set_caption(gameTitle)
|
|
|
|
|
|
|
|
# Set up audio system
|
|
|
|
pygame.mixer.pre_init(44100, -16, 2, 1024)
|
|
|
|
pygame.mixer.init()
|
|
|
|
pygame.mixer.set_num_channels(32)
|
|
|
|
pygame.mixer.set_reserved(0) # Reserve channel for cut scenes
|
|
|
|
|
|
|
|
# Enable key repeat for volume controls
|
|
|
|
pygame.key.set_repeat(500, 100)
|
|
|
|
|
|
|
|
# Load sound files
|
|
|
|
try:
|
|
|
|
soundFiles = [f for f in listdir("sounds/")
|
|
|
|
if isfile(join("sounds/", f))
|
|
|
|
and (f.split('.')[1].lower() in ["ogg", "wav"])]
|
|
|
|
except Exception as e:
|
|
|
|
print("No sounds found.")
|
|
|
|
speak("No sounds found.", False)
|
|
|
|
soundFiles = []
|
|
|
|
|
|
|
|
# Create dictionary of sound objects
|
|
|
|
soundData = {}
|
|
|
|
for f in soundFiles:
|
|
|
|
soundData[f.split('.')[0]] = pygame.mixer.Sound("sounds/" + f)
|
|
|
|
|
|
|
|
# Play intro sound if available
|
|
|
|
if 'game-intro' in soundData:
|
2025-02-08 16:13:10 -05:00
|
|
|
cut_scene(soundData, 'game-intro')
|
2025-02-04 05:12:31 -05:00
|
|
|
|
|
|
|
return soundData
|
|
|
|
|
|
|
|
def adjust_master_volume(change):
|
|
|
|
"""Adjust the master volume for all sounds.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
change (float): Amount to change volume by (positive or negative)
|
|
|
|
"""
|
|
|
|
global masterVolume
|
|
|
|
masterVolume = max(0.0, min(1.0, masterVolume + change))
|
|
|
|
# Update music volume
|
|
|
|
if pygame.mixer.music.get_busy():
|
|
|
|
pygame.mixer.music.set_volume(bgmVolume * masterVolume)
|
|
|
|
# Update all sound channels
|
|
|
|
for i in range(pygame.mixer.get_num_channels()):
|
|
|
|
channel = pygame.mixer.Channel(i)
|
|
|
|
if channel.get_busy():
|
|
|
|
current_volume = channel.get_volume()
|
|
|
|
if isinstance(current_volume, (int, float)):
|
|
|
|
# Mono audio
|
|
|
|
channel.set_volume(current_volume * masterVolume)
|
|
|
|
else:
|
|
|
|
# Stereo audio
|
|
|
|
left, right = current_volume
|
|
|
|
channel.set_volume(left * masterVolume, right * masterVolume)
|
|
|
|
|
|
|
|
def adjust_bgm_volume(change):
|
|
|
|
"""Adjust only the background music volume.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
change (float): Amount to change volume by (positive or negative)
|
|
|
|
"""
|
|
|
|
global bgmVolume
|
|
|
|
bgmVolume = max(0.0, min(1.0, bgmVolume + change))
|
|
|
|
if pygame.mixer.music.get_busy():
|
|
|
|
pygame.mixer.music.set_volume(bgmVolume * masterVolume)
|
2024-08-01 21:10:27 -04:00
|
|
|
|
2025-02-04 05:12:31 -05:00
|
|
|
def adjust_sfx_volume(change):
|
|
|
|
"""Adjust volume for sound effects only.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
change (float): Amount to change volume by (positive or negative)
|
|
|
|
"""
|
|
|
|
global sfxVolume
|
|
|
|
sfxVolume = max(0.0, min(1.0, sfxVolume + change))
|
|
|
|
# Update all sound channels except reserved ones
|
|
|
|
for i in range(pygame.mixer.get_num_channels()):
|
|
|
|
channel = pygame.mixer.Channel(i)
|
|
|
|
if channel.get_busy():
|
|
|
|
current_volume = channel.get_volume()
|
|
|
|
if isinstance(current_volume, (int, float)):
|
|
|
|
# Mono audio
|
|
|
|
channel.set_volume(current_volume * sfxVolume * masterVolume)
|
|
|
|
else:
|
|
|
|
# Stereo audio
|
|
|
|
left, right = current_volume
|
|
|
|
channel.set_volume(left * sfxVolume * masterVolume,
|
|
|
|
right * sfxVolume * masterVolume)
|
|
|
|
|
|
|
|
def play_bgm(music_file):
|
|
|
|
"""Play background music with proper volume settings.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
music_file (str): Path to the music file to play
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
pygame.mixer.music.stop()
|
|
|
|
pygame.mixer.music.load(music_file)
|
|
|
|
pygame.mixer.music.set_volume(bgmVolume * masterVolume)
|
|
|
|
pygame.mixer.music.play(-1) # Loop indefinitely
|
|
|
|
except Exception as e:
|
|
|
|
pass
|
|
|
|
|
2025-02-16 16:52:29 -05:00
|
|
|
def pause_game():
|
|
|
|
"""Pauses the game"""
|
|
|
|
speak("Game paused, press backspace to resume.")
|
2025-02-16 17:02:33 -05:00
|
|
|
try:
|
|
|
|
pygame.mixer.pause()
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
|
|
|
|
try:
|
|
|
|
pygame.mixer.music.pause()
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
|
2025-02-16 16:52:29 -05:00
|
|
|
while True:
|
|
|
|
event = pygame.event.wait()
|
2025-02-16 17:19:47 -05:00
|
|
|
if event.type == pygame.KEYDOWN and event.key == pygame.K_BACKSPACE: break
|
2025-02-16 17:02:33 -05:00
|
|
|
try:
|
2025-02-16 17:04:59 -05:00
|
|
|
pygame.mixer.unpause()
|
2025-02-16 17:02:33 -05:00
|
|
|
except:
|
|
|
|
pass
|
2025-02-16 16:52:29 -05:00
|
|
|
|
2025-02-16 17:02:33 -05:00
|
|
|
try:
|
2025-02-16 17:04:59 -05:00
|
|
|
pygame.mixer.music.unpause()
|
2025-02-16 17:02:33 -05:00
|
|
|
except:
|
|
|
|
pass
|
2025-02-16 16:52:29 -05:00
|
|
|
|
2025-02-04 05:12:31 -05:00
|
|
|
def get_input(prompt="Enter text:", text=""):
|
|
|
|
"""Display a dialog box for text input.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
prompt (str): Prompt text to display (default: "Enter text:")
|
|
|
|
text (str): Initial text in input box (default: "")
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
str: User input text, or None if cancelled
|
|
|
|
"""
|
2024-08-01 21:10:27 -04:00
|
|
|
app = wx.App(False)
|
|
|
|
dialog = wx.TextEntryDialog(None, prompt, "Input", text)
|
|
|
|
dialog.SetValue(text)
|
|
|
|
if dialog.ShowModal() == wx.ID_OK:
|
|
|
|
userInput = dialog.GetValue()
|
|
|
|
else:
|
|
|
|
userInput = None
|
|
|
|
dialog.Destroy()
|
|
|
|
return userInput
|
|
|
|
|
2025-02-04 05:12:31 -05:00
|
|
|
def speak(text, interrupt=True):
|
|
|
|
"""Speak text using the configured speech provider and display on screen.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
text (str): Text to speak and display
|
|
|
|
interrupt (bool): Whether to interrupt current speech (default: True)
|
|
|
|
"""
|
2025-02-14 23:58:26 -05:00
|
|
|
current_time = pygame.time.get_ticks()
|
|
|
|
|
|
|
|
# Check if this is the same text within the delay window
|
2025-02-15 00:06:23 -05:00
|
|
|
global lastSpoken
|
2025-02-14 23:58:26 -05:00
|
|
|
if (_lastSpoken["text"] == text and
|
|
|
|
current_time - _lastSpoken["time"] < _speechDelay):
|
|
|
|
return
|
|
|
|
|
|
|
|
# Update last spoken tracking
|
|
|
|
_lastSpoken["text"] = text
|
|
|
|
_lastSpoken["time"] = current_time
|
|
|
|
|
|
|
|
# Proceed with speech
|
2020-09-15 19:35:59 -04:00
|
|
|
if speechProvider == "speechd":
|
2025-02-04 05:12:31 -05:00
|
|
|
if interrupt:
|
|
|
|
spd.cancel()
|
2020-09-15 19:35:59 -04:00
|
|
|
spd.say(text)
|
|
|
|
else:
|
|
|
|
if speechProvider == "accessible_output2":
|
2025-02-04 05:12:31 -05:00
|
|
|
s.speak(text, interrupt=interrupt)
|
|
|
|
|
2024-07-22 16:08:42 -04:00
|
|
|
# Display the text on screen
|
|
|
|
screen = pygame.display.get_surface()
|
|
|
|
font = pygame.font.Font(None, 36)
|
|
|
|
# Wrap the text
|
2025-02-04 05:12:31 -05:00
|
|
|
max_width = screen.get_width() - 40 # Leave a 20-pixel margin on each side
|
|
|
|
wrapped_text = textwrap.wrap(text, width=max_width // font.size('A')[0])
|
2024-07-22 16:08:42 -04:00
|
|
|
# Render each line
|
2025-02-04 05:12:31 -05:00
|
|
|
text_surfaces = [font.render(line, True, (255, 255, 255)) for line in wrapped_text]
|
2024-07-22 16:08:42 -04:00
|
|
|
screen.fill((0, 0, 0)) # Clear screen with black
|
|
|
|
# Calculate total height of text block
|
2025-02-04 05:12:31 -05:00
|
|
|
total_height = sum(surface.get_height() for surface in text_surfaces)
|
2024-07-22 16:08:42 -04:00
|
|
|
# Start y-position (centered vertically)
|
2025-02-04 05:12:31 -05:00
|
|
|
currentY = (screen.get_height() - total_height) // 2
|
2024-07-22 16:08:42 -04:00
|
|
|
# Blit each line of text
|
2025-02-04 05:12:31 -05:00
|
|
|
for surface in text_surfaces:
|
|
|
|
text_rect = surface.get_rect(center=(screen.get_width() // 2, currentY + surface.get_height() // 2))
|
|
|
|
screen.blit(surface, text_rect)
|
2024-07-22 16:08:42 -04:00
|
|
|
currentY += surface.get_height()
|
|
|
|
pygame.display.flip()
|
2020-09-15 19:35:59 -04:00
|
|
|
|
2024-07-14 03:51:35 -04:00
|
|
|
def check_for_exit():
|
2025-02-04 05:12:31 -05:00
|
|
|
"""Check if user has pressed escape key.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
bool: True if escape was pressed, False otherwise
|
|
|
|
"""
|
2024-07-14 03:51:35 -04:00
|
|
|
for event in pygame.event.get():
|
2024-07-16 14:22:36 -04:00
|
|
|
if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
|
|
|
|
return True
|
|
|
|
return False
|
2024-07-14 03:51:35 -04:00
|
|
|
pygame.event.pump()
|
|
|
|
|
2020-09-15 19:35:59 -04:00
|
|
|
def exit_game():
|
2025-02-04 05:12:31 -05:00
|
|
|
"""Clean up and exit the game."""
|
|
|
|
if speechProvider == "speechd":
|
|
|
|
spd.close()
|
2020-09-15 19:35:59 -04:00
|
|
|
pygame.mixer.music.stop()
|
|
|
|
pygame.quit()
|
|
|
|
exit()
|
|
|
|
|
2024-07-05 17:24:23 -04:00
|
|
|
def calculate_volume_and_pan(player_pos, obj_pos):
|
2025-02-04 05:12:31 -05:00
|
|
|
"""Calculate volume and stereo panning based on relative positions.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
player_pos (float): Player's position on x-axis
|
|
|
|
obj_pos (float): Object's position on x-axis
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
tuple: (volume, left_vol, right_vol) values between 0 and 1
|
|
|
|
"""
|
2024-07-05 17:24:23 -04:00
|
|
|
distance = abs(player_pos - obj_pos)
|
|
|
|
max_distance = 12 # Maximum audible distance
|
2025-02-04 05:12:31 -05:00
|
|
|
|
2024-07-05 17:24:23 -04:00
|
|
|
if distance > max_distance:
|
|
|
|
return 0, 0, 0 # No sound if out of range
|
2025-02-04 05:12:31 -05:00
|
|
|
|
2024-07-05 17:24:23 -04:00
|
|
|
# Calculate volume (non-linear scaling for more noticeable changes)
|
2025-02-04 05:12:31 -05:00
|
|
|
# Apply masterVolume as the maximum possible volume
|
|
|
|
volume = (((max_distance - distance) / max_distance) ** 1.5) * masterVolume
|
|
|
|
|
2024-07-05 17:24:23 -04:00
|
|
|
# Determine left/right based on relative position
|
|
|
|
if player_pos < obj_pos:
|
|
|
|
# Object is to the right
|
|
|
|
left = max(0, 1 - (obj_pos - player_pos) / max_distance)
|
|
|
|
right = 1
|
|
|
|
elif player_pos > obj_pos:
|
|
|
|
# Object is to the left
|
|
|
|
left = 1
|
|
|
|
right = max(0, 1 - (player_pos - obj_pos) / max_distance)
|
2020-09-15 19:35:59 -04:00
|
|
|
else:
|
2024-07-05 17:24:23 -04:00
|
|
|
# Player is on the object
|
|
|
|
left = right = 1
|
2025-02-04 05:12:31 -05:00
|
|
|
|
2024-07-05 17:24:23 -04:00
|
|
|
return volume, left, right
|
|
|
|
|
2025-02-03 23:49:08 -05:00
|
|
|
def obj_play(sounds, soundName, player_pos, obj_pos, loop=True):
|
|
|
|
"""Play a sound with positional audio.
|
|
|
|
|
|
|
|
Args:
|
2025-02-04 05:12:31 -05:00
|
|
|
sounds (dict): Dictionary of sound objects
|
|
|
|
soundName (str): Name of sound to play
|
|
|
|
player_pos (float): Player's position for audio panning
|
|
|
|
obj_pos (float): Object's position for audio panning
|
|
|
|
loop (bool): Whether to loop the sound (default: True)
|
|
|
|
|
2025-02-03 23:49:08 -05:00
|
|
|
Returns:
|
2025-02-04 05:12:31 -05:00
|
|
|
pygame.mixer.Channel: Sound channel object, or None if out of range
|
2025-02-03 23:49:08 -05:00
|
|
|
"""
|
2024-07-05 17:24:23 -04:00
|
|
|
volume, left, right = calculate_volume_and_pan(player_pos, obj_pos)
|
|
|
|
if volume == 0:
|
|
|
|
return None # Don't play if out of range
|
2025-02-04 05:12:31 -05:00
|
|
|
|
2024-07-05 17:24:23 -04:00
|
|
|
# Play the sound on a new channel
|
2025-02-04 05:12:31 -05:00
|
|
|
channel = sounds[soundName].play(-1 if loop else 0)
|
|
|
|
if channel:
|
|
|
|
channel.set_volume(volume * left * sfxVolume,
|
|
|
|
volume * right * sfxVolume)
|
|
|
|
return channel
|
2025-02-03 23:59:46 -05:00
|
|
|
|
2025-02-04 05:12:31 -05:00
|
|
|
def obj_update(channel, player_pos, obj_pos):
|
|
|
|
"""Update positional audio for a playing sound.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
channel: Sound channel to update
|
|
|
|
player_pos (float): New player position
|
|
|
|
obj_pos (float): New object position
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
pygame.mixer.Channel: Updated channel, or None if sound should stop
|
|
|
|
"""
|
|
|
|
if channel is None:
|
2025-02-03 23:59:46 -05:00
|
|
|
return None
|
2025-02-04 05:12:31 -05:00
|
|
|
|
2025-02-03 23:59:46 -05:00
|
|
|
volume, left, right = calculate_volume_and_pan(player_pos, obj_pos)
|
|
|
|
if volume == 0:
|
2025-02-04 05:12:31 -05:00
|
|
|
channel.stop()
|
2025-02-03 23:59:46 -05:00
|
|
|
return None
|
2025-02-04 05:12:31 -05:00
|
|
|
|
2025-02-03 23:59:46 -05:00
|
|
|
# Apply the volume and pan
|
2025-02-04 05:12:31 -05:00
|
|
|
channel.set_volume(volume * left * sfxVolume,
|
|
|
|
volume * right * sfxVolume)
|
|
|
|
return channel
|
|
|
|
|
|
|
|
def obj_stop(channel):
|
|
|
|
"""Stop a playing sound channel.
|
2024-07-05 17:04:12 -04:00
|
|
|
|
2025-02-04 05:12:31 -05:00
|
|
|
Args:
|
|
|
|
channel: Sound channel to stop
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
None if stopped successfully, otherwise returns original channel
|
|
|
|
"""
|
2020-09-15 19:35:59 -04:00
|
|
|
try:
|
2025-02-04 05:12:31 -05:00
|
|
|
channel.stop()
|
2020-09-15 19:35:59 -04:00
|
|
|
return None
|
|
|
|
except:
|
2025-02-04 05:12:31 -05:00
|
|
|
return channel
|
2020-09-15 19:35:59 -04:00
|
|
|
|
2025-02-04 05:12:31 -05:00
|
|
|
def play_sound(sound, volume=1.0):
|
|
|
|
"""Play a sound with current volume settings applied.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
sound: pygame Sound object to play
|
|
|
|
volume: base volume for the sound (0.0-1.0, default: 1.0)
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
pygame.mixer.Channel: The channel the sound is playing on
|
|
|
|
"""
|
|
|
|
channel = sound.play()
|
|
|
|
if channel:
|
|
|
|
channel.set_volume(volume * sfxVolume * masterVolume)
|
|
|
|
return channel
|
|
|
|
|
|
|
|
def play_ambiance(sounds, soundNames, probability, randomLocation=False):
|
|
|
|
"""Play random ambient sounds with optional positional audio.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
sounds (dict): Dictionary of sound objects
|
|
|
|
soundNames (list): List of possible sound names to choose from
|
|
|
|
probability (int): Chance to play (1-100)
|
|
|
|
randomLocation (bool): Whether to randomize stereo position
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
pygame.mixer.Channel: Sound channel if played, None otherwise
|
|
|
|
"""
|
2024-07-14 03:51:35 -04:00
|
|
|
# Check if any of the sounds in the list is already playing
|
|
|
|
for soundName in soundNames:
|
|
|
|
if pygame.mixer.find_channel(True) and pygame.mixer.find_channel(True).get_busy():
|
2025-02-04 05:12:31 -05:00
|
|
|
return None
|
|
|
|
|
2024-07-14 03:51:35 -04:00
|
|
|
if random.randint(1, 100) > probability:
|
2025-02-04 05:12:31 -05:00
|
|
|
return None
|
|
|
|
|
2024-07-14 03:51:35 -04:00
|
|
|
# Choose a random sound from the list
|
|
|
|
ambianceSound = random.choice(soundNames)
|
|
|
|
channel = sounds[ambianceSound].play()
|
2025-02-04 05:12:31 -05:00
|
|
|
|
2024-07-14 03:51:35 -04:00
|
|
|
if randomLocation and channel:
|
2025-02-04 05:12:31 -05:00
|
|
|
leftVolume = random.random() * sfxVolume * masterVolume
|
|
|
|
rightVolume = random.random() * sfxVolume * masterVolume
|
|
|
|
channel.set_volume(leftVolume, rightVolume)
|
|
|
|
|
|
|
|
return channel
|
2024-07-14 03:51:35 -04:00
|
|
|
|
2025-02-04 05:12:31 -05:00
|
|
|
def play_random(sounds, soundName, pause=False, interrupt=False):
|
|
|
|
"""Play a random variation of a sound.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
sounds (dict): Dictionary of sound objects
|
|
|
|
soundName (str): Base name of sound (will match all starting with this)
|
|
|
|
pause (bool): Whether to pause execution until sound finishes
|
|
|
|
interrupt (bool): Whether to interrupt other sounds
|
|
|
|
"""
|
2020-09-15 19:35:59 -04:00
|
|
|
key = []
|
|
|
|
for i in sounds.keys():
|
|
|
|
if re.match("^" + soundName + ".*", i):
|
|
|
|
key.append(i)
|
2025-02-04 05:12:31 -05:00
|
|
|
|
|
|
|
if not key: # No matching sounds found
|
|
|
|
return
|
|
|
|
|
2020-09-15 19:35:59 -04:00
|
|
|
randomKey = random.choice(key)
|
2025-02-04 05:12:31 -05:00
|
|
|
|
|
|
|
if interrupt:
|
2020-09-15 19:35:59 -04:00
|
|
|
cut_scene(sounds, randomKey)
|
|
|
|
return
|
|
|
|
|
2025-02-04 05:12:31 -05:00
|
|
|
channel = sounds[randomKey].play()
|
|
|
|
if channel:
|
|
|
|
channel.set_volume(sfxVolume * masterVolume, sfxVolume * masterVolume)
|
2025-02-02 17:04:04 -05:00
|
|
|
|
2025-02-04 05:12:31 -05:00
|
|
|
if pause:
|
|
|
|
time.sleep(sounds[randomKey].get_length())
|
|
|
|
|
|
|
|
def play_random_positional(sounds, soundName, player_x, object_x):
|
|
|
|
"""Play a random variation of a sound with positional audio.
|
2025-02-02 17:04:04 -05:00
|
|
|
|
2025-02-04 05:12:31 -05:00
|
|
|
Args:
|
|
|
|
sounds (dict): Dictionary of sound objects
|
|
|
|
soundName (str): Base name of sound to match
|
|
|
|
player_x (float): Player's x position
|
|
|
|
object_x (float): Object's x position
|
|
|
|
|
2025-02-02 17:04:04 -05:00
|
|
|
Returns:
|
2025-02-04 05:12:31 -05:00
|
|
|
pygame.mixer.Channel: Sound channel if played, None otherwise
|
2025-02-02 17:04:04 -05:00
|
|
|
"""
|
|
|
|
keys = [k for k in sounds.keys() if k.startswith(soundName)]
|
|
|
|
if not keys:
|
|
|
|
return None
|
|
|
|
|
|
|
|
randomKey = random.choice(keys)
|
2025-02-04 05:12:31 -05:00
|
|
|
volume, left, right = calculate_volume_and_pan(player_x, object_x)
|
|
|
|
|
2025-02-02 17:04:04 -05:00
|
|
|
if volume == 0:
|
|
|
|
return None
|
|
|
|
|
|
|
|
channel = sounds[randomKey].play()
|
|
|
|
if channel:
|
2025-02-04 05:12:31 -05:00
|
|
|
channel.set_volume(volume * left * sfxVolume,
|
|
|
|
volume * right * sfxVolume)
|
2025-02-02 17:04:04 -05:00
|
|
|
return channel
|
|
|
|
|
2025-02-04 05:12:31 -05:00
|
|
|
def cut_scene(sounds, soundName):
|
|
|
|
"""Play a sound as a cut scene, stopping other sounds.
|
2025-02-02 17:04:04 -05:00
|
|
|
|
|
|
|
Args:
|
2025-02-04 05:12:31 -05:00
|
|
|
sounds (dict): Dictionary of sound objects
|
|
|
|
soundName (str): Name of sound to play
|
|
|
|
"""
|
|
|
|
pygame.event.clear()
|
|
|
|
pygame.mixer.stop()
|
|
|
|
channel = pygame.mixer.Channel(0)
|
|
|
|
channel.play(sounds[soundName])
|
|
|
|
while pygame.mixer.get_busy():
|
|
|
|
event = pygame.event.poll()
|
|
|
|
if event.type == pygame.KEYDOWN and event.key in [pygame.K_ESCAPE, pygame.K_RETURN, pygame.K_SPACE]:
|
|
|
|
pygame.mixer.stop()
|
|
|
|
pygame.event.pump()
|
|
|
|
|
|
|
|
def play_random_falling(sounds, soundName, player_x, object_x, start_y,
|
|
|
|
currentY=0, max_y=20, existing_channel=None):
|
|
|
|
"""Play or update a falling sound with positional audio and volume based on height.
|
2025-02-02 17:04:04 -05:00
|
|
|
|
2025-02-04 05:12:31 -05:00
|
|
|
Args:
|
|
|
|
sounds (dict): Dictionary of sound objects
|
|
|
|
soundName (str): Base name of sound to match
|
|
|
|
player_x (float): Player's x position
|
|
|
|
object_x (float): Object's x position
|
|
|
|
start_y (float): Starting Y position (0-20, higher = quieter start)
|
|
|
|
currentY (float): Current Y position (0 = ground level) (default: 0)
|
|
|
|
max_y (float): Maximum Y value (default: 20)
|
|
|
|
existing_channel: Existing sound channel to update (default: None)
|
|
|
|
|
2025-02-02 17:04:04 -05:00
|
|
|
Returns:
|
2025-02-04 05:12:31 -05:00
|
|
|
pygame.mixer.Channel: Sound channel for updating position/volume,
|
|
|
|
or None if sound should stop
|
2025-02-02 17:04:04 -05:00
|
|
|
"""
|
|
|
|
# Calculate horizontal positioning
|
2025-02-04 05:12:31 -05:00
|
|
|
volume, left, right = calculate_volume_and_pan(player_x, object_x)
|
2025-02-03 21:58:02 -05:00
|
|
|
|
2025-02-04 05:12:31 -05:00
|
|
|
# Calculate vertical fall volume multiplier (0 at max_y, 1 at y=0)
|
|
|
|
fallMultiplier = 1 - (currentY / max_y)
|
2025-02-02 17:04:04 -05:00
|
|
|
|
2025-02-03 21:58:02 -05:00
|
|
|
# Adjust final volumes
|
|
|
|
finalVolume = volume * fallMultiplier
|
|
|
|
finalLeft = left * finalVolume
|
|
|
|
finalRight = right * finalVolume
|
|
|
|
|
2025-02-04 05:12:31 -05:00
|
|
|
if existing_channel is not None:
|
2025-02-03 21:58:02 -05:00
|
|
|
if volume == 0: # Out of audible range
|
2025-02-04 05:12:31 -05:00
|
|
|
existing_channel.stop()
|
2025-02-03 21:58:02 -05:00
|
|
|
return None
|
2025-02-04 05:12:31 -05:00
|
|
|
existing_channel.set_volume(finalLeft * sfxVolume,
|
|
|
|
finalRight * sfxVolume)
|
|
|
|
return existing_channel
|
2025-02-03 21:58:02 -05:00
|
|
|
else: # Need to create new channel
|
|
|
|
if volume == 0: # Don't start if out of range
|
|
|
|
return None
|
|
|
|
|
|
|
|
# Find matching sound files
|
|
|
|
keys = [k for k in sounds.keys() if k.startswith(soundName)]
|
|
|
|
if not keys:
|
|
|
|
return None
|
|
|
|
|
|
|
|
randomKey = random.choice(keys)
|
|
|
|
channel = sounds[randomKey].play()
|
|
|
|
if channel:
|
2025-02-04 05:12:31 -05:00
|
|
|
channel.set_volume(finalLeft * sfxVolume,
|
|
|
|
finalRight * sfxVolume)
|
2025-02-03 21:58:02 -05:00
|
|
|
return channel
|
2025-02-02 17:04:04 -05:00
|
|
|
|
2020-09-15 19:35:59 -04:00
|
|
|
def display_text(text):
|
2025-02-04 05:12:31 -05:00
|
|
|
"""Display and speak text with navigation controls.
|
|
|
|
|
|
|
|
Allows users to:
|
2025-02-15 02:39:14 -05:00
|
|
|
- Navigate text line by line with arrow keys (skipping blank lines)
|
2025-02-04 05:12:31 -05:00
|
|
|
- Listen to full text with space
|
2025-02-15 02:39:14 -05:00
|
|
|
- Copy current line or full text (preserving blank lines)
|
2025-02-04 05:12:31 -05:00
|
|
|
- Exit with enter/escape
|
2025-02-08 19:21:14 -05:00
|
|
|
- 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
|
2025-02-04 05:12:31 -05:00
|
|
|
|
|
|
|
Args:
|
|
|
|
text (list): List of text lines to display
|
|
|
|
"""
|
2025-02-15 02:39:14 -05:00
|
|
|
# Store original text with blank lines for copying
|
|
|
|
originalText = text.copy()
|
|
|
|
|
|
|
|
# Create navigation text by filtering out blank lines
|
|
|
|
navText = [line for line in text if line.strip()]
|
|
|
|
|
|
|
|
# Add instructions at the start
|
|
|
|
instructions = ("Press space to read the whole text. Use up and down arrows to navigate "
|
|
|
|
"the text line by line. Press c to copy the current line to the clipboard "
|
|
|
|
"or t to copy the entire text. Press enter or escape when you are done reading.")
|
|
|
|
navText.insert(0, instructions)
|
|
|
|
|
|
|
|
# Add end marker
|
|
|
|
navText.append("End of text.")
|
|
|
|
|
2025-02-04 05:12:31 -05:00
|
|
|
currentIndex = 0
|
2025-02-15 02:39:14 -05:00
|
|
|
speak(navText[currentIndex])
|
2025-02-04 05:12:31 -05:00
|
|
|
|
2020-09-15 19:35:59 -04:00
|
|
|
while True:
|
|
|
|
event = pygame.event.wait()
|
|
|
|
if event.type == pygame.KEYDOWN:
|
2025-02-08 19:21:14 -05:00
|
|
|
# Check for Alt modifier
|
|
|
|
mods = pygame.key.get_mods()
|
|
|
|
alt_pressed = mods & pygame.KMOD_ALT
|
|
|
|
|
|
|
|
# Volume controls (require Alt)
|
|
|
|
if alt_pressed:
|
|
|
|
if event.key == pygame.K_PAGEUP:
|
|
|
|
adjust_master_volume(0.1)
|
|
|
|
elif event.key == pygame.K_PAGEDOWN:
|
|
|
|
adjust_master_volume(-0.1)
|
|
|
|
elif event.key == pygame.K_HOME:
|
|
|
|
adjust_bgm_volume(0.1)
|
|
|
|
elif event.key == pygame.K_END:
|
|
|
|
adjust_bgm_volume(-0.1)
|
|
|
|
elif event.key == pygame.K_INSERT:
|
|
|
|
adjust_sfx_volume(0.1)
|
|
|
|
elif event.key == pygame.K_DELETE:
|
|
|
|
adjust_sfx_volume(-0.1)
|
|
|
|
else:
|
|
|
|
if event.key in (pygame.K_ESCAPE, pygame.K_RETURN):
|
|
|
|
return
|
2025-02-04 05:12:31 -05:00
|
|
|
|
2025-02-15 02:39:14 -05:00
|
|
|
if event.key in [pygame.K_DOWN, pygame.K_s] and currentIndex < len(navText) - 1:
|
2025-02-08 19:21:14 -05:00
|
|
|
currentIndex += 1
|
2025-02-15 02:39:14 -05:00
|
|
|
speak(navText[currentIndex])
|
2025-02-08 19:21:14 -05:00
|
|
|
|
|
|
|
if event.key in [pygame.K_UP, pygame.K_w] and currentIndex > 0:
|
|
|
|
currentIndex -= 1
|
2025-02-15 02:39:14 -05:00
|
|
|
speak(navText[currentIndex])
|
2025-02-08 19:21:14 -05:00
|
|
|
|
|
|
|
if event.key == pygame.K_SPACE:
|
2025-02-15 02:39:14 -05:00
|
|
|
# Join with newlines to preserve spacing in speech
|
|
|
|
speak('\n'.join(originalText[1:-1]))
|
2025-02-08 19:21:14 -05:00
|
|
|
|
|
|
|
if event.key == pygame.K_c:
|
|
|
|
try:
|
2025-02-15 02:39:14 -05:00
|
|
|
pyperclip.copy(navText[currentIndex])
|
|
|
|
speak("Copied " + navText[currentIndex] + " to the clipboard.")
|
2025-02-08 19:21:14 -05:00
|
|
|
except:
|
|
|
|
speak("Failed to copy the text to the clipboard.")
|
|
|
|
|
|
|
|
if event.key == pygame.K_t:
|
|
|
|
try:
|
2025-02-15 02:39:14 -05:00
|
|
|
# Join with newlines to preserve blank lines in full text
|
2025-02-15 02:55:01 -05:00
|
|
|
pyperclip.copy(''.join(originalText[2:-1]))
|
2025-02-08 19:21:14 -05:00
|
|
|
speak("Copied entire message to the clipboard.")
|
|
|
|
except:
|
|
|
|
speak("Failed to copy the text to the clipboard.")
|
2025-02-04 05:12:31 -05:00
|
|
|
|
2020-09-15 19:35:59 -04:00
|
|
|
event = pygame.event.clear()
|
|
|
|
time.sleep(0.001)
|
|
|
|
|
2025-02-01 14:41:52 -05:00
|
|
|
def messagebox(text):
|
2025-02-04 05:12:31 -05:00
|
|
|
"""Display a simple message box with text.
|
|
|
|
|
|
|
|
Shows a message that can be repeated until the user chooses to continue.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
text (str): Message to display
|
|
|
|
"""
|
2025-02-01 15:14:29 -05:00
|
|
|
speak(text + "\nPress any key to repeat or enter to continue.")
|
2025-02-01 14:41:52 -05:00
|
|
|
while True:
|
|
|
|
event = pygame.event.wait()
|
|
|
|
if event.type == pygame.KEYDOWN:
|
2025-02-04 05:12:31 -05:00
|
|
|
if event.key in (pygame.K_ESCAPE, pygame.K_RETURN):
|
2025-02-01 15:05:13 -05:00
|
|
|
return
|
2025-02-04 05:12:31 -05:00
|
|
|
speak(text + "\nPress any key to repeat or enter to continue.")
|
|
|
|
|
|
|
|
def instructions():
|
|
|
|
"""Display game instructions from file.
|
|
|
|
|
|
|
|
Reads and displays instructions from 'files/instructions.txt'.
|
|
|
|
If file is missing, displays an error message.
|
|
|
|
"""
|
2025-02-08 19:21:14 -05:00
|
|
|
try:
|
|
|
|
pygame.mixer.music.pause()
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
|
2025-02-04 05:12:31 -05:00
|
|
|
try:
|
|
|
|
with open('files/instructions.txt', 'r') as f:
|
|
|
|
info = f.readlines()
|
|
|
|
except:
|
|
|
|
info = ["Instructions file is missing."]
|
|
|
|
display_text(info)
|
|
|
|
|
2025-02-08 19:21:14 -05:00
|
|
|
try:
|
|
|
|
pygame.mixer.music.unpause()
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
|
2025-02-04 05:12:31 -05:00
|
|
|
def credits():
|
|
|
|
"""Display game credits from file.
|
|
|
|
|
|
|
|
Reads and displays credits from 'files/credits.txt'.
|
|
|
|
Adds game name header before displaying.
|
|
|
|
If file is missing, displays an error message.
|
|
|
|
"""
|
2025-02-08 19:21:14 -05:00
|
|
|
try:
|
|
|
|
pygame.mixer.music.pause()
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
|
2025-02-04 05:12:31 -05:00
|
|
|
try:
|
|
|
|
with open('files/credits.txt', 'r') as f:
|
|
|
|
info = f.readlines()
|
|
|
|
# Add the header
|
|
|
|
info.insert(0, gameName + ": brought to you by Storm Dragon")
|
|
|
|
except:
|
|
|
|
info = ["Credits file is missing."]
|
|
|
|
display_text(info)
|
2025-02-01 14:59:46 -05:00
|
|
|
|
2025-02-08 19:21:14 -05:00
|
|
|
try:
|
|
|
|
pygame.mixer.music.unpause()
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
|
2020-09-15 19:35:59 -04:00
|
|
|
def learn_sounds(sounds):
|
2025-02-04 05:12:31 -05:00
|
|
|
"""Interactive menu for learning game sounds.
|
|
|
|
|
|
|
|
Allows users to:
|
|
|
|
- Navigate through available sounds
|
|
|
|
- Play selected sounds
|
|
|
|
- Return to menu with escape key
|
|
|
|
|
|
|
|
Args:
|
|
|
|
sounds (dict): Dictionary of available sound objects
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
str: "menu" if user exits with escape
|
|
|
|
"""
|
2020-09-15 19:35:59 -04:00
|
|
|
loop = True
|
2025-02-08 19:51:52 -05:00
|
|
|
try:
|
|
|
|
pygame.mixer.music.pause()
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
|
2025-02-04 05:12:31 -05:00
|
|
|
currentIndex = 0
|
|
|
|
|
|
|
|
# Get list of available sounds, excluding special sounds
|
|
|
|
soundFiles = [f for f in listdir("sounds/")
|
|
|
|
if isfile(join("sounds/", f))
|
|
|
|
and (f.split('.')[1].lower() in ["ogg", "wav"])
|
|
|
|
and (f.split('.')[0].lower() not in ["game-intro", "music_menu"])
|
|
|
|
and (not f.lower().startswith("_"))]
|
|
|
|
|
|
|
|
# Track last spoken index to avoid repetition
|
|
|
|
lastSpoken = -1
|
|
|
|
|
|
|
|
while loop:
|
|
|
|
if currentIndex != lastSpoken:
|
|
|
|
speak(soundFiles[currentIndex][:-4])
|
|
|
|
lastSpoken = currentIndex
|
|
|
|
|
2020-09-15 19:35:59 -04:00
|
|
|
event = pygame.event.wait()
|
|
|
|
if event.type == pygame.KEYDOWN:
|
2025-02-04 05:12:31 -05:00
|
|
|
if event.key == pygame.K_ESCAPE:
|
2025-02-08 19:51:52 -05:00
|
|
|
try:
|
|
|
|
pygame.mixer.music.unpause()
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
|
2025-02-04 05:12:31 -05:00
|
|
|
return "menu"
|
|
|
|
|
2025-02-08 19:51:52 -05:00
|
|
|
if event.key in [pygame.K_DOWN, pygame.K_s] and currentIndex < len(soundFiles) - 1:
|
2020-09-15 19:35:59 -04:00
|
|
|
pygame.mixer.stop()
|
2025-02-04 05:12:31 -05:00
|
|
|
currentIndex += 1
|
|
|
|
|
2025-02-07 02:12:34 -05:00
|
|
|
if event.key in [pygame.K_UP, pygame.K_w] and currentIndex > 0:
|
2020-09-15 19:35:59 -04:00
|
|
|
pygame.mixer.stop()
|
2025-02-04 05:12:31 -05:00
|
|
|
currentIndex -= 1
|
|
|
|
|
2020-09-15 19:35:59 -04:00
|
|
|
if event.key == pygame.K_RETURN:
|
|
|
|
try:
|
2025-02-04 05:12:31 -05:00
|
|
|
soundName = soundFiles[currentIndex][:-4]
|
2020-09-15 19:35:59 -04:00
|
|
|
pygame.mixer.stop()
|
|
|
|
sounds[soundName].play()
|
|
|
|
except:
|
2025-02-04 05:12:31 -05:00
|
|
|
lastSpoken = -1
|
2020-09-15 19:35:59 -04:00
|
|
|
speak("Could not play sound.")
|
2025-02-04 05:12:31 -05:00
|
|
|
|
2020-09-15 19:35:59 -04:00
|
|
|
event = pygame.event.clear()
|
|
|
|
time.sleep(0.001)
|
|
|
|
|
|
|
|
def game_menu(sounds, *options):
|
2025-02-04 05:12:31 -05:00
|
|
|
"""Display and handle the main game menu.
|
|
|
|
|
|
|
|
Provides menu navigation with:
|
|
|
|
- Up/Down arrows for selection
|
|
|
|
- Home/End for first/last option
|
|
|
|
- Enter to select
|
|
|
|
- Escape to exit
|
2025-02-08 19:21:14 -05:00
|
|
|
- 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
|
2025-02-04 05:12:31 -05:00
|
|
|
"""
|
2020-09-15 19:35:59 -04:00
|
|
|
loop = True
|
|
|
|
pygame.mixer.stop()
|
2025-02-04 05:12:31 -05:00
|
|
|
|
2020-09-15 19:35:59 -04:00
|
|
|
if pygame.mixer.music.get_busy():
|
|
|
|
pygame.mixer.music.unpause()
|
|
|
|
else:
|
2024-07-13 03:03:32 -04:00
|
|
|
try:
|
2025-02-04 05:12:31 -05:00
|
|
|
play_bgm("sounds/music_menu.ogg")
|
2024-07-13 03:03:32 -04:00
|
|
|
except:
|
|
|
|
pass
|
2025-02-04 05:12:31 -05:00
|
|
|
|
|
|
|
currentIndex = 0
|
|
|
|
lastSpoken = -1 # Track last spoken index
|
|
|
|
|
|
|
|
while loop:
|
|
|
|
if currentIndex != lastSpoken:
|
|
|
|
speak(options[currentIndex])
|
|
|
|
lastSpoken = currentIndex
|
|
|
|
|
2020-09-15 19:35:59 -04:00
|
|
|
event = pygame.event.wait()
|
|
|
|
if event.type == pygame.KEYDOWN:
|
2025-02-08 19:21:14 -05:00
|
|
|
# Check for Alt modifier
|
|
|
|
mods = pygame.key.get_mods()
|
|
|
|
alt_pressed = mods & pygame.KMOD_ALT
|
|
|
|
|
|
|
|
# Volume controls (require Alt)
|
|
|
|
if alt_pressed:
|
|
|
|
if event.key == pygame.K_PAGEUP:
|
|
|
|
adjust_master_volume(0.1)
|
|
|
|
elif event.key == pygame.K_PAGEDOWN:
|
|
|
|
adjust_master_volume(-0.1)
|
|
|
|
elif event.key == pygame.K_HOME:
|
|
|
|
adjust_bgm_volume(0.1)
|
|
|
|
elif event.key == pygame.K_END:
|
|
|
|
adjust_bgm_volume(-0.1)
|
|
|
|
elif event.key == pygame.K_INSERT:
|
|
|
|
adjust_sfx_volume(0.1)
|
|
|
|
elif event.key == pygame.K_DELETE:
|
|
|
|
adjust_sfx_volume(-0.1)
|
|
|
|
# Regular menu navigation (no Alt required)
|
|
|
|
else:
|
|
|
|
if event.key == pygame.K_ESCAPE:
|
|
|
|
exit_game()
|
|
|
|
elif event.key == pygame.K_HOME:
|
|
|
|
if currentIndex != 0:
|
|
|
|
currentIndex = 0
|
|
|
|
try:
|
|
|
|
sounds['menu-move'].play()
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
if options[currentIndex] != "donate":
|
|
|
|
pygame.mixer.music.unpause()
|
|
|
|
elif event.key == pygame.K_END:
|
|
|
|
if currentIndex != len(options) - 1:
|
|
|
|
currentIndex = len(options) - 1
|
|
|
|
try:
|
|
|
|
sounds['menu-move'].play()
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
if options[currentIndex] != "donate":
|
|
|
|
pygame.mixer.music.unpause()
|
|
|
|
elif event.key in [pygame.K_DOWN, pygame.K_s] and currentIndex < len(options) - 1:
|
|
|
|
currentIndex += 1
|
2025-02-04 05:12:31 -05:00
|
|
|
try:
|
|
|
|
sounds['menu-move'].play()
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
if options[currentIndex] != "donate":
|
|
|
|
pygame.mixer.music.unpause()
|
2025-02-08 19:21:14 -05:00
|
|
|
elif event.key in [pygame.K_UP, pygame.K_w] and currentIndex > 0:
|
|
|
|
currentIndex -= 1
|
2025-02-04 05:12:31 -05:00
|
|
|
try:
|
|
|
|
sounds['menu-move'].play()
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
if options[currentIndex] != "donate":
|
|
|
|
pygame.mixer.music.unpause()
|
2025-02-08 19:21:14 -05:00
|
|
|
elif event.key == pygame.K_RETURN:
|
2020-09-15 19:35:59 -04:00
|
|
|
try:
|
2025-02-08 19:21:14 -05:00
|
|
|
lastSpoken = -1
|
|
|
|
try:
|
|
|
|
sounds['menu-select'].play()
|
|
|
|
time.sleep(sounds['menu-select'].get_length())
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
eval(options[currentIndex] + "()")
|
2020-09-15 19:35:59 -04:00
|
|
|
except:
|
2025-02-08 19:21:14 -05:00
|
|
|
lastSpoken = -1
|
|
|
|
pygame.mixer.music.fadeout(500)
|
|
|
|
try:
|
|
|
|
pygame.mixer.music.fadeout(750)
|
|
|
|
time.sleep(1.0)
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
|
|
|
|
return options[currentIndex]
|
2025-02-04 05:12:31 -05:00
|
|
|
|
2020-09-15 19:35:59 -04:00
|
|
|
event = pygame.event.clear()
|
|
|
|
time.sleep(0.001)
|
|
|
|
|
|
|
|
def donate():
|
2025-02-04 05:12:31 -05:00
|
|
|
"""Open the donation webpage.
|
|
|
|
|
|
|
|
Pauses background music and opens the Ko-fi donation page.
|
|
|
|
"""
|
2020-09-15 19:35:59 -04:00
|
|
|
pygame.mixer.music.pause()
|
2022-01-09 01:59:30 -05:00
|
|
|
webbrowser.open('https://ko-fi.com/stormux')
|