diff --git a/pocket_friends/__main__.py b/pocket_friends/__main__.py index 8bc143a..c7ec3f8 100644 --- a/pocket_friends/__main__.py +++ b/pocket_friends/__main__.py @@ -2,11 +2,11 @@ Launch script for Pocket Friends. """ import os -from pathlib import Path import pygame import sys +from pathlib import Path from pocket_friends.game_files.game import main as game_main -from pocket_friends.development.dev_menu import main as dev_menu_main +#from pocket_friends.development.dev_menu import main as dev_menu_main if __name__ == '__main__': enable_dev = False @@ -14,16 +14,19 @@ if __name__ == '__main__': # enable dev mode if --dev argument is passed if len(sys.argv) > 0: for args in sys.argv: - if args == '--dev': - enable_dev = True + #if args == '--dev': [reimplement later] + # enable_dev = True if args == '--delete-save': save_dir = os.path.join(Path.home(), '.pocket_friends') os.remove(save_dir + '/save.json') - if not enable_dev: - game_main() - else: - dev_menu_main() + # Dev menu disabled for now, will reimplement later + #if not enable_dev: + # game_main() + #else: + # dev_menu_main() + + game_main() pygame.quit() sys.exit() diff --git a/pocket_friends/game_files/old_main_gamefile.py b/pocket_friends/game_files/old_main_gamefile.py new file mode 100644 index 0000000..cd6e8b5 --- /dev/null +++ b/pocket_friends/game_files/old_main_gamefile.py @@ -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 + # Run as if not on hardware + 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()