Compare commits
	
		
			3 Commits
		
	
	
		
			68e72f5d81
			...
			testing
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | e1451f69b0 | ||
|  | 401b2a8527 | ||
|  | 639198e8de | 
							
								
								
									
										942
									
								
								__init__.py
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										942
									
								
								__init__.py
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,942 +1,4 @@ | |||||||
| #!/usr/bin/env python3 | #!/bin/python | ||||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||||
| """Standard initializations and functions shared by all Storm Games. |  | ||||||
|  |  | ||||||
| This module provides core functionality for Storm Games including: | from .libstormgames import * | ||||||
| - 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 |  | ||||||
|  |  | ||||||
| class Scoreboard: |  | ||||||
|     """Handles score tracking and top 10 high scores for games. |  | ||||||
|      |  | ||||||
|     This class manages the scoring system including: |  | ||||||
|     - Score tracking |  | ||||||
|     - High score storage |  | ||||||
|     - Score updates and persistence |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     def __init__(self, starting_score=0): |  | ||||||
|         """Initialize a new scoreboard with optional starting score. |  | ||||||
|          |  | ||||||
|         Args: |  | ||||||
|             starting_score (int): Initial score value (default: 0) |  | ||||||
|         """ |  | ||||||
|         read_config() |  | ||||||
|         try: |  | ||||||
|             localConfig.add_section("scoreboard") |  | ||||||
|         except: |  | ||||||
|             pass |  | ||||||
|         self.score = starting_score |  | ||||||
|         self.old_scores = [] |  | ||||||
|         for i in range(1, 11): |  | ||||||
|             try: |  | ||||||
|                 self.old_scores.insert(i - 1, localConfig.getint("scoreboard", str(i))) |  | ||||||
|             except: |  | ||||||
|                 self.old_scores.insert(i - 1, 0) |  | ||||||
|         for i in range(1, 11): |  | ||||||
|             if self.old_scores[i - 1] is None: |  | ||||||
|                 self.old_scores[i - 1] = 0 |  | ||||||
|  |  | ||||||
|     def __del__(self): |  | ||||||
|         """Save scores when object is destroyed.""" |  | ||||||
|         self.update_scores() |  | ||||||
|         try: |  | ||||||
|             write_config() |  | ||||||
|         except: |  | ||||||
|             pass |  | ||||||
|  |  | ||||||
|     def decrease_score(self, points=1): |  | ||||||
|         """Decrease the current score. |  | ||||||
|          |  | ||||||
|         Args: |  | ||||||
|             points (int): Number of points to decrease (default: 1) |  | ||||||
|         """ |  | ||||||
|         self.score -= points |  | ||||||
|  |  | ||||||
|     def get_high_score(self, position=1): |  | ||||||
|         """Get a high score at specified position. |  | ||||||
|          |  | ||||||
|         Args: |  | ||||||
|             position (int): Position in high score list (1-10, default: 1) |  | ||||||
|              |  | ||||||
|         Returns: |  | ||||||
|             int: Score at specified position |  | ||||||
|         """ |  | ||||||
|         return self.old_scores[position - 1] |  | ||||||
|  |  | ||||||
|     def get_score(self): |  | ||||||
|         """Get current score. |  | ||||||
|          |  | ||||||
|         Returns: |  | ||||||
|             int: Current score |  | ||||||
|         """ |  | ||||||
|         return self.score |  | ||||||
|  |  | ||||||
|     def increase_score(self, points=1): |  | ||||||
|         """Increase the current score. |  | ||||||
|          |  | ||||||
|         Args: |  | ||||||
|             points (int): Number of points to increase (default: 1) |  | ||||||
|         """ |  | ||||||
|         self.score += points |  | ||||||
|  |  | ||||||
|     def new_high_score(self): |  | ||||||
|         """Check if current score qualifies as a new high score. |  | ||||||
|          |  | ||||||
|         Returns: |  | ||||||
|             int: Position of new high score (1-10), or None if not a high score |  | ||||||
|         """ |  | ||||||
|         for i, j in enumerate(self.old_scores): |  | ||||||
|             if self.score > j: |  | ||||||
|                 return i + 1 |  | ||||||
|         return None |  | ||||||
|  |  | ||||||
|     def update_scores(self): |  | ||||||
|         """Update the high score list with current score if qualified.""" |  | ||||||
|         # Update the scores |  | ||||||
|         for i, j in enumerate(self.old_scores): |  | ||||||
|             if self.score > j: |  | ||||||
|                 self.old_scores.insert(i, self.score) |  | ||||||
|                 break |  | ||||||
|         # Only keep the top 10 scores |  | ||||||
|         self.old_scores = self.old_scores[:10] |  | ||||||
|         # Update the scoreboard section of the games config file |  | ||||||
|         for i, j in enumerate(self.old_scores): |  | ||||||
|             localConfig.set("scoreboard", str(i + 1), str(j)) |  | ||||||
|  |  | ||||||
| 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: |  | ||||||
|         soundData['game-intro'].play() |  | ||||||
|         time.sleep(soundData['game-intro'].get_length()) |  | ||||||
|      |  | ||||||
|     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 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 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 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) |  | ||||||
|     """ |  | ||||||
|     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 |  | ||||||
|     - Listen to full text with space |  | ||||||
|     - Copy current line or full text |  | ||||||
|     - Exit with enter/escape |  | ||||||
|      |  | ||||||
|     Args: |  | ||||||
|         text (list): List of text lines to display |  | ||||||
|     """ |  | ||||||
|     currentIndex = 0 |  | ||||||
|     text.insert(0, "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.") |  | ||||||
|     text.append("End of text.") |  | ||||||
|     speak(text[currentIndex]) |  | ||||||
|      |  | ||||||
|     while True: |  | ||||||
|         event = pygame.event.wait() |  | ||||||
|         if event.type == pygame.KEYDOWN: |  | ||||||
|             if event.key in (pygame.K_ESCAPE, pygame.K_RETURN): |  | ||||||
|                 return |  | ||||||
|                  |  | ||||||
|             if event.key == pygame.K_DOWN and currentIndex < len(text) - 1: |  | ||||||
|                 currentIndex += 1 |  | ||||||
|                 speak(text[currentIndex]) |  | ||||||
|                  |  | ||||||
|             if event.key == pygame.K_UP and currentIndex > 0: |  | ||||||
|                 currentIndex -= 1 |  | ||||||
|                 speak(text[currentIndex]) |  | ||||||
|                  |  | ||||||
|             if event.key == pygame.K_SPACE: |  | ||||||
|                 speak(' '.join(text[1:])) |  | ||||||
|                  |  | ||||||
|             if event.key == pygame.K_c: |  | ||||||
|                 try: |  | ||||||
|                     pyperclip.copy(text[currentIndex]) |  | ||||||
|                     speak("Copied " + text[currentIndex] + " to the clipboard.") |  | ||||||
|                 except: |  | ||||||
|                     speak("Failed to copy the text to the clipboard.") |  | ||||||
|                      |  | ||||||
|             if event.key == pygame.K_t: |  | ||||||
|                 try: |  | ||||||
|                     pyperclip.copy(' '.join(text[1:-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: |  | ||||||
|         with open('files/instructions.txt', 'r') as f: |  | ||||||
|             info = f.readlines() |  | ||||||
|     except: |  | ||||||
|         info = ["Instructions file is missing."] |  | ||||||
|     display_text(info) |  | ||||||
|  |  | ||||||
| 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: |  | ||||||
|         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) |  | ||||||
|  |  | ||||||
| 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 |  | ||||||
|     pygame.mixer.music.pause() |  | ||||||
|     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: |  | ||||||
|                 return "menu" |  | ||||||
|                  |  | ||||||
|             if event.key == pygame.K_DOWN and currentIndex < len(soundFiles) - 1: |  | ||||||
|                 pygame.mixer.stop() |  | ||||||
|                 currentIndex += 1 |  | ||||||
|                  |  | ||||||
|             if event.key == pygame.K_UP 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 for all options |  | ||||||
|      |  | ||||||
|     Args: |  | ||||||
|         sounds (dict): Dictionary of sound objects |  | ||||||
|         *options: Variable list of menu options |  | ||||||
|          |  | ||||||
|     Returns: |  | ||||||
|         str: Selected menu option if not handled internally |  | ||||||
|     """ |  | ||||||
|     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: |  | ||||||
|             # Volume controls |  | ||||||
|             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: |  | ||||||
|                 if currentIndex != 0: |  | ||||||
|                     currentIndex = 0 |  | ||||||
|                     try: |  | ||||||
|                         sounds['menu-move'].play() |  | ||||||
|                     except: |  | ||||||
|                         pass |  | ||||||
|                     if options[currentIndex] != "donate": |  | ||||||
|                         pygame.mixer.music.unpause() |  | ||||||
|                 else: |  | ||||||
|                     adjust_bgm_volume(0.1) |  | ||||||
|             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() |  | ||||||
|                 else: |  | ||||||
|                     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) |  | ||||||
|             # Menu navigation |  | ||||||
|             elif event.key == pygame.K_ESCAPE: |  | ||||||
|                 exit_game() |  | ||||||
|             elif event.key == pygame.K_DOWN 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 == pygame.K_UP 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 |  | ||||||
|                     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') |  | ||||||
|   | |||||||
							
								
								
									
										511
									
								
								libstormgames.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										511
									
								
								libstormgames.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,511 @@ | |||||||
|  | #!/bin/python | ||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | """Standard initializations and functions shared by all games.""" | ||||||
|  |  | ||||||
|  | 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 | ||||||
|  | # 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() | ||||||
|  | import math | ||||||
|  | import numpy as np | ||||||
|  | import time | ||||||
|  |  | ||||||
|  | localConfig = configparser.ConfigParser() | ||||||
|  | globalConfig = configparser.ConfigParser() | ||||||
|  |  | ||||||
|  | class scoreboard(): | ||||||
|  |     'Handles scores and top 10' | ||||||
|  |  | ||||||
|  |     def __init__(self, startingScore = 0): | ||||||
|  |         read_config() | ||||||
|  |         try: | ||||||
|  |             localConfig.add_section("scoreboard") | ||||||
|  |         except: | ||||||
|  |             pass | ||||||
|  |         self.score = startingScore | ||||||
|  |         self.oldScores = [] | ||||||
|  |         for i in range(1, 11): | ||||||
|  |             try: | ||||||
|  |                 self.oldScores.insert(i - 1, localConfig.getint("scoreboard", str(i))) | ||||||
|  |             except: | ||||||
|  |                 pass | ||||||
|  |                 self.oldScores.insert(i - 1, 0) | ||||||
|  |         for i in range(1, 11): | ||||||
|  |             if self.oldScores[i - 1] == None: | ||||||
|  |                 self.oldScores[i - 1] = 0 | ||||||
|  |  | ||||||
|  |     def __del__(self): | ||||||
|  |         self.Update_Scores() | ||||||
|  |         try: | ||||||
|  |             write_config() | ||||||
|  |         except: | ||||||
|  |             pass | ||||||
|  |  | ||||||
|  |     def Decrease_Score(self, points = 1): | ||||||
|  |         self.score -= points | ||||||
|  |  | ||||||
|  |     def Get_High_Score(self, position = 1): | ||||||
|  |         return self.oldScores[position - 1] | ||||||
|  |  | ||||||
|  |     def Get_Score(self): | ||||||
|  |         return self.score | ||||||
|  |  | ||||||
|  |     def Increase_Score(self, points = 1): | ||||||
|  |         self.score += points | ||||||
|  |  | ||||||
|  |     def New_High_Score(self): | ||||||
|  |         for i, j in enumerate(self.oldScores): | ||||||
|  |             if self.score > j: return i + 1 | ||||||
|  |         return None | ||||||
|  |  | ||||||
|  |     def Update_Scores(self): | ||||||
|  |         # Update the scores | ||||||
|  |         for i, j in enumerate(self.oldScores): | ||||||
|  |             if self.score > j: | ||||||
|  |                 self.oldScores.insert(i, self.score) | ||||||
|  |                 break | ||||||
|  |         # Only keep the top 10 scores. | ||||||
|  |         self.oldScores = self.oldScores[:10] | ||||||
|  |         # Update the scoreboard section of the games config file. | ||||||
|  |         for i, j in enumerate(self.oldScores): | ||||||
|  |                 localConfig.set("scoreboard", str(i + 1), str(j)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def write_config(writeGlobal = False): | ||||||
|  |     if writeGlobal == False: | ||||||
|  |         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(readGlobal = False): | ||||||
|  |     if readGlobal == False: | ||||||
|  |         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 speak(text, interupt=True): | ||||||
|  |     if speechProvider == "speechd": | ||||||
|  |         if interupt: spd.cancel() | ||||||
|  |         spd.say(text) | ||||||
|  |     else: | ||||||
|  |         if speechProvider == "accessible_output2": | ||||||
|  |             s.speak(text, interrupt=True) | ||||||
|  |     # Display the text on screen | ||||||
|  |     screen = pygame.display.get_surface() | ||||||
|  |     font = pygame.font.Font(None, 36) | ||||||
|  |     # Wrap the text | ||||||
|  |     maxWidth = screen.get_width() - 40  # Leave a 20-pixel margin on each side | ||||||
|  |     wrappedText = textwrap.wrap(text, width=maxWidth // font.size('A')[0]) | ||||||
|  |     # Render each line | ||||||
|  |     textSurfaces = [font.render(line, True, (255, 255, 255)) for line in wrappedText] | ||||||
|  |     screen.fill((0, 0, 0))  # Clear screen with black | ||||||
|  |     # Calculate total height of text block | ||||||
|  |     totalHeight = sum(surface.get_height() for surface in textSurfaces) | ||||||
|  |     # Start y-position (centered vertically) | ||||||
|  |     currentY = (screen.get_height() - totalHeight) // 2 | ||||||
|  |     # Blit each line of text | ||||||
|  |     for surface in textSurfaces: | ||||||
|  |         textRect = surface.get_rect(center=(screen.get_width() // 2, currentY + surface.get_height() // 2)) | ||||||
|  |         screen.blit(surface, textRect) | ||||||
|  |         currentY += surface.get_height() | ||||||
|  |     pygame.display.flip() | ||||||
|  |  | ||||||
|  | def check_for_exit(): | ||||||
|  |     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(): | ||||||
|  |     if speechProvider == "speechd": spd.close() | ||||||
|  |     pygame.mixer.music.stop() | ||||||
|  |     pygame.quit() | ||||||
|  |     exit() | ||||||
|  |  | ||||||
|  | def initialize_gui(gameTitle): | ||||||
|  |     # Check for, and possibly create, storm-games path     | ||||||
|  |     global globalPath | ||||||
|  |     global gamePath | ||||||
|  |     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 | ||||||
|  |     global gameName | ||||||
|  |     gameName = gameTitle | ||||||
|  |     setproctitle(str.lower(str.replace(gameTitle, " ", ""))) | ||||||
|  |     # start pygame | ||||||
|  |     pygame.init() | ||||||
|  |     # start the display (required by the event loop) | ||||||
|  |     pygame.display.set_mode((800, 600)) | ||||||
|  |     pygame.display.set_caption(gameTitle) | ||||||
|  |     # Set 32 channels for sound by default | ||||||
|  |     pygame.mixer.pre_init(44100, -16, 2, 1024) | ||||||
|  |     pygame.mixer.init() | ||||||
|  |     pygame.mixer.set_num_channels(32) | ||||||
|  |     # Reserve the cut scene channel | ||||||
|  |     pygame.mixer.set_reserved(0) | ||||||
|  |     # Load sounds from the sound directory and creates a list like {'bottle': 'bottle.ogg'} | ||||||
|  |     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) | ||||||
|  |     #lets make a dict with pygame.mixer.Sound() objects {'bottle':<soundobject>} | ||||||
|  |     soundData = {} | ||||||
|  |     for f in soundFiles: | ||||||
|  |         soundData[f.split('.')[0]] = pygame.mixer.Sound("sounds/" + f) | ||||||
|  |     soundData['game-intro'].play() | ||||||
|  |     time.sleep(soundData['game-intro'].get_length()) | ||||||
|  |     return soundData | ||||||
|  |  | ||||||
|  | def generate_tone(frequency, duration=0.1, sample_rate=44100, volume = 0.2): | ||||||
|  |     t = np.linspace(0, duration, int(sample_rate * duration), False) | ||||||
|  |     tone = np.sin(2 * np.pi * frequency * t) | ||||||
|  |     stereo_tone = np.vstack((tone, tone)).T  # Create a 2D array for stereo | ||||||
|  |     stereo_tone = (stereo_tone * 32767).astype(np.int16) | ||||||
|  |     stereo_tone = (stereo_tone * 32767 * volume).astype(np.int16)  # Apply volume | ||||||
|  |     stereo_tone = np.ascontiguousarray(stereo_tone)  # Ensure C-contiguous array | ||||||
|  |     return pygame.sndarray.make_sound(stereo_tone) | ||||||
|  |  | ||||||
|  | def x_powerbar(): | ||||||
|  |     clock = pygame.time.Clock() | ||||||
|  |     screen = pygame.display.get_surface() | ||||||
|  |     position = -50  # Start from the leftmost position | ||||||
|  |     direction = 1   # Move right initially | ||||||
|  |     barHeight = 20 | ||||||
|  |     while True: | ||||||
|  |         frequency = 440  # A4 note | ||||||
|  |         leftVolume = (50 - position) / 100 | ||||||
|  |         rightVolume = (position + 50) / 100 | ||||||
|  |         tone = generate_tone(frequency) | ||||||
|  |         channel = tone.play() | ||||||
|  |         channel.set_volume(leftVolume, rightVolume) | ||||||
|  |         # Visual representation | ||||||
|  |         screen.fill((0, 0, 0)) | ||||||
|  |         barWidth = screen.get_width() - 40  # Leave 20px margin on each side | ||||||
|  |         pygame.draw.rect(screen, (100, 100, 100), (20, screen.get_height() // 2 - barHeight // 2, barWidth, barHeight)) | ||||||
|  |         markerPos = int(20 + (position + 50) / 100 * barWidth) | ||||||
|  |         pygame.draw.rect(screen, (255, 0, 0), (markerPos - 5, screen.get_height() // 2 - barHeight, 10, barHeight * 2)) | ||||||
|  |         pygame.display.flip() | ||||||
|  |         for event in pygame.event.get(): | ||||||
|  |             check_for_exit() | ||||||
|  |             if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE: | ||||||
|  |                 channel.stop() | ||||||
|  |                 return position  # This will return a value between -50 and 50 | ||||||
|  |         position += direction | ||||||
|  |         if position > 50: | ||||||
|  |             position = 50 | ||||||
|  |             direction = -1 | ||||||
|  |         elif position < -50: | ||||||
|  |             position = -50 | ||||||
|  |             direction = 1 | ||||||
|  |         clock.tick(40) # Speed of bar | ||||||
|  |  | ||||||
|  | def y_powerbar(): | ||||||
|  |     clock = pygame.time.Clock() | ||||||
|  |     screen = pygame.display.get_surface() | ||||||
|  |     power = 0 | ||||||
|  |     direction = 1  # 1 for increasing, -1 for decreasing | ||||||
|  |     barWidth = 20 | ||||||
|  |     while True: | ||||||
|  |         frequency = 220 + (power * 5)  # Adjust these values to change the pitch range | ||||||
|  |         tone = generate_tone(frequency) | ||||||
|  |         channel = tone.play() | ||||||
|  |         # Visual representation | ||||||
|  |         screen.fill((0, 0, 0)) | ||||||
|  |         barHeight = screen.get_height() - 40  # Leave 20px margin on top and bottom | ||||||
|  |         pygame.draw.rect(screen, (100, 100, 100), (screen.get_width() // 2 - barWidth // 2, 20, barWidth, barHeight)) | ||||||
|  |         markerPos = int(20 + (100 - power) / 100 * barHeight) | ||||||
|  |         pygame.draw.rect(screen, (255, 0, 0), (screen.get_width() // 2 - barWidth, markerPos - 5, barWidth * 2, 10)) | ||||||
|  |         pygame.display.flip() | ||||||
|  |         for event in pygame.event.get(): | ||||||
|  |             check_for_exit() | ||||||
|  |             if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE: | ||||||
|  |                 channel.stop() | ||||||
|  |                 return power | ||||||
|  |         power += direction | ||||||
|  |         if power >= 100 or power <= 0: | ||||||
|  |             direction *= -1  # Reverse direction at limits | ||||||
|  |         clock.tick(40) | ||||||
|  |  | ||||||
|  | def cut_scene(sounds, soundName): | ||||||
|  |     pygame.event.clear() | ||||||
|  |     pygame.mixer.stop() | ||||||
|  |     c = pygame.mixer.Channel(0) | ||||||
|  |     c.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 calculate_volume_and_pan(player_pos, obj_pos): | ||||||
|  |     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) | ||||||
|  |     volume = ((max_distance - distance) / max_distance) ** 1.5 | ||||||
|  |     # 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): | ||||||
|  |     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 | ||||||
|  |     x = sounds[soundName].play(-1) | ||||||
|  |     # Apply the volume and pan | ||||||
|  |     x.set_volume(volume * left, volume * right) | ||||||
|  |     return x | ||||||
|  | def obj_update(x, player_pos, obj_pos): | ||||||
|  |     if x is None: | ||||||
|  |         return None | ||||||
|  |     volume, left, right = calculate_volume_and_pan(player_pos, obj_pos) | ||||||
|  |     if volume == 0: | ||||||
|  |         x.stop() | ||||||
|  |         return None | ||||||
|  |     # Apply the volume and pan | ||||||
|  |     x.set_volume(volume * left, volume * right) | ||||||
|  |     return x | ||||||
|  |      | ||||||
|  | def obj_stop(x): | ||||||
|  |     # Tries to stop a playing object channel | ||||||
|  |     try: | ||||||
|  |         x.stop() | ||||||
|  |         return None | ||||||
|  |     except: | ||||||
|  |         return x | ||||||
|  |  | ||||||
|  | def play_ambiance(sounds, soundNames, probability, randomLocation = False): | ||||||
|  |     # 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 | ||||||
|  |     if random.randint(1, 100) > probability: | ||||||
|  |         return | ||||||
|  |     # Choose a random sound from the list | ||||||
|  |     ambianceSound = random.choice(soundNames) | ||||||
|  |     channel = sounds[ambianceSound].play() | ||||||
|  |     if randomLocation and channel: | ||||||
|  |         left_volume = random.random() | ||||||
|  |         right_volume = random.random() | ||||||
|  |         channel.set_volume(left_volume, right_volume) | ||||||
|  |     return channel  # Return the channel object for potential further manipulation | ||||||
|  |  | ||||||
|  | def play_random(sounds, soundName, pause = False, interrupt = False): | ||||||
|  |     key = [] | ||||||
|  |     for i in sounds.keys(): | ||||||
|  |         if re.match("^" + soundName + ".*", i): | ||||||
|  |             key.append(i) | ||||||
|  |     randomKey = random.choice(key) | ||||||
|  |     if interrupt == False: | ||||||
|  |         sounds[randomKey].play() | ||||||
|  |     else: | ||||||
|  |         cut_scene(sounds, randomKey) | ||||||
|  |         # Cut scenes override the pause option | ||||||
|  |         return | ||||||
|  |     if pause == True: | ||||||
|  |         time.sleep(sounds[randomKey].get_length()) | ||||||
|  |      | ||||||
|  | def instructions(): | ||||||
|  |     # Read in the instructions file | ||||||
|  |     try: | ||||||
|  |         with open('files/instructions.txt', 'r') as f: | ||||||
|  |             info = f.readlines() | ||||||
|  |     except: | ||||||
|  |         info = ["Instructions file is missing."] | ||||||
|  |     display_text(info) | ||||||
|  |  | ||||||
|  | def credits(): | ||||||
|  |     # Read in the credits file. | ||||||
|  |     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) | ||||||
|  |  | ||||||
|  | def display_text(text): | ||||||
|  |     i = 0 | ||||||
|  |     text.insert(0, "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.") | ||||||
|  |     text.append("End of text.") | ||||||
|  |     speak(text[i]) | ||||||
|  |     while True: | ||||||
|  |         event = pygame.event.wait() | ||||||
|  |         if event.type == pygame.KEYDOWN: | ||||||
|  |             if event.key == pygame.K_ESCAPE or event.key == pygame.K_RETURN: return | ||||||
|  |             if event.key == pygame.K_DOWN and i < len(text) - 1: i = i + 1 | ||||||
|  |             if event.key == pygame.K_UP and i > 0: i = i - 1 | ||||||
|  |             if event.key == pygame.K_SPACE: | ||||||
|  |                 speak(' '.join(text[1:])) | ||||||
|  |             else: | ||||||
|  |                 speak(text[i]) | ||||||
|  |             if event.key == pygame.K_c: | ||||||
|  |                 try: | ||||||
|  |                     pyperclip.copy(text[i]) | ||||||
|  |                     speak("Copied " + text[i] + " to the clipboard.") | ||||||
|  |                 except: | ||||||
|  |                     speak("Failed to copy the text to the clipboard.") | ||||||
|  |             if event.key == pygame.K_t: | ||||||
|  |                 try: | ||||||
|  |                     pyperclip.copy(' '.join(text[1:-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 learn_sounds(sounds): | ||||||
|  |     loop = True | ||||||
|  |     pygame.mixer.music.pause() | ||||||
|  |     i = 0 | ||||||
|  |     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("_"))] | ||||||
|  |     # j keeps track of last spoken index so it isn't voiced on key up. | ||||||
|  |     j = -1 | ||||||
|  |     while loop == True: | ||||||
|  |         if i != j: | ||||||
|  |             speak(soundFiles[i][:-4]) | ||||||
|  |             j = i | ||||||
|  |         event = pygame.event.wait() | ||||||
|  |         if event.type == pygame.KEYDOWN: | ||||||
|  |             if event.key == pygame.K_ESCAPE: return "menu" | ||||||
|  |             if event.key == pygame.K_DOWN and i < len(soundFiles) - 1: | ||||||
|  |                 pygame.mixer.stop() | ||||||
|  |                 i = i + 1 | ||||||
|  |             if event.key == pygame.K_UP and i > 0: | ||||||
|  |                 pygame.mixer.stop() | ||||||
|  |                 i = i - 1 | ||||||
|  |             if event.key == pygame.K_RETURN: | ||||||
|  |                 try: | ||||||
|  |                     soundName = soundFiles[i][:-4] | ||||||
|  |                     pygame.mixer.stop() | ||||||
|  |                     sounds[soundName].play() | ||||||
|  |                     continue | ||||||
|  |                 except: | ||||||
|  |                     j = -1 | ||||||
|  |                     speak("Could not play sound.") | ||||||
|  |                     continue | ||||||
|  |         event = pygame.event.clear() | ||||||
|  |         time.sleep(0.001) | ||||||
|  |  | ||||||
|  | def game_menu(sounds, *options): | ||||||
|  |     loop = True | ||||||
|  |     pygame.mixer.stop() | ||||||
|  |     if pygame.mixer.music.get_busy(): | ||||||
|  |         pygame.mixer.music.unpause() | ||||||
|  |     else: | ||||||
|  |         try: | ||||||
|  |             pygame.mixer.music.load("sounds/music_menu.ogg") | ||||||
|  |             pygame.mixer.music.set_volume(0.75) | ||||||
|  |             pygame.mixer.music.play(-1) | ||||||
|  |         except: | ||||||
|  |             pass | ||||||
|  |     i = 0 | ||||||
|  |     # j keeps track of last spoken index so it isn't voiced on key up. | ||||||
|  |     j = -1 | ||||||
|  |     while loop == True: | ||||||
|  |         if i != j: | ||||||
|  |             speak(options[i]) | ||||||
|  |             j = i | ||||||
|  |         event = pygame.event.wait() | ||||||
|  |         if event.type == pygame.KEYDOWN: | ||||||
|  |             if event.key == pygame.K_ESCAPE: exit_game() | ||||||
|  |             if event.key == pygame.K_DOWN and i < len(options) - 1: | ||||||
|  |                 i = i + 1 | ||||||
|  |                 try: | ||||||
|  |                     sounds['menu-move'].play() | ||||||
|  |                 except: | ||||||
|  |                     pass | ||||||
|  |                 if options[i] != "donate": pygame.mixer.music.unpause() | ||||||
|  |             if event.key == pygame.K_UP and i > 0: | ||||||
|  |                 i = i - 1 | ||||||
|  |                 try: | ||||||
|  |                     sounds['menu-move'].play() | ||||||
|  |                 except: | ||||||
|  |                     pass | ||||||
|  |                 if options[i] != "donate": pygame.mixer.music.unpause() | ||||||
|  |             if event.key == pygame.K_HOME and i != 0: | ||||||
|  |                 i = 0 | ||||||
|  |                 try: | ||||||
|  |                     sounds['menu-move'].play() | ||||||
|  |                 except: | ||||||
|  |                     pass | ||||||
|  |                 if options[i] != "donate": pygame.mixer.music.unpause() | ||||||
|  |             if event.key == pygame.K_END and i != len(options) - 1: | ||||||
|  |                 i = len(options) -1 | ||||||
|  |                 try: | ||||||
|  |                     sounds['menu-move'].play() | ||||||
|  |                 except: | ||||||
|  |                     pass | ||||||
|  |                 if options[i] != "donate": pygame.mixer.music.unpause() | ||||||
|  |             if event.key == pygame.K_RETURN: | ||||||
|  |                 try: | ||||||
|  |                     j = -1 | ||||||
|  |                     try: | ||||||
|  |                         sounds['menu-select'].play() | ||||||
|  |                         time.sleep(sounds['menu-select'].get_length()) | ||||||
|  |                     except: | ||||||
|  |                         pass | ||||||
|  |                     eval(options[i] + "()") | ||||||
|  |                     continue | ||||||
|  |                 except: | ||||||
|  |                     j = -1 | ||||||
|  |                     return options[i] | ||||||
|  |                     continue | ||||||
|  |         event = pygame.event.clear() | ||||||
|  |         time.sleep(0.001) | ||||||
|  |  | ||||||
|  | def donate(): | ||||||
|  |     pygame.mixer.music.pause() | ||||||
|  |     webbrowser.open('https://ko-fi.com/stormux') | ||||||
		Reference in New Issue
	
	Block a user