Compare commits

..

24 Commits

Author SHA1 Message Date
Storm Dragon
7f62e6ccca Forgot to specify global when checking the speech delay. 2025-02-15 00:06:23 -05:00
Storm Dragon
943e2acf53 Try to keep speech form stuttering with status keys. 2025-02-14 23:58:26 -05:00
Storm Dragon
c242fc6832 Make sure scores are int only. 2025-02-14 21:37:20 -05:00
Storm Dragon
6d2c6e04d8 Fixed a bug with score sorting. 2025-02-14 20:46:38 -05:00
Storm Dragon
97431c0c74 Updated the congrats message for the scoreboard. 2025-02-14 19:41:10 -05:00
Storm Dragon
4a15f951f0 Rewrote the scoreboard class. Added ability to enter name for scoreboard. 2025-02-14 16:50:04 -05:00
Storm Dragon
5a791510ea Rewrote the scoreboard class. 2025-02-14 16:45:28 -05:00
Storm Dragon
7cbbc64d27 Fixed a bug in learn sounds. Forgot to switch the if statement from == to in when converting to the list to also allow use of s and w for menu navigation. 2025-02-08 19:51:52 -05:00
Storm Dragon
b479811a98 Binding for volume keys changed to alt+pageup/down, alt+home/end, etc. 2025-02-08 19:21:14 -05:00
Storm Dragon
d5d737d0c0 Make intro sound skipable. 2025-02-08 16:13:46 -05:00
Storm Dragon
dd246db5be Added w and s as alternate navigation keys for libstormgames menus. 2025-02-07 02:12:34 -05:00
Storm Dragon
21216f361a removed duplicate function. 2025-02-06 12:37:35 -05:00
Storm Dragon
68e72f5d81 Play sound function added. Code restructure. Added volume controls for master volume, sounds only, and music only. 2025-02-04 05:12:31 -05:00
Storm Dragon
80fe2caff3 Oops, accidentally removed the obj_update function. 2025-02-03 23:59:46 -05:00
Storm Dragon
2df86c9c76 Add the ability for obj_play to play a sound once. 2025-02-03 23:49:08 -05:00
Storm Dragon
5fa90f9e84 Updated play_falling_random. 2025-02-03 21:58:02 -05:00
Storm Dragon
658709ebce Random play functions for positional audio and positional falling audio. 2025-02-02 17:04:04 -05:00
Storm Dragon
d5c79c0770 Hopefully last fix to messagebox. This turned out to be harder than I originally thought. 2025-02-01 15:14:29 -05:00
Storm Dragon
24f9a126d4 I think I was over complicating it. 2025-02-01 15:11:22 -05:00
Storm Dragon
c316d4e570 I think I actually got it this time. 2025-02-01 15:05:13 -05:00
Storm Dragon
e66655d75f Another attempt to keep messagebox from double speaking. 2025-02-01 14:59:46 -05:00
Storm Dragon
c5406d5089 Attempt to keep messagebox from double speaking. 2025-02-01 14:53:55 -05:00
Storm Dragon
b5b472eebe Added simple message box for spoken text that might need to be repeated. 2025-02-01 14:41:52 -05:00
Storm Dragon
9f03de15b8 Add a text entry using wx. 2024-08-01 21:10:27 -04:00
2 changed files with 1002 additions and 513 deletions

1004
__init__.py Normal file → Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -1,511 +0,0 @@
#!/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')