Return to repo list

heart-of-gold

Tactical RPG written in python, using pygame.
Return to HMagellan.com

commit 145eb531b57dc6da62051dec4fed8b2bc6b5c76a
parent b6978031f8ee6baccac7476dbf3f0cbafbbde75d
Author: Erik Letson <hmagellan@hmagellan.com>
Date:   Wed, 11 Nov 2020 13:47:40 -0600

Improved bus fetching system by a lot

Diffstat:
Mdata/board/testmap1/testmap1.json | 2+-
Mmain.py | 2--
Msrc/board.py | 41++++++++++++++++++++++++++++-------------
Asrc/bus.py | 118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/entity.py | 33++++-----------------------------
Msrc/game.py | 31+++++++++++++++++--------------
Msrc/images.py | 18++++++++++++------
Msrc/manager.py | 41++++++++++++++++++++++++++++++++++++++---
Msrc/menu.py | 18++++++++++++------
Msrc/piece.py | 47+++++++++++++++++++++++++++++------------------
Msrc/scene.py | 26++++++++++++++++----------
Msrc/sound.py | 18++++++++++++++++--
Msrc/subsystem.py | 123+++++++++++++++++--------------------------------------------------------------
13 files changed, 317 insertions(+), 201 deletions(-)

diff --git a/data/board/testmap1/testmap1.json b/data/board/testmap1/testmap1.json @@ -66,7 +66,7 @@ "animated" : true, "passable" : false, "tile" : [0, 2], - "team" : "Player", + "team" : "Enemy", "normal_stats" : { "LVL" : 1, "HP" : 100, diff --git a/main.py b/main.py @@ -2,7 +2,6 @@ import pygame from src import game ## GLOBAL TODO: -# 1. Decide on final method for managing the cursor. Should it be managed by EntityManager? BoardManager? GameInterface? A seperate manager object? # 2. The JSON loading methods are all over the place. Need to decide about whether or not things like units should be defined in individual JSONs and # referenced by a big meta JSON or if there should just be many individual JSONs for that. One problem with it is that when saving is implemented # there will have to be a way to distinguish between the basic versions of a unit and the ones that are leveled up. Maybe JSONs are not the correct @@ -11,7 +10,6 @@ from src import game # the game would be in a loaded state. This seems the ideal solution, and it can be put off a bit. # 3. Implement the "draw()" functionality of DirtySprite for all VGOs, which will be done in anything that manages a LayeredDirty group, e.g. in the # EntityManager object. -# 4. Entities should probably be passed their manager. VGOs probably do not need this. # 5. There should be a "Bus" subsystem object that is initialized by Game and that is passed to all managers. The bus should be able to handle internal # errors that arise in inter-manager communication. Additionally, all managers should have an "expose()" method run each frame that exposes their # important values to the bus (what these specific values are will be particular to the manager, and the actual exposed data should be a dict). Other diff --git a/src/board.py b/src/board.py @@ -1,6 +1,6 @@ import pygame, pytmx, os, queue from . import manager, entity -from .constants import BOARD_PATH +from .constants import BOARD_PATH, TILE_WIDTH, TILE_HEIGHT ############ # board.py # @@ -21,9 +21,9 @@ class BoardManager(manager.Manager): switching between Boards. """ - def __init__(self, game, bus): + def __init__(self, game, bus, name): - super().__init__(game, bus) + super().__init__(game, bus, name) # Board values self.current_board = None @@ -39,6 +39,7 @@ class BoardManager(manager.Manager): """ self.current_board = Board(self, boardname) self.load_overlay() + self.update(None) def load_overlay(self): """ @@ -50,7 +51,7 @@ class BoardManager(manager.Manager): if isinstance(layer, pytmx.TiledTileLayer): for x, y, gid in layer: if self.current_board.tmx_data.get_tile_properties_by_gid(gid)["Passable"] == 1: - e = entity.Entity(self.bus.fetch_sheet("board_overlays_1")) + e = entity.Entity(self.bus.fetch("sheet_manager", "sheets")["board_overlays_1"]) e.set_position((x * self.current_board.tmx_data.tilewidth, y * self.current_board.tmx_data.tileheight)) e.custom_flags = "OverlayGrid" self.board_overlay.add(e) @@ -114,11 +115,12 @@ class BoardManager(manager.Manager): self.load_overlay() # TEMP!!!! - self.create_move_range(self.bus.fetch_selected_piece()) + sp = self.bus.fetch("piece_manager", "selected_piece") + self.create_move_range(sp) for t in self.move_targets: - if not self.bus.fetch_is_ally_occupied_by_tile_pos(t, self.bus.fetch_selected_piece().team): - e = entity.Entity(self.bus.fetch_sheet("board_overlays_1"), (1, 0)) - e.set_position((t[0] * self.current_board.tmx_data.tilewidth, t[1] * self.current_board.tmx_data.tileheight)) + if not self.bus.check_is_ally_occupied_by_tile_pos(t, sp.team): + e = entity.Entity(self.bus.fetch("sheet_manager", "sheets")["board_overlays_1"], (1, 0)) + e.set_position((t[0] * TILE_WIDTH, t[1] * TILE_HEIGHT)) e.custom_flags = ("OverlayMove", t) self.board_overlay.add(e) @@ -146,7 +148,7 @@ class BoardManager(manager.Manager): for x, y, gid in layer: mx = piece.tile_pos[0] - x my = piece.tile_pos[1] - y - if (abs(mx) + abs(my)) <= movemax and self.current_board.tmx_data.get_tile_properties_by_gid(gid)["Passable"] == 1 and not self.bus.fetch_is_enemy_occupied_by_tile_pos((x, y), piece.team): + if (abs(mx) + abs(my)) <= movemax and self.current_board.tmx_data.get_tile_properties_by_gid(gid)["Passable"] == 1 and not self.bus.check_is_enemy_occupied_by_tile_pos((x, y), piece.team): distances[(x, y)] = movemax + 1 # So we are always greater than the max move # Next, calculate the move from the starting pos to each potential @@ -229,7 +231,20 @@ class BoardManager(manager.Manager): self.move_targets = {} self.previous_moves = {} - def update_board(self, surface = None): + def expose(self): + """ + Expose info about the board to the ManagerBus. + """ + data = { + "current_board" : self.current_board, + "board_dimensions" : self.current_board.board_dimensions, + "board_pixel_dimensions" : self.current_board.pixel_dimensions, + "board_tile_layer" : { (x, y) : gid for layer in self.current_board.tmx_data.visible_layers for x, y, gid in layer if isinstance(layer, pytmx.TiledTileLayer) }, + "board_overlay" : self.board_overlay + } + self.bus.record(self.name, data) + + def update_managed(self, surface = None): """ Update the current board. """ @@ -257,8 +272,8 @@ class Board(object): # Pytmx values self.tmx_data = pytmx.load_pygame(os.path.join(BOARD_PATH, self.boardname, self.filename)) - self.tile_dimensions = (self.tmx_data.width, self.tmx_data.height) - self.grid_dimensions = (self.tmx_data.width * self.tmx_data.tilewidth, self.tmx_data.height * self.tmx_data.tileheight) + self.board_dimensions = (self.tmx_data.width, self.tmx_data.height) + self.pixel_dimensions = (self.tmx_data.width * TILE_WIDTH, self.tmx_data.height * TILE_HEIGHT) def draw_board(self, surface = None): """ @@ -270,5 +285,5 @@ class Board(object): for x, y, gid in layer: t = self.tmx_data.get_tile_image_by_gid(gid) if t: - surface.blit(t, (x * self.tmx_data.tilewidth, y * self.tmx_data.tileheight)) + surface.blit(t, (x * TILE_HEIGHT, y * TILE_WIDTH)) diff --git a/src/bus.py b/src/bus.py @@ -0,0 +1,118 @@ +import pygame +from . import subsystem +from .constants import * + +########## +# bus.py # +########## + +# This file contains: +# 1. The ManagerBus subsystem, which handles inter-manager/inter-managed communication + +##################################### +# Section 1 - The ManagerBus object # +##################################### + +class ManagerBus(subsystem.GameSubsystem): + """ + 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): + + # Parent init + super().__init__(game) + + # Records: Internal data about managers from their "expose()" methods + self.records = {} + + def enter_manager(self, manager_name): + """ + Make an entry in the records for the given manager. The + "record()" and "fetch()" methods check to make sure an + entry exists before looking for a value. This method is + called when a manager is created. + """ + if manager_name not in self.records.keys(): + self.records[manager_name] = {} # Initial value is empty dict, replaced on first expose() + else: + print("BUS ERROR: " + manager_name + " already in records!") + + def record(self, manager, data): + """ + Record some data about a given manager, as long as it has + already been entered in the records. + """ + if manager in self.records.keys(): + if type(data) == dict: + self.records[manager] = data + else: + print("BUS ERROR: Provided data from " + manager + " not dict type!") + print(">> data was: " + data) + else: + print("BUS ERROR: " + manager + " not in records! Unable to record data!") + + def fetch(self, manager, 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 manager in self.records.keys(): + if value in self.records[manager].keys(): + return self.records[manager][value] + else: + print("BUS ERROR: " + value + " not in " + manager + " data record!") + else: + print("BUS ERROR: " + 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 + def check_is_enemy_occupied_by_tile_pos(self, tile_pos, team): + o = self.game.piece_manager.get_piece_by_tile(tile_pos) + return not (o == None or o.team == team) + def check_is_ally_occupied_by_tile_pos(self, tile_pos, team): + o = self.game.piece_manager.get_piece_by_tile(tile_pos) + return not (o == None or o.team != team) + def check_for_tile_by_screen_pos(self, position): + return self.game.board_manager.get_tile_at_position(position) + def check_for_tile_pos_by_screen_pos(self, position): + return self.game.board_manager.get_tile_pos_at_position(position) + def check_for_overlay_move_entity_by_screen_pos(self, position): + return self.game.board_manager.get_overlay_move_entity_at_pos(position) + def check_for_piece_path_by_previous_moves(self, start_tile, target_tile): + return self.game.board_manager.get_path_by_previous_moves(start_tile, target_tile) + + # Performs: All callable non-return behaviors of managers + # TODO: Error-checking would be one of the great additions possible here + def perform_play_sound(self, sound, channel = None, concurrent = False): + self.game.sound_manager.play_sound(sound, channel, concurrent) + def perform_load_board_overlay(self): + self.game.board_manager.load_overlay() + def perform_trigger_menu_button_at_pos(self, pos): + self.game.menu_manager.trigger_button_at_pos(pos) + def perform_select_piece_with_tile_cursor(self): + self.game.piece_manager.select_piece_with_tile_cursor() + def perform_position_tile_cursor(self, tile_pos): + self.game.piece_manager.position_tile_cursor(tile_pos) + def perform_set_piece_move_along_tile_path(self, piece, path): + self.game.piece_manager.set_piece_move_to_tile_path(piece, path) + def perform_continue_current_scene_script(self): + self.game.scene_manager.current_scene.continue_script() + def perform_display_move_range_of_piece(self, piece): + self.game.board_manager.display_as_move_range(self.game.piece_manager.get_piece_max_legal_move(self.game.piece_manager.selected_piece)) diff --git a/src/entity.py b/src/entity.py @@ -1,4 +1,4 @@ -import pygame, json, os, math +import pygame, json, os from . import manager from .constants import * @@ -6,31 +6,6 @@ from .constants import * # entity.py # ############# -# TODO: -# 2. An alternative to the above: Reimagine EntityManager as PieceManager, -# Meaning it is only responsible for board Piece entities. The BoardCursor -# could be handled by a seperate manager of its own, which could manage -# the cursor across multiple modes. This would mean splitting the EntityManager -# and Piece classes off into their own source file (a good option would -# be to combine them with Unit, remove UnitManager, and just make units -# be something PieceManager handles). All of this would be in line with -# the natural path that the code has been taking, whereby there is not -# one single management object for ALL entities, but rather entities and -# other Entity are drawn and updated by the managers that logically are -# already associated with them. Drawing entities is a pan-Manager thing, -# and one of the important distinctions between managers and subsystems. -# The codebase should probably reflect this by doing away with EntityManager -# entirely and refactoring it into PieceManager. EntityManager's functionality -# involving identifying entities by name/id could be moved to a subsystem -# object called ObjectOracle. This subsystem could be alerted each time -# a new object is created, and could maintain a list of references to -# those objects. It could manage ID by itself, and spread it across any -# Entity, not just Entities. Entity would know their own ID, and they could be -# accessed from the oracly by ID by any object with a basic call to -# game.oracle. It would be excellent for debug as well. It is possible that -# even the non-Entity objects like the managers should have IDs and be referred -# to by the oracle. - # 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 @@ -170,13 +145,13 @@ class Entity(pygame.sprite.DirtySprite): 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_def): + 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_def[0], tile_def[1]) - self.tile_gid = tile_def[2] + self.tile_pos = tile_pos + self.tile_gid = tile_gid def act(self): """ diff --git a/src/game.py b/src/game.py @@ -1,5 +1,5 @@ import pygame -from . import subsystem, manager, images, sound, board, entity, piece, menu, scene +from . import subsystem, bus, manager, images, sound, board, entity, piece, menu, scene from .constants import * ########### @@ -42,19 +42,19 @@ class Game(object): self.frame_clock = pygame.time.Clock() # Subsystems - self.manager_bus = subsystem.ManagerBus(self) + self.manager_bus = bus.ManagerBus(self) self.camera = subsystem.GameCamera(self) self.object_oracle = subsystem.ObjectOracle(self) self.save_system = subsystem.SaveSystem(self) self.interface = subsystem.GameInterface(self, self.manager_bus, self.camera) # Managers - self.sheet_manager = images.SheetManager(self, self.manager_bus) - self.sound_manager = sound.SoundManager(self, self.manager_bus) - self.menu_manager = menu.MenuManager(self, self.manager_bus) - self.board_manager = board.BoardManager(self, self.manager_bus) - self.piece_manager = piece.PieceManager(self, self.manager_bus) - self.scene_manager = scene.SceneManager(self, self.manager_bus) + self.sheet_manager = images.SheetManager(self, self.manager_bus, "sheet_manager") + self.sound_manager = sound.SoundManager(self, self.manager_bus, "sound_manager") + self.menu_manager = menu.MenuManager(self, self.manager_bus, "menu_manager") + self.board_manager = board.BoardManager(self, self.manager_bus, "board_manager") + self.piece_manager = piece.PieceManager(self, self.manager_bus, "piece_manager") + self.scene_manager = scene.SceneManager(self, self.manager_bus, "scene_manager") # Setup (This is WIP) self.sheet_manager.load_sheets_from_json("sheets.json") @@ -88,7 +88,7 @@ class Game(object): self.board_manager.load_board(data) self.piece_manager.load_pieces_from_file(data) self.piece_manager.load_tile_cursor("cursor1") - self.camera.load_camera_surface(self.board_manager.current_board.grid_dimensions) + self.camera.load_camera_surface(self.board_manager.current_board.pixel_dimensions) elif new_mode == STATE_MODES.Still_Scene_Mode: self.control_mode = CTRL_MODES.Still_Scene_Normal self.scene_manager.load_still_scene_from_file(data) @@ -124,19 +124,22 @@ class Game(object): # First, fill the entire screen with black self.screen.fill((200, 200, 200)) + # Next, update all managers that are mode-agnostic + self.sheet_manager.update(None) + self.sound_manager.update(None) + # Next, update all the subsurfaces/game objects and draw them # TODO: This is WIP and will change self.interface.update_interface() # State_Mode-specific actions (can be further subdivided by Control_Mode) if self.state_mode == STATE_MODES.Main_Menu_Mode: - self.menu_manager.update_current_menu(self.camera.camera_surface) + self.menu_manager.update(self.camera.camera_surface) elif self.state_mode == STATE_MODES.Battle_Mode: - self.board_manager.update_board(self.camera.camera_surface) - self.piece_manager.update_pieces(self.camera.camera_surface) - self.piece_manager.update_tile_cursor(self.camera.camera_surface) + self.board_manager.update(self.camera.camera_surface) + self.piece_manager.update(self.camera.camera_surface) elif self.state_mode == STATE_MODES.Still_Scene_Mode: - self.scene_manager.update_scene(self.camera.camera_surface) + self.scene_manager.update(self.camera.camera_surface) # Draw the camera to the screen #self.screen.blit(self.camera.camera_surface, self.camera.camera_surface_offset) diff --git a/src/images.py b/src/images.py @@ -35,15 +35,18 @@ class SheetManager(manager.Manager): them. """ - def __init__(self, game, bus): + def __init__(self, game, bus, name): - super().__init__(game, bus) + super().__init__(game, bus, name) # Important values self.sheets_def = {} self.loaded_sheets = {} 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. @@ -73,12 +76,15 @@ class SheetManager(manager.Manager): if name in self.loaded_sheets.keys(): return self.loaded_sheets[name] - def get_animation(self, name): + def expose(self): """ - Return the animation matching name if it exists. + Expose sheet info to the ManagerBus. """ - if name in self.loaded_animations.keys(): - return self.loaded_animations[name] + data = { + "sheets" : self.loaded_sheets, + "animations" : self.animations + } + self.bus.record(self.name, data) ############################ # Section 3 - Sheet Object # diff --git a/src/manager.py b/src/manager.py @@ -11,7 +11,6 @@ from .constants import * # updating the objects that a manager manages, and thus would have to be tailored to the specific manager. Implementation advantage would be that # managers could be called together as a group to update in game, and could possibly be stored in a dict rather than have individual attributes # they are associated with (would be very useful if implementing the ManagerBus() object). -# 3. Support for a ManagerBus() object (details pending, refer to main.py) # 5. Perhaps a generic "load_json" function that could be extended? The base function could store the JSON in a dict in the manager object, keyed by # file name. Extensions could be added for specific JSON fields/local values. This may not be possible, however, need to investigate... @@ -28,6 +27,14 @@ class Manager(subsystem.GameSubsystem): 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, sound, 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 @@ -45,17 +52,37 @@ class Manager(subsystem.GameSubsystem): self._effectual = x # Initialization - def __init__(self, game, bus): + def __init__(self, game, bus, name): # Initial values super().__init__(game) self.bus = bus + self.name = name # Property defaults self._activated = True self._effectual = False - # Other methods + # Bus records functionality + self.record_self() + + def record_self(self): + """ + Record this manager in the ManagerBus, so + that it can write/fetch values. + """ + self.bus.enter_manager(self.name) + + 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. @@ -73,6 +100,14 @@ class Manager(subsystem.GameSubsystem): elif ef["call"] == GAME_EFFECTS.ef_game_switch_mode.name: self.game.switch_mode(STATE_MODES[ef["data"][0]], ef["data"][1]) + 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 diff --git a/src/menu.py b/src/menu.py @@ -22,9 +22,9 @@ class MenuManager(manager.Manager): access those groups to draw them. """ - def __init__(self, game, bus): + def __init__(self, game, bus, name): - super().__init__(game, bus) + super().__init__(game, bus, name) # Fix properties self.effectual = True @@ -49,7 +49,13 @@ class MenuManager(manager.Manager): if b.rect.collidepoint(pos): self.trigger_effects(b.effects) - def update_current_menu(self, surface = None): + def expose(self): + """ + Expose menu info to the ManagerBus. + """ + pass + + def update_managed(self, surface = None): """ Update the current Menu object and all of its sub-entities. @@ -90,12 +96,12 @@ class Menu(object): if self.definition != None: # Load background, if any - self.background = entity.Entity(self.manager.bus.fetch_sheet(self.definition["bg_sheet"]), + self.background = entity.Entity(self.manager.bus.fetch("sheet_manager", "sheets")[self.definition["bg_sheet"]], tuple(self.definition["bg_sprite"]), None, False) # Load buttons, if any for b in self.definition["buttons"]: - nmb = MenuButton(self.manager.bus.fetch_sheet(self.definition["buttons"][b]["sheet"]), tuple(self.definition["buttons"][b]["sprite"]), - None, False, self.definition["buttons"][b]["effects"]) + nmb = MenuButton(self.manager.bus.fetch("sheet_manager", "sheets")[self.definition["buttons"][b]["sheet"]], + tuple(self.definition["buttons"][b]["sprite"]), None, False, self.definition["buttons"][b]["effects"]) nmb.rect.topleft = tuple(self.definition["buttons"][b]["pos"]) self.button_group.add(nmb) diff --git a/src/piece.py b/src/piece.py @@ -1,4 +1,4 @@ -import pygame, os, json, math +import pygame, os, json from . import manager, entity from .constants import * @@ -23,10 +23,10 @@ class PieceManager(manager.Manager): with pieces and tiles. """ - def __init__(self, game, bus): + def __init__(self, game, bus, name): # Parent initialization - super().__init__(game, bus) + super().__init__(game, bus, name) # Entity values self.pieces = pygame.sprite.LayeredDirty() @@ -52,9 +52,9 @@ class PieceManager(manager.Manager): # here. This should probably be so for all methods that load # from definition. for p in definition: - n_sheet = self.bus.fetch_sheet(definition[p]["sheet"]) + n_sheet = self.bus.fetch("sheet_manager", "sheets")[definition[p]["sheet"]] n_sprite = tuple(definition[p]["sprite"]) - n_anim = self.bus.fetch_animation(definition[p]["sheet"], definition[p]["animation"]) + n_anim = self.bus.fetch("sheet_manager", "animations")[definition[p]["sheet"]][definition[p]["animation"]] n_animated = definition[p]["animated"] n_name = definition[p]["name"] n_passable = definition[p]["passable"] @@ -63,7 +63,7 @@ class PieceManager(manager.Manager): n_team = definition[p]["team"] n_equip = definition[p]["equipment"] np = Piece(n_sheet, n_sprite, n_anim, n_animated, self, n_name, n_passable, n_nstats, n_astats, n_team, n_equip) - np.assign_tile(self.bus.fetch_tile_by_tile_pos(tuple(definition[p]["tile"]))) + np.assign_tile(tuple(definition[p]["tile"]), self.bus.fetch("board_manager", "board_tile_layer")[tuple(definition[p]["tile"])]) np.snap_to_tile() self.add_piece(np) @@ -87,7 +87,8 @@ class PieceManager(manager.Manager): """ Load a TileCursor object to highlight selected tiles. """ - self.tile_cursor = TileCursor(self.bus.fetch_sheet(sheet), (0, 0), self.bus.fetch_animation(sheet, "pulse"), True) + self.tile_cursor = TileCursor(self.bus.fetch("sheet_manager", "sheets")[sheet], (0, 0), + self.bus.fetch("sheet_manager", "animations")[sheet]["pulse"], True) def get_piece_by_name(self, name): """ @@ -117,7 +118,7 @@ class PieceManager(manager.Manager): Snap the TileCursor object's position to 'tile_pos' and set the tile GID there as the current tile_cursor's selected tile. """ - self.tile_cursor.assign_tile(tile_pos) + self.tile_cursor.assign_tile(tile_pos, self.bus.fetch("board_manager", "board_tile_layer")[tile_pos]) self.tile_cursor.snap_to_tile() def select_piece_with_tile_cursor(self): @@ -127,6 +128,7 @@ class PieceManager(manager.Manager): should become None. """ self.selected_piece = self.get_piece_by_tile(self.tile_cursor.tile_pos) + self.expose() def set_piece_move_to_tile_path(self, piece, path): """ @@ -142,27 +144,36 @@ class PieceManager(manager.Manager): """ m = piece.active_stats["MOVE"] legal_moves = [] - for x in range(0, self.bus.fetch_current_board_width()): - for y in range(0, self.bus.fetch_current_board_height()): + dims = self.bus.fetch("board_manager", "board_dimensions") + for x in range(0, dims[0]): + for y in range(0, dims[1]): mx = piece.tile_pos[0] - x my = piece.tile_pos[1] - y if (abs(mx) + abs(my)) <= m: legal_moves.append((x, y)) return legal_moves - def update_tile_cursor(self, surface = None): + def expose(self): """ - Update the tile cursor object. + Expose info about pieces to the ManagerBus. """ - if surface != None: - self.tile_cursor.update(surface) - - def update_pieces(self, surface = None): + data = { + "selected_piece" : self.selected_piece, + "tile_cursor" : self.tile_cursor, + "pieces" : self.pieces, + "pieces_by_name" : { p.name : p for p in self.pieces }, + "pieces_by_tile_pos" : { p.tile_pos : p for p in self.pieces } + } + self.bus.record(self.name, data) + + + def update_managed(self, surface = None): """ - Update all pieces. + Update the pieces and tile cursor. """ if surface != None: self.pieces.update(surface) + self.tile_cursor.update(surface) ############################### # Section 2 - The Piece class # @@ -238,7 +249,7 @@ class Piece(entity.Entity): self.facing = FACE_DIR.D if self.facing != oldface: - self.set_animation(self.manager.bus.fetch_animation(self.sheet.name, "walk_" + self.facing.name), True) + self.set_animation(self.manager.bus.fetch("sheet_manager", "animations")[self.sheet.name]["walk_" + self.facing.name], True) # Setup motion to next tile in the path next_tar = ((self.current_tile_path[self.current_tile_path_index][0] * TILE_WIDTH) + (TILE_WIDTH / 2), diff --git a/src/scene.py b/src/scene.py @@ -22,10 +22,10 @@ class SceneManager(manager.Manager): complicated scenes. """ - def __init__(self, game, bus): + def __init__(self, game, bus, name): # Parent initialization - super().__init__(game, bus) + super().__init__(game, bus, name) # Fix properties self.effectual = True @@ -39,7 +39,13 @@ class SceneManager(manager.Manager): """ self.current_scene = StillScene(self, scenefile) - def update_scene(self, surface = None): + def expose(self): + """ + Expose scene info to the ManagerBus. + """ + pass + + def update_managed(self, surface = None): """ Update and draw the current scene. Behaves differently whether the current scene is a StillScene or something @@ -114,19 +120,19 @@ class StillScene(object): Load a scene from file. """ # Load univeral elements - self.name_box = entity.Entity(self.manager.bus.fetch_sheet("still_scene_name_box_1")) + self.name_box = entity.Entity(self.manager.bus.fetch("sheet_manager", "sheets")["still_scene_name_box_1"]) self.name_box.set_position(self.name_box_pos) - self.text_box = entity.Entity(self.manager.bus.fetch_sheet("still_scene_text_box_1")) + self.text_box = entity.Entity(self.manager.bus.fetch("sheet_manager", "sheets")["still_scene_text_box_1"]) self.text_box.set_position(self.text_box_pos) - self.continue_prompt = entity.Entity(self.manager.bus.fetch_sheet("continue_prompt_1"), (0, 0), - self.manager.bus.fetch_animation("continue_prompt_1", "shimmer")) + self.continue_prompt = entity.Entity(self.manager.bus.fetch("sheet_manager", "sheets")["continue_prompt_1"], (0, 0), + self.manager.bus.fetch("sheet_manager", "animations")["continue_prompt_1"]["shimmer"]) self.continue_prompt.set_position(self.continue_prompt_pos) # Load from scene definition JSON scenedef = json.load(open(os.path.join(SCENE_JSON_PATH, scenefile))) self.name = scenedef["name"] - self.background = entity.Entity(self.manager.bus.fetch_sheet(scenedef["bg_sheet"]), tuple(scenedef["bg_sprite"])) + self.background = entity.Entity(self.manager.bus.fetch("sheet_manager", "sheets")[scenedef["bg_sheet"]], tuple(scenedef["bg_sprite"])) self.script = scenedef["script"] for f in scenedef["fonts"]: self.fonts[f] = pygame.font.Font(os.path.join(FONT_PATH, scenedef["fonts"][f][0]), scenedef["fonts"][f][1]) @@ -157,7 +163,7 @@ class StillScene(object): self.current_voice = self.script[self.script_index]["voice"] self.current_text_string = self.script[self.script_index]["line"] for c in self.script[self.script_index]["characters"]: - nc = entity.Entity(self.manager.bus.fetch_sheet(c["sheet"]), tuple(c["sprite"])) + nc = entity.Entity(self.manager.bus.fetch("sheet_manager", "sheets")[c["sheet"]], tuple(c["sprite"])) nc.set_position(tuple(c["pos"])) self.displayed_characters.add(nc) if self.script[self.script_index]["speaker"] != None: @@ -238,7 +244,7 @@ class StillScene(object): if self.voice_delay > 0: self.voice_delay -= 1 else: - self.manager.game.sound_manager.play_sound(self.current_voice, 0) + self.manager.bus.perform_play_sound(self.current_voice, 0) def continue_script(self): """ diff --git a/src/sound.py b/src/sound.py @@ -6,6 +6,11 @@ from .constants import SOUND_PATH, JSON_PATH # sound.py # ############ +# TODO: This should be a subsystem rather than a manager. Managers manage visible things. Plus, SoundManager +# doesn't ever need to refer thru the bus to anything. Should definitely be a subsystem, and it and other +# subsystems can start to be divided up into their own files (especially GameInterface, GameCamera, and +# ManagerBus). + # This file contains: # 1. SoundManager, which loads up all sounds and is then called to play them through the pygame mixer. @@ -19,15 +24,18 @@ class SoundManager(manager.Manager): files and is called to play them. """ - def __init__(self, game, bus): + def __init__(self, game, bus, name): # Parent initialization - super().__init__(game, bus) + super().__init__(game, bus, name) # Important values self.sounds = {} self.channels = [pygame.mixer.Channel(x) for x in range(0, pygame.mixer.get_num_channels() - 1)] + # This manager should perform an initial update + self.update(None) + def load_sounds_from_json(self, soundjson): """ Load up sounds from a definition JSON @@ -58,3 +66,9 @@ class SoundManager(manager.Manager): elif concurrent or self.sounds[soundname].get_num_channels() == 0: self.sounds[soundname].play() + def expose(self): + """ + Expose sound info to the ManagerBus. + """ + pass + diff --git a/src/subsystem.py b/src/subsystem.py @@ -6,11 +6,11 @@ from .constants import * ################ # This file contains: -# 1. The generic GameSubsystem class, which abstract subsystems (like Interface) are children of, as well as all Managers +# 1. The generic GameSubsystem class, which abstract subsystems (like GameInterface) are children of, as well as all Managers # 2. The GameInterface class, which handles game events such as keyboard and mouse input -# 3. The ManagerBus class, which is a communication mechanism used by Managers as well as other subsystems and ever game objects -# 4. The GameCamera class, which controls camera-oriented logic and drawing -# 5. The ObjectOracle class, which keeps track of all VGO objects and can refer to them, but does not manage or draw them +# 3. The GameCamera class, which controls camera-oriented logic and drawing +# 4. The ObjectOracle class, which keeps track of all Entity objects and can refer to them, but does not manage or draw them +# 5. The SaveSystem class, which handles saving and loading games ####################################### # Section 1 - The GameSubsystem class # @@ -24,6 +24,16 @@ class GameSubsystem(object): 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): @@ -109,17 +119,18 @@ class GameInterface(GameSubsystem): # Normal in-battle turn control if self.game.control_mode == CTRL_MODES.Turn_Normal: self.bus.perform_select_piece_with_tile_cursor() - if self.bus.fetch_selected_piece() != None: - self.bus.perform_display_move_range_of_piece(self.bus.fetch_selected_piece()) + sp = self.bus.fetch("piece_manager", "selected_piece") + if sp != None: + self.bus.perform_display_move_range_of_piece(sp) self.game.control_mode = CTRL_MODES.Turn_Select_Move # Selecting a move for the active piece control elif self.game.control_mode == CTRL_MODES.Turn_Select_Move: - if self.bus.fetch_overlay_move_entity_by_screen_pos(mousepos) != None: - targ = self.bus.fetch_tile_pos_by_screen_pos(mousepos) - to_path = self.bus.fetch_piece_path_by_previous_moves(self.bus.fetch_selected_piece_tile_pos(), targ) + if self.bus.check_for_overlay_move_entity_by_screen_pos(mousepos) != None: + targ = self.bus.check_for_tile_pos_by_screen_pos(mousepos) + to_path = self.bus.check_for_piece_path_by_previous_moves(self.bus.fetch("piece_manager", "selected_piece").tile_pos, targ) if to_path != None: - self.bus.perform_set_piece_move_along_tile_path(self.bus.fetch_selected_piece(), to_path) + self.bus.perform_set_piece_move_along_tile_path(self.bus.fetch("piece_manager", "selected_piece"), to_path) # TODO: Not like this self.game.lose_control(len(to_path) * PIECE_MOVE_DELAY, CTRL_MODES.Turn_Normal) self.bus.perform_load_board_overlay() @@ -147,9 +158,9 @@ class GameInterface(GameSubsystem): if self.game.state_mode == STATE_MODES.Battle_Mode: mouseraw = pygame.mouse.get_pos() mousepos = (mouseraw[0] - self.camera.camera_surface_offset[0], mouseraw[1] - self.camera.camera_surface_offset[1]) - tilepos = self.bus.fetch_tile_by_screen_pos(mousepos) + tilepos = self.bus.check_for_tile_by_screen_pos(mousepos) if tilepos != None: - self.bus.perform_position_tile_cursor(tilepos) + self.bus.perform_position_tile_cursor((tilepos[0], tilepos[1])) if self.game.control_mode == CTRL_MODES.Turn_Normal: for r in self.camera.scroll_rects: @@ -157,89 +168,7 @@ class GameInterface(GameSubsystem): self.camera.move_offset(r, SCROLL_SPEED) #################################### -# Section 3 - The ManagerBus class # -#################################### - -class ManagerBus(GameSubsystem): - """ - 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): - - # Parent init - super().__init__(game) - - # Fetches: All return values from managers and other subsystems - def fetch_sheet(self, name): - return self.game.sheet_manager.loaded_sheets[name] - def fetch_animation(self, sheet, anim): - return self.game.sheet_manager.animations[sheet][anim] - def fetch_piece_by_name(self, name): - return self.game.piece_manager.get_piece_by_name(name) - def fetch_piece_by_tile(self, tile_pos): - return self.game.piece_manager.get_piece_by_tile(tile_pos) - def fetch_selected_piece(self): - return self.game.piece_manager.selected_piece - def fetch_selected_piece_tile_pos(self): - return self.game.piece_manager.selected_piece.tile_pos - def fetch_is_enemy_occupied_by_tile_pos(self, tile_pos, team): - o = self.game.piece_manager.get_piece_by_tile(tile_pos) - return not (o == None or o.team == team) - def fetch_is_ally_occupied_by_tile_pos(self, tile_pos, team): - o = self.game.piece_manager.get_piece_by_tile(tile_pos) - return not (o == None or o.team != team) - def fetch_tile_by_screen_pos(self, position): - return self.game.board_manager.get_tile_at_position(position) - def fetch_tile_by_tile_pos(self, tile_pos): - return self.game.board_manager.get_tile_at_tile_pos(tile_pos) - def fetch_tile_pos_by_screen_pos(self, position): - return self.game.board_manager.get_tile_pos_at_position(position) - def fetch_piece_path_by_previous_moves(self, start_tile, target_tile): - return self.game.board_manager.get_path_by_previous_moves(start_tile, target_tile) - def fetch_overlay_move_entity_by_screen_pos(self, position): - return self.game.board_manager.get_overlay_move_entity_at_pos(position) - def fetch_current_board_width(self): - if self.game.board_manager.current_board != None: - return self.game.board_manager.current_board.tmx_data.width - else: - return None - def fetch_current_board_height(self): - if self.game.board_manager.current_board != None: - return self.game.board_manager.current_board.tmx_data.height - else: - return None - def fetch_camera_offset(self): - return self.game.camera.camera_surface_offset - - # Performs: All callable non-return behaviors of managers - # TODO: Error-checking would be one of the great additions possible here - def perform_load_board_overlay(self): - self.game.board_manager.load_overlay() - def perform_trigger_menu_button_at_pos(self, pos): - self.game.menu_manager.trigger_button_at_pos(pos) - def perform_select_piece_with_tile_cursor(self): - self.game.piece_manager.select_piece_with_tile_cursor() - def perform_position_tile_cursor(self, tile_pos): - self.game.piece_manager.position_tile_cursor(tile_pos) - def perform_set_piece_move_along_tile_path(self, piece, path): - self.game.piece_manager.set_piece_move_to_tile_path(piece, path) - def perform_continue_current_scene_script(self): - self.game.scene_manager.current_scene.continue_script() - def perform_display_move_range_of_piece(self, piece): - self.game.board_manager.display_as_move_range(self.game.piece_manager.get_piece_max_legal_move(self.game.piece_manager.selected_piece)) - -#################################### -# Section 4 - The GameCamera class # +# Section 3 - The GameCamera class # #################################### class GameCamera(GameSubsystem): @@ -301,7 +230,7 @@ class GameCamera(GameSubsystem): surface.blit(self.camera_surface, self.camera_surface_offset) ###################################### -# Section 5 - The ObjectOracle class # +# Section 4 - The ObjectOracle class # ###################################### class ObjectOracle(GameSubsystem): @@ -321,7 +250,7 @@ class ObjectOracle(GameSubsystem): super().__init__(game) #################################### -# Section 6 - The SaveSystem class # +# Section 5 - The SaveSystem class # #################################### class SaveSystem(GameSubsystem):