Compare commits
1 Commits
master
...
oop-cleanu
Author | SHA1 | Date | |
---|---|---|---|
e5584bd439 |
@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
![Pocket Friends](https://gitea.citruxx.com/ndyer/pocket-friends/raw/branch/master/pocket_friends/resources/images/promotional.png)
|
![Pocket Friends](https://gitea.citruxx.com/ndyer/pocket-friends/raw/branch/master/pocket_friends/game_files/resources/images/promotional.png)
|
||||||
|
|
||||||
[![License: GNU GPL v3.0](https://img.shields.io/badge/license-GNU%20GPL%20v3.0-blue)](LICENSE)
|
[![License: GNU GPL v3.0](https://img.shields.io/badge/license-GNU%20GPL%20v3.0-blue)](LICENSE)
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ There are currently no releases of the game. To install the current version on G
|
|||||||
## Installing From Source
|
## Installing From Source
|
||||||
|
|
||||||
Requirements:
|
Requirements:
|
||||||
- Python 3.7
|
- Python 3.10 or greater
|
||||||
- Pip
|
- Pip
|
||||||
- Git
|
- Git
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import pocket_friends
|
|||||||
|
|
||||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
PyInstaller.__main__.preprocess([
|
PyInstaller.__main__.run([
|
||||||
'{0}/pocket_friends/__main__.py'.format(script_dir),
|
'{0}/pocket_friends/__main__.py'.format(script_dir),
|
||||||
'--clean',
|
'--clean',
|
||||||
'--noconsole',
|
'--noconsole',
|
||||||
|
@ -1,4 +1 @@
|
|||||||
"""Pocket Friends is a game where you raise your own little pocket friend! These pocket friends, called bloops,
|
__version__ = 'dev_0.0.3'
|
||||||
are great little companions to have! You can feed them, play with them, and watch them grow up!"""
|
|
||||||
|
|
||||||
__version__ = 'dev_0.1.0'
|
|
||||||
|
@ -2,26 +2,24 @@
|
|||||||
Launch script for Pocket Friends.
|
Launch script for Pocket Friends.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
import pygame
|
import pygame
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pocket_friends.game_files.game import main as game_main
|
||||||
import pocket_friends.game as game
|
from pocket_friends.development.dev_menu import main as dev_menu_main
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
enable_dev = False
|
enable_dev = False
|
||||||
resolution = 240
|
|
||||||
|
|
||||||
|
# enable dev mode if --dev argument is passed
|
||||||
if len(sys.argv) > 0:
|
if len(sys.argv) > 0:
|
||||||
for arg in sys.argv:
|
for arg in sys.argv:
|
||||||
if arg == '--delete-save':
|
match arg:
|
||||||
|
case '--delete-save':
|
||||||
save_dir = os.path.join(Path.home(), '.pocket_friends')
|
save_dir = os.path.join(Path.home(), '.pocket_friends')
|
||||||
os.remove(save_dir + '/save.json')
|
os.remove(save_dir + '/save.json')
|
||||||
if '--res=' in arg:
|
|
||||||
resolution = int(arg.split('=')[1])
|
|
||||||
if arg == '--dev':
|
|
||||||
enable_dev = True
|
|
||||||
|
|
||||||
game.start_game(resolution=resolution, dev=enable_dev)
|
game_main()
|
||||||
|
|
||||||
pygame.quit()
|
pygame.quit()
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import pygame
|
|
||||||
|
|
||||||
|
|
||||||
def reboot_system():
|
|
||||||
os.system('sudo reboot')
|
|
||||||
|
|
||||||
|
|
||||||
def shutdown_system():
|
|
||||||
os.system('sudo shutdown now')
|
|
||||||
|
|
||||||
|
|
||||||
def update():
|
|
||||||
pygame.quit()
|
|
||||||
os.system('bash ~/update.sh')
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def restart_app():
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def show_black():
|
|
||||||
return 'show_black'
|
|
||||||
|
|
||||||
|
|
||||||
def quit():
|
|
||||||
sys.exit(0)
|
|
@ -1,34 +0,0 @@
|
|||||||
import pygame
|
|
||||||
|
|
||||||
|
|
||||||
class FunctionSelector:
|
|
||||||
|
|
||||||
def __init__(self, resources_dir, game_res, functions):
|
|
||||||
self.font = pygame.font.Font(resources_dir + '/fonts/5Pts5.ttf', 10)
|
|
||||||
self.functions = functions
|
|
||||||
self.max_lines = 6 # Max number of lines to be shown on screen at a time.
|
|
||||||
self.offset = 0
|
|
||||||
self.game_res = game_res
|
|
||||||
|
|
||||||
self.selected = 0
|
|
||||||
self.max_index = len(self.functions) - 1
|
|
||||||
self.selector = pygame.image.load(resources_dir + '/images/dev_menu/selector.png').convert_alpha()
|
|
||||||
|
|
||||||
def draw(self, surface):
|
|
||||||
for i in range(self.max_index + 1):
|
|
||||||
text = self.font.render(self.functions[i], False, (0, 0, 0))
|
|
||||||
surface.blit(text, (8, (-3 + (i * 6))))
|
|
||||||
surface.blit(self.selector, (0, (self.selected * 6)))
|
|
||||||
|
|
||||||
def scroll_down(self):
|
|
||||||
self.selected += 1
|
|
||||||
if self.selected > self.max_index:
|
|
||||||
self.selected = self.max_index
|
|
||||||
|
|
||||||
def scroll_up(self):
|
|
||||||
self.selected -= 1
|
|
||||||
if self.selected < 0:
|
|
||||||
self.selected = 0
|
|
||||||
|
|
||||||
def get_function(self):
|
|
||||||
return self.functions[self.selected]
|
|
@ -1,33 +0,0 @@
|
|||||||
import pygame
|
|
||||||
from pocket_friends.elements import surface
|
|
||||||
|
|
||||||
|
|
||||||
class Surface(surface.GameSurface):
|
|
||||||
def __init__(self, game_res, resources_dir, game_fps, **kwargs):
|
|
||||||
super().__init__(game_res, resources_dir, game_fps)
|
|
||||||
self.game_fps = game_fps
|
|
||||||
self.frames = -3 * game_fps
|
|
||||||
self.delay = 1
|
|
||||||
self.font = pygame.font.Font(resources_dir + '/fonts/5Pts5.ttf', 10)
|
|
||||||
self.title = pygame.image.load(resources_dir + '/images/debug/invalid.png').convert_alpha()
|
|
||||||
self.next_surface = '_dev_menu'
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
self.preprocess()
|
|
||||||
self.fill((0, 0, 0))
|
|
||||||
|
|
||||||
text_1 = self.font.render('Press B', False, (64, 64, 64))
|
|
||||||
text_2 = self.font.render('to return', False, (64, 64, 64))
|
|
||||||
|
|
||||||
if self.frames < 0:
|
|
||||||
self.blit(text_1, (3, 20))
|
|
||||||
self.blit(text_2, (3, 40))
|
|
||||||
|
|
||||||
self.frames += 1
|
|
||||||
if self.frames > self.game_fps:
|
|
||||||
self.frames = 0
|
|
||||||
|
|
||||||
for event in pygame.event.get():
|
|
||||||
if event.type == pygame.KEYDOWN:
|
|
||||||
if event.key == pygame.K_b:
|
|
||||||
self.running = False
|
|
@ -1,58 +0,0 @@
|
|||||||
import pygame
|
|
||||||
from pocket_friends.elements import surface
|
|
||||||
from pocket_friends._development._sprites import FunctionSelector
|
|
||||||
import pocket_friends._development._dev as dev
|
|
||||||
|
|
||||||
dev_functions = {
|
|
||||||
'Restart': 'reboot_system',
|
|
||||||
'Shutdown': 'shutdown_system',
|
|
||||||
'Update': 'update',
|
|
||||||
'Re-open App': 'restart_app',
|
|
||||||
'Black screen': 'show_black',
|
|
||||||
'!!! Close App': 'quit'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Surface(surface.GameSurface):
|
|
||||||
def __init__(self, game_res, resources_dir, game_fps, **kwargs):
|
|
||||||
super().__init__(game_res, resources_dir, game_fps)
|
|
||||||
self.frames = 1
|
|
||||||
self.game_fps = game_fps
|
|
||||||
self.delay = 1
|
|
||||||
self.font = pygame.font.Font(resources_dir + '/fonts/5Pts5.ttf', 10)
|
|
||||||
self.background = pygame.image.load(self.resource_dir + '/images/dev_menu/dev_bg.png').convert_alpha()
|
|
||||||
|
|
||||||
functions = []
|
|
||||||
for key in dev_functions.keys():
|
|
||||||
functions.append(key)
|
|
||||||
|
|
||||||
self.function_selector = FunctionSelector(resources_dir, game_res, functions)
|
|
||||||
|
|
||||||
def execute(self):
|
|
||||||
|
|
||||||
executing_function = getattr(dev, dev_functions.get(self.function_selector.get_function()))
|
|
||||||
output = executing_function()
|
|
||||||
if output == 'show_black':
|
|
||||||
self.next_surface = '_black_screen'
|
|
||||||
self.running = False
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
self.preprocess()
|
|
||||||
|
|
||||||
text = self.font.render('f: {0}'.format(self.frames), False, (128, 128, 128))
|
|
||||||
self.blit(text, (3, 68))
|
|
||||||
self.function_selector.draw(self)
|
|
||||||
|
|
||||||
self.frames += 1
|
|
||||||
self.frames %= self.game_fps
|
|
||||||
|
|
||||||
for event in pygame.event.get():
|
|
||||||
if event.type == pygame.KEYDOWN:
|
|
||||||
if event.key == pygame.K_UP:
|
|
||||||
self.function_selector.scroll_up()
|
|
||||||
if event.key == pygame.K_DOWN:
|
|
||||||
self.function_selector.scroll_down()
|
|
||||||
if event.key == pygame.K_a:
|
|
||||||
self.execute()
|
|
||||||
if event.key == pygame.K_b:
|
|
||||||
self.running = False
|
|
@ -1,28 +0,0 @@
|
|||||||
import pygame
|
|
||||||
from pocket_friends.elements import surface
|
|
||||||
|
|
||||||
|
|
||||||
class Surface(surface.GameSurface):
|
|
||||||
def __init__(self, game_res, resources_dir, game_fps, **kwargs):
|
|
||||||
super().__init__(game_res, resources_dir, game_fps)
|
|
||||||
self.frames = 1
|
|
||||||
self.game_fps = game_fps
|
|
||||||
self.delay = 1
|
|
||||||
self.font = pygame.font.Font(resources_dir + '/fonts/5Pts5.ttf', 10)
|
|
||||||
self.title = pygame.image.load(resources_dir + '/images/debug/invalid.png').convert_alpha()
|
|
||||||
self.next_surface = 'title'
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
self.preprocess()
|
|
||||||
|
|
||||||
self.blit(self.title, (0, -4))
|
|
||||||
text = self.font.render('Frames: {0}'.format(self.frames), False, (64, 64, 64))
|
|
||||||
self.blit(text, (3, 68))
|
|
||||||
|
|
||||||
self.frames += 1
|
|
||||||
self.frames %= self.game_fps
|
|
||||||
|
|
||||||
for event in pygame.event.get():
|
|
||||||
if event.type == pygame.KEYDOWN:
|
|
||||||
if event.key == pygame.K_b:
|
|
||||||
self.running = False
|
|
56
pocket_friends/development/FakeGPIO.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
"""
|
||||||
|
Module used to fake the RPi.GPIO module so that
|
||||||
|
the hardware can be run without the actual hardware.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Constants used by RPi.GPIO
|
||||||
|
BOARD = 0
|
||||||
|
IN = 0
|
||||||
|
FALLING = 0
|
||||||
|
|
||||||
|
|
||||||
|
def setmode(new_mode):
|
||||||
|
"""
|
||||||
|
Fake setmode function.
|
||||||
|
:param new_mode:
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def setup(channel, mode, initial=None, pull_up_down=None):
|
||||||
|
"""
|
||||||
|
Fake setup function.
|
||||||
|
:param channel:
|
||||||
|
:param mode:
|
||||||
|
:param initial:
|
||||||
|
:param pull_up_down:
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def add_event_detect(channel, edge_type, callback=None, bouncetime=0):
|
||||||
|
"""
|
||||||
|
Fake function to add a non-existent event detect.
|
||||||
|
:param channel:
|
||||||
|
:param edge_type:
|
||||||
|
:param callback:
|
||||||
|
:param bouncetime:
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def event_detected(channel):
|
||||||
|
"""
|
||||||
|
Fake function to detect an event. Always returns false.
|
||||||
|
:param channel:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup(channel=None):
|
||||||
|
"""
|
||||||
|
Fake cleanup function.
|
||||||
|
:param channel:
|
||||||
|
"""
|
||||||
|
pass
|
50
pocket_friends/development/button_test.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
"""
|
||||||
|
Module to test the GPIO input on the Raspberry Pi.
|
||||||
|
"""
|
||||||
|
from collections import deque
|
||||||
|
from ..hardware.gpio_handler import Constants, GPIOHandler
|
||||||
|
|
||||||
|
|
||||||
|
def button_test():
|
||||||
|
"""
|
||||||
|
GPIO button test. Checks for a GPIO input and prints it out, simple as that.
|
||||||
|
"""
|
||||||
|
running = True
|
||||||
|
|
||||||
|
# Exit code used to quit the button test.
|
||||||
|
exit_code = deque()
|
||||||
|
for button in ['j_d', 'j_d', 'j_u', 'j_u', 'j_d', 'j_d', 'j_u', 'j_u', 'a', 'a', 'b']:
|
||||||
|
exit_code.append(button)
|
||||||
|
|
||||||
|
# Input log to check for quitting out of the button test.
|
||||||
|
input_log = deque()
|
||||||
|
|
||||||
|
GPIOHandler.setup()
|
||||||
|
|
||||||
|
def log(pressed_button):
|
||||||
|
"""
|
||||||
|
Logs the pressed button into the input log.
|
||||||
|
:param pressed_button:
|
||||||
|
"""
|
||||||
|
input_log.append(pressed_button)
|
||||||
|
if len(input_log) > len(exit_code): # Don't let the input log exceed the length of the exit code.
|
||||||
|
input_log.popleft()
|
||||||
|
|
||||||
|
def check_exit():
|
||||||
|
"""
|
||||||
|
Check if the input log and the exit code are the same. If they are, quit the button test.
|
||||||
|
"""
|
||||||
|
nonlocal running
|
||||||
|
|
||||||
|
if exit_code == input_log:
|
||||||
|
running = False
|
||||||
|
|
||||||
|
while running:
|
||||||
|
for button in Constants.buttons:
|
||||||
|
code = Constants.buttons.get(button)
|
||||||
|
if GPIOHandler.get_press(code): # If a button is pressed, print it out and do a quit check.
|
||||||
|
print('event: {0}'.format(button))
|
||||||
|
log(button)
|
||||||
|
check_exit()
|
||||||
|
|
||||||
|
GPIOHandler.teardown()
|
152
pocket_friends/development/dev_menu.py
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
"""
|
||||||
|
Development menu for the hardware on Raspberry Pi. NOTE: THIS DOES NOTHING ON A COMPUTER!
|
||||||
|
"""
|
||||||
|
import pocket_friends.game_files.game
|
||||||
|
import importlib.util
|
||||||
|
import os
|
||||||
|
import pygame
|
||||||
|
import time
|
||||||
|
from .button_test import button_test
|
||||||
|
from .menus import Menu
|
||||||
|
from ..hardware.gpio_handler import GPIOHandler, Constants
|
||||||
|
|
||||||
|
try:
|
||||||
|
importlib.util.find_spec('RPi.GPIO')
|
||||||
|
import RPi.GPIO as GPIO
|
||||||
|
except ImportError:
|
||||||
|
import pocket_friends.development.FakeGPIO as GPIO
|
||||||
|
|
||||||
|
# Global variable to keep track of the current menu.
|
||||||
|
menu = 'main'
|
||||||
|
|
||||||
|
|
||||||
|
def run_button_test():
|
||||||
|
"""
|
||||||
|
Runs the GPIO button test.
|
||||||
|
"""
|
||||||
|
GPIOHandler.teardown()
|
||||||
|
button_test()
|
||||||
|
GPIOHandler.setup()
|
||||||
|
|
||||||
|
|
||||||
|
def clear_screen():
|
||||||
|
"""
|
||||||
|
Clears the screen.
|
||||||
|
"""
|
||||||
|
print("\n" * 20)
|
||||||
|
|
||||||
|
|
||||||
|
def start_game():
|
||||||
|
"""
|
||||||
|
Cleans the GPIO and starts the hardware.
|
||||||
|
"""
|
||||||
|
GPIOHandler.teardown()
|
||||||
|
pocket_friends.game_files.game.main()
|
||||||
|
pygame.quit()
|
||||||
|
GPIOHandler.setup()
|
||||||
|
|
||||||
|
|
||||||
|
def quit_menu():
|
||||||
|
"""
|
||||||
|
Quits the menu.
|
||||||
|
"""
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
def quit_with_error():
|
||||||
|
"""
|
||||||
|
Quits the menu with error code 3.
|
||||||
|
"""
|
||||||
|
exit(3)
|
||||||
|
|
||||||
|
|
||||||
|
def change_menu(new_menu):
|
||||||
|
"""
|
||||||
|
Changes the global menu variable for the dev menu
|
||||||
|
:param new_menu: the menu to change to
|
||||||
|
"""
|
||||||
|
global menu
|
||||||
|
menu = new_menu
|
||||||
|
clear_screen()
|
||||||
|
print('...')
|
||||||
|
time.sleep(0.75)
|
||||||
|
|
||||||
|
|
||||||
|
def shutdown():
|
||||||
|
"""
|
||||||
|
Shuts down the linux system.
|
||||||
|
"""
|
||||||
|
os.system('sudo shutdown now')
|
||||||
|
|
||||||
|
|
||||||
|
def restart():
|
||||||
|
"""
|
||||||
|
Restarts the linux system.
|
||||||
|
"""
|
||||||
|
os.system('sudo reboot')
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""
|
||||||
|
Starts the dev menu.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The following defines all of the options in the various different menus.
|
||||||
|
|
||||||
|
main_menu = Menu('Pocket Friends Dev Menu')
|
||||||
|
main_menu.add_option(Menu.Option('Start Game', start_game))
|
||||||
|
main_menu.add_option(Menu.Option('Button Test', run_button_test))
|
||||||
|
main_menu.add_option(Menu.Option('Restart Dev Menu', quit_with_error))
|
||||||
|
main_menu.add_option(Menu.Option('Shutdown Pi', change_menu, 'shutdown'))
|
||||||
|
main_menu.add_option(Menu.Option('Restart Pi', change_menu, 'restart'))
|
||||||
|
main_menu.add_option(Menu.Option('Quit Dev Menu', change_menu, 'quit'))
|
||||||
|
|
||||||
|
shutdown_confirm = Menu('Are you sure you want to shutdown?')
|
||||||
|
shutdown_confirm.add_option(Menu.Option('No', change_menu, 'main'))
|
||||||
|
shutdown_confirm.add_option(Menu.Option('Yes', shutdown))
|
||||||
|
|
||||||
|
restart_confirm = Menu('Are you sure you want to restart?')
|
||||||
|
restart_confirm.add_option(Menu.Option('No', change_menu, 'main'))
|
||||||
|
restart_confirm.add_option(Menu.Option('Yes', restart))
|
||||||
|
|
||||||
|
quit_confirm = Menu('Are you sure you want to exit?')
|
||||||
|
quit_confirm.add_option(Menu.Option('No', change_menu, 'main'))
|
||||||
|
quit_confirm.add_option(Menu.Option('Yes', quit_menu))
|
||||||
|
|
||||||
|
GPIOHandler.setup()
|
||||||
|
|
||||||
|
def menu_handler(current_menu):
|
||||||
|
"""
|
||||||
|
Draws the menu and handles the GPIO inputs
|
||||||
|
:param current_menu: the current menu being drawn on the screen
|
||||||
|
"""
|
||||||
|
current_menu.draw_menu()
|
||||||
|
|
||||||
|
while True: # Main GPIO input loop
|
||||||
|
|
||||||
|
# Limits how often the program checks for a GPIO input. Eases CPU usage.
|
||||||
|
time.sleep(0.125)
|
||||||
|
|
||||||
|
if GPIOHandler.get_press(Constants.buttons.get('j_d')):
|
||||||
|
current_menu.select_next()
|
||||||
|
break
|
||||||
|
if GPIOHandler.get_press(Constants.buttons.get('j_u')):
|
||||||
|
current_menu.select_prev()
|
||||||
|
break
|
||||||
|
if GPIOHandler.get_press(Constants.buttons.get('a')):
|
||||||
|
current_menu.run_selection()
|
||||||
|
break
|
||||||
|
|
||||||
|
while True: # Loop for drawing the menus.
|
||||||
|
|
||||||
|
while menu == 'main':
|
||||||
|
menu_handler(main_menu)
|
||||||
|
|
||||||
|
while menu == 'shutdown':
|
||||||
|
menu_handler(shutdown_confirm)
|
||||||
|
|
||||||
|
while menu == 'restart':
|
||||||
|
menu_handler(restart_confirm)
|
||||||
|
|
||||||
|
while menu == 'quit':
|
||||||
|
menu_handler(quit_confirm)
|
120
pocket_friends/development/menus.py
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
"""
|
||||||
|
Menu class to help with drawing menus on screen
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Menu:
|
||||||
|
"""
|
||||||
|
Menu class. Creates a menu with text to display and options
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, menu_text=''):
|
||||||
|
self._menu_text = menu_text
|
||||||
|
self._options = []
|
||||||
|
self._selection = 0
|
||||||
|
|
||||||
|
def add_option(self, option):
|
||||||
|
"""
|
||||||
|
Adds an option to the menu. Only allows instances of Menu.Option
|
||||||
|
:param option:
|
||||||
|
"""
|
||||||
|
if not isinstance(option, Menu.Option):
|
||||||
|
raise TypeError('option must be an instance of Menu.Option')
|
||||||
|
else:
|
||||||
|
self._options.append(option)
|
||||||
|
|
||||||
|
def get_option(self, index):
|
||||||
|
"""
|
||||||
|
Gets an option object given the index of the option. Raises IndexError if given
|
||||||
|
index is out of bounds.
|
||||||
|
:param index: the index of the option
|
||||||
|
:return: the option object
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return self._options[index]
|
||||||
|
except IndexError as ex:
|
||||||
|
raise IndexError('option index out of range') from ex
|
||||||
|
|
||||||
|
def select_next(self):
|
||||||
|
"""
|
||||||
|
Selects the next option in the list. Wraps around to the first option if
|
||||||
|
the last option is currently selected.
|
||||||
|
"""
|
||||||
|
self._selection += 1
|
||||||
|
if self._selection >= len(self._options):
|
||||||
|
self._selection = 0
|
||||||
|
|
||||||
|
def select_prev(self):
|
||||||
|
"""
|
||||||
|
Selects the previous option in the list. Wraps around to the last option if
|
||||||
|
the first option is currently selected.
|
||||||
|
"""
|
||||||
|
self._selection -= 1
|
||||||
|
if self._selection < 0:
|
||||||
|
if len(self._options) > 0:
|
||||||
|
self._selection = len(self._options) - 1
|
||||||
|
else:
|
||||||
|
self._selection = 0
|
||||||
|
|
||||||
|
def run_selection(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Runs the function that the currently selected option object points to.
|
||||||
|
:param args: arguments to be passed to the function
|
||||||
|
:param kwargs: keyword arguments to be passed to the function
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return self._options[self._selection].run(*args, **kwargs)
|
||||||
|
except IndexError as ex:
|
||||||
|
raise Exception('menu has no options, cannot run a non-existent option') from ex
|
||||||
|
|
||||||
|
def draw_menu(self):
|
||||||
|
"""
|
||||||
|
Draws the menu on screen with a leading 20 blank lines.
|
||||||
|
"""
|
||||||
|
print('\n' * 20)
|
||||||
|
|
||||||
|
print(self._menu_text + '\n')
|
||||||
|
|
||||||
|
for option in self._options:
|
||||||
|
selection_char = '>'
|
||||||
|
|
||||||
|
if self._options.index(option) != self._selection:
|
||||||
|
selection_char = ' '
|
||||||
|
|
||||||
|
print('{0} {1}'.format(selection_char, option.get_text()))
|
||||||
|
|
||||||
|
class Option:
|
||||||
|
"""
|
||||||
|
Class that defines options for the Menu class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, option_text='', function=None, *args, **kwargs):
|
||||||
|
self._option_text = option_text
|
||||||
|
self._function = function
|
||||||
|
self._default_args = args
|
||||||
|
self._default_kwargs = kwargs
|
||||||
|
|
||||||
|
def get_text(self):
|
||||||
|
"""
|
||||||
|
Returns the text to be displayed by the option
|
||||||
|
:return: the option text
|
||||||
|
"""
|
||||||
|
return self._option_text
|
||||||
|
|
||||||
|
def run(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Runs the function that the option object points to. Returns None if
|
||||||
|
there is no function or the given function is not valid.
|
||||||
|
:param args: arguments to be passed to the function
|
||||||
|
:param kwargs: keyword arguments to be passed to the function
|
||||||
|
:return: the return value of the function (if any)
|
||||||
|
"""
|
||||||
|
if len(args) == 0:
|
||||||
|
args = self._default_args
|
||||||
|
if len(kwargs) == 0:
|
||||||
|
kwargs = self._default_kwargs
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self._function(*args, **kwargs)
|
||||||
|
except TypeError:
|
||||||
|
return None
|
@ -1 +0,0 @@
|
|||||||
"""Submodule for use in the game. Helps with the drawing of various objects such as sprites and text boxes."""
|
|
@ -1,277 +0,0 @@
|
|||||||
import pygame
|
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
class SpriteSheet:
|
|
||||||
"""
|
|
||||||
Class to be used by sprites in order to give them a texture and an animation.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
images (list) List of all the sprites in the animation separated from the sprite sheet.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, sprite_sheet, texture_json):
|
|
||||||
"""
|
|
||||||
Creates a sprite sheet given a sprite image and its corresponding JSON file.
|
|
||||||
Args:
|
|
||||||
sprite_sheet (str): The path of the sprite sheet image component.
|
|
||||||
texture_json (str): The path of the sprite sheet JSON component, contains the number of frames in the
|
|
||||||
sprite sheet, and the width and height of an individual sprite from the sprite sheet.
|
|
||||||
"""
|
|
||||||
# Load in whole sprite sheet as one image.
|
|
||||||
sprite_sheet = pygame.image.load(sprite_sheet).convert_alpha()
|
|
||||||
self.images = []
|
|
||||||
|
|
||||||
# Get the sprite sheet json file.
|
|
||||||
with open(texture_json, 'r') as json_file:
|
|
||||||
img_attrib = json.load(json_file)
|
|
||||||
json_file.close()
|
|
||||||
|
|
||||||
# Count for how many images have been added in the image list
|
|
||||||
image_count = 0
|
|
||||||
|
|
||||||
# Get the sprite size as a tuple
|
|
||||||
sprite_size = img_attrib['width'], img_attrib['height']
|
|
||||||
|
|
||||||
# Iterate through every image location on the sprite sheet given the sprite size
|
|
||||||
for i in range(sprite_sheet.get_size()[1] // sprite_size[1]):
|
|
||||||
i *= sprite_size[1]
|
|
||||||
for j in range(sprite_sheet.get_size()[0] // sprite_size[0]):
|
|
||||||
j *= sprite_size[0]
|
|
||||||
|
|
||||||
# Create a new transparent surface
|
|
||||||
sprite = pygame.Surface(sprite_size, pygame.SRCALPHA)
|
|
||||||
# Blit the sprite onto the image
|
|
||||||
sprite.blit(sprite_sheet, (0, 0), (j, i, sprite_size[0], sprite_size[1]))
|
|
||||||
# Add the image to the list of images
|
|
||||||
self.images.append(sprite)
|
|
||||||
|
|
||||||
image_count += 1
|
|
||||||
|
|
||||||
# Break the loop if the specified number of frames has been reached.
|
|
||||||
if image_count >= img_attrib['frames']:
|
|
||||||
break
|
|
||||||
if image_count >= img_attrib['frames']:
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
class SelectionEgg(pygame.sprite.Sprite):
|
|
||||||
"""
|
|
||||||
Sprite to render the egg on the egg selection screen.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
egg_color (str): The color of the egg (also its name).
|
|
||||||
description (str): The description of the egg to be displayed when selected.
|
|
||||||
contentedness (int): How likely the egg is to stay happy, ranges from 0-5.
|
|
||||||
metabolism (int): How quickly the egg will get hungry, ranges from 0-5.
|
|
||||||
rect (pygame.Rect): Pygame rectangle used to position the egg on screen.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, egg_color, resources_dir):
|
|
||||||
"""
|
|
||||||
Creates a SelectionEgg object given an egg color and a resource location.
|
|
||||||
Args:
|
|
||||||
egg_color (str): The color egg that should be rendered.
|
|
||||||
resources_dir (str): The path of the resources directory.
|
|
||||||
"""
|
|
||||||
pygame.sprite.Sprite.__init__(self)
|
|
||||||
|
|
||||||
self.egg_color = egg_color
|
|
||||||
|
|
||||||
# Loads the JSON file of the egg to read in data.
|
|
||||||
with open(resources_dir + '/data/bloop_info/{0}.json'.format(egg_color), 'r') as save_file:
|
|
||||||
json_file = json.load(save_file)
|
|
||||||
save_file.close()
|
|
||||||
|
|
||||||
# Gets the description off the egg from the JSON file.
|
|
||||||
self.description = json_file.get('description')
|
|
||||||
self.contentedness = json_file.get('contentedness')
|
|
||||||
self.metabolism = json_file.get('metabolism')
|
|
||||||
|
|
||||||
# Load the egg from the given color and get the bounding rectangle for the image.
|
|
||||||
sprite_sheet = SpriteSheet(resources_dir + '/images/bloops/{0}/egg.png'.format(self.egg_color),
|
|
||||||
resources_dir + '/images/bloops/{0}/egg.json'.format(self.egg_color))
|
|
||||||
self._images = sprite_sheet.images
|
|
||||||
|
|
||||||
# Get the rectangle from the first image in the list
|
|
||||||
self.rect = self._images[0].get_rect()
|
|
||||||
self._index = 0
|
|
||||||
self.image = self._images[self._index]
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
"""
|
|
||||||
Update the sprite to the next animation frame.
|
|
||||||
"""
|
|
||||||
# Animate the sprite
|
|
||||||
self._index = (self._index + 1) % len(self._images)
|
|
||||||
self.image = self._images[self._index]
|
|
||||||
|
|
||||||
|
|
||||||
class InfoText:
|
|
||||||
"""
|
|
||||||
Class for drawing large amounts of text on the screen at a time
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, resources_dir, game_res, text='Test text.'):
|
|
||||||
"""
|
|
||||||
Creates an InfoText object to be used on a surface.
|
|
||||||
Args:
|
|
||||||
resources_dir (str): The full path of the game's resources directory
|
|
||||||
game_res (int): The internal resolution of the game. Used for correct scaling.
|
|
||||||
text (:obj:`str`, optional): The given text to render. Defaults to "Test text."'
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.font = pygame.font.Font(resources_dir + '/fonts/5Pts5.ttf', 10)
|
|
||||||
self.text = [] # Text broken up into a list according to how it will fit on screen.
|
|
||||||
self.max_lines = 6 # Max number of lines to be shown on screen at a time.
|
|
||||||
self.offset = 0
|
|
||||||
self.game_res = game_res
|
|
||||||
|
|
||||||
# Arrow icons to indicate scrolling
|
|
||||||
self.up_arrow = pygame.image.load(resources_dir + '/images/gui/up_arrow.png').convert_alpha()
|
|
||||||
self.down_arrow = pygame.image.load(resources_dir + '/images/gui/down_arrow.png').convert_alpha()
|
|
||||||
|
|
||||||
raw_text = text # Copy the text to a different variable to be cut up.
|
|
||||||
|
|
||||||
margins = 4.5
|
|
||||||
max_line_width = self.game_res - (margins * 2) # The maximum pixel width that drawn text can be.
|
|
||||||
cut_chars = '.,! ' # Characters that will be considered "cuts" aka when a line break can occur.
|
|
||||||
|
|
||||||
# Prevents freezing if the end of the string does not end in a cut character
|
|
||||||
# Will fix eventually more elegantly
|
|
||||||
if raw_text[-1:] not in cut_chars:
|
|
||||||
raw_text += ' '
|
|
||||||
|
|
||||||
# Calculating line breaks.
|
|
||||||
while len(raw_text) > 0:
|
|
||||||
index = 0
|
|
||||||
test_text = '' # Chunk of text to pseudo-render and test the width of.
|
|
||||||
|
|
||||||
# Loops until the testing text has reached the size limit.
|
|
||||||
while True:
|
|
||||||
|
|
||||||
# Break if the current index is larger than the remaining text.
|
|
||||||
if index + 1 > len(raw_text):
|
|
||||||
index -= 1
|
|
||||||
break
|
|
||||||
|
|
||||||
# Add one character to the testing text from the raw text.
|
|
||||||
test_text += raw_text[index]
|
|
||||||
# Get the width of the pseudo-rendered text.
|
|
||||||
text_width = self.font.size(test_text)[0]
|
|
||||||
|
|
||||||
# Break if the text is larger than the defined max width.
|
|
||||||
if text_width > max_line_width:
|
|
||||||
break
|
|
||||||
index += 1
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Gets the chunk of text to be added to the list.
|
|
||||||
text_chunk = raw_text[0:index + 1]
|
|
||||||
# Determines if the chunk of text has any break characters.
|
|
||||||
has_breaks = any(cut_chars in text_chunk for cut_chars in cut_chars)
|
|
||||||
|
|
||||||
# If the text has break characters, start with the last character and go backwards until
|
|
||||||
# one has been found, decreasing the index each time.
|
|
||||||
if has_breaks:
|
|
||||||
while raw_text[index] not in cut_chars:
|
|
||||||
index -= 1
|
|
||||||
text_chunk = raw_text[0:index + 1]
|
|
||||||
# If there are no break characters in the chunk, simply decrease the index by one and insert
|
|
||||||
# a dash at the end of the line to indicate the word continues.
|
|
||||||
else:
|
|
||||||
index -= 1
|
|
||||||
text_chunk = raw_text[0:index + 1]
|
|
||||||
text_chunk += '-'
|
|
||||||
|
|
||||||
# Append the text chunk to the list of text to draw.
|
|
||||||
self.text.append(text_chunk)
|
|
||||||
|
|
||||||
# Cut the text to repeat the process with the new cut string.
|
|
||||||
raw_text = raw_text[index + 1:]
|
|
||||||
|
|
||||||
def draw(self, surface):
|
|
||||||
"""
|
|
||||||
Draw the text on a given surface.
|
|
||||||
Args:
|
|
||||||
surface (:obj:`pygame.Surface`): The surface to draw the text on
|
|
||||||
"""
|
|
||||||
# Constants to help draw the text
|
|
||||||
line_separation = 7
|
|
||||||
left_margin = 3
|
|
||||||
top_margin = 25
|
|
||||||
bottom_margin = 10
|
|
||||||
|
|
||||||
# Draw the lines on the screen
|
|
||||||
for i in range(min(len(self.text), self.max_lines)):
|
|
||||||
text = self.font.render(self.text[i + self.offset], False, (64, 64, 64))
|
|
||||||
surface.blit(text, (left_margin, top_margin + (i * line_separation)))
|
|
||||||
|
|
||||||
# Draw the arrows if there is more text than is on screen.
|
|
||||||
if self.offset != 0:
|
|
||||||
surface.blit(self.up_arrow, ((self.game_res / 2) - (self.up_arrow.get_rect().width / 2), top_margin - 3))
|
|
||||||
if len(self.text) - (self.offset + 1) >= self.max_lines:
|
|
||||||
surface.blit(self.down_arrow,
|
|
||||||
((self.game_res / 2) - (self.down_arrow.get_rect().width / 2), self.game_res - bottom_margin))
|
|
||||||
|
|
||||||
def scroll_down(self):
|
|
||||||
"""
|
|
||||||
Scrolls the text on the screen down.
|
|
||||||
"""
|
|
||||||
# Ensures that the offset cannot be too big as to try to render non-existent lines.
|
|
||||||
if len(self.text) - (self.offset + 1) >= self.max_lines:
|
|
||||||
self.offset += 1
|
|
||||||
|
|
||||||
def scroll_up(self):
|
|
||||||
"""
|
|
||||||
Scrolls the text on the screen up.
|
|
||||||
"""
|
|
||||||
if self.offset > 0: # Ensures a non-zero offset is not possible.
|
|
||||||
self.offset -= 1
|
|
||||||
|
|
||||||
|
|
||||||
class EggInfo:
|
|
||||||
"""
|
|
||||||
Class to draw the contentedness and metabolism value off the egg on the info screen.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, resources_dir, contentedness, metabolism, location):
|
|
||||||
self.contentedness = contentedness
|
|
||||||
self.metabolism = metabolism
|
|
||||||
self.x = location[0]
|
|
||||||
self.y = location[1]
|
|
||||||
|
|
||||||
# Create a new surface to blit onto the other surface
|
|
||||||
self.surface = pygame.Surface((44, 15), pygame.SRCALPHA)
|
|
||||||
|
|
||||||
# Blit the two indicator icons on screen
|
|
||||||
smiley = pygame.image.load(resources_dir + '/images/gui/smiley.png').convert_alpha()
|
|
||||||
self.surface.blit(smiley, (0, 0))
|
|
||||||
apple = pygame.image.load(resources_dir + '/images/gui/apple.png').convert_alpha()
|
|
||||||
self.surface.blit(apple, (1, 9))
|
|
||||||
|
|
||||||
# Draw 5 stars. If the value of the contentedness is less than the current star, make it a blank star.
|
|
||||||
for i in range(5):
|
|
||||||
if i < self.contentedness:
|
|
||||||
star = pygame.image.load(resources_dir + '/images/gui/star.png').convert_alpha()
|
|
||||||
else:
|
|
||||||
star = pygame.image.load(resources_dir + '/images/gui/blank_star.png').convert_alpha()
|
|
||||||
self.surface.blit(star, (11 + (i * 6), 1))
|
|
||||||
|
|
||||||
# Draw 5 stars. If the value of the metabolism is less than the current star, make it a blank star.
|
|
||||||
for i in range(5):
|
|
||||||
if i < self.metabolism:
|
|
||||||
star = pygame.image.load(resources_dir + '/images/gui/star.png').convert_alpha()
|
|
||||||
else:
|
|
||||||
star = pygame.image.load(resources_dir + '/images/gui/blank_star.png').convert_alpha()
|
|
||||||
self.surface.blit(star, (11 + (i * 6), 10))
|
|
||||||
|
|
||||||
def draw(self, surface):
|
|
||||||
"""
|
|
||||||
Draw the info icons on a given surface.
|
|
||||||
Args:
|
|
||||||
surface (:obj:`pygame.Surface`): The surface to draw the text on
|
|
||||||
"""
|
|
||||||
# Blit the info onto the given surface.
|
|
||||||
surface.blit(self.surface, (self.x, self.y))
|
|
@ -1,58 +0,0 @@
|
|||||||
"""Module to aid in the drawing of surfaces to make switching from one screen to another easier."""
|
|
||||||
import pygame
|
|
||||||
from ..io.input_handler import InputHandler
|
|
||||||
|
|
||||||
|
|
||||||
class GameSurface(pygame.Surface):
|
|
||||||
"""
|
|
||||||
Class to be used as the backbone of all game surfaces.
|
|
||||||
|
|
||||||
The GameSurface class is what the game uses to draw all other elements on top of. It controls a number of game
|
|
||||||
operations, including input handling, frame rate limiting, and surface switching. It is a child class of Pygame's
|
|
||||||
Surface class and utilizes Pygame's sprite handling along with the ability to blit certain things on the screen
|
|
||||||
in order to draw elements easily.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
running: Boolean to tell whether the surface is running or not.
|
|
||||||
next_surface: String that represents the next surface to be switched to after halting.
|
|
||||||
resource_dir: String that contains the path of the game's main resource directory.
|
|
||||||
game_fps: An integer value of how many frames per second the game will run at.
|
|
||||||
additional_args: Dictionary of additional arguments to send to the next surface after halting.
|
|
||||||
background: A Pygame surface that will be drawn behind all other elements.
|
|
||||||
"""
|
|
||||||
def __init__(self, game_res: tuple, resources_dir: str, game_fps: int):
|
|
||||||
"""
|
|
||||||
Create a GameSurface object.
|
|
||||||
Args:
|
|
||||||
game_res: The internal resolution of the surface.
|
|
||||||
resources_dir: The path of the game's main resource directory.
|
|
||||||
game_fps: How many frames per second the game will run at.
|
|
||||||
"""
|
|
||||||
super().__init__(game_res, pygame.SRCALPHA)
|
|
||||||
self.running = True # Surfaces should be running by default
|
|
||||||
self.next_surface = None
|
|
||||||
self.resource_dir = resources_dir
|
|
||||||
self.game_fps = game_fps
|
|
||||||
self.additional_args = {}
|
|
||||||
self.dev_override = False
|
|
||||||
|
|
||||||
self.background = pygame.image.load(self.resource_dir + '/images/bg.png').convert_alpha()
|
|
||||||
|
|
||||||
self._clock = pygame.time.Clock()
|
|
||||||
self._input_handler = InputHandler(self._clock)
|
|
||||||
self._sprites = pygame.sprite.Group()
|
|
||||||
|
|
||||||
def preprocess(self):
|
|
||||||
"""
|
|
||||||
Draw the surface background and advance all the surface's sprites by one frame; Run the input handler.
|
|
||||||
"""
|
|
||||||
self._clock.tick(self.game_fps)
|
|
||||||
|
|
||||||
self.blit(self.background, (0, 0))
|
|
||||||
self._sprites.update()
|
|
||||||
self._sprites.draw(self)
|
|
||||||
|
|
||||||
self._input_handler.update()
|
|
||||||
if self._input_handler.dev_found:
|
|
||||||
self.dev_override = True
|
|
||||||
self.running = False
|
|
@ -1,95 +0,0 @@
|
|||||||
"""Main module for the game. Runs setting up the Pygame window and handles scene switching."""
|
|
||||||
|
|
||||||
import pygame
|
|
||||||
import os
|
|
||||||
import pocket_friends
|
|
||||||
import importlib
|
|
||||||
|
|
||||||
valid_surfaces = [
|
|
||||||
'_dev_menu',
|
|
||||||
'_black_screen',
|
|
||||||
'_error_screen',
|
|
||||||
'title',
|
|
||||||
'egg_select',
|
|
||||||
'selection_info',
|
|
||||||
]
|
|
||||||
|
|
||||||
# Add all the surface modules to a dictionary for easy switching
|
|
||||||
surface_modules = {}
|
|
||||||
for module in valid_surfaces:
|
|
||||||
if module[0] == '_':
|
|
||||||
surface_modules[module] = importlib.import_module('pocket_friends._development.surfaces.{0}'.format(module))
|
|
||||||
else:
|
|
||||||
surface_modules[module] = importlib.import_module('pocket_friends.surfaces.{0}'.format(module))
|
|
||||||
|
|
||||||
# FPS for the game to run at.
|
|
||||||
game_fps = 16
|
|
||||||
# The internal resolution of the game
|
|
||||||
game_res = 80
|
|
||||||
|
|
||||||
# Get the path for where all game resources are (images, fonts, sounds, etc.)
|
|
||||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
resources_dir = script_dir + '/resources'
|
|
||||||
|
|
||||||
# Makes Pygame draw on the display of the RPi.
|
|
||||||
os.environ['SDL_FBDEV'] = '/dev/fb1'
|
|
||||||
|
|
||||||
|
|
||||||
def start_game(resolution: int, dev: bool):
|
|
||||||
"""
|
|
||||||
Starts the game.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
resolution: Resolution to display the game at.
|
|
||||||
dev: Boolean to enable the developer menu at start or not
|
|
||||||
"""
|
|
||||||
|
|
||||||
pygame.init()
|
|
||||||
|
|
||||||
# Hide the cursor for the Pi display.
|
|
||||||
pygame.mouse.set_visible(False)
|
|
||||||
|
|
||||||
if dev:
|
|
||||||
starting_surface = '_dev_menu'
|
|
||||||
else:
|
|
||||||
starting_surface = 'title'
|
|
||||||
|
|
||||||
window = pygame.display.set_mode((resolution, resolution))
|
|
||||||
surface = surface_modules.get(starting_surface).Surface((game_res, game_res), resources_dir, game_fps)
|
|
||||||
|
|
||||||
# Only really useful for PCs. Does nothing on the Raspberry Pi.
|
|
||||||
pygame.display.set_caption('Pocket Friends {0}'.format(pocket_friends.__version__))
|
|
||||||
|
|
||||||
# Add an icon to the pygame window.
|
|
||||||
icon = pygame.image.load(resources_dir + '/icon/icon.png').convert_alpha()
|
|
||||||
pygame.display.set_icon(icon)
|
|
||||||
|
|
||||||
running = True
|
|
||||||
|
|
||||||
while running:
|
|
||||||
surface.update()
|
|
||||||
|
|
||||||
# The game is only 80x80px, however it is upscaled to whatever the running resolution is.
|
|
||||||
frame = pygame.transform.scale(surface, (resolution, resolution))
|
|
||||||
window.blit(frame, frame.get_rect())
|
|
||||||
|
|
||||||
# When the current surface is not running, check to make sure that the next surface will be valid
|
|
||||||
if not surface.running:
|
|
||||||
# Force the dev menu to appear if the flag has been passed
|
|
||||||
if surface.dev_override:
|
|
||||||
next_surface = '_dev_menu'
|
|
||||||
else:
|
|
||||||
next_surface = surface.next_surface
|
|
||||||
|
|
||||||
# Send to the error screen if the given surface isn't a valid one
|
|
||||||
if next_surface not in valid_surfaces:
|
|
||||||
next_surface = '_error_screen'
|
|
||||||
|
|
||||||
# Get the additional args to pass on from the ending surface to the next one
|
|
||||||
additional_args = surface.additional_args
|
|
||||||
# Create the new surface and pass through the additional argss
|
|
||||||
surface = surface_modules.get(next_surface).Surface((game_res, game_res), resources_dir,
|
|
||||||
game_fps, **additional_args)
|
|
||||||
pygame.display.flip()
|
|
||||||
|
|
||||||
pygame.quit()
|
|
0
pocket_friends/game_files/__init__.py
Normal file
984
pocket_friends/game_files/game.py
Normal file
@ -0,0 +1,984 @@
|
|||||||
|
"""
|
||||||
|
Main file for the entire game. Controls everything except for GPIO input.
|
||||||
|
"""
|
||||||
|
from collections import deque
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import pocket_friends
|
||||||
|
import pygame
|
||||||
|
from pygame.locals import *
|
||||||
|
from ..hardware.gpio_handler import Constants, GPIOHandler
|
||||||
|
|
||||||
|
# FPS for the entire game to run at.
|
||||||
|
game_fps = 16
|
||||||
|
# The resolution the game is rendered at.
|
||||||
|
game_res = 80
|
||||||
|
|
||||||
|
# Gets the directory of the script for importing and the save directory
|
||||||
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
save_dir = os.path.join(Path.home(), '.pocket_friends')
|
||||||
|
|
||||||
|
# Tries to make the save directory. Does nothing if it already exists.
|
||||||
|
try:
|
||||||
|
os.mkdir(save_dir)
|
||||||
|
except FileExistsError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SpriteSheet:
|
||||||
|
"""
|
||||||
|
Imports a sprite sheet as separate pygame images given an image file and a json file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, sprite_sheet, texture_json):
|
||||||
|
# Load in whole sprite sheet as one image.
|
||||||
|
self.sprite_sheet = pygame.image.load(sprite_sheet).convert_alpha()
|
||||||
|
self.images = []
|
||||||
|
|
||||||
|
# Get the sprite sheet json file.
|
||||||
|
with open(texture_json, 'r') as json_file:
|
||||||
|
self.img_attrib = json.load(json_file)
|
||||||
|
json_file.close()
|
||||||
|
|
||||||
|
# Count for how many images have been added in the image list
|
||||||
|
image_count = 0
|
||||||
|
|
||||||
|
# Get the sprite size as a tuple
|
||||||
|
sprite_size = self.img_attrib['width'], self.img_attrib['height']
|
||||||
|
|
||||||
|
# Iterate through every image location on the sprite sheet given the sprite size
|
||||||
|
for i in range(self.sprite_sheet.get_size()[1] // sprite_size[1]):
|
||||||
|
i *= sprite_size[1]
|
||||||
|
for j in range(self.sprite_sheet.get_size()[0] // sprite_size[0]):
|
||||||
|
j *= sprite_size[0]
|
||||||
|
|
||||||
|
# Create a new transparent surface
|
||||||
|
sprite = pygame.Surface(sprite_size, SRCALPHA)
|
||||||
|
# Blit the sprite onto the image
|
||||||
|
sprite.blit(self.sprite_sheet, (0, 0), (j, i, sprite_size[0], sprite_size[1]))
|
||||||
|
# Add the image to the list of images
|
||||||
|
self.images.append(sprite)
|
||||||
|
|
||||||
|
image_count += 1
|
||||||
|
|
||||||
|
# Break the loop if the specified number of frames has been reached.
|
||||||
|
if image_count >= self.img_attrib['frames']:
|
||||||
|
break
|
||||||
|
if image_count >= self.img_attrib['frames']:
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
class DataHandler:
|
||||||
|
"""
|
||||||
|
Class that handles the hardware attributes and save files.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# Attributes that are saved to a file to recover upon startup.
|
||||||
|
self.attributes = {
|
||||||
|
'version': pocket_friends.__version__,
|
||||||
|
'time_elapsed': 0,
|
||||||
|
'bloop': '',
|
||||||
|
'age': 0,
|
||||||
|
'health': 0,
|
||||||
|
'hunger': 0,
|
||||||
|
'happiness': 0,
|
||||||
|
'care_counter': 0,
|
||||||
|
'missed_care': 0,
|
||||||
|
'adult': 0,
|
||||||
|
'evolution_stage': '',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Frame counter
|
||||||
|
self.frames_passed = 0
|
||||||
|
|
||||||
|
def write_save(self):
|
||||||
|
"""
|
||||||
|
Writes attributes of class to "save.json" file.
|
||||||
|
"""
|
||||||
|
with open(save_dir + '/save.json', 'w') as save_file:
|
||||||
|
json.dump(self.attributes, save_file)
|
||||||
|
save_file.close()
|
||||||
|
|
||||||
|
def read_save(self):
|
||||||
|
"""
|
||||||
|
Reads from "save.json" and inserts into attributes dictionary. Creates file if it does not exist.
|
||||||
|
"""
|
||||||
|
# Open up the save file and read it into self.attributes.
|
||||||
|
try:
|
||||||
|
with open(save_dir + '/save.json', 'r') as save_file:
|
||||||
|
self.attributes = json.load(save_file)
|
||||||
|
save_file.close()
|
||||||
|
|
||||||
|
# If there is no save file, write one with the defaults.
|
||||||
|
except FileNotFoundError:
|
||||||
|
self.write_save()
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""
|
||||||
|
Run the game logic.
|
||||||
|
"""
|
||||||
|
self.frames_passed += 1
|
||||||
|
# Run logic of the game every second.
|
||||||
|
if self.frames_passed >= game_fps:
|
||||||
|
|
||||||
|
# Add one to the age of the bloop.
|
||||||
|
self.attributes['age'] += 1
|
||||||
|
|
||||||
|
# Save the data when the age of the bloop is a multiple of 10.
|
||||||
|
if self.attributes['age'] % 10 == 0:
|
||||||
|
self.write_save()
|
||||||
|
|
||||||
|
# Reset frame counter
|
||||||
|
self.frames_passed = 0
|
||||||
|
|
||||||
|
|
||||||
|
class PlaygroundFriend(pygame.sprite.Sprite):
|
||||||
|
"""
|
||||||
|
Class for the sprite of the creature on the main playground.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, data_handler):
|
||||||
|
pygame.sprite.Sprite.__init__(self)
|
||||||
|
|
||||||
|
# All attributes of the bloops
|
||||||
|
self.bloop = data_handler.attributes['bloop']
|
||||||
|
self.adult = data_handler.attributes['adult']
|
||||||
|
self.evolution_stage = data_handler.attributes['evolution_stage']
|
||||||
|
self.direction = 0
|
||||||
|
|
||||||
|
if self.evolution_stage == 'adult':
|
||||||
|
image = self.evolution_stage + self.adult
|
||||||
|
else:
|
||||||
|
image = self.evolution_stage
|
||||||
|
|
||||||
|
# Draw the correct bloop depending on the stage
|
||||||
|
sprite_sheet = SpriteSheet(script_dir + '/resources/images/bloops/{0}/{1}.png'.format(self.bloop, image),
|
||||||
|
script_dir + '/resources/images/bloops/{0}/{1}.json'.format(self.bloop, image))
|
||||||
|
|
||||||
|
# Load the images from the sprite sheet
|
||||||
|
self.images = sprite_sheet.images
|
||||||
|
|
||||||
|
# Put the egg in the middle of the screen.
|
||||||
|
self.rect = self.images[0].get_rect()
|
||||||
|
self.rect.x = (game_res / 2) - (self.rect.width / 2)
|
||||||
|
self.rect.y = (game_res / 2) - (self.rect.height / 2)
|
||||||
|
|
||||||
|
# Start animation at the beginning of the sprite sheet.
|
||||||
|
self.index = 0
|
||||||
|
self.image = self.images[self.index]
|
||||||
|
|
||||||
|
self.movement_frames = game_fps / 2 # How many frames pass before the bloop moves
|
||||||
|
self.current_frame = 0
|
||||||
|
|
||||||
|
def pet(self):
|
||||||
|
"""
|
||||||
|
Pet the bloop!
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""
|
||||||
|
Takes the images loaded and animates it, spacing it out equally for the framerate.
|
||||||
|
"""
|
||||||
|
|
||||||
|
margins = 9 # Margins for how far the bloop can move from the left and the right of the screen
|
||||||
|
movement_amount = 2 # Pixels that the bloop moves in one movement
|
||||||
|
|
||||||
|
self.current_frame += 1
|
||||||
|
|
||||||
|
# Check to see if the number of movement frames has passed
|
||||||
|
if self.current_frame >= self.movement_frames:
|
||||||
|
self.current_frame = 0
|
||||||
|
|
||||||
|
# Move only if the bloop is not in the egg stage
|
||||||
|
if self.evolution_stage != 'egg':
|
||||||
|
|
||||||
|
# Change direction if the bloop has reached either edge of the screen
|
||||||
|
if self.rect.x < margins:
|
||||||
|
self.direction = 1
|
||||||
|
elif self.rect.x > game_res - margins - self.rect.width:
|
||||||
|
self.direction = 0
|
||||||
|
|
||||||
|
# Move according to the direction.
|
||||||
|
if self.direction == 0:
|
||||||
|
self.rect.x -= movement_amount
|
||||||
|
else:
|
||||||
|
self.rect.x += movement_amount
|
||||||
|
|
||||||
|
# Animate the bloop
|
||||||
|
self.index = (self.index + 1) % len(self.images)
|
||||||
|
self.image = self.images[self.index]
|
||||||
|
|
||||||
|
|
||||||
|
class SelectionEgg(pygame.sprite.Sprite):
|
||||||
|
"""
|
||||||
|
Class for the eggs on the egg selection screen.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, egg_color):
|
||||||
|
pygame.sprite.Sprite.__init__(self)
|
||||||
|
|
||||||
|
self.egg_color = egg_color
|
||||||
|
|
||||||
|
# Loads the JSON file of the egg to read in data.
|
||||||
|
with open(script_dir + '/resources/data/bloop_info/{0}.json'.format(egg_color), 'r') as save_file:
|
||||||
|
json_file = json.load(save_file)
|
||||||
|
save_file.close()
|
||||||
|
|
||||||
|
# Gets the description off the egg from the JSON file.
|
||||||
|
self.description = json_file.get('description')
|
||||||
|
self.contentedness = json_file.get('contentedness')
|
||||||
|
self.metabolism = json_file.get('metabolism')
|
||||||
|
|
||||||
|
# Load the egg from the given color and get the bounding rectangle for the image.
|
||||||
|
sprite_sheet = SpriteSheet(script_dir + '/resources/images/bloops/{0}/egg.png'.format(self.egg_color),
|
||||||
|
script_dir + '/resources/images/bloops/{0}/egg.json'.format(self.egg_color))
|
||||||
|
self.images = sprite_sheet.images
|
||||||
|
|
||||||
|
# Get the rectangle from the first image in the list
|
||||||
|
self.rect = self.images[0].get_rect()
|
||||||
|
self.index = 0
|
||||||
|
self.image = self.images[self.index]
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""
|
||||||
|
Updates the sprite object.
|
||||||
|
"""
|
||||||
|
# Animate the sprite
|
||||||
|
self.index = (self.index + 1) % len(self.images)
|
||||||
|
self.image = self.images[self.index]
|
||||||
|
|
||||||
|
|
||||||
|
class EggInfo:
|
||||||
|
"""
|
||||||
|
Class to draw the contentedness and metabolism value off the egg on the info screen.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, contentedness, metabolism, location):
|
||||||
|
self.contentedness = contentedness
|
||||||
|
self.metabolism = metabolism
|
||||||
|
self.x = location[0]
|
||||||
|
self.y = location[1]
|
||||||
|
|
||||||
|
# Create a new surface to blit onto the other surface
|
||||||
|
self.surface = pygame.Surface((44, 15), SRCALPHA)
|
||||||
|
|
||||||
|
# Blit the two indicator icons on screen
|
||||||
|
smiley = pygame.image.load(script_dir + '/resources/images/gui/smiley.png').convert_alpha()
|
||||||
|
self.surface.blit(smiley, (0, 0))
|
||||||
|
apple = pygame.image.load(script_dir + '/resources/images/gui/apple.png').convert_alpha()
|
||||||
|
self.surface.blit(apple, (1, 9))
|
||||||
|
|
||||||
|
# Draw 5 stars. If the value of the contentedness is less than the current star, make it a blank star.
|
||||||
|
for i in range(5):
|
||||||
|
if i < self.contentedness:
|
||||||
|
star = pygame.image.load(script_dir + '/resources/images/gui/star.png').convert_alpha()
|
||||||
|
else:
|
||||||
|
star = pygame.image.load(script_dir + '/resources/images/gui/blank_star.png').convert_alpha()
|
||||||
|
self.surface.blit(star, (11 + (i * 6), 1))
|
||||||
|
|
||||||
|
# Draw 5 stars. If the value of the metabolism is less than the current star, make it a blank star.
|
||||||
|
for i in range(5):
|
||||||
|
if i < self.metabolism:
|
||||||
|
star = pygame.image.load(script_dir + '/resources/images/gui/star.png').convert_alpha()
|
||||||
|
else:
|
||||||
|
star = pygame.image.load(script_dir + '/resources/images/gui/blank_star.png').convert_alpha()
|
||||||
|
self.surface.blit(star, (11 + (i * 6), 10))
|
||||||
|
|
||||||
|
def draw(self, surface):
|
||||||
|
"""
|
||||||
|
Draw the info icons on a given surface.
|
||||||
|
:param surface: the surface to draw the icons on.
|
||||||
|
"""
|
||||||
|
# Blit the info onto the given surface.
|
||||||
|
surface.blit(self.surface, (self.x, self.y))
|
||||||
|
|
||||||
|
|
||||||
|
class InfoText:
|
||||||
|
"""
|
||||||
|
Class for drawing large amounts of text on the screen at a time
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, font, text='Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam commodo tempor '
|
||||||
|
'aliquet. Suspendisse placerat accumsan neque, nec volutpat nunc porta ut.'):
|
||||||
|
|
||||||
|
self.font = font
|
||||||
|
self.text = [] # Text broken up into a list according to how it will fit on screen.
|
||||||
|
self.max_lines = 6 # Max number of lines to be shown on screen at a time.
|
||||||
|
self.offset = 0
|
||||||
|
|
||||||
|
# Arrow icons to indicate scrolling
|
||||||
|
self.up_arrow = pygame.image.load(script_dir + '/resources/images/gui/up_arrow.png').convert_alpha()
|
||||||
|
self.down_arrow = pygame.image.load(script_dir + '/resources/images/gui/down_arrow.png').convert_alpha()
|
||||||
|
|
||||||
|
raw_text = text # Copy the text to a different variable to be cut up.
|
||||||
|
|
||||||
|
margins = 4.5
|
||||||
|
max_line_width = game_res - (margins * 2) # The maximum pixel width that drawn text can be.
|
||||||
|
cut_chars = '.,! ' # Characters that will be considered "cuts" aka when a line break can occur.
|
||||||
|
|
||||||
|
# Prevents freezing if the end of the string does not end in a cut character
|
||||||
|
# Will fix eventually more elegantly
|
||||||
|
if raw_text[-1:] not in cut_chars:
|
||||||
|
raw_text += ' '
|
||||||
|
|
||||||
|
# Calculating line breaks.
|
||||||
|
while len(raw_text) > 0:
|
||||||
|
index = 0
|
||||||
|
test_text = '' # Chunk of text to pseudo-render and test the width of.
|
||||||
|
|
||||||
|
# Loops until the testing text has reached the size limit.
|
||||||
|
while True:
|
||||||
|
|
||||||
|
# Break if the current index is larger than the remaining text.
|
||||||
|
if index + 1 > len(raw_text):
|
||||||
|
index -= 1
|
||||||
|
break
|
||||||
|
|
||||||
|
# Add one character to the testing text from the raw text.
|
||||||
|
test_text += raw_text[index]
|
||||||
|
# Get the width of the pseudo-rendered text.
|
||||||
|
text_width = font.size(test_text)[0]
|
||||||
|
|
||||||
|
# Break if the text is larger than the defined max width.
|
||||||
|
if text_width > max_line_width:
|
||||||
|
break
|
||||||
|
index += 1
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Gets the chunk of text to be added to the list.
|
||||||
|
text_chunk = raw_text[0:index + 1]
|
||||||
|
# Determines if the chunk of text has any break characters.
|
||||||
|
has_breaks = any(cut_chars in text_chunk for cut_chars in cut_chars)
|
||||||
|
|
||||||
|
# If the text has break characters, start with the last character and go backwards until
|
||||||
|
# one has been found, decreasing the index each time.
|
||||||
|
if has_breaks:
|
||||||
|
while raw_text[index] not in cut_chars:
|
||||||
|
index -= 1
|
||||||
|
text_chunk = raw_text[0:index + 1]
|
||||||
|
# If there are no break characters in the chunk, simply decrease the index by one and insert
|
||||||
|
# a dash at the end of the line to indicate the word continues.
|
||||||
|
else:
|
||||||
|
index -= 1
|
||||||
|
text_chunk = raw_text[0:index + 1]
|
||||||
|
text_chunk += '-'
|
||||||
|
|
||||||
|
# Append the text chunk to the list of text to draw.
|
||||||
|
self.text.append(text_chunk)
|
||||||
|
|
||||||
|
# Cut the text to repeat the process with the new cut string.
|
||||||
|
raw_text = raw_text[index + 1:]
|
||||||
|
|
||||||
|
def draw(self, surface):
|
||||||
|
"""
|
||||||
|
Draws the text on a given surface.
|
||||||
|
:param surface: The surface for the text to be drawn on.
|
||||||
|
"""
|
||||||
|
# Constants to help draw the text
|
||||||
|
line_separation = 7
|
||||||
|
left_margin = 3
|
||||||
|
top_margin = 25
|
||||||
|
bottom_margin = 10
|
||||||
|
|
||||||
|
# Draw the lines on the screen
|
||||||
|
for i in range(min(len(self.text), self.max_lines)):
|
||||||
|
text = self.font.render(self.text[i + self.offset], False, (64, 64, 64))
|
||||||
|
surface.blit(text, (left_margin, top_margin + (i * line_separation)))
|
||||||
|
|
||||||
|
# Draw the arrows if there is more text than is on screen.
|
||||||
|
if self.offset != 0:
|
||||||
|
surface.blit(self.up_arrow, ((game_res / 2) - (self.up_arrow.get_rect().width / 2), top_margin - 3))
|
||||||
|
if len(self.text) - (self.offset + 1) >= self.max_lines:
|
||||||
|
surface.blit(self.down_arrow,
|
||||||
|
((game_res / 2) - (self.down_arrow.get_rect().width / 2), game_res - bottom_margin))
|
||||||
|
|
||||||
|
def scroll_down(self):
|
||||||
|
"""
|
||||||
|
Scrolls the text on the screen down.
|
||||||
|
"""
|
||||||
|
# Ensures that the offset cannot be too big as to try to render non-existent lines.
|
||||||
|
if len(self.text) - (self.offset + 1) >= self.max_lines:
|
||||||
|
self.offset += 1
|
||||||
|
|
||||||
|
def scroll_up(self):
|
||||||
|
"""
|
||||||
|
Scrolls the text on the screen up.
|
||||||
|
"""
|
||||||
|
if self.offset > 0: # Ensures a non-zero offset is not possible.
|
||||||
|
self.offset -= 1
|
||||||
|
|
||||||
|
|
||||||
|
class MenuIcon(pygame.sprite.Sprite):
|
||||||
|
"""
|
||||||
|
Sprite for an icon on the main popup menu.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, icon):
|
||||||
|
pygame.sprite.Sprite.__init__(self)
|
||||||
|
self.icon = icon
|
||||||
|
|
||||||
|
# Load the sprite sheet from the icon name
|
||||||
|
sprite_sheet = SpriteSheet(script_dir + '/resources/images/gui/popup_menu/{0}.png'.format(self.icon),
|
||||||
|
script_dir + '/resources/images/gui/popup_menu/{0}.json'.format(self.icon))
|
||||||
|
self.images = sprite_sheet.images
|
||||||
|
|
||||||
|
# Get the rectangle from the first image in the list
|
||||||
|
self.rect = self.images[0].get_rect()
|
||||||
|
self.image = self.images[0]
|
||||||
|
|
||||||
|
def select(self):
|
||||||
|
"""
|
||||||
|
Change the icon sprite to the selected icon.
|
||||||
|
"""
|
||||||
|
self.image = self.images[1]
|
||||||
|
|
||||||
|
def deselect(self):
|
||||||
|
"""
|
||||||
|
Change the icon sprite to the not selected icon.
|
||||||
|
"""
|
||||||
|
self.image = self.images[0]
|
||||||
|
|
||||||
|
|
||||||
|
class PopupMenu:
|
||||||
|
"""
|
||||||
|
Class to create a popup menu that can be hidden and shown at will
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, position):
|
||||||
|
# Background frame of the popup menu
|
||||||
|
self.frame = pygame.image.load(script_dir + '/resources/images/gui/popup_menu/frame.png').convert_alpha()
|
||||||
|
|
||||||
|
self.draw_menu = False # Whether or not to draw the popup menu
|
||||||
|
self.menu_sprites = pygame.sprite.Group() # Sprite group for the icons
|
||||||
|
self.selected = 0 # The currently selected icon
|
||||||
|
|
||||||
|
# The names of the icons to be drawn
|
||||||
|
icon_names = ['apple', 'dumbbell', 'stats', 'controller', 'bed']
|
||||||
|
|
||||||
|
self.icons = []
|
||||||
|
# Create an icon sprite for each name in the list and add it to the icon list
|
||||||
|
for i in icon_names:
|
||||||
|
self.icons.append(MenuIcon(i))
|
||||||
|
|
||||||
|
# Add each sprite in the icon list to the sprite group
|
||||||
|
for i in range(len(self.icons)):
|
||||||
|
icon = self.icons[i]
|
||||||
|
if i == self.selected: # Make the default selected icon glow
|
||||||
|
icon.select()
|
||||||
|
|
||||||
|
# Calculate the position of the icon on screen
|
||||||
|
icon.rect.x = 10 + (i * 15) - (icon.image.get_width() / 2)
|
||||||
|
icon.rect.y = position[1] + self.frame.get_height() / 2 - icon.image.get_height() / 2
|
||||||
|
|
||||||
|
# Add the icon to the sprite group.
|
||||||
|
self.menu_sprites.add(icon)
|
||||||
|
|
||||||
|
def toggle(self):
|
||||||
|
"""
|
||||||
|
Toggles the menu on or off.
|
||||||
|
"""
|
||||||
|
self.draw_menu = not self.draw_menu
|
||||||
|
|
||||||
|
def next(self):
|
||||||
|
"""
|
||||||
|
Changes the selection to the next icon (to the right.)
|
||||||
|
"""
|
||||||
|
if self.draw_menu: # Only change if the menu is on screen
|
||||||
|
|
||||||
|
self.icons[self.selected].deselect() # Deselect the current icon
|
||||||
|
self.selected += 1 # Change selection to the next icon
|
||||||
|
if self.selected >= len(self.icons): # Wrap around if new value is invalid
|
||||||
|
self.selected = 0
|
||||||
|
self.icons[self.selected].select() # Select the newly selected icon
|
||||||
|
|
||||||
|
def prev(self):
|
||||||
|
"""
|
||||||
|
Changes the selection to the previous icon (to the left.)
|
||||||
|
"""
|
||||||
|
if self.draw_menu: # Only change if the menu is on screen
|
||||||
|
|
||||||
|
self.icons[self.selected].deselect() # Deselect the current icon
|
||||||
|
self.selected -= 1 # Change selection to the previous icon
|
||||||
|
if self.selected < 0: # Wrap around if new value is invalid
|
||||||
|
self.selected = len(self.icons) - 1
|
||||||
|
self.icons[self.selected].select() # Select the newly selected icon
|
||||||
|
|
||||||
|
def draw(self, surface):
|
||||||
|
"""
|
||||||
|
Draw the menu onto a given surface
|
||||||
|
:param surface: the surface to draw the menu on.
|
||||||
|
"""
|
||||||
|
# Draw the menu only if it is toggled on.
|
||||||
|
if self.draw_menu:
|
||||||
|
surface.blit(self.frame, (3, 3))
|
||||||
|
self.menu_sprites.draw(surface)
|
||||||
|
|
||||||
|
|
||||||
|
# Makes Pygame draw on the display of the RPi.
|
||||||
|
os.environ["SDL_FBDEV"] = "/dev/fb1"
|
||||||
|
|
||||||
|
# Useful for debugging on the PC. Imports a fake RPi.GPIO library if one is not found (which it can't
|
||||||
|
# be on a PC, RPi.GPIO cannot be installed outside of a Raspberry Pi.
|
||||||
|
try:
|
||||||
|
importlib.util.find_spec('RPi.GPIO')
|
||||||
|
import RPi.GPIO as GPIO
|
||||||
|
|
||||||
|
on_hardware = True
|
||||||
|
except ImportError:
|
||||||
|
import pocket_friends.development.FakeGPIO as GPIO
|
||||||
|
|
||||||
|
on_hardware = False
|
||||||
|
|
||||||
|
|
||||||
|
def game():
|
||||||
|
"""
|
||||||
|
Starts the game.
|
||||||
|
"""
|
||||||
|
pygame.init()
|
||||||
|
|
||||||
|
# Hide the cursor for the Pi display.
|
||||||
|
pygame.mouse.set_visible(False)
|
||||||
|
|
||||||
|
# The game is normally rendered at 80 pixels and upscaled from there. If changing displays, change the
|
||||||
|
# screen_size to reflect what the resolution of the new display is.
|
||||||
|
screen_size = 320
|
||||||
|
|
||||||
|
window = pygame.display.set_mode((screen_size, screen_size))
|
||||||
|
surface = pygame.Surface((game_res, game_res))
|
||||||
|
|
||||||
|
# Only really useful for PCs. Does nothing on the Raspberry Pi.
|
||||||
|
pygame.display.set_caption('Pocket Friends {0}'.format(pocket_friends.__version__))
|
||||||
|
|
||||||
|
# Add an icon to the pygame window.
|
||||||
|
icon = pygame.image.load(script_dir + '/resources/images/icon/icon.png').convert_alpha()
|
||||||
|
pygame.display.set_icon(icon)
|
||||||
|
|
||||||
|
clock = pygame.time.Clock()
|
||||||
|
|
||||||
|
# Font used for small text in the game. Bigger text is usually image files.
|
||||||
|
small_font = pygame.font.Font(script_dir + '/resources/fonts/5Pts5.ttf', 10)
|
||||||
|
|
||||||
|
# Default game state when the game first starts.
|
||||||
|
game_state = 'title'
|
||||||
|
running = True
|
||||||
|
data_handler = DataHandler()
|
||||||
|
|
||||||
|
# A group of all the sprites on screen. Used to update all sprites at onc
|
||||||
|
all_sprites = pygame.sprite.Group()
|
||||||
|
|
||||||
|
# Start the GPIO handler to take in buttons from the RPi HAT.
|
||||||
|
GPIOHandler.setup()
|
||||||
|
|
||||||
|
# Dev code used to exit the game. Default Down, Down, Up, Up, Down, Down, Up, Up, A, A, B
|
||||||
|
dev_code = deque()
|
||||||
|
for button in [Constants.buttons.get('j_d'), Constants.buttons.get('j_d'), Constants.buttons.get('j_u'),
|
||||||
|
Constants.buttons.get('j_u'), Constants.buttons.get('j_d'), Constants.buttons.get('j_d'),
|
||||||
|
Constants.buttons.get('j_u'), Constants.buttons.get('j_u'), Constants.buttons.get('a'),
|
||||||
|
Constants.buttons.get('a'), Constants.buttons.get('b')]:
|
||||||
|
dev_code.append(button)
|
||||||
|
|
||||||
|
# Log of the inputs.
|
||||||
|
input_log = deque()
|
||||||
|
|
||||||
|
# Time since last input. Used to help regulate double presses of buttons.
|
||||||
|
last_input_tick = 0
|
||||||
|
|
||||||
|
def draw():
|
||||||
|
"""
|
||||||
|
Draws the main pygame display.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Draws all the sprites on screen and scales the screen to the correct size from the rendered size.
|
||||||
|
all_sprites.update()
|
||||||
|
all_sprites.draw(surface)
|
||||||
|
frame = pygame.transform.scale(surface, (screen_size, screen_size))
|
||||||
|
window.blit(frame, frame.get_rect())
|
||||||
|
|
||||||
|
# Update the entire display.
|
||||||
|
pygame.display.flip()
|
||||||
|
|
||||||
|
def draw_bg():
|
||||||
|
"""
|
||||||
|
Draws the main game background image onto a given surface.
|
||||||
|
"""
|
||||||
|
bg_image = pygame.image.load(script_dir + '/resources/images/bg.png').convert()
|
||||||
|
surface.blit(bg_image, (0, 0))
|
||||||
|
|
||||||
|
def log_button(pressed_button):
|
||||||
|
"""
|
||||||
|
Logs the button presses to register the dev code.
|
||||||
|
:param pressed_button: The button code to be logged
|
||||||
|
"""
|
||||||
|
input_log.append(pressed_button)
|
||||||
|
if len(input_log) > len(dev_code):
|
||||||
|
input_log.popleft()
|
||||||
|
|
||||||
|
def create_event(pressed_button):
|
||||||
|
"""
|
||||||
|
Creates a pygame event with a given keyboard code
|
||||||
|
:param pressed_button:
|
||||||
|
"""
|
||||||
|
nonlocal last_input_tick
|
||||||
|
# Register a button click so long as the last button click happened no less than two frames ago
|
||||||
|
if pygame.time.get_ticks() - last_input_tick > clock.get_time() * 2 or not on_hardware:
|
||||||
|
pygame.event.post(pygame.event.Event(KEYDOWN, {'key': pressed_button}))
|
||||||
|
pygame.event.post(pygame.event.Event(KEYUP, {'key': pressed_button}))
|
||||||
|
log_button(pressed_button)
|
||||||
|
last_input_tick = pygame.time.get_ticks()
|
||||||
|
|
||||||
|
def check_dev_code():
|
||||||
|
"""
|
||||||
|
Checks if the dev code has been entered. If it has, stop the program.
|
||||||
|
"""
|
||||||
|
nonlocal running
|
||||||
|
|
||||||
|
if dev_code == input_log:
|
||||||
|
running = False
|
||||||
|
|
||||||
|
def handle_gpio():
|
||||||
|
"""
|
||||||
|
Handles getting GPIO button presses and making a pygame event when a press is detected.
|
||||||
|
"""
|
||||||
|
for pressed_button in Constants.buttons:
|
||||||
|
code = Constants.buttons.get(pressed_button)
|
||||||
|
|
||||||
|
# Check if a button has been pressed. If it has, create a pygame event for it.
|
||||||
|
if GPIOHandler.get_press(code):
|
||||||
|
create_event(code)
|
||||||
|
|
||||||
|
def keyboard_handler():
|
||||||
|
"""
|
||||||
|
Simulates key presses to GPIO button presses. Also handles quitting the game.
|
||||||
|
"""
|
||||||
|
nonlocal running
|
||||||
|
|
||||||
|
# Checks if a corresponding keyboard key has been pressed. If it has, emulate a button press.
|
||||||
|
for keyboard_event in pygame.event.get():
|
||||||
|
if keyboard_event.type == pygame.QUIT:
|
||||||
|
running = False
|
||||||
|
if keyboard_event.type == pygame.KEYDOWN:
|
||||||
|
if keyboard_event.key == pygame.K_a:
|
||||||
|
create_event(Constants.buttons.get('a'))
|
||||||
|
if keyboard_event.key == pygame.K_b:
|
||||||
|
create_event(Constants.buttons.get('b'))
|
||||||
|
if keyboard_event.key == pygame.K_PERIOD:
|
||||||
|
create_event(Constants.buttons.get('j_i'))
|
||||||
|
if keyboard_event.key == pygame.K_RIGHT:
|
||||||
|
create_event(Constants.buttons.get('j_r'))
|
||||||
|
if keyboard_event.key == pygame.K_LEFT:
|
||||||
|
create_event(Constants.buttons.get('j_l'))
|
||||||
|
if keyboard_event.key == pygame.K_DOWN:
|
||||||
|
create_event(Constants.buttons.get('j_d'))
|
||||||
|
if keyboard_event.key == pygame.K_UP:
|
||||||
|
create_event(Constants.buttons.get('j_u'))
|
||||||
|
if keyboard_event.key == pygame.K_ESCAPE:
|
||||||
|
running = False
|
||||||
|
|
||||||
|
def pre_handler():
|
||||||
|
"""
|
||||||
|
Runs at the beginning of each loop, handles drawing the background, controlling game speed, and
|
||||||
|
controlling the GPIO button inputs and keyboard handler
|
||||||
|
"""
|
||||||
|
# Regulate the speed of the game.
|
||||||
|
clock.tick(game_fps)
|
||||||
|
|
||||||
|
# Handle all inputs for both debugging and real GPIO button presses.
|
||||||
|
keyboard_handler()
|
||||||
|
handle_gpio()
|
||||||
|
check_dev_code()
|
||||||
|
|
||||||
|
# Draw the background.
|
||||||
|
draw_bg()
|
||||||
|
|
||||||
|
while running:
|
||||||
|
if game_state == 'title':
|
||||||
|
all_sprites.empty()
|
||||||
|
pre_handler()
|
||||||
|
|
||||||
|
# Draw the title image in the middle of the screen.
|
||||||
|
title_image = pygame.image.load(script_dir + '/resources/images/title.png').convert_alpha()
|
||||||
|
surface.blit(title_image, (0, 0))
|
||||||
|
draw()
|
||||||
|
|
||||||
|
# Show the title for 1 second then move on to the initialization phase of the game.
|
||||||
|
pygame.time.wait(1000)
|
||||||
|
game_state = 'init'
|
||||||
|
|
||||||
|
elif game_state == 'playground':
|
||||||
|
|
||||||
|
# Submenu used within the playground.
|
||||||
|
submenu = 'main'
|
||||||
|
|
||||||
|
while running and game_state == 'playground':
|
||||||
|
|
||||||
|
all_sprites.empty()
|
||||||
|
|
||||||
|
if submenu == 'main':
|
||||||
|
|
||||||
|
# Create the bloop and the menu
|
||||||
|
bloop = PlaygroundFriend(data_handler)
|
||||||
|
all_sprites.add(bloop)
|
||||||
|
popup_menu = PopupMenu((3, 3))
|
||||||
|
|
||||||
|
while running and game_state == 'playground' and submenu == 'main':
|
||||||
|
pre_handler()
|
||||||
|
data_handler.update()
|
||||||
|
|
||||||
|
for event in pygame.event.get():
|
||||||
|
if event.type == pygame.KEYDOWN:
|
||||||
|
if event.key == Constants.buttons.get('j_r'):
|
||||||
|
# Move selection to the next item
|
||||||
|
popup_menu.next()
|
||||||
|
if event.key == Constants.buttons.get('j_l'):
|
||||||
|
# Move selection to the previous item
|
||||||
|
popup_menu.prev()
|
||||||
|
if event.key == Constants.buttons.get('a'):
|
||||||
|
# Change submenu to the menu the icon points to
|
||||||
|
if popup_menu.draw_menu:
|
||||||
|
submenu = popup_menu.icons[popup_menu.selected].icon
|
||||||
|
else: # Pet the bloop otherwise
|
||||||
|
bloop.pet()
|
||||||
|
if event.key == Constants.buttons.get('b'):
|
||||||
|
# Toggle the popup menu on or off
|
||||||
|
popup_menu.toggle()
|
||||||
|
|
||||||
|
# Draw the popup menu if toggled on
|
||||||
|
popup_menu.draw(surface)
|
||||||
|
|
||||||
|
draw()
|
||||||
|
|
||||||
|
else: # Go to the error state if an invalid state is set.
|
||||||
|
game_state = None
|
||||||
|
|
||||||
|
elif game_state == 'init':
|
||||||
|
all_sprites.empty()
|
||||||
|
pre_handler()
|
||||||
|
draw()
|
||||||
|
|
||||||
|
# Read the save file.
|
||||||
|
data_handler.read_save()
|
||||||
|
|
||||||
|
# Determines if it is a new game or not by looking at the evolution stage. If it is -1, the egg has
|
||||||
|
# not been created yet, and the game sends you to the egg selection screen. If not, the game sends
|
||||||
|
# you to the playground.
|
||||||
|
if data_handler.attributes['bloop'] == '':
|
||||||
|
game_state = 'egg_select'
|
||||||
|
else:
|
||||||
|
game_state = 'playground'
|
||||||
|
|
||||||
|
elif game_state == 'egg_select':
|
||||||
|
|
||||||
|
# Submenu used within the egg selection menu.
|
||||||
|
submenu = 'main'
|
||||||
|
|
||||||
|
selected = 0
|
||||||
|
selected_color = ""
|
||||||
|
|
||||||
|
while running and game_state == 'egg_select':
|
||||||
|
|
||||||
|
all_sprites.empty()
|
||||||
|
|
||||||
|
if submenu == 'main':
|
||||||
|
|
||||||
|
# Creates and holds the egg objects in a list.
|
||||||
|
eggs = [SelectionEgg('dev_egg'), SelectionEgg('blue'), SelectionEgg('rainbow')]
|
||||||
|
|
||||||
|
# How many eggs per row should be displayed.
|
||||||
|
eggs_per_row = 3
|
||||||
|
distance_between_eggs = 36 / eggs_per_row
|
||||||
|
|
||||||
|
# Count the total rows.
|
||||||
|
total_rows = -(-len(eggs) // eggs_per_row)
|
||||||
|
distance_between_rows = 32 / eggs_per_row
|
||||||
|
|
||||||
|
# Determine the location of each egg.
|
||||||
|
for egg in eggs:
|
||||||
|
current_row = eggs.index(egg) // eggs_per_row
|
||||||
|
rows_after = total_rows - (current_row + 1)
|
||||||
|
egg_in_row = eggs.index(egg) % eggs_per_row
|
||||||
|
eggs_after = min(len(eggs) - (current_row * eggs_per_row), eggs_per_row) - (egg_in_row + 1)
|
||||||
|
|
||||||
|
x_offset = 32
|
||||||
|
y_offset = 30
|
||||||
|
|
||||||
|
# The x coordinate of an egg is determined by which egg in the row it is, and how many eggs
|
||||||
|
# are in that row. If there is only 1 egg in a row, it is in the middle of the screen. If
|
||||||
|
# there are two, they're on equal halves and so on.
|
||||||
|
x = x_offset - (eggs_after * distance_between_eggs) + (egg_in_row * distance_between_eggs)
|
||||||
|
y = y_offset - (rows_after * distance_between_rows) + (current_row * distance_between_rows)
|
||||||
|
|
||||||
|
egg.rect.x = x
|
||||||
|
egg.rect.y = y
|
||||||
|
|
||||||
|
# Add the egg to the sprite list.
|
||||||
|
all_sprites.add(egg)
|
||||||
|
|
||||||
|
def get_cursor_coords(selection):
|
||||||
|
"""
|
||||||
|
Gets the coordinates of an egg on the selection screen by index and returns it as a tuple
|
||||||
|
:param selection: index of the egg to be selected
|
||||||
|
:return: tuple of the coordinates of the selected egg
|
||||||
|
"""
|
||||||
|
cursor_x_offset = -2
|
||||||
|
cursor_y_offset = -2
|
||||||
|
|
||||||
|
return eggs[selection].rect.x + cursor_x_offset, eggs[selection].rect.y + cursor_y_offset
|
||||||
|
|
||||||
|
def sel_left():
|
||||||
|
"""
|
||||||
|
Select the egg to the left with constraints.
|
||||||
|
"""
|
||||||
|
nonlocal selected
|
||||||
|
|
||||||
|
if selected % eggs_per_row != 0:
|
||||||
|
selected -= 1
|
||||||
|
|
||||||
|
def sel_right():
|
||||||
|
"""
|
||||||
|
Select the egg to the right with constraints.
|
||||||
|
"""
|
||||||
|
nonlocal selected
|
||||||
|
|
||||||
|
row = selected // eggs_per_row
|
||||||
|
eggs_in_row = min(len(eggs) - (row * eggs_per_row), eggs_per_row)
|
||||||
|
|
||||||
|
if selected % eggs_per_row != eggs_in_row - 1:
|
||||||
|
selected += 1
|
||||||
|
|
||||||
|
def sel_up():
|
||||||
|
"""
|
||||||
|
Select the egg above with constraints.
|
||||||
|
"""
|
||||||
|
nonlocal selected
|
||||||
|
|
||||||
|
if selected // eggs_per_row != 0:
|
||||||
|
selected -= eggs_per_row
|
||||||
|
|
||||||
|
def sel_down():
|
||||||
|
"""
|
||||||
|
Select the egg below with constraints.
|
||||||
|
"""
|
||||||
|
nonlocal selected
|
||||||
|
|
||||||
|
if selected // eggs_per_row != total_rows - 1:
|
||||||
|
selected += eggs_per_row
|
||||||
|
|
||||||
|
while running and game_state == 'egg_select' and submenu == 'main':
|
||||||
|
|
||||||
|
pre_handler()
|
||||||
|
|
||||||
|
for event in pygame.event.get():
|
||||||
|
if event.type == pygame.KEYDOWN:
|
||||||
|
if event.key == Constants.buttons.get('j_r'):
|
||||||
|
sel_right()
|
||||||
|
if event.key == Constants.buttons.get('j_l'):
|
||||||
|
sel_left()
|
||||||
|
if event.key == Constants.buttons.get('j_d'):
|
||||||
|
sel_down()
|
||||||
|
if event.key == Constants.buttons.get('j_u'):
|
||||||
|
sel_up()
|
||||||
|
if event.key == Constants.buttons.get('a'):
|
||||||
|
# Advance to the egg info screen for the selected egg.
|
||||||
|
submenu = 'bloop_info'
|
||||||
|
|
||||||
|
# Draws the cursor on screen.
|
||||||
|
cursor = pygame.image.load(
|
||||||
|
script_dir + '/resources/images/gui/egg_selector.png').convert_alpha()
|
||||||
|
surface.blit(cursor, get_cursor_coords(selected))
|
||||||
|
|
||||||
|
selected_color = eggs[selected].egg_color
|
||||||
|
|
||||||
|
draw()
|
||||||
|
|
||||||
|
elif submenu == 'bloop_info':
|
||||||
|
|
||||||
|
# Draw the selected egg on screen
|
||||||
|
egg = SelectionEgg(selected_color)
|
||||||
|
egg.rect.x = 8
|
||||||
|
egg.rect.y = 3
|
||||||
|
all_sprites.add(egg)
|
||||||
|
|
||||||
|
# Info screen for the eggs.
|
||||||
|
info_text = InfoText(small_font, egg.description)
|
||||||
|
info_icons = EggInfo(egg.contentedness, egg.metabolism, (32, 4))
|
||||||
|
|
||||||
|
while running and game_state == 'egg_select' and submenu == 'bloop_info':
|
||||||
|
|
||||||
|
pre_handler()
|
||||||
|
|
||||||
|
for event in pygame.event.get():
|
||||||
|
if event.type == pygame.KEYDOWN:
|
||||||
|
if event.key == Constants.buttons.get('j_d'):
|
||||||
|
# Scroll down on the info screen.
|
||||||
|
info_text.scroll_down()
|
||||||
|
if event.key == Constants.buttons.get('j_u'):
|
||||||
|
# Scroll up on the info screen.
|
||||||
|
info_text.scroll_up()
|
||||||
|
if event.key == Constants.buttons.get('a'):
|
||||||
|
# Write save file with new attributes
|
||||||
|
data_handler.attributes['bloop'] = egg.egg_color
|
||||||
|
data_handler.attributes['health'] = 10
|
||||||
|
data_handler.attributes['hunger'] = 10
|
||||||
|
data_handler.attributes['happiness'] = 10
|
||||||
|
data_handler.attributes['evolution_stage'] = 'egg'
|
||||||
|
data_handler.write_save()
|
||||||
|
|
||||||
|
# Go to playground
|
||||||
|
game_state = 'playground'
|
||||||
|
if event.key == Constants.buttons.get('b'):
|
||||||
|
# Go back to the egg selection screen.
|
||||||
|
submenu = 'main'
|
||||||
|
|
||||||
|
# Draw the info screen.
|
||||||
|
info_text.draw(surface)
|
||||||
|
info_icons.draw(surface)
|
||||||
|
|
||||||
|
draw()
|
||||||
|
|
||||||
|
else: # Go to the error state if an invalid state is set.
|
||||||
|
game_state = None
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Error screen. This appears when an invalid game state has been selected.
|
||||||
|
|
||||||
|
all_sprites.empty()
|
||||||
|
frames_passed = 0 # Counter for frames, helps ensure the game isn't frozen.
|
||||||
|
|
||||||
|
while running and game_state != 'title':
|
||||||
|
|
||||||
|
pre_handler()
|
||||||
|
|
||||||
|
# Draw the error screen
|
||||||
|
error_screen = pygame.image.load(script_dir + '/resources/images/debug/invalid.png').convert_alpha()
|
||||||
|
surface.blit(error_screen, (0, -8))
|
||||||
|
|
||||||
|
# Counts the frames passed. Resets every second.
|
||||||
|
frames_passed += 1
|
||||||
|
if frames_passed >= game_fps:
|
||||||
|
frames_passed = 0
|
||||||
|
|
||||||
|
# Draws the frame counter.
|
||||||
|
frame_counter = small_font.render('frames: {0}'.format(frames_passed), False, (64, 64, 64))
|
||||||
|
surface.blit(frame_counter, (1, game_res - 10))
|
||||||
|
|
||||||
|
for event in pygame.event.get():
|
||||||
|
if event.type == pygame.KEYDOWN:
|
||||||
|
if event.key == Constants.buttons.get('b'):
|
||||||
|
# Reset back to the title screen.
|
||||||
|
game_state = 'title'
|
||||||
|
|
||||||
|
draw()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""
|
||||||
|
Calls the game() function to start the game.
|
||||||
|
"""
|
||||||
|
game()
|
||||||
|
|
||||||
|
GPIOHandler.teardown()
|
||||||
|
pygame.quit()
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 807 B After Width: | Height: | Size: 807 B |
Before Width: | Height: | Size: 610 B After Width: | Height: | Size: 610 B |
Before Width: | Height: | Size: 820 B After Width: | Height: | Size: 820 B |
Before Width: | Height: | Size: 740 B After Width: | Height: | Size: 740 B |
Before Width: | Height: | Size: 490 B After Width: | Height: | Size: 490 B |
Before Width: | Height: | Size: 736 B After Width: | Height: | Size: 736 B |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 422 B After Width: | Height: | Size: 422 B |
Before Width: | Height: | Size: 565 B After Width: | Height: | Size: 565 B |
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 864 B After Width: | Height: | Size: 864 B |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 246 B After Width: | Height: | Size: 246 B |
Before Width: | Height: | Size: 218 B After Width: | Height: | Size: 218 B |
Before Width: | Height: | Size: 188 B After Width: | Height: | Size: 188 B |
Before Width: | Height: | Size: 178 B After Width: | Height: | Size: 178 B |
Before Width: | Height: | Size: 217 B After Width: | Height: | Size: 217 B |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 419 B After Width: | Height: | Size: 419 B |
Before Width: | Height: | Size: 268 B After Width: | Height: | Size: 268 B |
Before Width: | Height: | Size: 350 B After Width: | Height: | Size: 350 B |
Before Width: | Height: | Size: 344 B After Width: | Height: | Size: 344 B |
Before Width: | Height: | Size: 235 B After Width: | Height: | Size: 235 B |
Before Width: | Height: | Size: 302 B After Width: | Height: | Size: 302 B |
Before Width: | Height: | Size: 258 B After Width: | Height: | Size: 258 B |
Before Width: | Height: | Size: 213 B After Width: | Height: | Size: 213 B |
Before Width: | Height: | Size: 213 B After Width: | Height: | Size: 213 B |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
0
pocket_friends/hardware/__init__.py
Normal file
71
pocket_friends/hardware/gpio_handler.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
"""
|
||||||
|
Module that helps with the handling of taking inputs from the GPIO pins on the Raspberry
|
||||||
|
Pi and converting them to events to be used in other places (pygame, etc.)
|
||||||
|
"""
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
try:
|
||||||
|
importlib.util.find_spec('RPi.GPIO')
|
||||||
|
import RPi.GPIO as GPIO
|
||||||
|
except ImportError:
|
||||||
|
import pocket_friends.development.FakeGPIO as GPIO
|
||||||
|
|
||||||
|
|
||||||
|
class Constants:
|
||||||
|
"""
|
||||||
|
Contains the constants used by the HAT to read in buttons
|
||||||
|
"""
|
||||||
|
buttons = {
|
||||||
|
'a': 31, # A button
|
||||||
|
'b': 29, # B button
|
||||||
|
'j_i': 7, # Joystick in
|
||||||
|
'j_u': 11, # Joystick up
|
||||||
|
'j_d': 15, # Joystick down
|
||||||
|
'j_l': 13, # Joystick left
|
||||||
|
'j_r': 16 # Joystick right
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class GPIOHandler:
|
||||||
|
"""
|
||||||
|
Class to handle the GPIO inputs from the buttons.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def setup():
|
||||||
|
"""
|
||||||
|
Primes the GPIO pins for reading the inputs of the buttons.
|
||||||
|
"""
|
||||||
|
GPIO.setmode(GPIO.BOARD)
|
||||||
|
|
||||||
|
GPIO.setup(Constants.buttons.get('a'), GPIO.IN)
|
||||||
|
GPIO.setup(Constants.buttons.get('b'), GPIO.IN)
|
||||||
|
GPIO.setup(Constants.buttons.get('j_i'), GPIO.IN)
|
||||||
|
GPIO.setup(Constants.buttons.get('j_u'), GPIO.IN)
|
||||||
|
GPIO.setup(Constants.buttons.get('j_d'), GPIO.IN)
|
||||||
|
GPIO.setup(Constants.buttons.get('j_l'), GPIO.IN)
|
||||||
|
GPIO.setup(Constants.buttons.get('j_r'), GPIO.IN)
|
||||||
|
|
||||||
|
GPIO.add_event_detect(Constants.buttons.get('a'), GPIO.FALLING)
|
||||||
|
GPIO.add_event_detect(Constants.buttons.get('b'), GPIO.FALLING)
|
||||||
|
GPIO.add_event_detect(Constants.buttons.get('j_i'), GPIO.FALLING)
|
||||||
|
GPIO.add_event_detect(Constants.buttons.get('j_u'), GPIO.FALLING)
|
||||||
|
GPIO.add_event_detect(Constants.buttons.get('j_d'), GPIO.FALLING)
|
||||||
|
GPIO.add_event_detect(Constants.buttons.get('j_l'), GPIO.FALLING)
|
||||||
|
GPIO.add_event_detect(Constants.buttons.get('j_r'), GPIO.FALLING)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def teardown():
|
||||||
|
"""
|
||||||
|
Cleans up the GPIO handler.
|
||||||
|
"""
|
||||||
|
GPIO.cleanup()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_press(button):
|
||||||
|
"""
|
||||||
|
Returns true if a button has moved from not pressed to pressed.
|
||||||
|
:param button: button to be detected
|
||||||
|
:return: True if the button is has been pressed, False otherwise
|
||||||
|
"""
|
||||||
|
return GPIO.event_detected(button)
|
@ -1,2 +0,0 @@
|
|||||||
"""Subpackage for handling all I/O operations including keyboard input (GPIO input when connected to a Raspberry Pi)
|
|
||||||
and save data reading and writing."""
|
|
@ -1,55 +0,0 @@
|
|||||||
import pocket_friends
|
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
class SaveData:
|
|
||||||
"""
|
|
||||||
Class used to read and write save data for the game
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
attributes (dict): Dictionary containing all the attributes to read and write from a save file.
|
|
||||||
"""
|
|
||||||
def __init__(self):
|
|
||||||
"""
|
|
||||||
Constructs the object with all starting values.
|
|
||||||
"""
|
|
||||||
# Attributes that are saved to a file to recover upon startup.
|
|
||||||
self.attributes = {
|
|
||||||
'version': pocket_friends.__version__,
|
|
||||||
'time_elapsed': 0,
|
|
||||||
'bloop': '',
|
|
||||||
'age': 0,
|
|
||||||
'health': 0,
|
|
||||||
'hunger': 0,
|
|
||||||
'happiness': 0,
|
|
||||||
'care_counter': 0,
|
|
||||||
'missed_care': 0,
|
|
||||||
'adult': 0,
|
|
||||||
'evolution_stage': '',
|
|
||||||
}
|
|
||||||
|
|
||||||
# Frame counter
|
|
||||||
self.frames_passed = 0
|
|
||||||
|
|
||||||
def write_save(self):
|
|
||||||
"""
|
|
||||||
Writes attributes of the save object to "save.json" file.
|
|
||||||
"""
|
|
||||||
with open(save_dir + '/save.json', 'w') as save_file:
|
|
||||||
json.dump(self.attributes, save_file)
|
|
||||||
save_file.close()
|
|
||||||
|
|
||||||
def read_save(self):
|
|
||||||
"""
|
|
||||||
Reads from "save.json" and inserts into the save object's attributes dictionary. Creates file if it does not
|
|
||||||
exist.
|
|
||||||
"""
|
|
||||||
# Open up the save file and read it into self.attributes.
|
|
||||||
try:
|
|
||||||
with open(save_dir + '/save.json', 'r') as save_file:
|
|
||||||
self.attributes = json.load(save_file)
|
|
||||||
save_file.close()
|
|
||||||
|
|
||||||
# If there is no save file, write one with the defaults.
|
|
||||||
except FileNotFoundError:
|
|
||||||
self.write_save()
|
|
@ -1,83 +0,0 @@
|
|||||||
import pygame
|
|
||||||
from collections import deque
|
|
||||||
|
|
||||||
class InputHandler:
|
|
||||||
"""
|
|
||||||
Class that is implemented into surfaces in order to control the
|
|
||||||
pressing of buttons on both the real hardware and on a keyboard.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
clock (pygame.time.Clock): Pygame clock used for input time calculations.
|
|
||||||
last_input_tick (int): The tick that the last input was registered on.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, pygame_clock, tick_check=True):
|
|
||||||
"""
|
|
||||||
Create a InputHandler object using a given Pygame clock.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
pygame_clock (:obj:`pygame.time.Clock`): A pygame clock to use as the clock for input time calculations.
|
|
||||||
tick_check (bool, optional): Bool to ignore inputs that happen to quickly after another. Defaults to True.
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.clock = pygame_clock
|
|
||||||
self.tick_check = tick_check
|
|
||||||
self.last_input_tick = 0
|
|
||||||
self.dev_check = deque()
|
|
||||||
self.dev_code = deque()
|
|
||||||
for button in [pygame.K_DOWN, pygame.K_DOWN, pygame.K_UP, pygame.K_UP, pygame.K_DOWN, pygame.K_LEFT,
|
|
||||||
pygame.K_RIGHT, pygame.K_LEFT, pygame.K_a]:
|
|
||||||
self.dev_code.append(button)
|
|
||||||
self.dev_found = False
|
|
||||||
|
|
||||||
def create_event(self, pressed_button):
|
|
||||||
"""
|
|
||||||
Create a pygame event given a GPIO code and post it to the pygame event handler.
|
|
||||||
Args:
|
|
||||||
pressed_button (int): The GPIO code to be registered and pressed.
|
|
||||||
|
|
||||||
"""
|
|
||||||
# Register a button click so long as the last button click happened no less than two frames ago
|
|
||||||
if self.tick_check:
|
|
||||||
if pygame.time.get_ticks() - self.last_input_tick > self.clock.get_time() * 2:
|
|
||||||
pygame.event.post(pygame.event.Event(pygame.KEYDOWN, {'key': pressed_button}))
|
|
||||||
pygame.event.post(pygame.event.Event(pygame.KEYUP, {'key': pressed_button}))
|
|
||||||
self.dev_check.append(pressed_button)
|
|
||||||
if len(self.dev_check) > len(self.dev_code):
|
|
||||||
self.dev_check.popleft()
|
|
||||||
else:
|
|
||||||
pygame.event.post(pygame.event.Event(pygame.KEYDOWN, {'key': pressed_button}))
|
|
||||||
pygame.event.post(pygame.event.Event(pygame.KEYUP, {'key': pressed_button}))
|
|
||||||
self.last_input_tick = pygame.time.get_ticks()
|
|
||||||
|
|
||||||
def handle_keyboard(self):
|
|
||||||
"""Handle keyboard presses and generate corresponding GPIO codes to create events."""
|
|
||||||
|
|
||||||
# Checks if a corresponding keyboard key has been pressed. If it has, emulate a button press.
|
|
||||||
for keyboard_event in pygame.event.get():
|
|
||||||
if keyboard_event.type == pygame.QUIT:
|
|
||||||
running = False
|
|
||||||
if keyboard_event.type == pygame.KEYDOWN:
|
|
||||||
if keyboard_event.key == pygame.K_a:
|
|
||||||
self.create_event(pygame.K_a)
|
|
||||||
if keyboard_event.key == pygame.K_b:
|
|
||||||
self.create_event(pygame.K_b)
|
|
||||||
if keyboard_event.key == pygame.K_KP_ENTER:
|
|
||||||
self.create_event(pygame.K_KP_ENTER)
|
|
||||||
if keyboard_event.key == pygame.K_RIGHT:
|
|
||||||
self.create_event(pygame.K_RIGHT)
|
|
||||||
if keyboard_event.key == pygame.K_LEFT:
|
|
||||||
self.create_event(pygame.K_LEFT)
|
|
||||||
if keyboard_event.key == pygame.K_DOWN:
|
|
||||||
self.create_event(pygame.K_DOWN)
|
|
||||||
if keyboard_event.key == pygame.K_UP:
|
|
||||||
self.create_event(pygame.K_UP)
|
|
||||||
if keyboard_event.key == pygame.K_ESCAPE:
|
|
||||||
running = False
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
"""Run either the GPIO handler or the keyboard handler to check for input and create events."""
|
|
||||||
self.handle_keyboard()
|
|
||||||
if self.dev_code == self.dev_check:
|
|
||||||
self.dev_found = True
|
|
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 839 B |
@ -1,129 +0,0 @@
|
|||||||
import pygame
|
|
||||||
from pocket_friends.elements import sprites
|
|
||||||
from pocket_friends.elements import surface
|
|
||||||
|
|
||||||
|
|
||||||
class Surface(surface.GameSurface):
|
|
||||||
def __init__(self, game_res, resources_dir, game_fps, **kwargs):
|
|
||||||
super().__init__(game_res, resources_dir, game_fps)
|
|
||||||
|
|
||||||
preselected_color = None
|
|
||||||
for key in kwargs.keys():
|
|
||||||
if key == 'selected_color':
|
|
||||||
preselected_color = kwargs.get(key)
|
|
||||||
|
|
||||||
egg_list = [
|
|
||||||
'dev_egg',
|
|
||||||
'blue',
|
|
||||||
'rainbow'
|
|
||||||
]
|
|
||||||
self.eggs = []
|
|
||||||
for egg in egg_list:
|
|
||||||
self.eggs.append(sprites.SelectionEgg(egg, self.resource_dir))
|
|
||||||
|
|
||||||
self.eggs_per_row = 3
|
|
||||||
distance_between_eggs = 36 / self.eggs_per_row
|
|
||||||
|
|
||||||
# Count the total rows.
|
|
||||||
self.total_rows = -(-len(self.eggs) // self.eggs_per_row)
|
|
||||||
distance_between_rows = 32 / self.eggs_per_row
|
|
||||||
|
|
||||||
# Determine the location of each egg.
|
|
||||||
for egg in self.eggs:
|
|
||||||
current_row = self.eggs.index(egg) // self.eggs_per_row
|
|
||||||
rows_after = self.total_rows - (current_row + 1)
|
|
||||||
egg_in_row = self.eggs.index(egg) % self.eggs_per_row
|
|
||||||
eggs_after = min(len(self.eggs) - (current_row * self.eggs_per_row), self.eggs_per_row) - (egg_in_row + 1)
|
|
||||||
|
|
||||||
x_offset = 32
|
|
||||||
y_offset = 30
|
|
||||||
|
|
||||||
# The x coordinate of an egg is determined by which egg in the row it is, and how many eggs
|
|
||||||
# are in that row. If there is only 1 egg in a row, it is in the middle of the screen. If
|
|
||||||
# there are two, they're on equal halves and so on.
|
|
||||||
x = x_offset - (eggs_after * distance_between_eggs) + (egg_in_row * distance_between_eggs)
|
|
||||||
y = y_offset - (rows_after * distance_between_rows) + (current_row * distance_between_rows)
|
|
||||||
|
|
||||||
egg.rect.x = x
|
|
||||||
egg.rect.y = y
|
|
||||||
|
|
||||||
# Add the egg to the sprite list.
|
|
||||||
self._sprites.add(egg)
|
|
||||||
|
|
||||||
self.selected_egg = 0
|
|
||||||
self.selected_color = ''
|
|
||||||
if preselected_color is not None:
|
|
||||||
self.selected_color = self.eggs[self.selected_egg].egg_color
|
|
||||||
for i in range(len(self.eggs)):
|
|
||||||
if self.eggs[i].egg_color == preselected_color:
|
|
||||||
self.selected_egg = i
|
|
||||||
|
|
||||||
def get_cursor_coords(self):
|
|
||||||
"""
|
|
||||||
Gets the coordinates of an egg on the selection screen by index and returns it as a tuple
|
|
||||||
:return: tuple of the coordinates of the selected egg
|
|
||||||
"""
|
|
||||||
cursor_x_offset = -2
|
|
||||||
cursor_y_offset = -2
|
|
||||||
|
|
||||||
return (self.eggs[self.selected_egg].rect.x + cursor_x_offset,
|
|
||||||
self.eggs[self.selected_egg].rect.y + cursor_y_offset)
|
|
||||||
|
|
||||||
def sel_left(self):
|
|
||||||
"""
|
|
||||||
Select the egg to the left with constraints.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self.selected_egg % self.eggs_per_row != 0:
|
|
||||||
self.selected_egg -= 1
|
|
||||||
|
|
||||||
def sel_right(self):
|
|
||||||
"""
|
|
||||||
Select the egg to the right with constraints.
|
|
||||||
"""
|
|
||||||
|
|
||||||
row = self.selected_egg // self.eggs_per_row
|
|
||||||
eggs_in_row = min(len(self.eggs) - (row * self.eggs_per_row), self.eggs_per_row)
|
|
||||||
|
|
||||||
if self.selected_egg % self.eggs_per_row != eggs_in_row - 1:
|
|
||||||
self.selected_egg += 1
|
|
||||||
|
|
||||||
def sel_up(self):
|
|
||||||
"""
|
|
||||||
Select the egg above with constraints.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self.selected_egg // self.eggs_per_row != 0:
|
|
||||||
self.selected_egg -= self.eggs_per_row
|
|
||||||
|
|
||||||
def sel_down(self):
|
|
||||||
"""
|
|
||||||
Select the egg below with constraints.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self.selected_egg // self.eggs_per_row != self.total_rows - 1:
|
|
||||||
self.selected_egg += self.eggs_per_row
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
self.preprocess()
|
|
||||||
|
|
||||||
cursor = pygame.image.load(
|
|
||||||
self.resource_dir + '/images/gui/egg_selector.png').convert_alpha()
|
|
||||||
self.blit(cursor, self.get_cursor_coords())
|
|
||||||
|
|
||||||
self.selected_color = self.eggs[self.selected_egg].egg_color
|
|
||||||
|
|
||||||
for event in pygame.event.get():
|
|
||||||
if event.type == pygame.KEYDOWN:
|
|
||||||
if event.key == pygame.K_RIGHT:
|
|
||||||
self.sel_right()
|
|
||||||
if event.key == pygame.K_LEFT:
|
|
||||||
self.sel_left()
|
|
||||||
if event.key == pygame.K_DOWN:
|
|
||||||
self.sel_down()
|
|
||||||
if event.key == pygame.K_UP:
|
|
||||||
self.sel_up()
|
|
||||||
if event.key == pygame.K_a:
|
|
||||||
self.additional_args = {'selected_egg': self.selected_color}
|
|
||||||
self.next_surface = 'selection_info'
|
|
||||||
self.running = False
|
|
@ -1,63 +0,0 @@
|
|||||||
"""Module for selection info. Shows egg stats after an egg has been selected from the starting
|
|
||||||
screen. Contains the surface object to be rendered."""
|
|
||||||
|
|
||||||
import pygame
|
|
||||||
from pocket_friends.elements import sprites
|
|
||||||
from pocket_friends.elements import surface
|
|
||||||
|
|
||||||
|
|
||||||
class Surface(surface.GameSurface):
|
|
||||||
"""
|
|
||||||
Surface object for the selection info screen. Contains the selected egg, egg properties, and description in a
|
|
||||||
scrollable text box. Child class of surface.GameSurface.
|
|
||||||
"""
|
|
||||||
def __init__(self, game_res: tuple, resources_dir: str, game_fps: int, **kwargs: str):
|
|
||||||
"""
|
|
||||||
Create a selection info surface object.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
game_res: The internal resolution of the surface.
|
|
||||||
resources_dir: The path of the game's main resource directory.
|
|
||||||
game_fps: How many frames per second the game will run at.
|
|
||||||
|
|
||||||
Keyword Args:
|
|
||||||
selected_egg: The egg that was selected by the previous screen.
|
|
||||||
"""
|
|
||||||
super().__init__(game_res, resources_dir, game_fps)
|
|
||||||
|
|
||||||
self._selected_egg = None
|
|
||||||
for key in kwargs.keys():
|
|
||||||
if key == 'selected_egg':
|
|
||||||
self._selected_egg = kwargs.get(key)
|
|
||||||
|
|
||||||
egg = sprites.SelectionEgg(self._selected_egg, resources_dir)
|
|
||||||
egg.rect.x = 8
|
|
||||||
egg.rect.y = 3
|
|
||||||
self._sprites.add(egg)
|
|
||||||
|
|
||||||
self._info_text = sprites.InfoText(resources_dir, game_res[0], egg.description)
|
|
||||||
self._info_icons = sprites.EggInfo(resources_dir, egg.contentedness, egg.metabolism, (32, 4))
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
"""
|
|
||||||
Draw objects on the screen and advance the surface logic by one frame; Handle user input.
|
|
||||||
"""
|
|
||||||
self.preprocess()
|
|
||||||
|
|
||||||
self._info_text.draw(self)
|
|
||||||
self._info_icons.draw(self)
|
|
||||||
|
|
||||||
for event in pygame.event.get():
|
|
||||||
if event.type == pygame.KEYDOWN:
|
|
||||||
if event.key == pygame.K_DOWN:
|
|
||||||
self._info_text.scroll_down()
|
|
||||||
if event.key == pygame.K_UP:
|
|
||||||
self._info_text.scroll_up()
|
|
||||||
if event.key == pygame.K_a:
|
|
||||||
self.running = False
|
|
||||||
self.additional_args = {'selected_color': self._selected_egg}
|
|
||||||
self.next_surface = 'playground'
|
|
||||||
if event.key == pygame.K_b:
|
|
||||||
self.running = False
|
|
||||||
self.additional_args = {'selected_color': self._selected_egg}
|
|
||||||
self.next_surface = 'egg_select'
|
|
@ -1,37 +0,0 @@
|
|||||||
"""Title screen module. Contains the surface object to be rendered on the screen."""
|
|
||||||
import pygame
|
|
||||||
from pocket_friends.elements import surface
|
|
||||||
|
|
||||||
|
|
||||||
class Surface(surface.GameSurface):
|
|
||||||
"""
|
|
||||||
Surface object for the title screen. Child class of surface.GameSurface.
|
|
||||||
"""
|
|
||||||
def __init__(self, game_res: tuple, resources_dir: str, game_fps: int):
|
|
||||||
"""
|
|
||||||
Create a title screen surface object.
|
|
||||||
Args:
|
|
||||||
game_res: The internal resolution of the surface.
|
|
||||||
resources_dir: The path of the game's main resource directory.
|
|
||||||
game_fps: How many frames per second the game will run at.
|
|
||||||
"""
|
|
||||||
|
|
||||||
super().__init__(game_res, resources_dir, game_fps)
|
|
||||||
|
|
||||||
self._frames = 0 # Counter for the number of frames passed
|
|
||||||
self._delay = 1 # Delay in seconds that the title should stay on screen
|
|
||||||
|
|
||||||
# Image of the title screen
|
|
||||||
self._title = pygame.image.load(resources_dir + '/images/title.png').convert_alpha()
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
"""
|
|
||||||
Advance the surface logic by one frame.
|
|
||||||
"""
|
|
||||||
self.preprocess()
|
|
||||||
self.blit(self._title, (0, 0))
|
|
||||||
|
|
||||||
self._frames += 1
|
|
||||||
if self._frames > self.game_fps * self._delay:
|
|
||||||
self.next_surface = 'egg_select'
|
|
||||||
self.running = False
|
|
@ -1 +1 @@
|
|||||||
pygame~=1.9.6
|
pygame~=2.1.2
|