libstormgames/__init__.py
2025-02-16 17:04:59 -05:00

1044 lines
34 KiB
Python
Executable File

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""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
"""
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
import textwrap
import webbrowser
import math
import numpy as np
import time
import wx
# 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()
# Configuration objects
localConfig = configparser.ConfigParser()
globalConfig = configparser.ConfigParser()
# Volume control globals
bgmVolume = 0.75 # Default background music volume
sfxVolume = 1.0 # Default sound effects volume
masterVolume = 1.0 # Default master volume
# Handle speech delays so we don't get stuttering
_lastSpoken = {"text": None, "time": 0}
_speechDelay = 250 # ms
class Scoreboard:
"""Handles high score tracking with player names."""
def __init__(self, score=0):
"""Initialize scoreboard with optional starting score."""
read_config()
self.currentScore = score
self.highScores = []
try:
localConfig.add_section("scoreboard")
except:
pass
# Load existing high scores
for i in range(1, 11):
try:
score = localConfig.getint("scoreboard", f"score_{i}")
name = localConfig.get("scoreboard", f"name_{i}")
self.highScores.append({
'name': name,
'score': score
})
except:
self.highScores.append({
'name': "Player",
'score': 0
})
# Sort high scores by score value in descending order
self.highScores.sort(key=lambda x: x['score'], reverse=True)
def get_score(self):
"""Get current score."""
return self.currentScore
def get_high_scores(self):
"""Get list of high scores."""
return self.highScores
def decrease_score(self, points=1):
"""Decrease the current score."""
self.currentScore -= int(points)
def increase_score(self, points=1):
"""Increase the current score."""
self.currentScore += int(points)
def check_high_score(self):
"""Check if current score qualifies as a high score.
Returns:
int: Position (1-10) if high score, None if not
"""
for i, entry in enumerate(self.highScores):
if self.currentScore > entry['score']:
return i + 1
return None
def add_high_score(self):
"""Add current score to high scores if it qualifies.
Returns:
bool: True if score was added, False if not
"""
position = self.check_high_score()
if position is None:
return False
# Prompt for name using get_input
name = get_input("New high score! Enter your name:", "Player")
if name is None: # User cancelled
name = "Player"
# Insert new score at correct position
self.highScores.insert(position - 1, {
'name': name,
'score': self.currentScore
})
# Keep only top 10
self.highScores = self.highScores[:10]
# 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'])
write_config()
speak(f"Congratulations {name}! You got position {position} on the scoreboard!")
time.sleep(1)
return True
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:
with open(gamePath + "/config.ini", 'w') as configfile:
localConfig.write(configfile)
else:
with open(globalPath + "/config.ini", 'w') as configfile:
globalConfig.write(configfile)
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:
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
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:
cut_scene(soundData, 'game-intro')
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)
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
def pause_game():
"""Pauses the game"""
speak("Game paused, press backspace to resume.")
try:
pygame.mixer.pause()
except:
pass
try:
pygame.mixer.music.pause()
except:
pass
while True:
event = pygame.event.wait()
if event.type == pygame.KEYDOWN and not event.key == pygame.K_BACKSPACE: continue
try:
pygame.mixer.unpause()
except:
pass
try:
pygame.mixer.music.unpause()
except:
pass
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
"""
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
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)
"""
current_time = pygame.time.get_ticks()
# Check if this is the same text within the delay window
global lastSpoken
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
if speechProvider == "speechd":
if interrupt:
spd.cancel()
spd.say(text)
else:
if speechProvider == "accessible_output2":
s.speak(text, interrupt=interrupt)
# Display the text on screen
screen = pygame.display.get_surface()
font = pygame.font.Font(None, 36)
# Wrap the text
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])
# Render each line
text_surfaces = [font.render(line, True, (255, 255, 255)) for line in wrapped_text]
screen.fill((0, 0, 0)) # Clear screen with black
# Calculate total height of text block
total_height = sum(surface.get_height() for surface in text_surfaces)
# Start y-position (centered vertically)
currentY = (screen.get_height() - total_height) // 2
# Blit each line of text
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)
currentY += surface.get_height()
pygame.display.flip()
def check_for_exit():
"""Check if user has pressed escape key.
Returns:
bool: True if escape was pressed, False otherwise
"""
for event in pygame.event.get():
if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
return True
return False
pygame.event.pump()
def exit_game():
"""Clean up and exit the game."""
if speechProvider == "speechd":
spd.close()
pygame.mixer.music.stop()
pygame.quit()
exit()
def calculate_volume_and_pan(player_pos, obj_pos):
"""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
"""
distance = abs(player_pos - obj_pos)
max_distance = 12 # Maximum audible distance
if distance > max_distance:
return 0, 0, 0 # No sound if out of range
# Calculate volume (non-linear scaling for more noticeable changes)
# Apply masterVolume as the maximum possible volume
volume = (((max_distance - distance) / max_distance) ** 1.5) * masterVolume
# 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)
else:
# Player is on the object
left = right = 1
return volume, left, right
def obj_play(sounds, soundName, player_pos, obj_pos, loop=True):
"""Play a sound with positional audio.
Args:
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)
Returns:
pygame.mixer.Channel: Sound channel object, or None if out of range
"""
volume, left, right = calculate_volume_and_pan(player_pos, obj_pos)
if volume == 0:
return None # Don't play if out of range
# Play the sound on a new channel
channel = sounds[soundName].play(-1 if loop else 0)
if channel:
channel.set_volume(volume * left * sfxVolume,
volume * right * sfxVolume)
return channel
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:
return None
volume, left, right = calculate_volume_and_pan(player_pos, obj_pos)
if volume == 0:
channel.stop()
return None
# Apply the volume and pan
channel.set_volume(volume * left * sfxVolume,
volume * right * sfxVolume)
return channel
def obj_stop(channel):
"""Stop a playing sound channel.
Args:
channel: Sound channel to stop
Returns:
None if stopped successfully, otherwise returns original channel
"""
try:
channel.stop()
return None
except:
return channel
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
"""
# 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():
return None
if random.randint(1, 100) > probability:
return None
# Choose a random sound from the list
ambianceSound = random.choice(soundNames)
channel = sounds[ambianceSound].play()
if randomLocation and channel:
leftVolume = random.random() * sfxVolume * masterVolume
rightVolume = random.random() * sfxVolume * masterVolume
channel.set_volume(leftVolume, rightVolume)
return channel
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
"""
key = []
for i in sounds.keys():
if re.match("^" + soundName + ".*", i):
key.append(i)
if not key: # No matching sounds found
return
randomKey = random.choice(key)
if interrupt:
cut_scene(sounds, randomKey)
return
channel = sounds[randomKey].play()
if channel:
channel.set_volume(sfxVolume * masterVolume, sfxVolume * masterVolume)
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.
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
Returns:
pygame.mixer.Channel: Sound channel if played, None otherwise
"""
keys = [k for k in sounds.keys() if k.startswith(soundName)]
if not keys:
return None
randomKey = random.choice(keys)
volume, left, right = calculate_volume_and_pan(player_x, object_x)
if volume == 0:
return None
channel = sounds[randomKey].play()
if channel:
channel.set_volume(volume * left * sfxVolume,
volume * right * sfxVolume)
return channel
def cut_scene(sounds, soundName):
"""Play a sound as a cut scene, stopping other sounds.
Args:
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.
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)
Returns:
pygame.mixer.Channel: Sound channel for updating position/volume,
or None if sound should stop
"""
# Calculate horizontal positioning
volume, left, right = calculate_volume_and_pan(player_x, object_x)
# Calculate vertical fall volume multiplier (0 at max_y, 1 at y=0)
fallMultiplier = 1 - (currentY / max_y)
# Adjust final volumes
finalVolume = volume * fallMultiplier
finalLeft = left * finalVolume
finalRight = right * finalVolume
if existing_channel is not None:
if volume == 0: # Out of audible range
existing_channel.stop()
return None
existing_channel.set_volume(finalLeft * sfxVolume,
finalRight * sfxVolume)
return existing_channel
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:
channel.set_volume(finalLeft * sfxVolume,
finalRight * sfxVolume)
return channel
def display_text(text):
"""Display and speak text with navigation controls.
Allows users to:
- Navigate text line by line with arrow keys (skipping blank lines)
- Listen to full text with space
- Copy current line or full text (preserving blank lines)
- Exit with enter/escape
- 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
Args:
text (list): List of text lines to display
"""
# 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.")
currentIndex = 0
speak(navText[currentIndex])
while True:
event = pygame.event.wait()
if event.type == pygame.KEYDOWN:
# 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
if event.key in [pygame.K_DOWN, pygame.K_s] and currentIndex < len(navText) - 1:
currentIndex += 1
speak(navText[currentIndex])
if event.key in [pygame.K_UP, pygame.K_w] and currentIndex > 0:
currentIndex -= 1
speak(navText[currentIndex])
if event.key == pygame.K_SPACE:
# Join with newlines to preserve spacing in speech
speak('\n'.join(originalText[1:-1]))
if event.key == pygame.K_c:
try:
pyperclip.copy(navText[currentIndex])
speak("Copied " + navText[currentIndex] + " to the clipboard.")
except:
speak("Failed to copy the text to the clipboard.")
if event.key == pygame.K_t:
try:
# Join with newlines to preserve blank lines in full text
pyperclip.copy(''.join(originalText[2:-1]))
speak("Copied entire message to the clipboard.")
except:
speak("Failed to copy the text to the clipboard.")
event = pygame.event.clear()
time.sleep(0.001)
def messagebox(text):
"""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
"""
speak(text + "\nPress any key to repeat or enter to continue.")
while True:
event = pygame.event.wait()
if event.type == pygame.KEYDOWN:
if event.key in (pygame.K_ESCAPE, pygame.K_RETURN):
return
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.
"""
try:
pygame.mixer.music.pause()
except:
pass
try:
with open('files/instructions.txt', 'r') as f:
info = f.readlines()
except:
info = ["Instructions file is missing."]
display_text(info)
try:
pygame.mixer.music.unpause()
except:
pass
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.
"""
try:
pygame.mixer.music.pause()
except:
pass
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)
try:
pygame.mixer.music.unpause()
except:
pass
def learn_sounds(sounds):
"""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
"""
loop = True
try:
pygame.mixer.music.pause()
except:
pass
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
event = pygame.event.wait()
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
try:
pygame.mixer.music.unpause()
except:
pass
return "menu"
if event.key in [pygame.K_DOWN, pygame.K_s] and currentIndex < len(soundFiles) - 1:
pygame.mixer.stop()
currentIndex += 1
if event.key in [pygame.K_UP, pygame.K_w] and currentIndex > 0:
pygame.mixer.stop()
currentIndex -= 1
if event.key == pygame.K_RETURN:
try:
soundName = soundFiles[currentIndex][:-4]
pygame.mixer.stop()
sounds[soundName].play()
except:
lastSpoken = -1
speak("Could not play sound.")
event = pygame.event.clear()
time.sleep(0.001)
def game_menu(sounds, *options):
"""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
- 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
"""
loop = True
pygame.mixer.stop()
if pygame.mixer.music.get_busy():
pygame.mixer.music.unpause()
else:
try:
play_bgm("sounds/music_menu.ogg")
except:
pass
currentIndex = 0
lastSpoken = -1 # Track last spoken index
while loop:
if currentIndex != lastSpoken:
speak(options[currentIndex])
lastSpoken = currentIndex
event = pygame.event.wait()
if event.type == pygame.KEYDOWN:
# 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
try:
sounds['menu-move'].play()
except:
pass
if options[currentIndex] != "donate":
pygame.mixer.music.unpause()
elif event.key in [pygame.K_UP, pygame.K_w] and currentIndex > 0:
currentIndex -= 1
try:
sounds['menu-move'].play()
except:
pass
if options[currentIndex] != "donate":
pygame.mixer.music.unpause()
elif event.key == pygame.K_RETURN:
try:
lastSpoken = -1
try:
sounds['menu-select'].play()
time.sleep(sounds['menu-select'].get_length())
except:
pass
eval(options[currentIndex] + "()")
except:
lastSpoken = -1
pygame.mixer.music.fadeout(500)
try:
pygame.mixer.music.fadeout(750)
time.sleep(1.0)
except:
pass
return options[currentIndex]
event = pygame.event.clear()
time.sleep(0.001)
def donate():
"""Open the donation webpage.
Pauses background music and opens the Ko-fi donation page.
"""
pygame.mixer.music.pause()
webbrowser.open('https://ko-fi.com/stormux')