commit 073889e774ef6fb9084515296c89e351bec55c9d
parent 5e082e72fb8dabc2bc095f3cc456143f62c23cee
Author: Erik Letson <hmagellan@hmagellan.com>
Date: Sat, 5 Jun 2021 13:10:10 -0500
checkpoint in basic engine implement
Diffstat:
A | src/bus.py | | | 129 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | src/camera.py | | | 78 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | src/entity.py | | | 207 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
M | src/envvars.py | | | 48 | ++++++++++++++++++++++++++++++++++++++++++++++++ |
M | src/game.py | | | 7 | +++++++ |
A | src/images.py | | | 171 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | src/interface.py | | | 145 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | src/manager.py | | | 112 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | src/sound.py | | | 60 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | src/subsystem.py | | | 86 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
10 files changed, 1043 insertions(+), 0 deletions(-)
diff --git a/src/bus.py b/src/bus.py
@@ -0,0 +1,129 @@
+import pygame
+from . import subsystem
+from .envvars import *
+
+##########
+# bus.py #
+##########
+
+# This file contains:
+# 1. The Bus class, which is an abstraction of a communication bus
+# 2. The SystemBus subsystem, which handles subsystem communication
+# 3. The ManagerBus subsystem, which handles inter-manager/inter-managed communication
+
+#############################
+# Section 1 - The Bus class #
+#############################
+
+class Bus(subsystem.GameSubsystem):
+
+ def __init__(self, game, name, bus = None):
+
+ # Parent init
+ super().__init__(game, name, bus)
+
+ # Collection of elements
+ self.elements = {}
+
+ def enter_element(self, element_name):
+ """
+ Make an entry in the records for the given element. This
+ entry is checked for existence in many cases before doing
+ other things. This method should be called when an element
+ is created MOST OF THE TIME.
+ """
+ if element_name not in self.elements.keys():
+ self.elements[element_name] = {} # Initial value is empty dict
+ else:
+ raise GameBusError(self.name + "BUS SAYS: " + element_name + " already in records!")
+
+ def record(self, element_name, data):
+ """
+ Record some data about a given element, as long as it has
+ already been entered in the records.
+ """
+ if element_name in self.elements.keys():
+ if type(data) == dict:
+ self.elements[element_name] = data
+ else:
+ raise GameBusError("BUS SAYS: Provided data: " + data + " from " + element_name + " not dict type!")
+ else:
+ raise GameBusError("BUS SAYS: " + element_name + " not in records! Unable to record data!")
+
+###################################
+# Section 2 - The SystemBus class #
+###################################
+
+class SystemBus(Bus):
+ """
+ Communication bus that game elements use to
+ talk to game subsystems. Less featureful than
+ the ManagerBus class.
+ """
+ # What SHOULD go through SystemBus:
+ # * Subsystem-to-Subsystem communication
+ # * ANY object talking to a Subsystem BESIDES Game
+ # What SHOULD NOT go through ManagerBus:
+ # * Direct communication from Game to a Subsystem
+
+ def __init__(self, game, name, bus = None):
+
+ # Parent init
+ super().__init__(game, name, bus)
+
+ # Collection of systems
+ self.elements = {}
+
+ # Performs: All callable non-return behaviors of subsystems.
+
+####################################
+# Section 3 - The ManagerBus class #
+####################################
+
+class ManagerBus(Bus):
+ """
+ Communication bus that managers talk to each
+ other and their own subordinate objects through.
+ """
+ # What SHOULD go through ManagerBus:
+ # * Manager-to-Manager communication
+ # * Manager-to-external-object communication
+ # * ANY object talking to a manager BESIDES Game
+ # What SHOULD NOT go through ManagerBus:
+ # * Direct communication from Game to a manager
+ # * A Manager talking to an object it manages
+ # * An object talking to its own manager
+
+ def __init__(self, game, name, bus):
+
+ # Parent init
+ super().__init__(game, name, bus)
+
+ # Collection of managers
+ self.elements = {}
+
+ def fetch(self, element, value):
+ """
+ Fetch a given value from a manager. Refers to the bus's
+ internal records of current object values, updated by the
+ manager itself via the "expose()" functionality of the manager
+ objects.
+ """
+ if element in self.records.keys():
+ if value in self.records[manager].keys():
+ return self.records[manager][value]
+ else:
+ raise GameBusError("BUS SAYS: " + value + " not in " + manager + " data record!")
+ else:
+ raise GameBusError("BUS SAYS: " + manager + " not in records! Unable to fetch data!")
+
+ # Checks: Computes & returns handled partially by Bus itself.
+ # Checks are basically the halfway between fetches and performs,
+ # meaning they do something with the manager they talk to, check
+ # out that value, and return some info based on that. The checks
+ # are dynamic, meaning they are not best handled by static fetching.
+ # check_is_* = return bool
+ # check_for_* = return something else
+
+ # Performs: All callable non-return behaviors of managers.
+
diff --git a/src/camera.py b/src/camera.py
@@ -0,0 +1,78 @@
+import pygame
+from . import subsystem
+from .envvar import *
+
+#############
+# camera.py #
+#############
+
+# This file contains:
+# 1. The GameCamera class, which manages the camera surface
+
+####################################
+# Section 1 - The GameCamera class #
+####################################
+
+class GameCamera(subsystem.GameSubsystem):
+ """
+ Object that controls the camera surface and
+ moves it in relation to input. The camera
+ surface is then drawn onto the game's real
+ screen at an offset determined by input.
+ """
+
+ def __init__(self, game, name, bus):
+
+ # Parent init
+ super().__init__(game, name, bus)
+
+ # Camera surface values
+ self.camera_surface = pygame.Surface((0, 0)).convert() # This surface is initialized whenever a new map or other game area is loaded
+ self.camera_surface_rect = self.camera_surface.get_rect()
+ self.camera_surface_offset = (0, 0) # The rect topleft is never changed; rather, this value is used by game to draw the camera surface at an offset
+
+ def load_camera_surface(self, dimensions, init_offset = (0, 0)):
+ """
+ Load up the camera surface as a new PyGame
+ surface object of the necessary size. Also
+ resets the camera offset.
+ """
+ self.camera_surface = pygame.Surface(dimensions).convert()
+ self.camera_surface_rect = self.camera_surface.get_rect()
+ self.camera_surface_offset = init_offset
+
+ def move_offset(self, rel_offset, speed_multiple = 1):
+ """
+ Move the offset by the given relative offset.
+ Speed can also be increased or decreased by
+ passing a multiple here. Handles not moving
+ the camera view outside of game screen
+ completely.
+ """
+ changex = 0
+ changey = 0
+ if not ((self.camera_surface_offset[0] <= -(SCREEN_WIDTH / 2) and rel_offset[0] < 0) or (self.camera_surface_offset[0] >= (SCREEN_WIDTH / 2) and rel_offset[0] > 0)):
+ changex = rel_offset[0] * speed_multiple
+ if not ((self.camera_surface_offset[1] <= -SCREEN_HEIGHT and rel_offset[1] < 0) or (self.camera_surface_offset[1] >= (SCREEN_HEIGHT / 2) and rel_offset[1] > 0)):
+ changey = rel_offset[1] * speed_multiple
+ self.camera_surface_offset = (self.camera_surface_offset[0] + changex, self.camera_surface_offset[1] + changey)
+
+ def snap_to_position(self, position):
+ """
+ Snap the camera to a position, relative
+ to the screen. The given position should
+ be coordinates on the camera surface, and
+ this method will compute that to produce
+ an offset such that the camera surface
+ appears centered to that position in
+ relation to the game screen.
+ """
+ self.camera_surface_offset = ((SCREEN_WIDTH / 2) - position[0], (SCREEN_HEIGHT / 2) - position[1])
+
+ def update_camera(self, surface = None):
+ """
+ Update and draw the camera contents.
+ """
+ if surface != None:
+ surface.blit(self.camera_surface, self.camera_surface_offset)
+
diff --git a/src/entity.py b/src/entity.py
@@ -0,0 +1,207 @@
+import pygame, json, os
+from . import manager
+from .constants import *
+
+#############
+# entity.py #
+#############
+
+# This file contains:
+# 1. The Entity class that is the extension of pygame.Sprite used throughout the game
+# 2. Various kinds of specialized entity classes that don't fit in other source files
+
+# TODO: This should eventually use LayeredDirty sprites for performance reasons
+
+############################
+# Section 1 - Entity class #
+############################
+
+class Entity(pygame.sprite.DirtySprite):
+ """
+ The parent of all visible objects. The Entity object itself is
+ essentially an extended, customised version of the PyGame Sprite
+ object. Entity supports animations, motions (movement over time),
+ and tile operations, but none of that is required.
+ """
+
+ def __init__(self, sheet, sprite = (0, 0), animation = None, animated = False):
+
+ # Parent initialization
+ super().__init__()
+
+ # Saved values
+ self.sheet = sheet
+ self.sprite = sprite
+ self.image = sheet.sprites[sprite]
+ self.rect = self.image.get_rect()
+ self.rect.topleft = (0, 0)
+ self.flipped = (False, False)
+ self.custom_flags = None # Used to pass special info to Entities
+
+ # DirtySprite class defaults
+ self.visible = 1
+ self.dirty = 0
+ self.layer = 0
+
+ # Animation values
+ self.animated = animated
+ self.animation = {}
+ self.animation_timer = 0
+ self.animation_frame = 0
+ if animated:
+ self.set_animation(animation, animated)
+
+ # Motion values
+ self.motion = {}
+ self.motion_timer = 0
+
+ # Tile values
+ self.tile_pos = (-1, -1)
+ self.tile_gid = None
+
+ def set_image(self, sheet, sprite = (0, 0)):
+ """
+ Set the image of the Entity to a new image.
+ """
+ self.sheet = sheet
+ self.sprite = sprite
+ self.image = sheet.sprites[sprite]
+ old_tp = self.rect.topleft
+ self.rect = self.image.get_rect()
+ self.rect.topleft = old_tp
+
+ def set_sprite(self, sprite):
+ """
+ Set the Entity's sprite to another one on the board.
+ The argument 'sprite' is a tuple (x, y). This is used
+ to assign an image from the saved sheet to non-animated
+ Entity objects.
+ """
+ if sprite in self.sheet.sprites.keys():
+ self.image = self.sheet.sprites[sprite]
+ self.sprite = sprite
+
+ def set_animation(self, new_animation, play = False, init_frame = 0):
+ """
+ Assign a new animation to this Entity and configure all
+ necessary values to get it playing.
+ """
+ self.animation = new_animation
+ self.animation_frame = init_frame
+ self.animation_timer = new_animation[init_frame]["timer"]
+ self.image = self.sheet.sprites[new_animation[init_frame]["sprite"]]
+ self.animated = play
+
+ def set_motion(self, target_pos, speed):
+ """
+ Assign a new motion to this Entity and start playing it.
+ Unlike animations, motions must be played if assigned. Note:
+ Motions go by rect center, rather than topleft.
+ """
+ self.motion = {
+ "target" : target_pos,
+ "speed" : speed,
+ "direction" : ((target_pos[0] - self.rect.center[0]) / speed, (target_pos[1] - self.rect.center[1]) / speed),
+ "timer" : speed
+ }
+
+ def set_position(self, pos):
+ """
+ Assign the rect topleft to a raw position tuple, independent
+ of e.g. a tile.
+ """
+ self.rect.topleft = pos
+
+ def set_center_position(self, pos):
+ """
+ Assign the rect center to a raw position tuple, independent
+ of e.g. a tile.
+ """
+ self.rect.center = pos
+
+ def set_flip(self, hz = None, vt = None):
+ """
+ Set the flip tuple valse to True or False. hz is for horizontal
+ flipping and vt is vertical. If either is None, that value will
+ not be changed.
+ """
+ if hz == None:
+ hz = self.flipped[0]
+ if vt == None:
+ vt = self.flipped[1]
+ self.flipped = (hz, vt)
+
+ def animate(self):
+ """
+ Play the current animation. This method is called as part of
+ the update() logic.
+ """
+ if self.animation_timer > 0:
+ self.animation_timer -= 1
+ else:
+ if self.animation_frame < len(self.animation) - 1:
+ self.animation_frame += 1
+ else:
+ self.animation_frame = 0
+ self.animation_timer = self.animation[self.animation_frame]["timer"]
+ self.image = self.sheet.sprites[self.animation[self.animation_frame]["sprite"]]
+
+ def motion_move(self):
+ """
+ Perform current motion, if it is set. Note: Motions go by
+ rect center, rather than topleft.
+ """
+ if self.motion != {} and not self.rect.collidepoint(self.motion["target"]):
+ if self.motion_timer == 0:
+ mx = self.rect.center[0] + self.motion["direction"][0]
+ my = self.rect.center[1] + self.motion["direction"][1]
+ self.rect.center = (mx, my)
+ self.motion_timer = self.motion["speed"]
+ else:
+ self.motion_timer -= 1
+ else:
+ if self.motion != {}:
+ self.set_center_position(self.motion["target"]) # Make sure we end up in exactly the right spot
+ self.motion = {}
+
+ def snap_to_tile(self):
+ """
+ Snap the Entity to its current tile.
+ """
+ if self.tile_pos != (-1, -1):
+ self.set_position((self.tile_pos[0] * TILE_WIDTH, self.tile_pos[1] * TILE_HEIGHT))
+
+ def assign_tile(self, tile_pos, tile_gid):
+ """
+ Assign a tile as this entity object's occupied tile.
+ Assume the values are legitimate.
+ """
+ self.tile_pos = tile_pos
+ self.tile_gid = tile_gid
+
+ def act(self):
+ """
+ This method is called as part of update() and is meant
+ to be overwritten in subclasses with any logic that needs
+ to happen each update.
+ """
+ pass
+
+ def update(self, surface = None, to_animate = True):
+ """
+ Draw the Entity to the surface. Also calls act() for update
+ logic for specific Entity children, and animate() to animate
+ the sprite image.
+ """
+ self.act()
+ if surface != None:
+ self.motion_move()
+ if self.animated and to_animate:
+ self.animate()
+ surface.blit(pygame.transform.flip(self.image, self.flipped[0], self.flipped[1]), self.rect)
+
+#########################################
+# Section 2 - Various Entity subclasses #
+#########################################
+
+# TODO:
diff --git a/src/envvars.py b/src/envvars.py
@@ -1,4 +1,52 @@
+import os, enum, json, pathlib
+
+##############
+# envvars.py #
+##############
+
+# This file contains:
+# 1. Important environment values that exist independent of any one source file
+
+# TODO: This file should source from a settings file
+
+#########################
+# Section 1 - Constants #
+#########################
+
# Game settings
SCREEN_WIDTH = 1024
SCREEN_HEIGHT = 768
FRAMERATE = 60
+
+# Tile settings
+BASE_TILE_WIDTH = 16
+BASE_TILE_HEIGHT = 16
+TILE_SCALE_FACTOR = 4
+COLORKEY = (255, 0, 255)
+
+# File paths
+DATA_PATH = os.path.join(os.getcwd(), "data")
+IMAGE_PATH = os.path.join(DATA_PATH, "img")
+SOUND_PATH = os.path.join(DATA_PATH, "snd")
+FONT_PATH = os.path.join(DATA_PATH, "font")
+TILE_PATH = os.path.join(DATA_PATH, "tsx")
+JSON_PATH = os.path.join(DATA_PATH, "json")
+SAVE_PATH = os.path.join(str(pathlib.Path.home), ".local", "share", "heart-of-gold", "savegames")
+
+# Stats constants
+# TODO: Could be totally changed going forward
+CORE_STATS = ["STR", "FIN", "MAG", "LUK"]
+METER_STATS = ["HP", "MP"]
+META_STATS = ["LVL", "EXP", "RNG"]
+
+# Enums
+STATE_MODES = enum.Enum('STATE_MODES', 'Main_Menu_Mode Overworld_Mode Location_Mode Basic_Battle_Mode Location_Battle_Mode')
+CTRL_MODES = enum.Enum('CTRL_MODES', 'No_Control Main_Menu_Normal')
+GAME_EFFECTS = enum.Enum('GAME_EFFECTS', 'ef_game_dummy ef_game_quit ef_game_switch_mode ef_game_switch_control')
+TEAMS = enum.Enum('TEAMS', 'Player Ally Neutral Enemy Other')
+
+# Error types
+class GameBusError(Exception):
+ """
+ Raised when an error occurs as part of Bus operation.
+ """
diff --git a/src/game.py b/src/game.py
@@ -1,4 +1,5 @@
import pygame
+from . import interface, bus, camera
from .envvars import *
###########
@@ -29,6 +30,12 @@ class Game(object):
self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
self.frame_clock = pygame.time.Clock()
+ # Subsystem objects
+ self.subsystem_bus = bus.SystemBus(self)
+ self.manager_bus = bus.ManagerBus(self)
+ self.camera = camera.Camera(self)
+ self.interface = interface.GameInterface(self, self.manager_bus, self.subsystem_bus)
+
def shift_frames(self, framerate = FRAMERATE):
"""
Shift to the next frame of the game at the specified
diff --git a/src/images.py b/src/images.py
@@ -0,0 +1,171 @@
+import pygame, os, json
+from . import manager
+from .envvars import *
+
+#############
+# images.py #
+#############
+
+# This file contains:
+# 1. The 'SheetManager' class, which is a Manager object that handles sheets (e.g. loading)
+# 2. The 'Sheet' class, which is used to represent enumerated spritesheets
+
+###################################
+# Section 2 - SheetManager Object #
+###################################
+
+class SheetManager(manager.Manager):
+ """
+ SheetManager handles loading and managing sheets. It is
+ talked to in order to get loaded sheets. SheetManager also
+ knows all animations and is talked to to get info about
+ them.
+ """
+
+ def __init__(self, game, bus, camera, name):
+
+ super().__init__(game, bus, camera, name)
+
+ # Important values
+ self.sheets_def = {}
+ self.loaded_sheets = {}
+ self.anims_def = {}
+ self.animations = {}
+
+ # This manager should perform an initial update
+ self.update(None)
+
+ def load_sheets_from_json(self, sheet_json):
+ """
+ Load sheets from the given definition JSON file. Note
+ that sheets must be loaded AFTER animations.
+ """
+ with open(os.path.join(JSON_PATH, sheet_json)) as df: self.sheets_def = json.load(df)
+
+ for j in self.sheets_def:
+ an = {}
+ if self.sheets_def[j]["anim_class"] != None:
+ an = self.animations[self.sheets_def[j]["anim_class"]]
+ self.loaded_sheets[j] = Sheet(self, self.sheets_def[j]["filename"], j, tuple(self.sheets_def[j]["dimensions"]), self.sheets_def[j]["total_sprites"], an, self.sheets_def[j]["alpha"])
+
+ def load_animations_from_json(self, anim_json):
+ """
+ Load animations from the given definition JSON file.
+ Note that animations must be loaded BEFORE sheets.
+ """
+ with open(os.path.join(JSON_PATH, anim_json)) as anf: self.anims_def = json.load(anf)
+
+ self.animations = self.anims_def
+ for c in self.anims_def:
+ for a in self.anims_def[c]:
+ for i in range(0, len(self.anims_def[c][a])):
+ for v in self.anims_def[c][a][i]:
+ if type(self.anims_def[c][a][i][v]) == list:
+ self.animations[c][a][i][v] = tuple(self.anims_def[c][a][i][v])
+
+ def load_image(self, filename, alpha = False):
+ """
+ Load an image file as a PyGame image. This is generic
+ and can function even outside of the normal sheet
+ dynamic that the game uses.
+ """
+ if alpha:
+ im = pygame.image.load(os.path.join(IMAGE_PATH, filename)).convert_alpha()
+ else:
+ im = pygame.image.load(os.path.join(IMAGE_PATH, filename)).convert()
+ return im
+
+ def get_sheet(self, name):
+ """
+ Return the sheet matching name if it exists.
+ """
+ if name in self.loaded_sheets.keys():
+ return self.loaded_sheets[name]
+
+ def expose(self):
+ """
+ Expose sheet info to the ManagerBus.
+ """
+ data = {
+ "sheets" : self.loaded_sheets,
+ "animations" : self.animations
+ }
+ self.bus.record(self.name, data)
+
+############################
+# Section 3 - Sheet Object #
+############################
+
+class Sheet(object):
+ """
+ Class for the enumerated spritesheets that are used to get
+ images for entities. Supports regularized spritesheets only
+ (that is, all sprites are the same dimensions).
+ """
+
+ def __init__(self, manager, filename, name, sprite_size, total_sprites, animations, alpha):
+
+ # Important values
+ self.manager = manager
+ self.filename = filename
+ self.name = name
+ self.sprite_dimensions = sprite_size
+ self.total_sprites = total_sprites
+ self.sprites = {}
+ self.animations = animations
+ self.alpha = alpha
+
+ # Try and load all sprites on the sheet, or log if we failed
+ if not self.load_sheet(filename, sprite_size, total_sprites):
+ self.sprites = {}
+ print("Failed to load sheet: " + filename)
+
+ def load_sheet(self, filename, sprite_size, total_sprites):
+ """
+ Load a sheet and divide it into subsurfaces for use as
+ images by sprite entities.
+ """
+
+ # First, attempt to load the image file. Failing that, create
+ # a sheet as a NoneType object
+ try:
+ self.sheet = self.manager.load_image(filename, self.alpha)
+ except:
+ self.sheet = None
+
+ # Next, if the sheet exists, divide it into sprites
+ if self.sheet != None:
+ self.sprites = {}
+
+ # Values to track progress
+ x = 0
+ y = 0
+
+ # While there are still more sprites to load, load them
+ while len(self.sprites) < total_sprites:
+
+ # Get a rect that can be used to make a subsurface of the sheet
+ new_rect = pygame.Rect((x * sprite_size[0], y * sprite_size[1]), sprite_size)
+
+ # Load image, store in our sprites dict, and set colorkey or alpha
+ if not self.alpha:
+ self.sprites[(x, y)] = self.sheet.subsurface(new_rect).convert()
+ self.sprites[(x, y)].set_colorkey(COLORKEY)
+ else:
+ self.sprites[(x, y)] = self.sheet.subsurface(new_rect).convert_alpha()
+
+ # Scoot over to the right
+ x += 1
+
+ # If we're hanging off the right side, scoot down and start
+ # over again from the far left
+ if x * sprite_size[0] >= self.sheet.get_width():
+ x = 0
+ y += 1
+
+ # After the while loop, return True for success
+ return True
+
+ # No sheet exists, return False for failure
+ else:
+ return False
diff --git a/src/interface.py b/src/interface.py
@@ -0,0 +1,145 @@
+import pygame
+from . import subsystem
+from .envvars import *
+
+################
+# interface.py #
+################
+
+# This file contains:
+# 1. The GameInterface class, which handles all kinds of interaction from the player
+
+#######################################
+# Section 1 - The GameInterface class #
+#######################################
+
+class GameInterface(subsystem.GameSubsystem):
+ """
+ GameInterface handles all PyGame events, meaning
+ it is responsible for all input and the responses
+ to that input. GameInterface takes extra arguments
+ compared to other subsystems, representing its need
+ to directly access manager calls via bus and the
+ game camera.
+ """
+
+ def __init__(self, game, name, bus, manager_bus):
+
+ # Parent init
+ super().__init__(game, name, bus)
+
+ # Busses for direct communication
+ self.manager_bus = manager_bus
+
+ # Others
+ self.left_double_clicking = False
+ self.left_double_click_timer = 0
+ self.left_double_click_mousepos = None
+ self.right_double_clicking = False
+ self.right_double_click_timer = 0
+ self.right_double_click_mousepos = None
+ self.old_mousepos = None
+ self.key_bools = [ False for k in range(0, 350) ]
+
+ def handle_events(self, events):
+ """
+ Handle any kind of PyGame event and react appropriately.
+ """
+ for event in events:
+ if event.type == pygame.KEYDOWN:
+ self.handle_key_press(event)
+ elif event.type == pygame.KEYUP:
+ self.handle_key_release(event)
+ elif event.type == pygame.MOUSEBUTTONDOWN:
+ self.handle_mouse_click(event)
+ elif event.type == pygame.MOUSEBUTTONUP:
+ self.handle_mouse_release(event)
+ elif event.type == pygame.QUIT:
+ self.game.quit_game()
+
+ def handle_key_press(self, event):
+ """
+ React to a key being pressed.
+ """
+ if event.key < len(self.key_bools):
+ self.key_bools[event.key] = True
+
+ def handle_key_release(self, event):
+ """
+ React to a key being released.
+ """
+ if event.key < len(self.key_bools):
+ self.key_bools[event.key] = False
+
+ def react_to_keys(self):
+ """
+ React to certain pressed/not-pressed statuses
+ of keys on a mode-by-mode basis. Called during
+ update.
+ """
+ pass
+
+ def handle_mouse_click(self, event):
+ """
+ React to a mousebutton being clicked.
+ """
+ # First, get important mouse positional info, namely unoffset mouse position and camera-offset mouse position
+ mouseraw = pygame.mouse.get_pos()
+ mousepos = (mouseraw[0] - self.camera.camera_surface_offset[0], mouseraw[1] - self.camera.camera_surface_offset[1])
+
+ # Handle left-click
+ if event.button == 1:
+
+ # Set up for double click
+ if self.left_double_click_timer == 0 and not self.left_double_clicking:
+ self.left_double_click_timer = 15
+ self.left_double_click_mousepos = mousepos
+ elif not self.left_double_clicking and mousepos == self.left_double_click_mousepos:
+ self.left_double_clicking = True
+ self.left_double_click_timer = 0
+
+ # Handle right-click
+ elif event.button == 3:
+
+ # Set up for double click
+ if self.right_double_click_timer == 0 and not self.right_double_clicking:
+ self.right_double_click_timer = 15
+ self.right_double_click_mousepos = mousepos
+ elif not self.right_double_clicking and mousepos == self.right_double_click_mousepos:
+ self.right_double_clicking = True
+ self.right_double_click_timer = 0
+
+ # Keepover
+ self.old_mousepos = mousepos
+
+ def handle_mouse_release(self, event):
+ """
+ React to a mousebutton being released.
+ """
+ # First, get important mouse positional info, namely unoffset mouse position and camera-offset mouse position
+ mouseraw = pygame.mouse.get_pos()
+ mousepos = (mouseraw[0] - self.camera.camera_surface_offset[0], mouseraw[1] - self.camera.camera_surface_offset[1])
+
+ # Handle left-release
+ if event.button == 1:
+ pass
+
+ # Handle right-release
+ elif event.button == 3:
+ pass
+
+ def update_interface(self):
+ """
+ Update interface elements (such as the cursor) once
+ per frame. This is not the same as a drawing update for
+ e.g. an Entity, and is logic-only.
+ """
+ # UNIVERSAL UPDATES
+ # Doubleclick countdown
+ if self.left_double_click_timer > 0:
+ self.left_double_click_timer -= 1
+ else:
+ self.left_double_clicking = False
+
+ # React to keys
+ self.react_to_keys()
diff --git a/src/manager.py b/src/manager.py
@@ -0,0 +1,112 @@
+import pygame
+from . import subsystem
+from .envvars import *
+
+##############
+# manager.py #
+##############
+
+# This file contains:
+# 1. The Manager object, which is the parent of other managers that perform functions directly subordinate to the Game object.
+
+##################################
+# Section 1 - The Manager Object #
+##################################
+
+class Manager(subsystem.GameSubsystem):
+ """
+ Fairly simple parent type for the various
+ managers of different game elements, such as
+ the play board or the UI.
+ """
+ # What SHOULD be a Manager:
+ # * ANY object that both MANAGES SOME GAME
+ # RESOURCE (image, entity, etc.) and NEEDS
+ # TO BE ABLE TO COMMUNICATE WITH OBJECTS
+ # OUTSIDE ITS OWN SCOPE (that is, needs
+ # bus access). Additionally, Managers will
+ # commonly have to draw entities to the
+ # screen (though not all managers do this).
+
+ # Properties
+ @property
+ def activated(self):
+ return self._activated
+ @activated.setter
+ def activated(self, x):
+ self._activated = x
+
+ @property
+ def effectual(self):
+ return self._effectual
+ @effectual.setter
+ def effectual(self, x):
+ self._effectual = x
+
+ # Initialization
+ def __init__(self, game, name, bus, system_bus):
+
+ # Initial values
+ super().__init__(game, name, bus)
+ self.system_bus = system_bus
+
+ # Property defaults
+ self._activated = True
+ self._effectual = False
+
+ def expose(self):
+ """
+ Expose info about this object to the ManagerBus
+ object. Overwritten in child objects. Expose is
+ called for sure once per frame in an active
+ manager via "update()", but it is safe to call
+ it elsewhere as well.
+ """
+ pass
+
+ def trigger_effects(self, effect_list):
+ """
+ Triggers game effects as defined in JSONs.
+ This method can be extended in child objects
+ that need to be able to trigger unique
+ effects by using the effect_extension method.
+ """
+ # effect_list is ALWAYS a LIST of DICTS
+ if self.activated and self.effectual:
+ for ef in effect_list:
+ if ef["call"] == GAME_EFFECTS.ef_game_quit.name:
+ self.game.quit_game()
+ # Switch mode ALWAYS has a two-part list for its data, with the second part being able to
+ # be whatever game needs for that particular mode (even compound data types such as list/dict)
+ elif ef["call"] == GAME_EFFECTS.ef_game_switch_mode.name:
+ self.game.switch_mode(STATE_MODES[ef["data"][0]], ef["data"][1])
+ elif ef["call"] == GAME_EFFECTS.ef_game_switch_control.name:
+ self.game.control_mode = CTRL_MODES[ef["data"]]
+ else:
+ self.effect_extension(ef)
+
+ def effect_extension(self, effect):
+ """
+ Target for extension of trigger_effects method.
+ This method should have a single branching
+ conditional statement inside it that is unique
+ to the manager that needs extra effects. It is
+ used to check only one effect at a time and should
+ not contain a loop like trigger_effects.
+ """
+ pass
+
+ def update(self, surface):
+ """
+ Generic update logic for managers.
+ """
+ if self.activated:
+ self.expose()
+ self.update_managed(surface)
+
+ def update_managed(self, surface):
+ """
+ Abstract method to be overwritten with
+ update logic in child objects.
+ """
+ pass
diff --git a/src/sound.py b/src/sound.py
@@ -0,0 +1,60 @@
+import pygame, os, json
+from . import subsystem
+from .constants import *
+
+############
+# sound.py #
+############
+
+# This file contains:
+# 1. SoundManager, which loads up all sounds and is then called to play them through the pygame mixer.
+
+############################
+# Section 1 - SoundManager #
+############################
+
+class SoundSystem(subsystem.GameSubsystem):
+ """
+ This object loads sounds as defined in JSON
+ files and is called to play them.
+ """
+
+ def __init__(self, game, name, bus):
+
+ # Parent initialization
+ super().__init__(game, name, bus)
+
+ # Important values
+ self.sounds = {}
+ self.channels = [pygame.mixer.Channel(x) for x in range(0, pygame.mixer.get_num_channels() - 1)]
+
+ def load_sounds_from_json(self, soundjson):
+ """
+ Load up sounds from a definition JSON
+ file.
+ """
+ with open(os.path.join(JSON_PATH, soundjson)) as sj: j = json.load(sj)
+
+ for s in j["sounds"]:
+ self.sounds[s["name"]] = pygame.mixer.Sound(os.path.join(SOUND_PATH, s["filename"]))
+ self.sounds[s["name"]].set_volume(float(s["volume"]))
+
+ def play_sound(self, soundname, channel = None, concurrent = False):
+ """
+ Play a loaded sound by name. If the sound is
+ already playing, stop it unless concurrent is
+ True. A new channel should be chosen to play
+ the sound on dynamically, unless channel is
+ a legal channel id int.
+ """
+ # TODO: There could (should?) be some channel-checking logic here
+ if soundname != None:
+ if channel != None and not self.channels[channel].get_busy():
+ try:
+ self.channels[channel].play(self.sounds[soundname])
+ except IndexError:
+ # TODO: Should be bus error here
+ print("Failed to play sound " + soundname + " on channel " + channel)
+ elif concurrent or self.sounds[soundname].get_num_channels() == 0:
+ self.sounds[soundname].play()
+
diff --git a/src/subsystem.py b/src/subsystem.py
@@ -0,0 +1,86 @@
+import pygame, os, json
+from .envvars import *
+
+################
+# subsystem.py #
+################
+
+# This file contains:
+# 1. The generic GameSubsystem class, which abstract subsystems (like GameInterface) are children of, as well as all Managers
+# 2. The SaveSystem class, which handles saving and loading games
+
+#######################################
+# Section 1 - The GameSubsystem class #
+#######################################
+
+class GameSubsystem(object):
+ """
+ Abstract class subordinate to game which
+ Managers and other subsystems are specialized
+ versions of. GameSubsystem is pretty much the
+ most abstract possible parent for these kinds
+ of objects and is very simple.
+ """
+ # What SHOULD be a Subsystem:
+ # * Any object that CONTROLS A CRITICAL
+ # GAME FUNCTION, IS DIRECTLY SUBORDINATE
+ # TO THE Game OBJECT, and DOES NOT
+ # MANAGE ANY ENTITIES OR RESOURCES BY
+ # ITSELF. Game Subsystems can and often
+ # do need ManagerBus access, but they don't
+ # manage any objects themselves, nor do
+ # they expose their own data to the
+ # ManagerBus.
+
+ def __init__(self, game, name, bus = None):
+
+ self.game = game
+ self.name = name
+ self.bus = bus
+
+ self.record_self(bus)
+
+ def record_self(self, bus = None):
+ """
+ Record this system in a given bus, for communication
+ purposes.
+ """
+ if bus != None:
+ bus.enter_element(self.name)
+
+####################################
+# Section 2 - The SaveSystem class #
+####################################
+
+class SaveSystem(GameSubsystem):
+ """
+ The SaveSystem object handles saving to and loading
+ from a game save JSON file. This large JSON file is
+ subdivided into smaller objects called definitions
+ that are transformed at load time to Python dicts.
+ These definitions are passed to Game, managers, and
+ other subsystems in order to generate the gameplay
+ environment.
+ """
+
+ def __init__(self, game, name, bus):
+
+ # Parent init
+ super().__init__(game)
+
+ # Environment
+ self.active_save_env = {}
+
+ def save_to_file(self, filename):
+ # TODO: Check if it exists and prompt to overwrite (maybe in a another object???)
+ # TODO: Make cross-platform compatible
+ with open(os.path.join(SAVE_PATH, filename)) as sf: json.dump(self.active_save_env, sf)
+
+ def load_from_file(self, filename):
+ with open(os.path.join(SAVE_PATH, filename)) as lf: self.active_save_env = json.load(lf)
+
+ def apply_environment(self):
+ pass
+
+ def update_environment(self, key, value):
+ pass