Top

game_world module

import os, sys, time, importlib, json
from collections import namedtuple

import sdl2

import game_object, game_util_objects, game_hud, game_room
import collision, vector
from camera import Camera
from art import ART_DIR
from charset import CHARSET_DIR
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.
    "Gravity applied to all objects who are affected by gravity."
    bg_color = [0., 0., 0., 1.]
    "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
    draw_hud = True
    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.last_click_on_ui = False
        self.properties = None
        "Our WorldPropertiesObject"
        self.globals = None
        "Our WorldGlobalsObject - not required"
        self.camera = GameCamera(self.app)
        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 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)
        # don't bother cycling if only one object found
        if len(objects) == 1 and objects[0].selectable and \
           not objects[0] in self.selected_objects:
                return objects[0]
        # cycle through objects at point til an unselected one is found
        use_next = False
        for obj in objects:
            if not obj.selectable:
                continue
            if len(self.selected_objects) == 0:
                return obj
            elif use_next:
                return obj
            elif obj in self.selected_objects:
                use_next = True
        return None
    
    def get_objects_at(self, x, y):
        "Return list of all objects whose bounds fall within given point."
        objects = []
        for obj in self.objects.values():
            # only allow selecting of visible objects
            # (can still be selected via list panel)
            if obj.visible and not obj.locked and obj.is_point_inside(x, y):
                objects.append(obj)
        return objects
    
    def clicked(self, button):
        x, y, z = vector.screen_to_world(self.app, self.app.mouse_x,
                                         self.app.mouse_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 %s' % new_obj.name)
            return
        # 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 unclicked(self, button):
        # 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
            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)
        # forget drag object offsets
        self.drag_objects.clear()
        for obj in self.selected_objects:
            obj.enable_collision()
            if obj.collision_shape_type == collision.CST_TILE:
                obj.collision.create_shapes()
            obj.collision.update()
        # no mod keys: select only object under cursor, deselect all if none
        if not self.app.il.ctrl_pressed and not self.app.il.shift_pressed:
            objects = self.get_objects_at(x, y)
            if len(objects) == 0:
                self.deselect_all()
                return
        # ctrl: remove object under cursor from selection
        if self.app.il.ctrl_pressed:
            # unselect first selected object found under mouse
            objects = self.get_objects_at(x, y)
            if len(objects) > 0:
                self.deselect_object(objects[0])
            return
        next_obj = self.pick_next_object_at(x, y)
        # clicked on nothing?
        if not next_obj:
            self.deselect_all()
            return
        # shift: add to current selection
        if not self.app.il.shift_pressed:
            self.deselect_all()
        self.select_object(next_obj)
    
    def mouse_moved(self, dx, dy):
        if self.app.ui.active_dialog:
            return
        # if last onclick was a UI element, don't drag
        if self.last_click_on_ui:
            return
        # 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 not obj 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 %s already exists!' % 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 %s' % 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 %s" % 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 = '%s.%s' % (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 = '%s.%s.%s.' % (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 not self.app.documents_dir 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."
        self.paused = not self.paused
        s = 'Game %spaused.' % ['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 not event.type 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_input_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 not obj 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 not obj 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 as e:
            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 not 'try_object_method' in line and line.strip() != 'method()':
                    self.app.log(line.rstrip())
                s = 'Error in %s.%s! See console.' % (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)
        #
        # 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 = '%s%s' % (self.game_dir, filename)
        else:
            # state filename example:
            # games/mytestgame2/1431116386.gs
            timestamp = int(time.time())
            filename = '%s%s.%s' % (self.game_dir, timestamp,
                                     STATE_FILE_EXTENSION)
        json.dump(d, open(filename, 'w'),
                  sort_keys=True, indent=1)
        self.app.log('Saved game state %s to disk.' % 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 not type(v) is 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('%s reset to class defaults' % 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('%s created from %s' % (new_objects[0].name, obj.name))
        elif len(new_objects) > 1:
            self.app.log('%s new objects created' % 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 not other_obj is self and other_obj.name == new_name:
                print("Can't rename %s to %s, name already in use" % (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 not class_name 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 not class_name 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 %s already exists!' % 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 not room_name 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 not new_room_name in self.rooms:
            self.app.log("Couldn't change to missing room %s" % 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 %s" % 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:
            self.app.log('Loaded game state from %s' % 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%s report:' % 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('%s objects:' % 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("""
        %s arts in objects, %s arts loaded,
        %s HUD arts, %s HUD renderables,
        %s renderables, %s debug renderables,
        %s collideables, %s collideable viz renderables,
        %s attachments""" % (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('%s charsets loaded, %s palettes' % (len(self.app.charsets),
                                                   len(self.app.palettes)))
        print('%s arts loaded for edit' % 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 = []

Module variables

var ART_DIR

var CHARSET_DIR

var DEFAULT_STATE_FILENAME

var GAME_SCRIPTS_DIR

var PALETTE_DIR

var SOUNDS_DIR

var STARTER_SCRIPT

var STATE_FILE_EXTENSION

var TOP_GAME_DIR

Classes

class GameCamera

class GameCamera(Camera):
    pan_friction = 0.2
    use_bounds = False

Ancestors (in MRO)

Class variables

var base_max_pan_speed

var far_z

var fov

var logg

var max_x

var max_y

var max_zoom

var max_zoom_speed

var min_velocity

var min_x

var min_y

var min_zoom

var mouse_pan_rate

var near_z

var pan_accel

var pan_friction

var pan_max_pct

var pan_min_pct

var pan_zoom_increase_factor

var start_x

var start_y

var start_zoom

var use_bounds

var x_tilt

var y_tilt

var zoom_accel

var zoom_friction

Static methods

def __init__(

self, app)

Initialize self. See help(type(self)) for accurate signature.

def __init__(self, app):
    self.app = app
    self.reset()
    self.max_pan_speed = self.base_max_pan_speed
    # set True when "zoom extents" toggles on
    self.zoomed_extents = False
    self.saved_x, self.saved_y, self.saved_z = 0, 0, self.start_zoom

def calc_projection_matrix(

self)

def calc_projection_matrix(self):
    self.projection_matrix = self.get_perspective_matrix()

def calc_view_matrix(

self)

def calc_view_matrix(self):
    eye = vector.Vec3(self.x, self.y, self.z)
    up = vector.Vec3(0, 1, 0)
    target = vector.Vec3(eye.x + self.x_tilt, eye.y + self.y_tilt, 0)
    # view axes
    forward = (target - eye).normalize()
    side = forward.cross(up).normalize()
    upward = side.cross(forward)
    m = [[side.x, upward.x, -forward.x, 0],
         [side.y, upward.y, -forward.y, 0],
         [side.z, upward.z, -forward.z, 0],
         [-eye.dot(side), -eye.dot(upward), eye.dot(forward), 1]]
    self.view_matrix = np.array(m, dtype=np.float32)
    self.look_x, self.look_y, self.look_z = side, upward, forward

def get_base_zoom(

self)

returns camera Z needed for 1:1 pixel zoom

def get_base_zoom(self):
    "returns camera Z needed for 1:1 pixel zoom"
    wh = self.app.window_height
    ch = self.app.ui.active_art.charset.char_height
    # TODO: understand why this produces correct result for 8x8 charsets
    if ch == 8:
        ch = 16
    return wh / ch

def get_current_zoom_pct(

self)

returns % of base (1:1) for current camera

def get_current_zoom_pct(self):
    "returns % of base (1:1) for current camera"
    return (self.get_base_zoom() / self.z) * 100

def get_ortho_matrix(

self, width=None, height=None)

def get_ortho_matrix(self, width=None, height=None):
    width, height = width or self.app.window_width, height or self.app.window_height
    m = np.eye(4, 4, dtype=np.float32)
    left, bottom = 0, 0
    right, top = width, height
    far_z, near_z = -1, 1
    x = 2 / (right - left)
    y = 2 / (top - bottom)
    z = -2 / (self.far_z - self.near_z)
    wx = -(right + left) / (right - left)
    wy = -(top + bottom) / (top - bottom)
    wz = -(self.far_z + self.near_z) / (self.far_z - self.near_z)
    m = [[ x,  0,  0, 0],
         [ 0,  y,  0, 0],
         [ 0,  0,  z, 0],
         [wx, wy, wz, 0]]
    return np.array(m, dtype=np.float32)

def get_perspective_matrix(

self)

def get_perspective_matrix(self):
    zmul = (-2 * self.near_z * self.far_z) / (self.far_z - self.near_z)
    ymul = 1 / math.tan(self.fov * math.pi / 360)
    aspect = self.app.window_width / self.app.window_height
    xmul = ymul / aspect
    m = [[xmul,    0,    0,  0],
         [   0, ymul,    0,  0],
         [   0,    0,   -1, -1],
         [   0,    0, zmul,  0]]
    return np.array(m, dtype=np.float32)

def mouse_pan(

self, dx, dy)

pan view based on mouse delta

def mouse_pan(self, dx, dy):
    "pan view based on mouse delta"
    if dx == 0 and dy == 0:
        return
    m = ((1 * self.pan_zoom_increase_factor) * self.z) / self.min_zoom
    m /= self.max_zoom
    self.x -= dx / self.mouse_pan_rate * m
    self.y += dy / self.mouse_pan_rate * m
    self.vel_x = self.vel_y = 0
    self.mouse_panned = True

def pan(

self, dx, dy, keyboard=False)

def pan(self, dx, dy, keyboard=False):
    # modify pan speed based on zoom according to a factor
    m = (self.pan_zoom_increase_factor * self.z) / self.min_zoom
    self.vel_x += dx * self.pan_accel * m
    self.vel_y += dy * self.pan_accel * m
    # for brevity, app passes in whether user appears to be keyboard editing
    if keyboard:
        self.app.keyboard_editing = True

def reset(

self)

def reset(self):
    self.x, self.y = self.start_x, self.start_y
    self.z = self.start_zoom
    # store look vectors so world/screen space conversions can refer to it
    self.look_x, self.look_y, self.look_z = None,None,None
    self.vel_x, self.vel_y, self.vel_z = 0,0,0
    self.mouse_panned, self.moved_this_frame = False, False
    # GameObject to focus on
    self.focus_object = None
    self.calc_projection_matrix()
    self.calc_view_matrix()

def set_for_art(

self, art)

def set_for_art(self, art):
    # set limits
    self.max_x = art.width * art.quad_width
    self.min_y = -art.height * art.quad_height
    # use saved pan/zoom
    self.set_loc(art.camera_x, art.camera_y, art.camera_z)

def set_loc(

self, x, y, z)

def set_loc(self, x, y, z):
    self.x, self.y, self.z = x, y, (z or self.z) # z optional

def set_loc_from_obj(

self, game_object)

def set_loc_from_obj(self, game_object):
    self.set_loc(game_object.x, game_object.y, game_object.z)

def set_to_base_zoom(

self)

def set_to_base_zoom(self):
    self.z = self.get_base_zoom()

def set_zoom(

self, z)

def set_zoom(self, z):
    # TODO: set lerp target, clear if keyboard etc call zoom()
    self.z = z

def toggle_zoom_extents(

self, override=None)

def toggle_zoom_extents(self, override=None):
    if override is not None:
        self.zoomed_extents = not override
    if self.zoomed_extents:
        self.x, self.y, self.z = self.saved_x, self.saved_y, self.saved_z
    else:
        self.saved_x, self.saved_y, self.saved_z = self.x, self.y, self.z
        # TODO: more involved zoom-extents that picks the best zoom level
        self.set_to_base_zoom()
        # center camera on art
        art = self.app.ui.active_art
        self.x = (art.width * art.quad_width) / 2
        self.y = -(art.height * art.quad_height) / 2
    # kill all camera velocity when snapping
    self.vel_x, self.vel_y, self.vel_z = 0, 0, 0
    self.zoomed_extents = not self.zoomed_extents

def update(

self)

def update(self):
    # zoom-proportional pan scale is based on art
    if self.app.ui.active_art:
        speed_scale = clamp(self.get_current_zoom_pct(),
                            self.pan_min_pct, self.pan_max_pct)
        self.max_pan_speed = self.base_max_pan_speed / (speed_scale / 100)
    else:
        self.max_pan_speed = self.base_max_pan_speed
    # remember last position to see if it changed
    self.last_x, self.last_y, self.last_z = self.x, self.y, self.z
    # if focus object is set, use it for X and Y transforms
    if self.focus_object:
        # track towards target
        # TODO: revisit this for better feel later
        dx, dy = self.focus_object.x - self.x, self.focus_object.y - self.y
        l = math.sqrt(dx ** 2 + dy ** 2)
        if l != 0 and l > 0.1:
            il = 1 / l
            dx *= il
            dy *= il
            self.x += dx * self.pan_friction
            self.y += dy * self.pan_friction
    else:
        # clamp velocity
        self.vel_x = clamp(self.vel_x, -self.max_pan_speed, self.max_pan_speed)
        self.vel_y = clamp(self.vel_y, -self.max_pan_speed, self.max_pan_speed)
        # apply friction
        self.vel_x *= 1 - self.pan_friction
        self.vel_y *= 1 - self.pan_friction        
        if abs(self.vel_x) < self.min_velocity:
            self.vel_x = 0
        if abs(self.vel_y) < self.min_velocity:
            self.vel_y = 0
        # if camera moves, we're not in zoom-extents state anymore
        if self.vel_x or self.vel_y:
            self.zoomed_extents = False
        # move
        self.x += self.vel_x
        self.y += self.vel_y
    # process Z separately
    self.vel_z = clamp(self.vel_z, -self.max_zoom_speed, self.max_zoom_speed)
    self.vel_z *= 1 - self.zoom_friction
    if abs(self.vel_z) < self.min_velocity:
        self.vel_z = 0
    # as bove, if zooming turn off zoom-extents state
    if self.vel_z:
        self.zoomed_extents = False
    self.z += self.vel_z
    # keep within bounds
    if self.use_bounds:
        self.x = clamp(self.x, self.min_x, self.max_x)
        self.y = clamp(self.y, self.min_y, self.max_y)
        self.z = clamp(self.z, self.min_zoom, self.max_zoom)
    # set view matrix from xyz
    self.calc_view_matrix()
    if self.logg:
        self.app.log('camera x=%s, y=%s, z=%s' % (self.x, self.y, self.z))
    self.moved_this_frame = self.mouse_panned or self.x != self.last_x or self.y != self.last_y or self.z != self.last_z
    self.mouse_panned = False

def window_resized(

self)

def window_resized(self):
    self.calc_projection_matrix()

def zoom(

self, dz, keyboard=False)

def zoom(self, dz, keyboard=False):
    self.vel_z += dz * self.zoom_accel
    if keyboard:
        self.app.keyboard_editing = True

def zoom_proportional(

self, direction)

zooms in or out via increments of 1:1 pixel scales for active art

def zoom_proportional(self, direction):
    "zooms in or out via increments of 1:1 pixel scales for active art"
    if not self.app.ui.active_art:
        return
    self.zoomed_extents = False
    base_zoom = self.get_base_zoom()
    # build span of all 1:1 zoom increments
    zooms = []
    m = 1
    while base_zoom / m > self.min_zoom:
        zooms.append(base_zoom / m)
        m *= 2
    zooms.reverse()
    m = 1
    while base_zoom * m < self.max_zoom:
        zooms.append(base_zoom * m)
        m *= 2
    # set zoom to nearest increment in direction we're heading
    if direction > 0:
        zooms.reverse()
        for zoom in zooms:
            if self.z > zoom:
                self.z = zoom
                break
    elif direction < 0:
        for zoom in zooms:
            if self.z < zoom:
                self.z = zoom
                break
    # kill all Z velocity for camera so we don't drift out of 1:1
    self.vel_z = 0

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.

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.
    "Gravity applied to all objects who are affected by gravity."
    bg_color = [0., 0., 0., 1.]
    "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
    draw_hud = True
    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.last_click_on_ui = False
        self.properties = None
        "Our WorldPropertiesObject"
        self.globals = None
        "Our WorldGlobalsObject - not required"
        self.camera = GameCamera(self.app)
        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 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)
        # don't bother cycling if only one object found
        if len(objects) == 1 and objects[0].selectable and \
           not objects[0] in self.selected_objects:
                return objects[0]
        # cycle through objects at point til an unselected one is found
        use_next = False
        for obj in objects:
            if not obj.selectable:
                continue
            if len(self.selected_objects) == 0:
                return obj
            elif use_next:
                return obj
            elif obj in self.selected_objects:
                use_next = True
        return None
    
    def get_objects_at(self, x, y):
        "Return list of all objects whose bounds fall within given point."
        objects = []
        for obj in self.objects.values():
            # only allow selecting of visible objects
            # (can still be selected via list panel)
            if obj.visible and not obj.locked and obj.is_point_inside(x, y):
                objects.append(obj)
        return objects
    
    def clicked(self, button):
        x, y, z = vector.screen_to_world(self.app, self.app.mouse_x,
                                         self.app.mouse_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 %s' % new_obj.name)
            return
        # 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 unclicked(self, button):
        # 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
            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)
        # forget drag object offsets
        self.drag_objects.clear()
        for obj in self.selected_objects:
            obj.enable_collision()
            if obj.collision_shape_type == collision.CST_TILE:
                obj.collision.create_shapes()
            obj.collision.update()
        # no mod keys: select only object under cursor, deselect all if none
        if not self.app.il.ctrl_pressed and not self.app.il.shift_pressed:
            objects = self.get_objects_at(x, y)
            if len(objects) == 0:
                self.deselect_all()
                return
        # ctrl: remove object under cursor from selection
        if self.app.il.ctrl_pressed:
            # unselect first selected object found under mouse
            objects = self.get_objects_at(x, y)
            if len(objects) > 0:
                self.deselect_object(objects[0])
            return
        next_obj = self.pick_next_object_at(x, y)
        # clicked on nothing?
        if not next_obj:
            self.deselect_all()
            return
        # shift: add to current selection
        if not self.app.il.shift_pressed:
            self.deselect_all()
        self.select_object(next_obj)
    
    def mouse_moved(self, dx, dy):
        if self.app.ui.active_dialog:
            return
        # if last onclick was a UI element, don't drag
        if self.last_click_on_ui:
            return
        # 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 not obj 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 %s already exists!' % 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 %s' % 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 %s" % 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 = '%s.%s' % (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 = '%s.%s.%s.' % (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 not self.app.documents_dir 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."
        self.paused = not self.paused
        s = 'Game %spaused.' % ['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 not event.type 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_input_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 not obj 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 not obj 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 as e:
            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 not 'try_object_method' in line and line.strip() != 'method()':
                    self.app.log(line.rstrip())
                s = 'Error in %s.%s! See console.' % (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)
        #
        # 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 = '%s%s' % (self.game_dir, filename)
        else:
            # state filename example:
            # games/mytestgame2/1431116386.gs
            timestamp = int(time.time())
            filename = '%s%s.%s' % (self.game_dir, timestamp,
                                     STATE_FILE_EXTENSION)
        json.dump(d, open(filename, 'w'),
                  sort_keys=True, indent=1)
        self.app.log('Saved game state %s to disk.' % 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 not type(v) is 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('%s reset to class defaults' % 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('%s created from %s' % (new_objects[0].name, obj.name))
        elif len(new_objects) > 1:
            self.app.log('%s new objects created' % 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 not other_obj is self and other_obj.name == new_name:
                print("Can't rename %s to %s, name already in use" % (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 not class_name 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 not class_name 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 %s already exists!' % 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 not room_name 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 not new_room_name in self.rooms:
            self.app.log("Couldn't change to missing room %s" % 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 %s" % 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:
            self.app.log('Loaded game state from %s' % 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%s report:' % 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('%s objects:' % 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("""
        %s arts in objects, %s arts loaded,
        %s HUD arts, %s HUD renderables,
        %s renderables, %s debug renderables,
        %s collideables, %s collideable viz renderables,
        %s attachments""" % (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('%s charsets loaded, %s palettes' % (len(self.app.charsets),
                                                   len(self.app.palettes)))
        print('%s arts loaded for edit' % 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 = []

Ancestors (in MRO)

Class variables

var bg_color

OpenGL wiewport color to render behind everything else, ie the void.

var builtin_base_classes

var builtin_module_names

var collision_enabled

If False, CollisionLord won't bother thinking about collision at all.

var draw_debug_objects

If False, objects with is_debug=True won't be drawn.

var draw_hud

var game_title

Title for game, shown in window titlebar when not editing

var globals_object_class_name

String name of WorldGlobalsObject class to use.

var gravity_x

var gravity_y

var gravity_z

var hud_class_name

String name of HUD class to use

var list_only_current_room_objects

If True, list UI will only show objects in current room.

var object_grid_snap

var player_camera_lock

If True, camera will be locked to player's location.

var properties_object_class_name

var room_camera_changes_enabled

If True, snap camera to new room's associated camera marker.

var show_all_rooms

If True, show all rooms not just current one.

var show_bounds_all

var show_collision_all

var show_origin_all

Static methods

def __init__(

self, app)

Initialize self. See help(type(self)) for accurate signature.

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.last_click_on_ui = False
    self.properties = None
    "Our WorldPropertiesObject"
    self.globals = None
    "Our WorldGlobalsObject - not required"
    self.camera = GameCamera(self.app)
    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 add_room(

self, new_room_name, new_room_classname='GameRoom')

Add a new Room with given name of (optional) given class.

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 %s already exists!' % 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 change_room(

self, new_room_name)

Set world's current active room to Room with given name.

def change_room(self, new_room_name):
    "Set world's current active room to Room with given name."
    if not new_room_name in self.rooms:
        self.app.log("Couldn't change to missing room %s" % 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 clicked(

self, button)

def clicked(self, button):
    x, y, z = vector.screen_to_world(self.app, self.app.mouse_x,
                                     self.app.mouse_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 %s' % new_obj.name)
        return
    # 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 create_new_game(

self, new_game_dir, new_game_title)

Create appropriate dirs and files for a new game, return success.

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 %s already exists!' % 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 deselect_all(

self)

Deselect all objects.

def deselect_all(self):
    "Deselect all objects."
    self.selected_objects = []
    self.app.ui.object_selection_changed()

def deselect_object(

self, obj)

Remove given object from our list of selected objects.

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 destroy(

self)

def destroy(self):
    self.unload_game()
    self.art_loaded = []

def disable_player_camera_lock(

self)

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 duplicate_object(

self, obj)

Create a duplicate of given object.

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 duplicate_selected_objects(

self)

Duplicate all selected objects. Calls GW.duplicate_object

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('%s created from %s' % (new_objects[0].name, obj.name))
    elif len(new_objects) > 1:
        self.app.log('%s new objects created' % len(new_objects))

def edit_art_for_selected(

self)

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 enable_player_camera_lock(

self)

def enable_player_camera_lock(self):
    if self.player:
        self.camera.focus_object = self.player

def frame_begin(

self)

Run at start of game loop iteration, before input/update/render.

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)

def frame_update(self):
    for obj in self.objects.values():
        obj.frame_update()

def get_all_objects_of_type(

self, class_name, allow_subclasses=True)

Return list of all objects found with given class name.

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 get_class_by_name(

self, class_name)

Return Class object for given class name.

def get_class_by_name(self, class_name):
    "Return Class object for given class name."
    return self.classes.get(class_name, None)

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.

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 not obj 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 not obj 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 get_elapsed_time(

self)

Return total time world has been running (ie not paused) in milliseconds.

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 get_first_object_of_type(

self, class_name, allow_subclasses=True)

Return first object found with given class name.

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_objects_at(

self, x, y)

Return list of all objects whose bounds fall within given point.

def get_objects_at(self, x, y):
    "Return list of all objects whose bounds fall within given point."
    objects = []
    for obj in self.objects.values():
        # only allow selecting of visible objects
        # (can still be selected via list panel)
        if obj.visible and not obj.locked and obj.is_point_inside(x, y):
            objects.append(obj)
    return objects

def handle_input(

self, event, shift_pressed, alt_pressed, ctrl_pressed)

def handle_input(self, event, shift_pressed, alt_pressed, ctrl_pressed):
    # pass event's key to any objects that want to handle it
    if not event.type 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_input_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)

def is_music_playing(

self)

Return True if there is music playing.

def is_music_playing(self):
    "Return True if there is music playing."
    return self.app.al.is_music_playing()

def load_game_state(

self, filename='start')

Load game state with given filename.

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 %s" % 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:
        self.app.log('Loaded game state from %s' % 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()

def mouse_moved(

self, dx, dy)

def mouse_moved(self, dx, dy):
    if self.app.ui.active_dialog:
        return
    # if last onclick was a UI element, don't drag
    if self.last_click_on_ui:
        return
    # 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 move_selected(

self, move_x, move_y, move_z)

Shift position of selected objects by given x,y,z amount.

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 pick_next_object_at(

self, x, y)

Return next unselected object at given point.

def pick_next_object_at(self, x, y):
    "Return next unselected object at given point."
    objects = self.get_objects_at(x, y)
    # don't bother cycling if only one object found
    if len(objects) == 1 and objects[0].selectable and \
       not objects[0] in self.selected_objects:
            return objects[0]
    # cycle through objects at point til an unselected one is found
    use_next = False
    for obj in objects:
        if not obj.selectable:
            continue
        if len(self.selected_objects) == 0:
            return obj
        elif use_next:
            return obj
        elif obj in self.selected_objects:
            use_next = True
    return None

def play_music(

self, music_filename, fade_in_time=0)

Play given music file in any SDL2_mixer-supported format.

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 post_update(

self)

Run after GameWorld.update.

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 pre_update(

self)

Run GO and Room pre_updates before GameWorld.update

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 remove_room(

self, room_name)

Delete Room with given name.

def remove_room(self, room_name):
    "Delete Room with given name."
    if not room_name in self.rooms:
        return
    room = self.rooms.pop(room_name)
    if room is self.current_room:
        self.current_room = None
    room.destroy()

def rename_object(

self, obj, new_name)

Give specified object a new name. Doesn't accept already-in-use names.

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 not other_obj is self and other_obj.name == new_name:
            print("Can't rename %s to %s, name already in use" % (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 rename_room(

self, room, new_room_name)

Rename given Room to new given name.

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 render(

self)

Sort and draw all objects in Game Mode world.

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)
    #
    # 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 report(

self)

Print (not log) information about current world state.

def report(self):
    "Print (not log) information about current world state."
    print('--------------\n%s report:' % 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('%s objects:' % 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("""
    %s arts in objects, %s arts loaded,
    %s HUD arts, %s HUD renderables,
    %s renderables, %s debug renderables,
    %s collideables, %s collideable viz renderables,
    %s attachments""" % (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('%s charsets loaded, %s palettes' % (len(self.app.charsets),
                                               len(self.app.palettes)))
    print('%s arts loaded for edit' % len(self.app.art_loaded_for_edit))

def reset_game(

self)

Reset currently loaded game to last loaded state.

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 reset_object_in_place(

self, obj)

"Reset" given object to its class defaults. Actually destroys object in place and creates a new one.

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('%s reset to class defaults' % obj.name)
        if obj is self.player:
            self.player = spawned
        obj.destroy()

def save_last_state(

self)

Save over last loaded state.

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.

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 = '%s%s' % (self.game_dir, filename)
    else:
        # state filename example:
        # games/mytestgame2/1431116386.gs
        timestamp = int(time.time())
        filename = '%s%s.%s' % (self.game_dir, timestamp,
                                 STATE_FILE_EXTENSION)
    json.dump(d, open(filename, 'w'),
              sort_keys=True, indent=1)
    self.app.log('Saved game state %s to disk.' % filename)
    self.app.update_window_title()

def select_object(

self, obj, force=False)

Add given object to our list of selected objects.

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 not obj in self.selected_objects:
        self.selected_objects.append(obj)
    self.app.ui.object_selection_changed()

def set_for_all_objects(

self, name, value)

Set given variable name to given value for all 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 set_game_dir(

self, dir_name, reset=False)

Load game from given game directory.

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 %s' % 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 %s" % dir_name)

def spawn_object_from_data(

self, object_data)

Spawn a new object with properties populated from given data dict.

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 not class_name 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 spawn_object_of_class(

self, class_name, x=None, y=None)

Spawn a new object of given class name at given location.

def spawn_object_of_class(self, class_name, x=None, y=None):
    "Spawn a new object of given class name at given location."
    if not class_name 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 stop_music(

self)

Stop any currently playing music.

def stop_music(self):
    "Stop any currently playing music."
    self.app.al.stop_all_music()

def toggle_all_bounds_viz(

self)

Toggle visibility of boxes for all object bounds.

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.

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 toggle_all_origin_viz(

self)

Toggle visibility of XYZ markers for all object origins.

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_grid_snap(

self)

def toggle_grid_snap(self):
    self.object_grid_snap = not self.object_grid_snap

def toggle_pause(

self)

Toggles game pause state.

def toggle_pause(self):
    "Toggles game pause state."
    self.paused = not self.paused
    s = 'Game %spaused.' % ['un', ''][self.paused]
    self.app.ui.message_line.post_line(s)

def toggle_player_camera_lock(

self)

Toggle whether camera is locked to player's location.

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 try_object_method(

self, obj, method, args=())

Try to run given object's given method, printing error if encountered.

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 as e:
        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 not 'try_object_method' in line and line.strip() != 'method()':
                self.app.log(line.rstrip())
            s = 'Error in %s.%s! See console.' % (obj.name, method.__name__)
            self.app.ui.message_line.post_line(s, 10, True)

def unclicked(

self, button)

def unclicked(self, button):
    # 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
        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)
    # forget drag object offsets
    self.drag_objects.clear()
    for obj in self.selected_objects:
        obj.enable_collision()
        if obj.collision_shape_type == collision.CST_TILE:
            obj.collision.create_shapes()
        obj.collision.update()
    # no mod keys: select only object under cursor, deselect all if none
    if not self.app.il.ctrl_pressed and not self.app.il.shift_pressed:
        objects = self.get_objects_at(x, y)
        if len(objects) == 0:
            self.deselect_all()
            return
    # ctrl: remove object under cursor from selection
    if self.app.il.ctrl_pressed:
        # unselect first selected object found under mouse
        objects = self.get_objects_at(x, y)
        if len(objects) > 0:
            self.deselect_object(objects[0])
        return
    next_obj = self.pick_next_object_at(x, y)
    # clicked on nothing?
    if not next_obj:
        self.deselect_all()
        return
    # shift: add to current selection
    if not self.app.il.shift_pressed:
        self.deselect_all()
    self.select_object(next_obj)

def unload_game(

self)

Unload currently loaded game.

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 update(

self)

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

Instance variables

var app

Application that created this world.

var art_loaded

var camera

var cl

var classname_to_spawn

var current_room

var drag_objects

Offsets for objects player is edit-dragging

var game_dir

Currently loaded game directory.

var game_name

Game's internal name, based on its directory.

var globals

Our WorldGlobalsObject - not required

var hud

var last_click_on_ui

var last_state_loaded

var modules

var new_objects

Dict of just-spawned objects, added to above on update() after spawn

var objects

Dict of objects by name:object

var paused

var player

var properties

Our WorldPropertiesObject

var rooms

Dict of rooms by name:room

var selected_objects

var sounds_dir

var updates

Number of updates this we have performed.

class RenderItem

RenderItem(obj, layer, sort_value)

Ancestors (in MRO)

Instance variables

var layer

Alias for field number 1

var obj

Alias for field number 0

var sort_value

Alias for field number 2