class Editor (object): """A puzzle editor (Game backend). Takes arguments to pass to Editor.load. METHODS load change resize insert delete set_block undo redo click_tile switch_puzzle reset menu ATTRIBUTES game: as given. event_handler: as given. ID: as given. selector: puzzle used to select blocks/surfaces to add. editor: the puzzle being edited. editor_rect: the rect the editor puzzle is drawn in, or None if unknown. puzzle: the current visible puzzle (editor or selector). editing: whether the current puzzle is editor. changes: a list of changes that have been made to the puzzle. state: the current position in the changes list. """ def __init__ (self, game, event_handler, ID = None, defn = None): self.game = game # add event handlers event_handler.add_event_handlers({ pygame.MOUSEBUTTONDOWN: self._click, pygame.MOUSEBUTTONUP: self._unclick, pygame.MOUSEMOTION: lambda e: setattr(self, 'mouse_moved', True) }) pzl_args = ( eh.MODE_ONDOWN_REPEAT, max(int(conf.MOVE_INITIAL_DELAY * conf.FPS), 1), max(int(conf.MOVE_REPEAT_DELAY * conf.FPS), 1) ) menu_args = ( eh.MODE_ONDOWN_REPEAT, max(int(conf.MENU_INITIAL_DELAY * conf.FPS), 1), max(int(conf.MENU_REPEAT_DELAY * conf.FPS), 1) ) od = eh.MODE_ONDOWN held = eh.MODE_HELD l = conf.KB_LAYOUT event_handler.add_key_handlers([ (conf.KEYS_MOVE_LEFT[l], [(self._move, (0,))]) + pzl_args, (conf.KEYS_MOVE_UP[l], [(self._move, (1,))]) + pzl_args, (conf.KEYS_MOVE_RIGHT[l], [(self._move, (2,))]) + pzl_args, (conf.KEYS_MOVE_DOWN[l], [(self._move, (3,))]) + pzl_args, (conf.KEYS_BACK, self.menu, od), (conf.KEYS_TAB, self.switch_puzzle, od), (conf.KEYS_INSERT, self._insert_cb, od), (conf.KEYS_DEL, self.delete, od), (conf.KEYS_UNDO, self.undo) + menu_args, (conf.KEYS_REDO, self.redo) + menu_args, (conf.KEYS_RESET, self.reset, od) ]) self.event_handler = event_handler # create block/surface selection grid blocks = xrange(conf.MAX_ID + 1) surfaces = xrange(-1, conf.MIN_ID - 1, -1) self._selector_defn = '3 {0}\n{1}\n\n{2}\n{3}'.format( max(len(blocks), len(surfaces)), '\n'.join('{0} 0 {1}'.format(b, i) for i, b in enumerate(blocks)), '\n'.join('{0} 1 {1}'.format(b, i) for i, b in enumerate(blocks)), '\n'.join('{0} 2 {1}'.format(s, i) for i, s in enumerate(surfaces)) ) self.selector = Puzzle(game, self._selector_defn) self.FRAME = conf.FRAME self.load(ID, defn) def load (self, ID = None, defn = None): """Load a level. load([ID][, defn]) ID: custom level ID. defn: if ID is not given, load a level from this definition; if this is not given, load a blank level. """ if ID is None: if defn is None: # blank grid defn = conf.BLANK_LEVEL self.ID = None else: # get data from file d = conf.LEVEL_DIR_DRAFT if ID[0] == 2 else conf.LEVEL_DIR_CUSTOM with open(d + ID[1]) as f: defn = f.read() self.ID = ID[1] if hasattr(self, 'editor'): self.editor.load(defn) else: self.editor = Puzzle(self.game, defn) self.editor_rect = None self.editor.deselect() self.editor.select((0, 0)) self.selector.deselect() self.selector.select((0, 0), True) self.puzzle = self.editor self.editing = True self.dirty = True self.changes = [] self.state = 0 self.mouse_moved = False self.resizing = False def change (self, *data): """Make a change to the puzzle; takes data to store in self.changes.""" cs = self.changes # purge 'future' states cs = cs[:self.state] cs.append(data) # purge oldest state if need to (don't need to increment state, then) if len(cs) > conf.UNDO_LEVELS > 0: cs.pop(0) else: self.state += 1 self.changes = cs def resize (self, amount, dirn): """Like Editor.editor.resize, but do some wrapper stuff.""" lost = self.editor.resize(amount, dirn) # might not have changed if lost is not False: self.dirty = True self.change('resize', amount, dirn, lost) def _move (self, key, event, mods, direction): """Callback for arrow keys.""" resize = False mods = (mods & pygame.KMOD_SHIFT, mods & pygame.KMOD_ALT) shrink = bool(mods[direction <= 1]) grow = bool(mods[direction > 1]) resize = shrink ^ grow if resize: # things could get messy if we're already mouse-resizing if not self.resizing: self.resize(1 if grow else -1, direction) else: # move selection self.puzzle.move_selected(direction) def insert (self): """Insert a block or surface at the current position.""" if not self.editing: return # get type and ID of selected tile in selector puzzle col, row = self.selector.selected.keys()[0] x, y = self.editor.selected.keys()[0] is_block = col == 0 and row <= conf.MAX_ID if is_block: ID = row # will be here for col = 0 if past end of blocks elif col == 0 or col == 1: ID = row if ID > conf.MAX_ID: ID = conf.DEFAULT_SURFACE else: ID = -row - 1 if ID >= 0: ID = conf.DEFAULT_SURFACE # make changes to selected tile in editor puzzle if necessary current = self.editor.grid[x][y] if is_block: if current[1] is None or current[1].type != ID: old_b = current[1] b = self.editor.add_block((BoringBlock, ID), x, y) self.game.play_snd('place_block') self.change('set_block', old_b, b, x, y) else: if current[0] != ID: old_ID = current[0] self.editor.set_surface(x, y, ID) self.game.play_snd('place_surface') self.change('set_surface', old_ID, ID, x, y) def _insert_cb (self, *args): """Callback for conf.KEYS_INSERT.""" if self.editing: self.insert() else: self.switch_puzzle() def delete (self, *args): """Delete a block or surface in the currently selected tile.""" if self.editing: x, y = self.editor.selected.keys()[0] data = self.editor.grid[x][y] snd = True # delete block, if any if data[1] is not None: b = self.editor.rm_block(None, x, y) self.change('set_block', b, None, x, y) # set surface to blank if not already elif data[0] != conf.S_BLANK: s = self.editor.set_surface(x, y, conf.S_BLANK) self.change('set_surface', s, conf.S_BLANK, x, y) else: snd = False if snd: self.game.play_snd('delete') def set_block (self, b, x, y): """Add or remove a block to or from the puzzle. set_block(b, x, y) b: BoringBlock instance, or None to remove. x, y: tile position. """ if b is None: self.editor.rm_block(None, x, y) else: self.editor.add_block(b, x, y) def undo (self, *args): """Undo changes to the puzzle.""" if self.state > 0: self.state -= 1 # get change data data = self.changes[self.state] c, data = data[0], data[1:] # make the change to the puzzle if c == 'set_block': old_b, new_b, x, y = data self.set_block(old_b, x, y) elif c == 'set_surface': old, new, x, y = data self.editor.set_surface(x, y, old) elif c == 'resize': amount, direction, lost = data self.editor.resize(-amount, (direction - 2) % 4) # restore stuff that was lost in the resize for obj, x, y in lost: if isinstance(obj, int): self.editor.set_surface(x, y, obj) else: self.set_block(obj, x, y) self.dirty = True def redo (self, *args): """Redo undone changes.""" if self.state < len(self.changes): # get change data data = self.changes[self.state] c, data = data[0], data[1:] self.state += 1 # make the change to the puzzle if c == 'set_block': old_b, new_b, x, y = data self.set_block(new_b, x, y) elif c == 'set_surface': old, new, x, y = data self.editor.set_surface(x, y, new) elif c == 'resize': amount, direction, lost = data self.editor.resize(amount, direction) self.dirty = True def click_tile (self, insert, pos): """Insert or delete a block or surface at the given position. click_tile(insert, pos) insert: whether to insert the current block or surface (else delete). pos: on-screen position to try to perform the action at. """ # get clicked tile p = self.editor.point_tile(pos) if p: # clicked a tile in self.editor: switch to and select if not self.editing: self.switch_puzzle() self.editor.deselect() self.editor.select(p) (self.insert if insert else self.delete)() else: p = self.selector.point_tile(pos) if p: # clicked a tile in self.selector: switch to selector, then # select the tile if self.editing: self.switch_puzzle() self.selector.deselect() self.selector.select(p) def _click (self, evt): """Handle mouse clicks.""" button = evt.button pos = evt.pos if button in (1, 3): # left-click to insert, right-click to delete self.click_tile(button == 1, pos) elif button == 2: rel_pos = self.editor.point_pos(pos) # make sure we're clicking in the grid if rel_pos is None: return self._resize_sides = [] # exclude a zone in the middle b = conf.RESIZE_DEAD_ZONE_BOUNDARY for i in (0, 1): if b < rel_pos[i] < 1 - b: self._resize_sides.append(None) else: # the axes we can resize on depends on where we start from self._resize_sides.append(1 if rel_pos[i] > .5 else -1) if self._resize_sides == [None, None]: return self.resizing = list(pos) def _unclick (self, evt): """Handle mouse click release.""" if evt.button == 2 and self.resizing: self.resizing = False del self._resize_sides def switch_puzzle (self, *args): """Switch selected puzzle between editor and block selector.""" self.editing = not self.editing pzls = (self.editor, self.selector) if self.editing: new, old = pzls else: old, new = pzls # deselect old and select new for colour, pzl in enumerate((new, old)): pos = pzl.selected.keys()[0] pzl.deselect() pzl.select(pos, colour) self.puzzle = new def reset (self, *args): """Confirm resetting the puzzle.""" if self.state > 0: self.game.start_backend(Menu, 1, self) # else nothing to reset def menu (self, *args): """Show the editor menu.""" self.game.start_backend(Menu, 0, self) def _do_reset (self): """Actually reset the puzzle.""" # just reset to original state - to whatever was loaded, if anything while self.state > 0: self.undo() def update (self): """Handle mouse movement.""" if self.mouse_moved: pos = pygame.mouse.get_pos() if self.resizing: # change puzzle size if middle-click-dragging old_pos = self.resizing for i in (0, 1): side = self._resize_sides[i] if side is None: # can't resize on this axis continue diff = pos[i] - old_pos[i] threshold = min(conf.RESIZE_LENGTH * conf.RES[0], self.editor.tile_size(i)) while abs(diff) >= threshold: # resize sign = 1 if diff > 0 else -1 self.resize(sign * side, i + 2 * (sign == 1)) # prepare for resizing again old_pos[i] += sign * threshold diff -= sign * threshold else: # change selection based on mouse position # get tile under mouse tile = self.editor.point_tile(pos) if tile: # editor: select tile under mouse if not self.editing: self.switch_puzzle() self.editor.deselect() self.editor.select(tile) else: # selector: just make sure it's the current puzzle tile = self.selector.point_tile(pos) if tile: if self.editing: self.switch_puzzle() self.mouse_moved = False def draw (self, screen): """Draw the puzzles.""" w, h = screen.get_size() w1 = int(conf.EDITOR_WIDTH * w) w2 = w - w1 pad = int(conf.EDITOR_ARROW_PADDING * w) if self.dirty: screen.fill(conf.BG[conf.THEME]) # get puzzle sizes e = self.editor.tiler s = self.selector.tiler if w1 != s.offset[0]: # screen size changed: need to change puzzle positions e.offset = (pad, pad) s.offset = (w1, 0) s.reset() # draw puzzles drawn1 = self.editor.draw(screen, self.dirty, (w1 - 2 * pad, h - 2 * pad)) drawn2 = self.selector.draw(screen, self.dirty, (w2, h)) # and return the sum list of rects to draw in drawn = [] for d in (drawn1, drawn2): if d: drawn += d # draw arrows if self.dirty: self.editor_rect = drawn1 # TODO to_draw = range(8) special = [] drawn = True else: # TODO to_draw = [] special = [] drawn += [] if to_draw: # get draw area position and size l, t, w, h = self.editor_rect[0] tl = (l - pad, t - pad) br = (l + w, t + h) sz = (w + 2 * pad, h + 2 * pad) # generate required arrow images fn = conf.IMG_DIR + conf.THEME + os.sep + '{0}.png' imgs = [] for ID in ('arrow', 'arrow-special'): imgs.append(self.game.img(fn.format(ID), (pad / 2, None))) for i in xrange(1, 4): imgs.append(None) img_s = [img for img in imgs if img is not None][0].get_size() # draw for i in to_draw: p = [0, 0] axis = (i / 2) % 2 p[axis] = (br if i > 3 else tl)[axis] + img_s[0] * (i % 2) p[not axis] = tl[not axis] + (sz[not axis] - img_s[1]) / 2 img = (axis + 2 * (i % 2)) % 4 if i in special: img += 4 if imgs[img] is None: source = imgs[4 * (i in special)] imgs[img] = pygame.transform.rotate(source, -90 * img) screen.blit(imgs[img], p) self.dirty = False return drawn