Source code for engine.gfx.graphics

"""Specialised types of graphics.

---NODOC---

TODO:
 - Animation: rework to use Graphic callbacks
 - Tilemap:
    - should change if given Graphics change (tile_types), like Animation
        - graphic.own(tilemap)
        - use Graphic callbacks
    - should provide tile setters/getters
    - .update_from from_disk=True should call Graphic.reload() on graphics
    - only prerender tiles as requested
 - tiled graphic
    - graphic form is like Tilemap's tile_graphic
    - takes multiple rects to cover all of them with edges matching up
 - particle system
 - *Grid take (tiled graphic)/(args thereto) instead of just colour for bg

---NODOC---

"""

from os.path import splitext

import pygame as pg
from pygame import Rect

from ..conf import conf
from ..text import option_defaults as text_option_defaults
from .. import util as gameutil
from .graphic import Graphic


[docs]class Colour (Graphic): """A solid rect of colour. Colour(colour, rect, layer=0) :arg colour: a colour to draw, as accepted by :func:`engine.util.normalise_colour`. :arg rect: Pygame-style rect to draw in, or just a ``(width, height)`` size to use a rect with position ``(0, 0)``. :arg layer: as taken by :class:`Graphic <engine.gfx.graphic.Graphic>`. :meth:`fill` corresponds to a builtin transform. """ _i = Graphic._builtin_transforms.index('crop') _builtin_transforms = Graphic._builtin_transforms[:_i] + ('fill',) + \ Graphic._builtin_transforms[_i:] def __init__ (self, colour, rect, layer=0): if len(rect) == 2 and isinstance(rect[0], (int, float)): # just got size rect = ((0, 0), rect) rect = Rect(rect) # converts surface and sets opaque to True Graphic.__init__(self, pg.Surface(rect.size), rect.topleft, layer) self._colour = (0, 0, 0, 255) self.fill(colour) @property def colour (self): """As taken by constructor; set as necessary.""" return self._colour @colour.setter def colour (self, colour): self.fill(colour) def _gen_mods_fill (self, src_sz, first_time, last_args, colour): colour = gameutil.normalise_colour(colour) if first_time or gameutil.normalise_colour(last_args[0]) != colour: def apply_fn (g): g._colour = colour def undo_fn (g): g._colour = (0, 0, 0, 255) mods = (apply_fn, undo_fn) else: mods = None return (mods, src_sz) def _fill (self, src, dest, dirty, last_args, colour): colour = gameutil.normalise_colour(colour) if colour == (0, 0, 0, 255): return (src, dirty) if dest is not None and src.get_size() == dest.get_size(): # we can reuse dest last_colour = gameutil.normalise_colour(last_args[0]) if colour[3] < 255 and not gameutil.has_alpha(dest): # newly transparent dest = dest.convert_alpha() if dirty is True or last_colour != colour: # need to refill everything dest.fill(colour) return (dest, True) elif dirty: # same colour, some areas changed for r in dirty: dest.fill(colour, r) return (dest, dirty) else: # same as last time return (dest, False) # create new surface and fill new_sfc = pg.Surface(src.get_size()) if colour[3] < 255: # non-opaque: need to convert to alpha new_sfc = new_sfc.convert_alpha() else: new_sfc = new_sfc.convert() new_sfc.fill(colour) return (new_sfc, True)
[docs] def fill (self, colour): """Fill with the given colour (like :attr:`colour`).""" self.transform('fill', colour) self._colour = colour return self
[docs]class Text (Graphic): """Graphic displaying rendered text. Text(text, renderer, pos=(0, 0), options={}, layer=0) :arg text: text to render; may contain line breaks to display separate lines. :arg renderer: :class:`text.TextRenderer <engine.text.TextRenderer>` instance or the name a renderer is stored under in :attr:`Game.text_renderers <engine.game.Game.text_renderers>`. :arg pos: as taken by :class:`Graphic <engine.gfx.graphic.Graphic>`. :arg options: rendering options to override defaults, as taken by :meth:`TextRenderer.render() <engine.text.TextRenderer.render>`. All options can be get and set as properties of this instance, and all are guaranteed to exist, even if not given in this argument. :arg layer: as taken by :class:`Graphic <engine.gfx.graphic.Graphic>`. """ def __init__ (self, text, renderer, pos=(0, 0), options={}, layer=0): self._last_text = self._text = text self._renderer = None self.renderer = renderer # retrieves from game self._last_renderer = self._renderer self._options = dict(options) # want to always be normalised for better testing for changes self.renderer.normalise_options(self._options) self._last_options = self._options.copy() sfc, lines = self._render_text() #: Number of lines of text rendered. self.nlines = lines Graphic.__init__(self, sfc, pos, layer) def _update_rect (self): # set size from current text/renderer/options size = self._renderer.get_info(self._text, self._options)[2] if size != self.orig_sfc.get_size(): # repositions based on anchor self.size_changed(size) @property def text (self): """Text to render (as taken by constructor).""" return self._text @text.setter def text (self, text): if text != self._text: self._text = text self._update_rect() @property def renderer (self): """:class:`text.TextRenderer <engine.text.TextRenderer>` instance to use.""" return self._renderer @renderer.setter def renderer (self, renderer): if isinstance(renderer, basestring): renderer = conf.GAME.text_renderers[renderer] old_renderer = self._renderer self._renderer = renderer # None if calling from constructor if old_renderer is not None and renderer != old_renderer: self._update_rect() def __getattr__ (self, attr): if attr in text_option_defaults: if attr in self._options: return self._options[attr] else: return getattr(self._renderer, attr) # guaranteed to exist else: return object.__getattribute__(self, attr) def __setattr__ (self, attr, val): if attr in text_option_defaults: opts = {attr: val} # make sure all stored options are normalised self._renderer.normalise_options(opts) val = opts[attr] if val != self._options.get(attr): self._options[attr] = val self._update_rect() else: object.__setattr__(self, attr, val) def _render_text (self): # actually render text, and return the result return self._renderer.render(self._text, self._options) def render (self): """:inherit:""" changed = False if self._last_text != self._text: changed = True self._last_text = self._text if self._last_renderer != self._renderer: changed = True self._last_renderer = self._renderer if self._last_options != self._options: changed = True self._last_options = self._options.copy() if changed: self.orig_sfc, self.nlines = self._render_text() # handles any earlier change to self.rect Graphic.render(self)
[docs]class Animation (Graphic): """An animated graphic. Animation(imgs, pos=(0, 0), layer=0[, scheduler], pool=conf.DEFAULT_RESOURCE_POOL, res_mgr=conf.GAME.resources) :arg imgs: a sequence of images as part of the animation; each can be a Pygame surface, a filename to load an surface from, or a :class:`Graphic <engine.gfx.graphic.Graphic>` instance (in which case it is removed from any :class:`GraphicsManager <engine.gfx.container.GraphicsManager>` it is in). Note that a :class:`util.Spritemap <engine.gfx.util.Spritemap>` instance is a valid form for this argument. :arg scheduler: :class:`sched.Scheduler <engine.sched.Scheduler>` instance to use for timing; if not given, animations can only be played when the graphic is contained by a :class:`GraphicsManager <engine.gfx.container.GraphicsManager>` (and trying to do so otherwise raises ``RuntimeError``). Other arguments are as taken by :class:`Graphic <engine.gfx.graphic.Graphic>`. Note that when an animation is playing and the image changes, :attr:`Graphic.anchor <engine.gfx.graphic.Graphic.anchor>` is respected. For example, to play the frames in a spritemap consisting of a single row:: Animation(Spritemap('map.png', 4)).add('run', frame_time=.1).play('run') """ def __init__ (self, imgs, pos=(0, 0), layer=0, scheduler=None, pool=conf.DEFAULT_RESOURCE_POOL, res_mgr=None): self._resource_pool = pool self._resource_manager = res_mgr if len(imgs) == 0: raise ValueError('animation requires at least one image') load_img = self._load_img gs = [] #: ``list`` of ``imgs`` as passed to the constructor, except that they #: are never filenames. self.graphics = gs for img in imgs: if isinstance(img, basestring): img = load_img(img) elif isinstance(img, Graphic): img.own(self) gs.append(img) # graphics is non-empty due to the exception above self._graphic = 0 Graphic.__init__(self, self._get_sfc(0), pos, layer, pool, res_mgr) #: ``{name: (indices, frame_time)}`` frame sequences ('animations') as #: added through :meth:`add`. self.sequences = {} self._frame_time = None self._speed = 1 #: The ``scheduler`` argument passed to the constructor. self.scheduler = scheduler #: The currently playing sequence (name), or ``None``. self.playing = None #: ``list`` of queued sequences to play after the current sequence has #: finished, each ``(name, repeat, frame_time, cb)`` as added through #: :meth:`queue`, starting with the first to be played. Items may be #: removed manually. self.queued = [] #: The current repeat of the playing sequence, starting from ``0`` if #: it hasn't started repeating yet, or ``None``. self.repeat = None #: The total number of repeats that will be performed for the currently #: playing sequence (not including the first playthrough), or ``True`` #: if it will play forever, or ``None``. self.repeats = None #: The current frame in the currently playing sequence (within the #: current repeat), as an integer starting from ``0``, or ``None``. self.frame = None self._timer_id = None # scheduler timeout ID self._frame_time_source = None # 'default', 'sequence' or 'runtime' self._playing_frame_time = None # set when we start playing self._new_frame_time = None # set to flag a frame time change self._playing_cb = None @property def graphic (self): """The currently visible graphic, as an index in :attr:`graphics`.""" return self._graphic @graphic.setter def graphic (self, i): i = int(i) if i == self._graphic: return self.orig_sfc = self._get_sfc(i) self._graphic = i @property def frame_time (self): """A default for the time between animation frames. This is a value in seconds, that applies for all animation sequences. If not set, a value must be defined for each sequence. Changes do not take effect until the current frame has finished, if any is running. """ return self._frame_time @frame_time.setter def frame_time (self, t): self._frame_time = t if self._frame_time_source == 'default': self._update_timer() @property def speed (self): """Running speed of any animation. This is a multiplier, where ``1`` is the default speed, and anything higher decreases frame times. Changes do not take effect until the current frame has finished, if any. """ return self._speed @speed.setter def speed (self, speed): if speed <= 0: raise ValueError('speed must be positive') if speed != self._speed: self._speed = speed self._update_timer() def _get_sfc (self, i): # get the surface corresponding to the given index in self.graphics sfc = self.graphics[i] if isinstance(sfc, Graphic): sfc = sfc.surface return sfc def _get_sched (self): s = self.scheduler if s is None: owner = self.owner if owner is None: raise RuntimeError('no scheduler is available') s = owner.scheduler return s def _update_timer (self): # update timer speed from data if self.playing is None: # nothing to do return if self._frame_time_source == 'default': t = self._frame_time else: # don't change if from sequence/runtime t = self._playing_frame_time # schedule for application next time we get a callback self._new_frame_time = float(t) / self._speed
[docs] def add (self, name, *indices, **kwargs): """Add a sequence to :attr:`sequences` to play back later. add(name, *indices[, frame_time]) -> self :arg name: the name to give the sequence (any hashable object). If a sequence with this name already exists, it is overwritten; if it is currently playing or queued, it is stopped/unqueued. :arg indices: any number of indices in :attr:`graphics`, defining the sequence of frames, or pass none for all frames in order. :arg frame_time: a default for the time between animation frames in seconds whenever this sequence is played. If not given, a value must be defined either through the constructor or each time the sequence is played. """ if not indices: indices = range(len(self.graphics)) if not indices: raise ValueError('a sequence must contain at least one frame') self.unqueue(name) if name == self.playing: self.stop() self.sequences[name] = (indices, kwargs.get('frame_time')) return self
[docs] def add_multi (self, sequences): """Add a number of frame sequences. add_multi(sequences) -> self :arg sequences: ``{name: data}``, where ``data`` can be an ``indices`` ``list`` or ``(indices[, frame_time])``, and each of these is as taken by :meth:`add`. """ add = self.add for name, data in sequences.iteritems(): if (data and (hasattr(data[0], '__len__') and hasattr(data[0], '__getitem__'))): # got (indices, ...) if len(data) == 1: indices = data[0] frame_time = None elif len(data) == 2: indices, frame_time = data else: raise TypeError('invalid arguments: {0}'.format(data)) else: # got indices indices = data frame_time = None self.add(name, *indices, frame_time=frame_time) return self
[docs] def rm (self, *names): """Remove sequences with the given names. rm(*names) -> self Missing items are ignored. """ seqs = self.sequences for name in names: if name in seqs: del seqs[name] return self
def _next_frame (self): # called through scheduler to move to the next frame assert self.playing is not None indices = self.sequences[self.playing][0] self.frame += 1 if self.frame == len(indices): # reached the end of the sequence if self.repeats is not True and self.repeat == self.repeats: # no repeats left self.playing = self.repeat = self.repeats = self.frame = None # no need to reset other attributes, since they're private if self._playing_cb is not None: self._playing_cb() if self.playing is None and self.queued: self.play(*self.queued.pop()) return False else: self.repeat += 1 self.frame = 0 self.graphic = indices[self.frame] if self._new_frame_time is not None: # adjust speed for next frame self._timer_id = self._get_sched().add_timeout( self._next_frame, self._new_frame_time ) self._new_frame_time = None return False else: return True
[docs] def play (self, name, repeat=True, frame_time=None, cb=None): """Play a frame sequence. play(name, repeat=True[, frame_time][, cb]) -> self :arg name: sequence name to play. :arg repeat: whether to repeat the sequence once it has finished. ``True`` to repeat forever, else a number of repeats to perform (that is, the sequence is played ``(repeats + 1)`` times). (``False`` is also valid.) :arg frame_time: the time between animation frames in seconds. If not given, a value must be defined either as through the constructor or in :meth:`add`. :arg cb: a function to call when the animation ends (but not if it is stopped through :meth:`stop` or by starting another animation). If a sequence is already being played, that sequence is canceled. """ # cancel current sequence s = self._get_sched() if self.playing is not None: s.rm_timeout(self._timer_id) # initialise attributes self.playing = name self.repeat = 0 if repeat is False: repeat = 0 self.repeats = repeat self.frame = 0 indices, seq_t = self.sequences[name] # show first frame now (sequences are guaranteed to have non-0 length) self.graphic = indices[0] if frame_time is None: if seq_t is None: if self._frame_time is None: raise RuntimeError('no frame_time is defined (sequence: ' '\'{0})\''.format(name)) else: frame_time = self._frame_time self._frame_time_source = 'default' else: frame_time = seq_t self._frame_time_source = 'sequence' else: self._frame_time_source = 'runtime' frame_time = float(frame_time) / self._speed # start the scheduler self._timer_id = s.add_timeout(self._next_frame, frame_time) self._playing_frame_time = frame_time self._playing_cb = cb return self
[docs] def pause (self): """Pause the currently running sequence, if any. pause() -> self """ if self.playing is not None: self._get_sched().pause_timeout(self._timer_id) return self
[docs] def unpause (self): """Unpause the currently running sequence, if paused. unpause() -> self """ if self.playing is not None: self._get_sched().unpause_timeout(self._timer_id) return self
[docs] def stop (self, n_queued=0): """Stop the currently running sequence, if any. stop(n_queued) -> self :arg n_queued: the number of subsequent queued sequences to cancel after stopping the running sequence. """ if self.playing: self._get_sched().rm_timeout(self._timer_id) self.playing = self.repeat = self.repeats = self.frame = None # no need to reset other attributes, since they're private for i in xrange(n_queued): self.queued.pop(0) if self.queued: self.play(*self.queued.pop()) return self
[docs] def queue (self, name, repeat=True, frame_time=None, cb=None): """Queue a frame sequence for playing after any running sequence. queue(name, repeat=True[, frame_time][, cb]) -> self Arguments are as taken by :meth:`play`. """ if self.playing: self.queued.append((name, repeat, frame_time, cb)) else: self.play(name, repeat, frame_time, cb) return self
[docs] def queue_multi (self, *sequences): """Queue multiple frame sequences. queue_multi(*sequences) -> self :arg sequences: any number of ``(name, repeat=True[, frame_time][, cb])`` tuples, where arguments are as taken by :meth:`play`. """ queue = self.queue for args in sequences: queue(*args) return self
[docs] def unqueue (self, *names): """Remove frame sequences from the queue by name. unqueue(*names) -> self :arg names: any number of sequence names; missing items are ignored. """ queued = self.queued for name in names: # iterate in reverse to avoid changing indices as we remove items for i, data in reversed(list(enumerate(queued))): if data[0] == name: queued.pop(i) return self
def render (self): """:inherit:""" # set orig_dirty where the graphic is dirty, if a graphic g = self.graphics[self.graphic] if isinstance(g, Graphic): sfc = g.surface # after render() and before _pre_draw(), ._dirty is relative to # top-left = (0, 0) if g._dirty: orig_sfc = self._orig_sfc if sfc is orig_sfc: # same, so need to flag dirty areas if g._dirty is True: self._orig_dirty = True else: self.dirty(*g._dirty) else: # different, so change out the surface self.orig_sfc = sfc g._dirty = [] Graphic.render(self)
[docs]class Tilemap (Graphic): """A finite, flat grid of tiles. Tilemap(grid, tile_data[, tile_types], pos=(0, 0), layer=0[, translate_type], cache_graphic=False, pool=conf.DEFAULT_RESOURCE_POOL, res_mgr=conf.GAME.resources) :arg grid: a :class:`util.grid.Grid <engine.util.grid.Grid>` defining the size and shape of the tiles in the tilemap, or the ``tile_size`` argument to :class:`util.grid.Grid <engine.util.grid.Grid>` to create a new one with standard parameters. :arg tile_data: a way of determining the tile type ID for each ``(x, y)`` tile in the grid, which is any object. This can be: - a list of columns, where each column is a list of IDs; - a string with rows delimited by line breaks and each row a whitespace-delimited set of string IDs; - ``(s, col_delim, row_delim)`` to specify custom delimiter characters for a string ``s``, where either or both delimiters can be ``None`` to split by whitespace/line breaks; - a filename from which to load a string with delimited IDs (the name may not contain whitespace); - ``(filename, col_delim, row_delim)`` for a custom-delimited string in a file; - a :class:`Graphic <engine.gfx.graphic.Graphic>`, Pygame surface or filename (may not contain whitespace) to load an image from, and use the ``(r, g, b[, a])`` colour tuples of the pixels in the surface as IDs; - if ``grid`` is a :class:`util.grid.Grid <engine.util.grid.Grid>`: a function that takes ``col`` and ``row`` arguments as column and row indices in the grid, and returns the corresponding tile type ID; or - if ``grid`` is not a :class:`util.grid.Grid <engine.util.grid.Grid>`: ``(get_tile_type, w, h)``, where get_tile_type is a function as defined previously, and ``w`` and ``h`` are the width and height of the grid, in tiles. :arg tile_types: a ``tile_type_id -> tile_graphic`` mapping---either a function or an object that supports indexing. If not given, the identity function is used. ``tile_type_id`` is the tile type ID obtained from the ``tile_data`` argument. ``tile_graphic`` determines how the tile should be drawn; it may be: - ``None`` for an an empty (transparent) tile; - a colour (as taken by :func:`engine.util.normalise_colour`) to fill with; - a :class:`Graphic <engine.gfx.graphic.Graphic>`, Pygame surface or filename to load from, to copy aligned to the centre of the tile, clipped to fit; or - ``(graphic, alignment=0, rect=graphic_rect)`` with ``alignment`` or ``rect`` in any order or omitted, and ``graphic`` as in the above form. ``alignment`` is as taken by :func:`engine.util.align_rect`, and ``rect`` is the Pygame-style rect within the source surface of ``graphic`` to copy from. Regardless of ``alignment``, ``rect`` is clipped to fit in the tile around its centre. Note that a :class:`util.Spritemap <engine.gfx.util.Spritemap>` is a valid form for this argument. :arg pos,layer: as taken by :class:`Graphic <engine.gfx.graphic.Graphic>`. :arg translate_type: a function that takes tile type IDs obtained from the ``tile_data`` argument and returns the ID to use with the ``tile_types`` argument in obtaining ``tile_graphic``; does nothing by default. :arg cache_graphic: whether to cache and reuse ``tile_graphic`` for each tile type. You might want to pass ``True`` if requesting ``tile_graphic`` from ``tile_types`` generates a surface. If ``True``, tile type IDs must be hashable (after translation by ``translate_type``). :arg pool,res_mgr: as taken by :class:`Graphic <engine.gfx.graphic.Graphic>`. This is meant to be used for static tilemaps---that is, where the appearance of each tile type never changes. """ def __init__ (self, grid, tile_data, tile_types=None, pos=(0, 0), layer=0, translate_type=None, cache_graphic=False, pool=conf.DEFAULT_RESOURCE_POOL, res_mgr=None): if tile_types is None: tile_types = lambda g: g elif not callable(tile_types): types = tile_types tile_types = lambda tile_type_id: types[tile_type_id] self._type_to_graphic = tile_types if translate_type is None: translate_type = lambda tile_type_id: tile_type_id self._translate_type = translate_type self._cache_graphic = cache_graphic self._cache = {} # set these before _parse_data, since it calls _load_img which uses # them - but can't init Graphic yet because we don't know the size self._resource_pool = pool self._resource_manager = res_mgr self._tile_data, ncols, nrows = self._parse_data(tile_data, grid, False) if not isinstance(grid, gameutil.grid.Grid): grid = gameutil.grid.Grid((ncols, nrows), grid) #: The :class:`util.grid.Grid <engine.util.grid.Grid>` covered. self.grid = grid # apply initial data Graphic.__init__(self, gameutil.blank_sfc(grid.size), pos, layer, pool, res_mgr) update = self._update tile_data = self._tile_data for col, row, tile_rect in grid.tile_rects(True): update(col, row, tile_data[col][row], tile_rect) def _parse_data (self, tile_data, grid, force_load): # parse tile data if not tile_data: return ([], 0, 0) if isinstance(tile_data, basestring): if (len(tile_data.split()) == 1 and splitext(tile_data)[1][1:].lower() in ('png', 'jpg', 'jpeg', 'gif')): # image file tile_data = self._load_img(tile_data, force_load=force_load) else: # string/text file tile_data = (tile_data, None, None) if isinstance(tile_data, Graphic): tile_data = tile_data.surface if isinstance(tile_data, pg.Surface): tile_data = [[tuple(c) for c in col] for col in pg.surfarray.array3d(tile_data)] if isinstance(tile_data[0], basestring): s, col, row = tile_data if len(s.split()) == 1: with open(s) as f: s = f.read(s) if row is None: s = s.splitlines() else: s = s.split(row) if col is None: tile_data = [l.split() for l in s] else: tile_data = [l.split(col) for l in s] # list of rows -> list of columns tile_data = zip(*tile_data) if callable(tile_data): if not isinstance(grid, gameutil.grid.Grid): raise ValueError('got function for tile_data, but grid is ' \ 'not a Grid instance') tile_data = (tile_data, grid.ncols, grid.nrows) if callable(tile_data[0]): f, ncols, nrows = tile_data tile_data = [] for i in xrange(ncols): col = [] tile_data.append(col) for j in xrange(nrows): col.append(f(i, j)) # now tile_data is a list of columns ncols = len(tile_data) nrows = len(tile_data[0]) if (isinstance(grid, gameutil.grid.Grid) and grid.ntiles != (ncols, nrows)): msg = 'tile_data has invalid dimensions: got {0}, expected {1}' raise ValueError(msg.format((ncols, nrows), grid.ntiles)) translate_type = self._translate_type tile_data = [[translate_type(tile_type_id) for tile_type_id in col] for col in tile_data] return (tile_data, ncols, nrows) def _update (self, col, row, tile_type_id, tile_rect=None): if self._cache_graphic: if tile_type_id in self._cache: g = self._cache[tile_type_id] else: g = self._type_to_graphic(tile_type_id) self._cache[tile_type_id] = g else: g = self._type_to_graphic(tile_type_id) dest = self._orig_sfc if tile_rect is None: tile_rect = self.grid.tile_rect(col, row) if isinstance(g, (Graphic, pg.Surface, basestring)): g = (g,) if (g is not None and isinstance(g[0], (Graphic, pg.Surface, basestring))): sfc = g[0] if isinstance(sfc, basestring): sfc = self._load_img(sfc) elif isinstance(sfc, Graphic): sfc = sfc.surface if len(g) == 1: alignment = rect = None else: if isinstance(g[1], int) or len(g[1]) == 2: alignment = g[1] rect = None else: alignment = None rect = g[1] if len(g) == 3: if rect is None: rect = g[2] else: alignment = g[2] if alignment is None: alignment = 0 if rect is None: rect = sfc.get_rect() # clip rect to fit in tile_rect dest_rect = Rect(rect) dest_rect.center = tile_rect.center fit = dest_rect.clip(tile_rect) rect = Rect(rect) rect.move_ip(fit.x - dest_rect.x, fit.y - dest_rect.y) rect.size = dest_rect.size # copy rect to tile_rect with alignment pos = gameutil.align_rect(rect, tile_rect, alignment) dest.blit(sfc, pos, rect) else: if g is None: g = (0, 0, 0, 0) # now we have a colour dest.fill(gameutil.normalise_colour(g), tile_rect) return tile_rect def __getitem__ (self, i): col, row = i return self._tile_data[col][row] def __setitem__ (self, i, tile_type_id): col, row = i tile_type_id = self._translate_type(tile_type_id) if tile_type_id != self._tile_data[col][row]: rect = self._update(col, row, tile_type_id) self._tile_data[col][row] = tile_type_id self.dirty(rect)
[docs] def update_from (self, tile_data, from_disk=False): """Update tiles from a new set of data. :arg tile_data: as taken by the constructor. :arg from_disk: whether to force reloading from disk, if passing an image filename. """ tile_data = self._parse_data(tile_data, self.grid, from_disk)[0] for i, col in enumerate(tile_data): for j, tile_type_id in enumerate(col): self[(i, j)] = tile_type_id
[docs]class Grid (Graphic): """Drawable wrapper for :class:`util.grid.Grid <engine.util.grid.Grid>`. Grid(grid, gap_colour='aaa', bg_colour='0000', pos=(0, 0), layer=0) :arg grid: a :class:`util.grid.Grid <engine.util.grid.Grid>` instance. :arg gap_colour: colour in-between tiles, as accepted by :func:`engine.util.normalise_colour`; may have alpha. :arg bg_colour: colour within tiles, as accepted by :func:`engine.util.normalise_colour`. :arg pos,layer: as taken by :class:`Graphic <engine.gfx.graphic.Graphic>`. """ def __init__ (self, grid, gap_colour='aaa', bg_colour='0000', pos=(0, 0), layer=0): gap_colour = gameutil.normalise_colour(gap_colour) bg_colour = gameutil.normalise_colour(bg_colour) sfc = pg.Surface(grid.size) sfc = (sfc.convert_alpha() if gap_colour[3] < 255 or bg_colour[3] < 255 else sfc.convert()) # fill with gaps and add in tiles sfc.fill(gap_colour) if bg_colour != gap_colour: for rect in grid.tile_rects(): sfc.fill(bg_colour, rect) Graphic.__init__(self, sfc, pos, layer)
[docs]class InfiniteGrid (Graphic): """Drawable wrapper for :class:`util.grid.InfiniteGrid <engine.util.grid.InfiniteGrid>`. InfiniteGrid(grid, rect, gap_colour='aaa', bg_colour='0000', pos=(0, 0), layer=0) :arg grid: a :class:`util.grid.InfiniteGrid<engine.util.grid.InfiniteGrid>` instance. :arg rect: Pygame-style rect within ``grid`` to draw. :arg gap_colour: colour in-between tiles, as accepted by :func:`engine.util.normalise_colour`; may have alpha. :arg bg_colour: colour within tiles, as accepted by :func:`engine.util.normalise_colour`. :arg pos,layer: as taken by :class:`Graphic <engine.gfx.graphic.Graphic>`. """ def __init__ (self, grid, rect, gap_colour='aaa', bg_colour='0000', pos=(0, 0), layer=0): #: As passed to the constructor. self.grid = grid self._view_rect = self._gap_colour = self._bg_colour = None rect = Rect(rect) Graphic.__init__(self, pg.Surface(rect.size), pos, layer) self.gap_colour = gap_colour self.bg_colour = bg_colour self.view_rect = rect def _get_alpha (self): # determine whether orig_sfc will have any alpha return self._gap_colour[3] < 255 or self._bg_colour[3] < 255 def _fix_alpha (self, sfc): # convert given surface (to be orig_sfc) to alpha if necessary if self._get_alpha() and not gameutil.has_alpha(sfc): sfc = sfc.convert_alpha() return sfc def _draw_tiles (self, sfc): # draw tiles on the given surface ir = gameutil.ir c = self._bg_colour if c != self._gap_colour: offset = (-self._view_rect.x, -self._view_rect.y) for r in self.grid.tile_rects(self._view_rect): sfc.fill(c, Rect([ir(x) for x in r]).move(offset)) def _render_grid (self): # draw grid to a surface and set as orig_sfc size = self._view_rect.size if size != self._orig_sfc.get_size(): sfc = pg.Surface(size) if self._get_alpha(): sfc = sfc.convert_alpha() else: sfc = self._fix_alpha(self._orig_sfc) # draw grid to surface sfc.fill(self._gap_colour) self._draw_tiles(sfc) self.orig_sfc = sfc @property def view_rect (self): """As the ``rect`` argument taken by the constructor. :attr:`Graphic.anchor <engine.gfx.graphic.Graphic.anchor>` is respected when this is changed. """ return self._view_rect @view_rect.setter def view_rect (self, rect): rect = Rect(rect) if rect != self._view_rect: self._view_rect = rect self._render_grid() @property def gap_colour (self): """As passed to the constructor.""" return self._gap_colour @gap_colour.setter def gap_colour (self, colour): colour = gameutil.normalise_colour(colour) old_colour = self._gap_colour if colour != old_colour: self._gap_colour = colour if old_colour is not None: self._render_grid() # else we're still in the constructor @property def bg_colour (self): """As passed to the constructor.""" return self._bg_colour @bg_colour.setter def bg_colour (self, colour): colour = gameutil.normalise_colour(colour) old_colour = self._bg_colour if colour != old_colour: self._bg_colour = colour if old_colour is not None: # no need to re-render: just re-fill tiles self._draw_tiles(self._fix_alpha(self._orig_sfc)) self.dirty()
# else we're still in the constructor