import importlib import json import math import os import sys import time import traceback from collections import namedtuple import sdl2 import collision import game_hud import game_object import game_room import game_util_objects import vector from art import ART_DIR from camera import Camera from charset import CHARSET_DIR from grid import GameGrid from palette import PALETTE_DIR TOP_GAME_DIR = "games/" DEFAULT_STATE_FILENAME = "start" STATE_FILE_EXTENSION = "gs" GAME_SCRIPTS_DIR = "scripts/" SOUNDS_DIR = "sounds/" # generic starter script with a GO and Player subclass STARTER_SCRIPT = """ from game_object import GameObject from game_util_objects import Player class MyGamePlayer(Player): "Generic starter player class for newly created games." art_src = 'default_player' # no "move" state art, so just use stand state for now move_state = 'stand' class MyGameObject(GameObject): "Generic starter object class for newly created games." def update(self): # write "hello" in a color that shifts over time color = self.art.palette.get_random_color_index() self.art.write_string(0, 0, 3, 2, 'hello!', color) # run parent class update GameObject.update(self) """ # Quickie class to debug render order RenderItem = namedtuple("RenderItem", ["obj", "layer", "sort_value"]) class GameCamera(Camera): pan_friction = 0.2 use_bounds = False class GameWorld: """ Holds global state for Game Mode. Spawns, manages, and renders GameObjects. Properties serialized via WorldPropertiesObject. Global state can be controlled via a WorldGlobalsObject. """ game_title = "Untitled Game" "Title for game, shown in window titlebar when not editing" gravity_x, gravity_y, gravity_z = 0.0, 0.0, 0.0 "Gravity applied to all objects who are affected by gravity." bg_color = [0.0, 0.0, 0.0, 1.0] "OpenGL wiewport color to render behind everything else, ie the void." hud_class_name = "GameHUD" "String name of HUD class to use" properties_object_class_name = "WorldPropertiesObject" globals_object_class_name = "WorldGlobalsObject" "String name of WorldGlobalsObject class to use." player_camera_lock = True "If True, camera will be locked to player's location." object_grid_snap = True # editable properties # TODO: # update_when_unfocused = False # "If True, game sim will update even when window doesn't have input focus" draw_hud = True allow_pause = True "If False, user cannot pause game sim" collision_enabled = True "If False, CollisionLord won't bother thinking about collision at all." # toggles for "show all" debug viz modes show_collision_all = False show_bounds_all = False show_origin_all = False show_all_rooms = False "If True, show all rooms not just current one." draw_debug_objects = True "If False, objects with is_debug=True won't be drawn." room_camera_changes_enabled = True "If True, snap camera to new room's associated camera marker." list_only_current_room_objects = False "If True, list UI will only show objects in current room." builtin_module_names = ["game_object", "game_util_objects", "game_hud", "game_room"] builtin_base_classes = ( game_object.GameObject, game_hud.GameHUD, game_room.GameRoom, ) def __init__(self, app): self.app = app "Application that created this world." self.game_dir = None "Currently loaded game directory." self.sounds_dir = None self.game_name = None "Game's internal name, based on its directory." self.selected_objects = [] self.hovered_objects = [] self.hovered_focus_object = None "Set by check_hovers(), to the object that will be selected if edit mode user clicks" self.last_click_on_ui = False self.last_mouse_click_x, self.last_mouse_click_y = 0, 0 self.properties = None "Our WorldPropertiesObject" self.globals = None "Our WorldGlobalsObject - not required" self.camera = GameCamera(self.app) self.grid = GameGrid(self.app) self.grid.visible = False self.player = None self.paused = False self._pause_time = 0 self.updates = 0 "Number of updates this we have performed." self.modules = { "game_object": game_object, "game_util_objects": game_util_objects, "game_hud": game_hud, "game_room": game_room, } self.classname_to_spawn = None self.objects = {} "Dict of objects by name:object" self.new_objects = {} "Dict of just-spawned objects, added to above on update() after spawn" self.rooms = {} "Dict of rooms by name:room" self.current_room = None self.cl = collision.CollisionLord(self) self.hud = None self.art_loaded = [] self.drag_objects = {} "Offsets for objects player is edit-dragging" self.last_state_loaded = DEFAULT_STATE_FILENAME def play_music(self, music_filename, fade_in_time=0): "Play given music file in any SDL2_mixer-supported format." music_filename = self.game_dir + SOUNDS_DIR + music_filename self.app.al.set_music(music_filename) self.app.al.start_music(music_filename) def pause_music(self): self.app.al.pause_music() def resume_music(self): self.app.al.resume_music() def stop_music(self): "Stop any currently playing music." self.app.al.stop_all_music() def is_music_playing(self): "Return True if there is music playing." return self.app.al.is_music_playing() def pick_next_object_at(self, x, y): "Return next unselected object at given point." objects = self.get_objects_at(x, y) # early out if len(objects) == 0: return None # don't bother cycling if only one object found if ( len(objects) == 1 and objects[0].selectable and objects[0] not in self.selected_objects ): return objects[0] # cycle through objects at point til an unselected one is found for obj in objects: if not obj.selectable: continue if obj in self.selected_objects: continue return obj return None def get_objects_at(self, x, y, allow_locked=False): "Return list of all objects whose bounds fall within given point." objects = [] for obj in self.objects.values(): if obj.locked and not allow_locked: continue # only allow selecting of visible objects # (can still be selected via list panel) if obj.visible and obj.is_point_inside(x, y): objects.append(obj) # sort objects in Z, highest first objects.sort(key=lambda obj: obj.z, reverse=True) return objects def select_click(self): x, y, z = vector.screen_to_world(self.app, self.app.mouse_x, self.app.mouse_y) # remember last place we clicked self.last_mouse_click_x, self.last_mouse_click_y = x, y if self.classname_to_spawn: new_obj = self.spawn_object_of_class(self.classname_to_spawn, x, y) if self.current_room: self.current_room.add_object(new_obj) self.app.ui.message_line.post_line("Spawned {}".format(new_obj.name)) return objects = self.get_objects_at(x, y) next_obj = self.pick_next_object_at(x, y) if len(objects) == 0: self.deselect_all() # ctrl: unselect first selected object found under mouse elif self.app.il.ctrl_pressed: for obj in self.selected_objects: if obj in objects: self.deselect_object(obj) break # shift: add to current selection elif self.app.il.shift_pressed: self.select_object(next_obj) # no mod keys: select only object under cursor, deselect all if none elif not next_obj and len(objects) == 0: self.deselect_all() # special case: must use shift-click for objects # beneath (lower Z) topmost elif next_obj is not objects[0]: pass else: self.select_object(next_obj) # remember object offsets from cursor for dragging for obj in self.selected_objects: self.drag_objects[obj.name] = (obj.x - x, obj.y - y, obj.z - z) def clicked(self, button): # if edit UI is up, select stuff if self.app.ui.is_game_edit_ui_visible(): if button == sdl2.SDL_BUTTON_LEFT: self.select_click() # else pass clicks to any objects under mouse else: x, y, z = vector.screen_to_world( self.app, self.app.mouse_x, self.app.mouse_y ) # 'locked" only relevant to edit mode, ignore it if in play mode objects = self.get_objects_at(x, y, allow_locked=True) for obj in objects: if obj.handle_mouse_events and ( not obj.locked or not self.app.can_edit ): obj.clicked(button, x, y) if obj.consume_mouse_events: break def select_unclick(self): # clicks on UI are consumed and flag world to not accept unclicks # (keeps unclicks after dialog dismiss from deselecting objects) if self.last_click_on_ui: self.last_click_on_ui = False # clear drag objects, since we're leaving valid drag context # fixes unwanted drag after eg ESC exiting a menu self.drag_objects.clear() return # if we're clicking to spawn something, don't drag/select if self.classname_to_spawn: return x, y, z = vector.screen_to_world(self.app, self.app.mouse_x, self.app.mouse_y) # remember selected objects now, they might be deselected but still # need to have their collision turned back on. selected_objects = self.selected_objects[:] if len(self.selected_objects) > 0 and not self.app.il.shift_pressed: # if mouse has traveled much since click, deselect all objects # except one mouse is over. dx = self.last_mouse_click_x - x dy = self.last_mouse_click_y - y if math.sqrt(dx**2 + dy**2) < 1.5: for obj in self.get_objects_at(x, y): if obj in self.selected_objects: self.deselect_all() self.select_object(obj) break # end drag, forget drag object offsets self.drag_objects.clear() for obj in selected_objects: obj.enable_collision() if obj.collision_shape_type == collision.CST_TILE: obj.collision.create_shapes() obj.collision.update() def unclicked(self, button): if self.app.ui.is_game_edit_ui_visible(): if button == sdl2.SDL_BUTTON_LEFT: self.select_unclick() else: x, y, z = vector.screen_to_world( self.app, self.app.mouse_x, self.app.mouse_y ) objects = self.get_objects_at(x, y) for obj in objects: if obj.handle_mouse_events: obj.unclicked(button, x, y) def check_hovers(self): "Update objects on their mouse hover status" x, y, z = vector.screen_to_world(self.app, self.app.mouse_x, self.app.mouse_y) new_hovers = self.get_objects_at(x, y) # if this object will be selected on left click; draw bounds & label if self.app.ui.is_game_edit_ui_visible(): next_obj = self.pick_next_object_at(x, y) self.hovered_focus_object = next_obj # if in play mode, notify objects who have begun to be hovered else: for obj in new_hovers: if obj.handle_mouse_events and obj not in self.hovered_objects: obj.hovered(x, y) # check for objects un-hovered by this move for obj in self.hovered_objects: if obj.handle_mouse_events and obj not in new_hovers: obj.unhovered(x, y) self.hovered_objects = new_hovers def mouse_wheeled(self, wheel_y): x, y, z = vector.screen_to_world(self.app, self.app.mouse_x, self.app.mouse_y) objects = self.get_objects_at(x, y, allow_locked=True) for obj in objects: if obj.handle_mouse_events and (not obj.locked or not self.app.can_edit): obj.mouse_wheeled(wheel_y) if obj.consume_mouse_events: break def mouse_moved(self, dx, dy): if self.app.ui.active_dialog: return # bail if mouse didn't move (in world space - include camera) last input if ( self.app.mouse_dx == 0 and self.app.mouse_dy == 0 and not self.camera.moved_this_frame ): return # if last onclick was a UI element, don't drag if self.last_click_on_ui: return self.check_hovers() # not dragging anything? if len(self.selected_objects) == 0: return # 0-length drags cause unwanted snapping if dx == 0 and dy == 0: return # set dragged objects to mouse + offset from mouse when drag started x, y, z = vector.screen_to_world(self.app, self.app.mouse_x, self.app.mouse_y) for obj_name, offset in self.drag_objects.items(): obj = self.objects[obj_name] if obj.locked: continue obj.disable_collision() obj.x = x + offset[0] obj.y = y + offset[1] obj.z = z + offset[2] if self.object_grid_snap: obj.x = round(obj.x) obj.y = round(obj.y) # if odd width/height, origin will be between quads and # edges will be off-grid; nudge so that edges are on-grid if obj.art.width % 2 != 0: obj.x += obj.art.quad_width / 2 if obj.art.height % 2 != 0: obj.y += obj.art.quad_height / 2 def select_object(self, obj, force=False): "Add given object to our list of selected objects." if not self.app.can_edit: return if obj and (obj.selectable or force) and obj not in self.selected_objects: self.selected_objects.append(obj) self.app.ui.object_selection_changed() def deselect_object(self, obj): "Remove given object from our list of selected objects." if obj in self.selected_objects: self.selected_objects.remove(obj) self.app.ui.object_selection_changed() def deselect_all(self): "Deselect all objects." self.selected_objects = [] self.app.ui.object_selection_changed() def create_new_game(self, new_game_dir, new_game_title): "Create appropriate dirs and files for a new game, return success." self.unload_game() new_dir = self.app.documents_dir + TOP_GAME_DIR + new_game_dir + "/" if os.path.exists(new_dir): self.app.log("Game dir {} already exists!".format(new_game_dir)) return False os.mkdir(new_dir) os.mkdir(new_dir + ART_DIR) os.mkdir(new_dir + GAME_SCRIPTS_DIR) os.mkdir(new_dir + SOUNDS_DIR) os.mkdir(new_dir + CHARSET_DIR) os.mkdir(new_dir + PALETTE_DIR) # create a generic starter script with a GO and Player subclass f = open(new_dir + GAME_SCRIPTS_DIR + new_game_dir + ".py", "w") f.write(STARTER_SCRIPT) f.close() # load game self.set_game_dir(new_game_dir) self.properties = self.spawn_object_of_class("WorldPropertiesObject") self.objects.update(self.new_objects) self.new_objects = {} # HACK: set some property defaults, no idea why they don't take :[ self.collision_enabled = self.properties.collision_enabled = True self.game_title = self.properties.game_title = new_game_title self.save_to_file(DEFAULT_STATE_FILENAME) return True def unload_game(self): "Unload currently loaded game." for obj in self.objects.values(): obj.destroy() self.cl.reset() self.camera.reset() self.player = None self.globals = None self.properties = None if self.hud: self.hud.destroy() self.hud = None self.objects, self.new_objects = {}, {} self.rooms = {} # art_loaded is cleared when game dir is set self.selected_objects = [] self.app.al.stop_all_music() def get_first_object_of_type(self, class_name, allow_subclasses=True): "Return first object found with given class name." c = self.get_class_by_name(class_name) for obj in self.objects.values(): # use isinstance so we catch subclasses if c and allow_subclasses: if isinstance(obj, c): return obj elif type(obj).__name__ == class_name: return obj def get_all_objects_of_type(self, class_name, allow_subclasses=True): "Return list of all objects found with given class name." c = self.get_class_by_name(class_name) objects = [] for obj in self.objects.values(): if c and allow_subclasses: if isinstance(obj, c): objects.append(obj) elif type(obj).__name__ == class_name: objects.append(obj) return objects def set_for_all_objects(self, name, value): "Set given variable name to given value for all objects." for obj in self.objects.values(): setattr(obj, name, value) def edit_art_for_selected(self): if len(self.selected_objects) == 0: return self.app.exit_game_mode() for obj in self.selected_objects: for art_filename in obj.get_all_art(): self.app.load_art_for_edit(art_filename) def move_selected(self, move_x, move_y, move_z): "Shift position of selected objects by given x,y,z amount." for obj in self.selected_objects: obj.x += move_x obj.y += move_y obj.z += move_z def reset_game(self): "Reset currently loaded game to last loaded state." if self.game_dir: self.load_game_state(self.last_state_loaded) def set_game_dir(self, dir_name, reset=False): "Load game from given game directory." if self.game_dir and dir_name == self.game_dir: self.load_game_state(DEFAULT_STATE_FILENAME) return # loading a new game, wipe art list self.art_loaded = [] # check in user documents dir first game_dir = TOP_GAME_DIR + dir_name doc_game_dir = self.app.documents_dir + game_dir for d in [doc_game_dir, game_dir]: if not os.path.exists(d): continue self.game_dir = d self.game_name = dir_name if not d.endswith("/"): self.game_dir += "/" self.app.log("Game data folder is now {}".format(self.game_dir)) # set sounds dir before loading state; some obj inits depend on it self.sounds_dir = self.game_dir + SOUNDS_DIR if reset: # load in a default state, eg start.gs self.load_game_state(DEFAULT_STATE_FILENAME) else: # if no reset load submodules into namespace from the get-go self._import_all() self.classes = self._get_all_loaded_classes() break if not self.game_dir: self.app.log("Couldn't find game directory {}".format(dir_name)) def _remove_non_current_game_modules(self): """ Remove modules from previously-loaded games from both sys and GameWorld's dicts. """ modules_to_remove = [] games_dir_prefix = TOP_GAME_DIR.replace("/", "") this_game_dir_prefix = "{}.{}".format(games_dir_prefix, self.game_name) for module_name in sys.modules: # remove any module that isn't for this game or part of its path if ( module_name != games_dir_prefix and module_name != this_game_dir_prefix and module_name.startswith(games_dir_prefix) and not module_name.startswith(this_game_dir_prefix + ".") ): modules_to_remove.append(module_name) for module_name in modules_to_remove: sys.modules.pop(module_name) if module_name in self.modules: self.modules.pop(module_name) def _get_game_modules_list(self): "Return list of current game's modules from its scripts/ dir" # build list of module files modules_list = self.builtin_module_names[:] # create appropriately-formatted python import path module_path_prefix = "{}.{}.{}.".format( TOP_GAME_DIR.replace("/", ""), self.game_name, GAME_SCRIPTS_DIR.replace("/", ""), ) for filename in os.listdir(self.game_dir + GAME_SCRIPTS_DIR): # exclude emacs temp files and special world start script if not filename.endswith(".py"): continue if filename.startswith(".#"): continue new_module_name = module_path_prefix + filename.replace(".py", "") modules_list.append(new_module_name) return modules_list def _import_all(self): """ Populate GameWorld.modules with the modules GW._get_all_loaded_classes refers to when finding classes to spawn. """ # on first load, documents dir may not be in import path if self.app.documents_dir not in sys.path: sys.path += [self.app.documents_dir] # clean modules dict before (re)loading anything self._remove_non_current_game_modules() # make copy of old modules table for import vs reload check old_modules = self.modules.copy() self.modules = {} # load/reload new modules for module_name in self._get_game_modules_list(): try: # always reload built in modules if ( module_name in self.builtin_module_names or module_name in old_modules ): m = importlib.reload(old_modules[module_name]) else: m = importlib.import_module(module_name) self.modules[module_name] = m except Exception as e: self.app.log_import_exception(e, module_name) def toggle_pause(self): "Toggles game pause state." if not self.allow_pause: return self.paused = not self.paused s = "Game {}paused.".format(["un", ""][self.paused]) self.app.ui.message_line.post_line(s) def get_elapsed_time(self): """ Return total time world has been running (ie not paused) in milliseconds. """ return self.app.get_elapsed_time() - self._pause_time def enable_player_camera_lock(self): if self.player: self.camera.focus_object = self.player def disable_player_camera_lock(self): # change only if player has focus if self.player and self.camera.focus_object is self.player: self.camera.focus_object = None def toggle_player_camera_lock(self): "Toggle whether camera is locked to player's location." if self.player and self.camera.focus_object is self.player: self.disable_player_camera_lock() else: self.enable_player_camera_lock() def toggle_grid_snap(self): self.object_grid_snap = not self.object_grid_snap def handle_input(self, event, shift_pressed, alt_pressed, ctrl_pressed): # pass event's key to any objects that want to handle it if event.type not in [sdl2.SDL_KEYDOWN, sdl2.SDL_KEYUP]: return key = sdl2.SDL_GetKeyName(event.key.keysym.sym).decode() key = key.lower() args = (key, shift_pressed, alt_pressed, ctrl_pressed) for obj in self.objects.values(): if obj.handle_key_events: if event.type == sdl2.SDL_KEYDOWN: self.try_object_method(obj, obj.handle_key_down, args) elif event.type == sdl2.SDL_KEYUP: self.try_object_method(obj, obj.handle_key_up, args) # TODO: handle_ functions for other types of input def get_colliders_at_point( self, point_x, point_y, include_object_names=[], include_class_names=[], exclude_object_names=[], exclude_class_names=[], ): """ Return lists of colliding objects and shapes at given point that pass given filters. Includes are processed before excludes. """ whitelist_objects = len(include_object_names) > 0 whitelist_classes = len(include_class_names) > 0 blacklist_objects = len(exclude_object_names) > 0 blacklist_classes = len(exclude_class_names) > 0 # build list of objects to check # always ignore non-colliders colliders = [] for obj in self.objects.values(): if obj.should_collide(): colliders.append(obj) check_objects = [] if whitelist_objects or whitelist_classes: # list of class names -> list of classes include_classes = [ self.get_class_by_name(class_name) for class_name in include_class_names ] # only given objects of given classes if whitelist_objects and whitelist_classes: for obj_name in include_object_names: obj = self.objects[obj_name] for c in include_classes: if isinstance(obj, c) and obj not in check_objects: check_objects.append(obj) # only given objects of any class elif whitelist_objects and not whitelist_classes: check_objects += [ self.objects[obj_name] for obj_name in include_object_names ] # all colliders of given classes elif whitelist_classes: for obj in colliders: for c in include_classes: if isinstance(obj, c) and obj not in check_objects: check_objects.append(obj) else: check_objects = colliders[:] check_objects_unfiltered = check_objects[:] if blacklist_objects or blacklist_classes: exclude_classes = [ self.get_class_by_name(class_name) for class_name in exclude_class_names ] for obj in check_objects_unfiltered: if obj.name in exclude_object_names: check_objects.remove(obj) continue for c in exclude_classes: if isinstance(obj, c): check_objects.remove(obj) # check all objects that passed the filter(s) and build hit list hit_objects = [] hit_shapes = [] for obj in check_objects: # check bounds if not obj.is_point_inside(point_x, point_y): continue # point overlaps with tile collision? if obj.collision_shape_type == collision.CST_TILE: tile_x, tile_y = obj.get_tile_at_point(point_x, point_y) if (tile_x, tile_y) in obj.collision.tile_shapes: hit_objects.append(obj) hit_shapes.append(obj.collision.tile_shapes[(tile_x, tile_y)]) else: # point overlaps with primitive shape(s)? for shape in obj.collision.shapes: if shape.is_point_inside(point_y, point_y): hit_objects.append(obj) hit_shapes.append(shape) return hit_objects, hit_shapes def frame_begin(self): "Run at start of game loop iteration, before input/update/render." for obj in self.objects.values(): obj.art.updated_this_tick = False obj.frame_begin() def frame_update(self): for obj in self.objects.values(): obj.frame_update() def try_object_method(self, obj, method, args=()): "Try to run given object's given method, printing error if encountered." # print('running %s.%s' % (obj.name, method.__name__)) try: method(*args) if method.__name__ == "update": obj.last_update_failed = False except Exception: if method.__name__ == "update" and obj.last_update_failed: return obj.last_update_failed = True for line in traceback.format_exc().split("\n"): if ( line and "try_object_method" not in line and line.strip() != "method()" ): self.app.log(line.rstrip()) s = "Error in {}.{}! See console.".format(obj.name, method.__name__) self.app.ui.message_line.post_line(s, 10, True) def pre_update(self): "Run GO and Room pre_updates before GameWorld.update" # add newly spawned objects to table self.objects.update(self.new_objects) self.new_objects = {} # run pre_first_update / pre_update on all appropriate objects for obj in self.objects.values(): if not obj.pre_first_update_run: self.try_object_method(obj, obj.pre_first_update) obj.pre_first_update_run = True # only run pre_update if not paused elif not self.paused and ( obj.is_in_current_room() or obj.update_if_outside_room ): # update timers # (copy timers list in case a timer removes itself from object) for timer in list(obj.timer_functions_pre_update.values())[:]: timer.update() obj.pre_update() for room in self.rooms.values(): if not room.pre_first_update_run: room.pre_first_update() room.pre_first_update_run = True def update(self): self.mouse_moved(self.app.mouse_dx, self.app.mouse_dy) if self.properties: self.properties.update_from_world() if not self.paused: # update objects based on movement, then resolve collisions for obj in self.objects.values(): if obj.is_in_current_room() or obj.update_if_outside_room: # update timers for timer in list(obj.timer_functions_update.values())[:]: timer.update() self.try_object_method(obj, obj.update) # subclass update may not call GameObject.update, # set last update time here once we're sure it's done obj.last_update_end = self.get_elapsed_time() if self.collision_enabled: self.cl.update() for room in self.rooms.values(): room.update() # display debug text for selected object(s) for obj in self.selected_objects: s = obj.get_debug_text() if s: self.app.ui.debug_text.post_lines(s) # remove objects marked for destruction to_destroy = [] for obj in self.objects.values(): if obj.should_destroy: to_destroy.append(obj.name) for obj_name in to_destroy: self.objects.pop(obj_name) if len(to_destroy) > 0: self.app.ui.edit_list_panel.items_changed() if self.hud: self.hud.update() if self.paused: self._pause_time += self.app.get_elapsed_time() - self.app.last_time else: self.updates += 1 def post_update(self): "Run after GameWorld.update." if self.paused: return for obj in self.objects.values(): if obj.is_in_current_room() or obj.update_if_outside_room: # update timers for timer in list(obj.timer_functions_post_update.values())[:]: timer.update() obj.post_update() def render(self): "Sort and draw all objects in Game Mode world." visible_objects = [] for obj in self.objects.values(): if obj.should_destroy: continue obj.update_renderables() # filter out objects outside current room here # (if no current room or object is in no rooms, render it always) in_room = self.current_room is None or obj.is_in_current_room() hide_debug = obj.is_debug and not self.draw_debug_objects # respect object's "should render at all" flag if obj.visible and not hide_debug and (self.show_all_rooms or in_room): visible_objects.append(obj) # # process non "Y sort" objects first # draw_order = [] collision_items = [] y_objects = [] for obj in visible_objects: if obj.y_sort: y_objects.append(obj) continue for i, z in enumerate(obj.art.layers_z): # ignore invisible layers if not obj.art.layers_visibility[i]: continue # only draw collision layer if show collision is set, OR if # "draw collision layer" is set if ( obj.collision_shape_type == collision.CST_TILE and obj.col_layer_name == obj.art.layer_names[i] and not obj.draw_col_layer ): if obj.show_collision: item = RenderItem(obj, i, z + obj.z) collision_items.append(item) continue item = RenderItem(obj, i, z + obj.z) draw_order.append(item) draw_order.sort(key=lambda item: item.sort_value, reverse=False) # # process "Y sort" objects # y_objects.sort(key=lambda obj: obj.y, reverse=True) # draw layers of each Y-sorted object in Z order for obj in y_objects: items = [] for i, z in enumerate(obj.art.layers_z): if not obj.art.layers_visibility[i]: continue if ( obj.collision_shape_type == collision.CST_TILE and obj.col_layer_name == obj.art.layer_names[i] ): if obj.show_collision: item = RenderItem(obj, i, 0) collision_items.append(item) continue item = RenderItem(obj, i, z) items.append(item) items.sort(key=lambda item: item.sort_value, reverse=False) for item in items: draw_order.append(item) for item in draw_order: item.obj.render(item.layer) self.grid.render() # # draw debug stuff: collision tiles, origins/boxes, debug lines # for item in collision_items: item.obj.render(item.layer) for obj in self.objects.values(): obj.render_debug() if self.hud and self.draw_hud: self.hud.render() def save_last_state(self): "Save over last loaded state." # strip down to base filename w/o extension :/ last_state = self.last_state_loaded last_state = os.path.basename(last_state) last_state = os.path.splitext(last_state)[0] self.save_to_file(last_state) def save_to_file(self, filename=None): "Save current world state to a file." objects = [] for obj in self.objects.values(): if obj.should_save: objects.append(obj.get_dict()) d = {"objects": objects} # save rooms if any exist if len(self.rooms) > 0: rooms = [room.get_dict() for room in self.rooms.values()] d["rooms"] = rooms if self.current_room: d["current_room"] = self.current_room.name if filename and filename != "": if not filename.endswith(STATE_FILE_EXTENSION): filename += "." + STATE_FILE_EXTENSION filename = "{}{}".format(self.game_dir, filename) else: # state filename example: # games/mytestgame2/1431116386.gs timestamp = int(time.time()) filename = "{}{}.{}".format(self.game_dir, timestamp, STATE_FILE_EXTENSION) json.dump(d, open(filename, "w"), sort_keys=True, indent=1) self.app.log("Saved game state {} to disk.".format(filename)) self.app.update_window_title() def _get_all_loaded_classes(self): """ Return classname,class dict of all GameObject classes in loaded modules. """ classes = {} for module in self.modules.values(): for k, v in module.__dict__.items(): # skip anything that's not a game class if type(v) is not type: continue base_classes = ( game_object.GameObject, game_hud.GameHUD, game_room.GameRoom, ) # TODO: find out why above works but below doesn't!! O___O # base_classes = self.builtin_base_classes if issubclass(v, base_classes): classes[k] = v return classes def get_class_by_name(self, class_name): "Return Class object for given class name." return self.classes.get(class_name, None) def reset_object_in_place(self, obj): """ "Reset" given object to its class defaults. Actually destroys object in place and creates a new one. """ x, y = obj.x, obj.y obj_class = obj.__class__.__name__ spawned = self.spawn_object_of_class(obj_class, x, y) if spawned: self.app.log("{} reset to class defaults".format(obj.name)) if obj is self.player: self.player = spawned obj.destroy() def duplicate_selected_objects(self): "Duplicate all selected objects. Calls GW.duplicate_object" new_objects = [] for obj in self.selected_objects: new_objects.append(self.duplicate_object(obj)) # report on objects created if len(new_objects) == 1: self.app.log("{} created from {}".format(new_objects[0].name, obj.name)) elif len(new_objects) > 1: self.app.log("{} new objects created".format(len(new_objects))) def duplicate_object(self, obj): "Create a duplicate of given object." d = obj.get_dict() # offset new object's location x, y = d["x"], d["y"] x += obj.renderable.width y -= obj.renderable.height d["x"], d["y"] = x, y # new object needs a unique name, use a temp one until object exists # for real and we can give it a proper, more-likely-to-be-unique one d["name"] = obj.name + " TEMP COPY NAME" new_obj = self.spawn_object_from_data(d) # give object a non-duplicate name self.rename_object(new_obj, new_obj.get_unique_name()) # tell object's rooms about it for room_name in new_obj.rooms: self.world.rooms[room_name].add_object(new_obj) # update list after changes have been applied to object self.app.ui.edit_list_panel.items_changed() return new_obj def rename_object(self, obj, new_name): "Give specified object a new name. Doesn't accept already-in-use names." self.objects.update(self.new_objects) for other_obj in self.objects.values(): if other_obj is not self and other_obj.name == new_name: self.app.ui.message_line.post_line( "Can't rename {} to {}, name already in use".format( obj.name, new_name ) ) return self.objects.pop(obj.name) old_name = obj.name obj.name = new_name self.objects[obj.name] = obj for room in self.rooms.values(): if obj in room.objects.values(): room.objects.pop(old_name) room.objects[obj.name] = self def spawn_object_of_class(self, class_name, x=None, y=None): "Spawn a new object of given class name at given location." if class_name not in self.classes: # no need for log here, import_all prints exception cause # self.app.log("Couldn't find class %s" % class_name) return d = {"class_name": class_name} if x is not None and y is not None: d["x"], d["y"] = x, y new_obj = self.spawn_object_from_data(d) self.app.ui.edit_list_panel.items_changed() return new_obj def spawn_object_from_data(self, object_data): "Spawn a new object with properties populated from given data dict." # load module and class class_name = object_data.get("class_name", None) if not class_name or class_name not in self.classes: # no need for log here, import_all prints exception cause # self.app.log("Couldn't parse class %s" % class_name) return obj_class = self.classes[class_name] # pass in object data new_object = obj_class(self, object_data) return new_object def add_room(self, new_room_name, new_room_classname="GameRoom"): "Add a new Room with given name of (optional) given class." if new_room_name in self.rooms: self.log("Room called {} already exists!".format(new_room_name)) return new_room_class = self.classes[new_room_classname] new_room = new_room_class(self, new_room_name) self.rooms[new_room.name] = new_room def remove_room(self, room_name): "Delete Room with given name." if room_name not in self.rooms: return room = self.rooms.pop(room_name) if room is self.current_room: self.current_room = None room.destroy() def change_room(self, new_room_name): "Set world's current active room to Room with given name." if new_room_name not in self.rooms: self.app.log("Couldn't change to missing room {}".format(new_room_name)) return old_room = self.current_room self.current_room = self.rooms[new_room_name] # tell old and new rooms they've been exited and entered, respectively if old_room: old_room.exited(self.current_room) self.current_room.entered(old_room) def rename_room(self, room, new_room_name): "Rename given Room to new given name." old_name = room.name room.name = new_room_name self.rooms.pop(old_name) self.rooms[new_room_name] = room # update all objects in this room for obj in self.objects.values(): if old_name in obj.rooms: obj.rooms.pop(old_name) obj.rooms[new_room_name] = room def load_game_state(self, filename=DEFAULT_STATE_FILENAME): "Load game state with given filename." if not os.path.exists(filename): filename = self.game_dir + filename if not filename.endswith(STATE_FILE_EXTENSION): filename += "." + STATE_FILE_EXTENSION self.app.enter_game_mode() self.unload_game() # tell list panel to reset, its contents might get jostled self.app.ui.edit_list_panel.game_reset() # import all submodules and catalog classes self._import_all() self.classes = self._get_all_loaded_classes() try: d = json.load(open(filename)) # self.app.log('Loading game state %s...' % filename) except: self.app.log("Couldn't load game state from {}".format(filename)) # self.app.log(sys.exc_info()) return errors = False # spawn objects for obj_data in d["objects"]: obj = self.spawn_object_from_data(obj_data) if not obj: errors = True # spawn a WorldPropertiesObject if one doesn't exist for obj in self.new_objects.values(): if type(obj).__name__ == self.properties_object_class_name: self.properties = obj break if not self.properties: self.properties = self.spawn_object_of_class( self.properties_object_class_name, 0, 0 ) # spawn a WorldGlobalStateObject self.globals = self.spawn_object_of_class(self.globals_object_class_name, 0, 0) # just for first update, merge new objects list into objects list self.objects.update(self.new_objects) # create rooms for room_data in d.get("rooms", []): # get room class room_class_name = room_data.get("class_name", None) room_class = self.classes.get(room_class_name, game_room.GameRoom) room = room_class(self, room_data["name"], room_data) self.rooms[room.name] = room start_room = self.rooms.get(d.get("current_room", None), None) if start_room: self.change_room(start_room.name) # spawn hud hud_class = self.classes[d.get("hud_class", self.hud_class_name)] self.hud = hud_class(self) self.hud_class_name = hud_class.__name__ if not errors and self.app.init_success: self.app.log("Loaded game state from {}".format(filename)) self.last_state_loaded = filename self.set_for_all_objects("show_collision", self.show_collision_all) self.set_for_all_objects("show_bounds", self.show_bounds_all) self.set_for_all_objects("show_origin", self.show_origin_all) self.app.update_window_title() self.app.ui.edit_list_panel.items_changed() # self.report() def report(self): "Print (not log) information about current world state." print("--------------\n{} report:".format(self)) obj_arts, obj_rends, obj_dbg_rends, obj_cols, obj_col_rends = 0, 0, 0, 0, 0 attachments = 0 # create merged dict of existing and just-spawned objects all_objects = self.objects.copy() all_objects.update(self.new_objects) print("{} objects:".format(len(all_objects))) for obj in all_objects.values(): obj_arts += len(obj.arts) if obj.renderable is not None: obj_rends += 1 if obj.origin_renderable is not None: obj_dbg_rends += 1 if obj.bounds_renderable is not None: obj_dbg_rends += 1 if obj.collision: obj_cols += 1 obj_col_rends += len(obj.collision.renderables) attachments += len(obj.attachments) print( """ {} arts in objects, {} arts loaded, {} HUD arts, {} HUD renderables, {} renderables, {} debug renderables, {} collideables, {} collideable viz renderables, {} attachments""".format( obj_arts, len(self.art_loaded), len(self.hud.arts), len(self.hud.renderables), obj_rends, obj_dbg_rends, obj_cols, obj_col_rends, attachments, ) ) self.cl.report() print( "{} charsets loaded, {} palettes".format( len(self.app.charsets), len(self.app.palettes) ) ) print("{} arts loaded for edit".format(len(self.app.art_loaded_for_edit))) def toggle_all_origin_viz(self): "Toggle visibility of XYZ markers for all object origins." self.show_origin_all = not self.show_origin_all self.set_for_all_objects("show_origin", self.show_origin_all) def toggle_all_bounds_viz(self): "Toggle visibility of boxes for all object bounds." self.show_bounds_all = not self.show_bounds_all self.set_for_all_objects("show_bounds", self.show_bounds_all) def toggle_all_collision_viz(self): "Toggle visibility of debug lines for all object Collideables." self.show_collision_all = not self.show_collision_all self.set_for_all_objects("show_collision", self.show_collision_all) def destroy(self): self.unload_game() self.art_loaded = []