2020-09-15 19:35:59 -04:00
#!/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
2024-07-22 16:08:42 -04:00
import textwrap
2020-09-15 19:35:59 -04:00
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
2024-07-13 03:03:32 -04:00
import numpy as np
2020-09-15 19:35:59 -04:00
import time
2024-08-01 21:10:27 -04:00
import wx
2020-09-15 19:35:59 -04:00
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
2024-07-22 16:08:42 -04:00
2024-08-01 21:10:27 -04:00
def get_input ( prompt = " Enter text: " , text = " " ) :
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
2024-07-22 16:08:42 -04:00
def speak ( text , interupt = True ) :
2020-09-15 19:35:59 -04:00
if speechProvider == " speechd " :
2024-07-22 16:08:42 -04:00
if interupt : spd . cancel ( )
2020-09-15 19:35:59 -04:00
spd . say ( text )
else :
if speechProvider == " accessible_output2 " :
s . speak ( text , interrupt = True )
2024-07-22 16:08:42 -04:00
# Display the text on screen
screen = pygame . display . get_surface ( )
font = pygame . font . Font ( None , 36 )
# Wrap the text
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 ( )
2020-09-15 19:35:59 -04:00
2024-07-14 03:51:35 -04:00
def check_for_exit ( ) :
for event in pygame . event . get ( ) :
2024-07-16 14:22:36 -04:00
if event . type == pygame . KEYDOWN and event . key == pygame . K_ESCAPE :
return True
return False
2024-07-14 03:51:35 -04:00
pygame . event . pump ( )
2020-09-15 19:35:59 -04:00
def exit_game ( ) :
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)
2024-07-22 16:08:42 -04:00
pygame . display . set_mode ( ( 800 , 600 ) )
2020-09-15 19:35:59 -04:00
pygame . display . set_caption ( gameTitle )
# Set 32 channels for sound by default
2024-07-13 03:03:32 -04:00
pygame . mixer . pre_init ( 44100 , - 16 , 2 , 1024 )
2020-09-15 19:35:59 -04:00
pygame . mixer . init ( )
pygame . mixer . set_num_channels ( 32 )
# Reserve the cut scene channel
pygame . mixer . set_reserved ( 0 )
2024-07-14 03:51:35 -04:00
# Load sounds from the sound directory and creates a list like {'bottle': 'bottle.ogg'}
2024-07-13 03:03:32 -04:00
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 :
2024-07-14 03:51:35 -04:00
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
2024-07-13 03:03:32 -04:00
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 ( )
2024-07-22 22:20:15 -04:00
screen = pygame . display . get_surface ( )
2024-07-14 03:51:35 -04:00
position = - 50 # Start from the leftmost position
direction = 1 # Move right initially
2024-07-22 22:20:15 -04:00
barHeight = 20
2024-07-13 03:03:32 -04:00
while True :
frequency = 440 # A4 note
2024-07-22 22:20:15 -04:00
leftVolume = ( 50 - position ) / 100
rightVolume = ( position + 50 ) / 100
2024-07-13 03:03:32 -04:00
tone = generate_tone ( frequency )
channel = tone . play ( )
2024-07-22 22:20:15 -04:00
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 ( )
2024-07-13 03:03:32 -04:00
for event in pygame . event . get ( ) :
2024-07-22 22:20:15 -04:00
check_for_exit ( )
2024-07-13 03:03:32 -04:00
if event . type == pygame . KEYDOWN and event . key == pygame . K_SPACE :
channel . stop ( )
2024-07-14 03:51:35 -04:00
return position # This will return a value between -50 and 50
2024-07-13 03:03:32 -04:00
position + = direction
2024-07-14 03:51:35 -04:00
if position > 50 :
position = 50
direction = - 1
elif position < - 50 :
position = - 50
direction = 1
clock . tick ( 40 ) # Speed of bar
2024-07-13 03:03:32 -04:00
def y_powerbar ( ) :
clock = pygame . time . Clock ( )
2024-07-22 22:20:15 -04:00
screen = pygame . display . get_surface ( )
2024-07-13 03:03:32 -04:00
power = 0
direction = 1 # 1 for increasing, -1 for decreasing
2024-07-22 22:20:15 -04:00
barWidth = 20
2024-07-13 03:03:32 -04:00
while True :
frequency = 220 + ( power * 5 ) # Adjust these values to change the pitch range
tone = generate_tone ( frequency )
channel = tone . play ( )
2024-07-22 22:20:15 -04:00
# 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 ( )
2024-07-13 03:03:32 -04:00
for event in pygame . event . get ( ) :
2024-07-22 22:20:15 -04:00
check_for_exit ( )
2024-07-13 03:03:32 -04:00
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
2024-07-14 03:51:35 -04:00
clock . tick ( 40 )
2020-09-15 19:35:59 -04:00
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 ( )
2024-07-05 17:24:23 -04:00
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 )
2020-09-15 19:35:59 -04:00
else :
2024-07-05 17:24:23 -04:00
# Player is on the object
left = right = 1
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
2024-07-05 17:04:12 -04:00
x = sounds [ soundName ] . play ( - 1 )
2024-07-05 17:24:23 -04:00
# Apply the volume and pan
x . set_volume ( volume * left , volume * right )
2024-07-05 17:04:12 -04:00
return x
2024-07-05 17:24:23 -04:00
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 )
2024-07-05 17:04:12 -04:00
return x
2020-09-15 19:35:59 -04:00
def obj_stop ( x ) :
# Tries to stop a playing object channel
try :
x . stop ( )
return None
except :
return x
2024-07-14 03:51:35 -04:00
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
2020-09-15 19:35:59 -04:00
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 ( ) )
2025-02-02 17:04:04 -05:00
def play_random_positional ( sounds , soundName , playerX , objectX ) :
""" Play a random sound with positional audio.
Args :
sounds : Dictionary of sound objects
soundName : Base name of sound ( e . g . ' falling_skull ' will match ' falling_skull1 ' , ' falling_skull2 ' , etc . )
playerX : Player ' s x position for audio panning
objectX : Object ' s x position for audio panning
Returns :
The sound channel object for updating position
"""
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 ( playerX , objectX )
if volume == 0 :
return None
channel = sounds [ randomKey ] . play ( )
if channel :
channel . set_volume ( volume * left , volume * right )
return channel
def play_random_falling ( sounds , soundName , playerX , objectX , startY , currentY = 0 , maxY = 20 ) :
""" Play a random sound with positional audio that increases in volume as it ' falls ' .
Args :
sounds : Dictionary of sound objects
soundName : Base name of sound ( e . g . ' falling_skull ' will match ' falling_skull1 ' , ' falling_skull2 ' , etc . )
playerX : Player ' s x position for audio panning
objectX : Object ' s x position for audio panning
startY : Starting Y position ( 0 - 20 , higher = quieter start )
currentY : Current Y position ( 0 = ground level )
maxY : Maximum Y value ( default 20 )
Returns :
The sound channel object for updating position / volume
"""
keys = [ k for k in sounds . keys ( ) if k . startswith ( soundName ) ]
if not keys :
return None
randomKey = random . choice ( keys )
# Calculate horizontal positioning
volume , left , right = calculate_volume_and_pan ( playerX , objectX )
if volume == 0 :
return None
# Calculate vertical fall volume multiplier (0 at maxY, 1 at y=0)
fallMultiplier = 1 - ( currentY / maxY )
channel = sounds [ randomKey ] . play ( )
if channel :
channel . set_volume ( volume * left * fallMultiplier , volume * right * fallMultiplier )
return channel
2020-09-15 19:35:59 -04:00
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 )
2025-02-01 14:41:52 -05:00
def messagebox ( text ) :
2025-02-01 15:14:29 -05:00
speak ( text + " \n Press any key to repeat or enter to continue. " )
2025-02-01 14:41:52 -05:00
while True :
event = pygame . event . wait ( )
if event . type == pygame . KEYDOWN :
2025-02-01 15:11:22 -05:00
if event . key == pygame . K_ESCAPE or event . key == pygame . K_RETURN :
2025-02-01 15:05:13 -05:00
return
2025-02-01 15:14:29 -05:00
else :
speak ( text + " \n Press any key to repeat or enter to continue. " )
2025-02-01 14:59:46 -05:00
2020-09-15 19:35:59 -04:00
def learn_sounds ( sounds ) :
loop = True
pygame . mixer . music . pause ( )
i = 0
2024-07-14 03:51:35 -04:00
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 ( " _ " ) ) ]
2020-09-15 19:35:59 -04:00
# 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 :
2024-07-13 03:03:32 -04:00
try :
pygame . mixer . music . load ( " sounds/music_menu.ogg " )
pygame . mixer . music . set_volume ( 0.75 )
pygame . mixer . music . play ( - 1 )
except :
pass
2020-09-15 19:35:59 -04:00
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 ( )
2022-01-09 01:59:30 -05:00
webbrowser . open ( ' https://ko-fi.com/stormux ' )