all repos — Legends-RPG @ 2ada8efcd445f4fdc94020fcb7f03a34dca285af

A fantasy mini-RPG built with Python and Pygame.

data/tmx.py (view raw)

  1# "Tiled" TMX loader/renderer and more
  2# Copyright 2012 Richard Jones <richard@mechanicalcat.net>
  3# This code is placed in the Public Domain.
  4
  5# TODO: support properties on more things
  6
  7import sys
  8import struct
  9import pygame
 10from pygame.locals import *
 11from pygame import Rect
 12from xml.etree import ElementTree
 13
 14
 15class Tile(object):
 16    def __init__(self, gid, surface, tileset):
 17        self.gid = gid
 18        self.surface = surface
 19        self.tile_width = tileset.tile_width
 20        self.tile_height = tileset.tile_height
 21        self.properties = {}
 22
 23    @classmethod
 24    def fromSurface(cls, surface):
 25        '''Create a new Tile object straight from a pygame Surface.
 26
 27        Its tile_width and tile_height will be set using the Surface dimensions.
 28        Its gid will be 0.
 29        '''
 30        class ts:
 31            tile_width, tile_height = surface.get_size()
 32        return cls(0, surface, ts)
 33
 34    def loadxml(self, tag):
 35        props = tag.find('properties')
 36        if props is None:
 37            return
 38        for c in props.findall('property'):
 39            # store additional properties.
 40            name = c.attrib['name']
 41            value = c.attrib['value']
 42
 43            # TODO hax
 44            if value.isdigit():
 45                value = int(value)
 46            self.properties[name] = value
 47
 48    def __repr__(self):
 49        return '<Tile %d>' % self.gid
 50
 51
 52class Tileset(object):
 53    def __init__(self, name, tile_width, tile_height, firstgid):
 54        self.name = name
 55        self.tile_width = tile_width
 56        self.tile_height = tile_height
 57        self.firstgid = firstgid
 58        self.tiles = []
 59        self.properties = {}
 60
 61    @classmethod
 62    def fromxml(cls, tag, firstgid=None):
 63        if 'source' in tag.attrib:
 64            firstgid = int(tag.attrib['firstgid'])
 65            with open(tag.attrib['source']) as f:
 66                tileset = ElementTree.fromstring(f.read())
 67            return cls.fromxml(tileset, firstgid)
 68
 69        name = tag.attrib['name']
 70        if firstgid is None:
 71            firstgid = int(tag.attrib['firstgid'])
 72        tile_width = int(tag.attrib['tilewidth'])
 73        tile_height = int(tag.attrib['tileheight'])
 74
 75        tileset = cls(name, tile_width, tile_height, firstgid)
 76
 77        for c in tag.getchildren():
 78            if c.tag == "image":
 79                # create a tileset
 80                tileset.add_image(c.attrib['source'])
 81            elif c.tag == 'tile':
 82                gid = tileset.firstgid + int(c.attrib['id'])
 83                tileset.get_tile(gid).loadxml(c)
 84        return tileset
 85
 86    def add_image(self, file):
 87        image = pygame.image.load(file).convert_alpha()
 88        if not image:
 89            sys.exit("Error creating new Tileset: file %s not found" % file)
 90        id = self.firstgid
 91        for line in xrange(image.get_height() / self.tile_height):
 92            for column in xrange(image.get_width() / self.tile_width):
 93                pos = Rect(column * self.tile_width, line * self.tile_height,
 94                    self.tile_width, self.tile_height)
 95                self.tiles.append(Tile(id, image.subsurface(pos), self))
 96                id += 1
 97
 98    def get_tile(self, gid):
 99        return self.tiles[gid - self.firstgid]
100
101
102class Tilesets(dict):
103    def add(self, tileset):
104        for i, tile in enumerate(tileset.tiles):
105            i += tileset.firstgid
106            self[i] = tile
107
108
109class Cell(object):
110    '''Layers are made of Cells (or empty space).
111
112    Cells have some basic properties:
113
114    x, y - the cell's index in the layer
115    px, py - the cell's pixel position
116    left, right, top, bottom - the cell's pixel boundaries
117
118    Additionally the cell may have other properties which are accessed using
119    standard dictionary methods:
120
121       cell['property name']
122
123    You may assign a new value for a property to or even delete an existing
124    property from the cell - this will not affect the Tile or any other Cells
125    using the Cell's Tile.
126    '''
127    def __init__(self, x, y, px, py, tile):
128        self.x, self.y = x, y
129        self.px, self.py = px, py
130        self.tile = tile
131        self.topleft = (px, py)
132        self.left = px
133        self.right = px + tile.tile_width
134        self.top = py
135        self.bottom = py + tile.tile_height
136        self.center = (px + tile.tile_width // 2, py + tile.tile_height // 2)
137        self._added_properties = {}
138        self._deleted_properties = set()
139
140    def __repr__(self):
141        return '<Cell %s,%s %d>' % (self.px, self.py, self.tile.gid)
142
143    def __contains__(self, key):
144        if key in self._deleted_properties:
145            return False
146        return key in self._added_properties or key in self.tile.properties
147
148    def __getitem__(self, key):
149        if key in self._deleted_properties:
150            raise KeyError(key)
151        if key in self._added_properties:
152            return self._added_properties[key]
153        if key in self.tile.properties:
154            return self.tile.properties[key]
155        raise KeyError(key)
156
157    def __setitem__(self, key, value):
158        self._added_properties[key] = value
159
160    def __delitem__(self, key):
161        self._deleted_properties.add(key)
162
163    def intersects(self, other):
164        '''Determine whether this Cell intersects with the other rect (which has
165        .x, .y, .width and .height attributes.)
166        '''
167        if self.px + self.tile.tile_width < other.x:
168            return False
169        if other.x + other.width < self.px:
170            return False
171        if self.py + self.tile.tile_height < other.y:
172            return False
173        if other.y + other.height < self.py:
174            return False
175        return True
176
177
178class LayerIterator(object):
179    '''Iterates over all the cells in a layer in column,row order.
180    '''
181    def __init__(self, layer):
182        self.layer = layer
183        self.i, self.j = 0, 0
184
185    def next(self):
186        if self.i == self.layer.width - 1:
187            self.j += 1
188            self.i = 0
189        if self.j == self.layer.height - 1:
190            raise StopIteration()
191        value = self.layer[self.i, self.j]
192        self.i += 1
193        return value
194
195
196class Layer(object):
197    '''A 2d grid of Cells.
198
199    Layers have some basic properties:
200
201        width, height - the dimensions of the Layer in cells
202        tile_width, tile_height - the dimensions of each cell
203        px_width, px_height - the dimensions of the Layer in pixels
204        tilesets - the tilesets used in this Layer (a Tilesets instance)
205        properties - any properties set for this Layer
206        cells - a dict of all the Cell instances for this Layer, keyed off
207                (x, y) index.
208
209    Additionally you may look up a cell using direct item access:
210
211       layer[x, y] is layer.cells[x, y]
212
213    Note that empty cells will be set to None instead of a Cell instance.
214    '''
215    def __init__(self, name, visible, map):
216        self.name = name
217        self.visible = visible
218        self.position = (0, 0)
219        # TODO get from TMX?
220        self.px_width = map.px_width
221        self.px_height = map.px_height
222        self.tile_width = map.tile_width
223        self.tile_height = map.tile_height
224        self.width = map.width
225        self.height = map.height
226        self.tilesets = map.tilesets
227        self.group = pygame.sprite.Group()
228        self.properties = {}
229        self.cells = {}
230
231    def __repr__(self):
232        return '<Layer "%s" at 0x%x>' % (self.name, id(self))
233
234    def __getitem__(self, pos):
235        return self.cells.get(pos)
236
237    def __setitem__(self, pos, tile):
238        x, y = pos
239        px = x * self.tile_width
240        py = y * self.tile_width
241        self.cells[pos] = Cell(x, y, px, py, tile)
242
243    def __iter__(self):
244        return LayerIterator(self)
245
246    @classmethod
247    def fromxml(cls, tag, map):
248        layer = cls(tag.attrib['name'], int(tag.attrib.get('visible', 1)), map)
249
250        data = tag.find('data')
251        if data is None:
252            raise ValueError('layer %s does not contain <data>' % layer.name)
253
254        data = data.text.strip()
255        data = data.decode('base64').decode('zlib')
256        data = struct.unpack('<%di' % (len(data)/4,), data)
257        assert len(data) == layer.width * layer.height
258        for i, gid in enumerate(data):
259            if gid < 1: continue   # not set
260            tile = map.tilesets[gid]
261            x = i % layer.width
262            y = i // layer.width
263            layer.cells[x,y] = Cell(x, y, x*map.tile_width, y*map.tile_height, tile)
264
265        return layer
266
267    def update(self, dt, *args):
268        pass
269
270    def set_view(self, x, y, w, h, viewport_ox=0, viewport_oy=0):
271        self.view_x, self.view_y = x, y
272        self.view_w, self.view_h = w, h
273        x -= viewport_ox
274        y -= viewport_oy
275        self.position = (x, y)
276
277    def draw(self, surface):
278        '''Draw this layer, limited to the current viewport, to the Surface.
279        '''
280        ox, oy = self.position
281        w, h = self.view_w, self.view_h
282        for x in range(ox, ox + w + self.tile_width, self.tile_width):
283            i = x // self.tile_width
284            for y in range(oy, oy + h + self.tile_height, self.tile_height):
285                j = y // self.tile_height
286                if (i, j) not in self.cells:
287                    continue
288                cell = self.cells[i, j]
289                surface.blit(cell.tile.surface, (cell.px - ox, cell.py - oy))
290
291    def find(self, *properties):
292        '''Find all cells with the given properties set.
293        '''
294        r = []
295        for propname in properties:
296            for cell in self.cells.values():
297                if cell and propname in cell:
298                    r.append(cell)
299        return r
300
301    def match(self, **properties):
302        '''Find all cells with the given properties set to the given values.
303        '''
304        r = []
305        for propname in properties:
306            for cell in self.cells.values():
307                if propname not in cell:
308                    continue
309                if properties[propname] == cell[propname]:
310                    r.append(cell)
311        return r
312
313    def collide(self, rect, propname):
314        '''Find all cells the rect is touching that have the indicated property
315        name set.
316        '''
317        r = []
318        for cell in self.get_in_region(rect.left, rect.top, rect.right,
319                rect.bottom):
320            if not cell.intersects(rect):
321                continue
322            if propname in cell:
323                r.append(cell)
324        return r
325
326    def get_in_region(self, x1, y1, x2, y2):
327        '''Return cells (in [column][row]) that are within the map-space
328        pixel bounds specified by the bottom-left (x1, y1) and top-right
329        (x2, y2) corners.
330
331        Return a list of Cell instances.
332        '''
333        i1 = max(0, x1 // self.tile_width)
334        j1 = max(0, y1 // self.tile_height)
335        i2 = min(self.width, x2 // self.tile_width + 1)
336        j2 = min(self.height, y2 // self.tile_height + 1)
337        return [self.cells[i, j]
338            for i in range(int(i1), int(i2))
339                for j in range(int(j1), int(j2))
340                    if (i, j) in self.cells]
341
342    def get_at(self, x, y):
343        '''Return the cell at the nominated (x, y) coordinate.
344
345        Return a Cell instance or None.
346        '''
347        i = x // self.tile_width
348        j = y // self.tile_height
349        return self.cells.get((i, j))
350
351    def neighbors(self, index):
352        '''Return the indexes of the valid (ie. within the map) cardinal (ie.
353        North, South, East, West) neighbors of the nominated cell index.
354
355        Returns a list of 2-tuple indexes.
356        '''
357        i, j = index
358        n = []
359        if i < self.width - 1:
360            n.append((i + 1, j))
361        if i > 0:
362            n.append((i - 1, j))
363        if j < self.height - 1:
364            n.append((i, j + 1))
365        if j > 0:
366            n.append((i, j - 1))
367        return n
368
369
370class Object(object):
371    '''An object in a TMX object layer.
372name: The name of the object. An arbitrary string.
373type: The type of the object. An arbitrary string.
374x: The x coordinate of the object in pixels.
375y: The y coordinate of the object in pixels.
376width: The width of the object in pixels (defaults to 0).
377height: The height of the object in pixels (defaults to 0).
378gid: An reference to a tile (optional).
379visible: Whether the object is shown (1) or hidden (0). Defaults to 1.
380    '''
381    def __init__(self, type, x, y, width=0, height=0, name=None,
382            gid=None, tile=None, visible=1):
383        self.type = type
384        self.px = x
385        self.left = x
386        if tile:
387            y -= tile.tile_height
388            width = tile.tile_width
389            height = tile.tile_height
390        self.py = y
391        self.top = y
392        self.width = width
393        self.right = x + width
394        self.height = height
395        self.bottom = y + height
396        self.name = name
397        self.gid = gid
398        self.tile = tile
399        self.visible = visible
400        self.properties = {}
401
402        self._added_properties = {}
403        self._deleted_properties = set()
404
405    def __repr__(self):
406        if self.tile:
407            return '<Object %s,%s %s,%s tile=%d>' % (self.px, self.py, self.width, self.height, self.gid)
408        else:
409            return '<Object %s,%s %s,%s>' % (self.px, self.py, self.width, self.height)
410
411    def __contains__(self, key):
412        if key in self._deleted_properties:
413            return False
414        if key in self._added_properties:
415            return True
416        if key in self.properties:
417            return True
418        return self.tile and key in self.tile.properties
419
420    def __getitem__(self, key):
421        if key in self._deleted_properties:
422            raise KeyError(key)
423        if key in self._added_properties:
424            return self._added_properties[key]
425        if key in self.properties:
426            return self.properties[key]
427        if self.tile and key in self.tile.properties:
428            return self.tile.properties[key]
429        raise KeyError(key)
430
431    def __setitem__(self, key, value):
432        self._added_properties[key] = value
433
434    def __delitem__(self, key):
435        self._deleted_properties.add(key)
436
437    def draw(self, surface, view_x, view_y):
438        if not self.visible:
439            return
440        x, y = (self.px - view_x, self.py - view_y)
441        if self.tile:
442            surface.blit(self.tile.surface, (x, y))
443        else:
444            r = pygame.Rect((x, y), (self.width, self.height))
445            pygame.draw.rect(surface, (255, 100, 100), r, 2)
446
447    @classmethod
448    def fromxml(cls, tag, map):
449        if 'gid' in tag.attrib:
450            gid = int(tag.attrib['gid'])
451            tile = map.tilesets[gid]
452            w = tile.tile_width
453            h = tile.tile_height
454        else:
455            gid = None
456            tile = None
457            w = int(tag.attrib['width'])
458            h = int(tag.attrib['height'])
459
460        o = cls(tag.attrib.get('type', 'rect'), int(tag.attrib['x']),
461            int(tag.attrib['y']), w, h, tag.attrib.get('name'), gid, tile,
462            int(tag.attrib.get('visible', 1)))
463
464        props = tag.find('properties')
465        if props is None:
466            return o
467
468        for c in props.findall('property'):
469            # store additional properties.
470            name = c.attrib['name']
471            value = c.attrib['value']
472
473            # TODO hax
474            if value.isdigit():
475                value = int(value)
476            o.properties[name] = value
477        return o
478
479    def intersects(self, x1, y1, x2, y2):
480        if x2 < self.px:
481            return False
482        if y2 < self.py:
483            return False
484        if x1 > self.px + self.width:
485            return False
486        if y1 > self.py + self.height:
487            return False
488        return True
489
490
491class ObjectLayer(object):
492    '''A layer composed of basic primitive shapes.
493
494    Actually encompasses a TMX <objectgroup> but even the TMX documentation
495    refers to them as object layers, so I will.
496
497    ObjectLayers have some basic properties:
498
499        position - ignored (cannot be edited in the current Tiled editor)
500        name - the name of the object group.
501        color - the color used to display the objects in this group.
502        opacity - the opacity of the layer as a value from 0 to 1.
503        visible - whether the layer is shown (1) or hidden (0).
504        objects - the objects in this Layer (Object instances)
505    '''
506    def __init__(self, name, color, objects, opacity=1,
507            visible=1, position=(0, 0)):
508        self.name = name
509        self.color = color
510        self.objects = objects
511        self.opacity = opacity
512        self.visible = visible
513        self.position = position
514        self.properties = {}
515
516    def __repr__(self):
517        return '<ObjectLayer "%s" at 0x%x>' % (self.name, id(self))
518
519    @classmethod
520    def fromxml(cls, tag, map):
521        layer = cls(tag.attrib['name'], tag.attrib.get('color'), [],
522            float(tag.attrib.get('opacity', 1)),
523            int(tag.attrib.get('visible', 1)))
524        for object in tag.findall('object'):
525            layer.objects.append(Object.fromxml(object, map))
526        for c in tag.findall('property'):
527            # store additional properties.
528            name = c.attrib['name']
529            value = c.attrib['value']
530
531            # TODO hax
532            if value.isdigit():
533                value = int(value)
534            layer.properties[name] = value
535        return layer
536
537    def update(self, dt, *args):
538        pass
539
540    def set_view(self, x, y, w, h, viewport_ox=0, viewport_oy=0):
541        self.view_x, self.view_y = x, y
542        self.view_w, self.view_h = w, h
543        x -= viewport_ox
544        y -= viewport_oy
545        self.position = (x, y)
546
547    def draw(self, surface):
548        '''Draw this layer, limited to the current viewport, to the Surface.
549        '''
550        if not self.visible:
551            return
552        ox, oy = self.position
553        w, h = self.view_w, self.view_h
554        for object in self.objects:
555            object.draw(surface, self.view_x, self.view_y)
556
557    def find(self, *properties):
558        '''Find all cells with the given properties set.
559        '''
560        r = []
561        for propname in properties:
562            for object in self.objects:
563                if object and propname in object or propname in self.properties:
564                    r.append(object)
565        return r
566
567    def match(self, **properties):
568        '''Find all objects with the given properties set to the given values.
569        '''
570        r = []
571        for propname in properties:
572            for object in self.objects:
573                if propname in object:
574                    val = object[propname]
575                elif propname in self.properties:
576                    val = self.properties[propname]
577                else:
578                    continue
579                if properties[propname] == val:
580                    r.append(object)
581        return r
582
583    def collide(self, rect, propname):
584        '''Find all objects the rect is touching that have the indicated
585        property name set.
586        '''
587        r = []
588        for object in self.get_in_region(rect.left, rect.top, rect.right,
589                rect.bottom):
590            if propname in object or propname in self.properties:
591                r.append(object)
592        return r
593
594    def get_in_region(self, x1, y1, x2, y2):
595        '''Return objects that are within the map-space
596        pixel bounds specified by the bottom-left (x1, y1) and top-right
597        (x2, y2) corners.
598
599        Return a list of Object instances.
600        '''
601        return [obj for obj in self.objects if obj.intersects(x1, y1, x2, y2)]
602
603    def get_at(self, x, y):
604        '''Return the first object found at the nominated (x, y) coordinate.
605
606        Return an Object instance or None.
607        '''
608        for object in self.objects:
609            if object.contains(x,y):
610                return object
611
612
613class SpriteLayer(pygame.sprite.AbstractGroup):
614    def __init__(self):
615        super(SpriteLayer, self).__init__()
616        self.visible = True
617
618    def set_view(self, x, y, w, h, viewport_ox=0, viewport_oy=0):
619        self.view_x, self.view_y = x, y
620        self.view_w, self.view_h = w, h
621        x -= viewport_ox
622        y -= viewport_oy
623        self.position = (x, y)
624
625    def draw(self, screen):
626        ox, oy = self.position
627        w, h = self.view_w, self.view_h
628        for sprite in self.sprites():
629            sx, sy = sprite.rect.topleft
630            screen.blit(sprite.image, (sx-ox, sy-oy))
631
632class Layers(list):
633    def __init__(self):
634        self.by_name = {}
635
636    def add_named(self, layer, name):
637        self.append(layer)
638        self.by_name[name] = layer
639
640    def __getitem__(self, item):
641        if isinstance(item, int):
642            return self[item]
643        return self.by_name[item]
644
645class TileMap(object):
646    '''A TileMap is a collection of Layers which contain gridded maps or sprites
647    which are drawn constrained by a viewport.
648
649    And breathe.
650
651    TileMaps are loaded from TMX files which sets the .layers and .tilesets
652    properties. After loading additional SpriteLayers may be added.
653
654    A TileMap's rendering is restricted by a viewport which is defined by the
655    size passed in at construction time and the focus set by set_focus() or
656    force_focus().
657
658    TileMaps have a number of properties:
659
660        width, height - the dimensions of the tilemap in cells
661        tile_width, tile_height - the dimensions of the cells in the map
662        px_width, px_height - the dimensions of the tilemap in pixels
663        properties - any properties set on the tilemap in the TMX file
664        layers - all layers of this tilemap as a Layers instance
665        tilesets - all tilesets of this tilemap as a Tilesets instance
666        fx, fy - viewport focus point
667        view_w, view_h - viewport size
668        view_x, view_y - viewport offset (origin)
669        viewport - a Rect instance giving the current viewport specification
670
671    '''
672    def __init__(self, size, origin=(0,0)):
673        self.px_width = 0
674        self.px_height = 0
675        self.tile_width = 0
676        self.tile_height = 0
677        self.width = 0
678        self.height  = 0
679        self.properties = {}
680        self.layers = Layers()
681        self.tilesets = Tilesets()
682        self.fx, self.fy = 0, 0             # viewport focus point
683        self.view_w, self.view_h = size     # viewport size
684        self.view_x, self.view_y = origin   # viewport offset
685        self.viewport = Rect(origin, size)
686
687    def update(self, dt, *args):
688        for layer in self.layers:
689            layer.update(dt, *args)
690
691    def draw(self, screen):
692        for layer in self.layers:
693            if layer.visible:
694                layer.draw(screen)
695
696    @classmethod
697    def load(cls, filename, viewport):
698        with open(filename) as f:
699            map = ElementTree.fromstring(f.read())
700
701        # get most general map informations and create a surface
702        tilemap = TileMap(viewport)
703        tilemap.width = int(map.attrib['width'])
704        tilemap.height  = int(map.attrib['height'])
705        tilemap.tile_width = int(map.attrib['tilewidth'])
706        tilemap.tile_height = int(map.attrib['tileheight'])
707        tilemap.px_width = tilemap.width * tilemap.tile_width
708        tilemap.px_height = tilemap.height * tilemap.tile_height
709
710        for tag in map.findall('tileset'):
711            tilemap.tilesets.add(Tileset.fromxml(tag))
712
713        for tag in map.findall('layer'):
714            layer = Layer.fromxml(tag, tilemap)
715            tilemap.layers.add_named(layer, layer.name)
716
717        for tag in map.findall('objectgroup'):
718            layer = ObjectLayer.fromxml(tag, tilemap)
719            tilemap.layers.add_named(layer, layer.name)
720
721        return tilemap
722
723    _old_focus = None
724    def set_focus(self, fx, fy, force=False):
725        '''Determine the viewport based on a desired focus pixel in the
726        Layer space (fx, fy) and honoring any bounding restrictions of
727        child layers.
728
729        The focus will always be shifted to ensure no child layers display
730        out-of-bounds data, as defined by their dimensions px_width and px_height.
731        '''
732        # The result is that all chilren will have their viewport set, defining
733        # which of their pixels should be visible.
734        fx, fy = int(fx), int(fy)
735        self.fx, self.fy = fx, fy
736
737        a = (fx, fy)
738
739        # check for NOOP (same arg passed in)
740        if not force and self._old_focus == a:
741            return
742        self._old_focus = a
743
744        # get our viewport information, scaled as appropriate
745        w = int(self.view_w)
746        h = int(self.view_h)
747        w2, h2 = w//2, h//2
748
749        if self.px_width <= w:
750            # this branch for centered view and no view jump when
751            # crossing the center; both when world width <= view width
752            restricted_fx = self.px_width / 2
753        else:
754            if (fx - w2) < 0:
755                restricted_fx = w2       # hit minimum X extent
756            elif (fx + w2) > self.px_width:
757                restricted_fx = self.px_width - w2       # hit maximum X extent
758            else:
759                restricted_fx = fx
760        if self.px_height <= h:
761            # this branch for centered view and no view jump when
762            # crossing the center; both when world height <= view height
763            restricted_fy = self.px_height / 2
764        else:
765            if (fy - h2) < 0:
766                restricted_fy = h2       # hit minimum Y extent
767            elif (fy + h2) > self.px_height:
768                restricted_fy = self.px_height - h2       # hit maximum Y extent
769            else:
770                restricted_fy = fy
771
772        # ... and this is our focus point, center of screen
773        self.restricted_fx = int(restricted_fx)
774        self.restricted_fy = int(restricted_fy)
775
776        # determine child view bounds to match that focus point
777        x, y = int(restricted_fx - w2), int(restricted_fy - h2)
778        self.viewport.x = x
779        self.viewport.y = y
780
781        self.childs_ox = x - self.view_x
782        self.childs_oy = y - self.view_y
783
784        for layer in self.layers:
785            layer.set_view(x, y, w, h, self.view_x, self.view_y)
786
787    def force_focus(self, fx, fy):
788        '''Force the manager to focus on a point, regardless of any managed layer
789        visible boundaries.
790
791        '''
792        # This calculation takes into account the scaling of this Layer (and
793        # therefore also its children).
794        # The result is that all chilren will have their viewport set, defining
795        # which of their pixels should be visible.
796        self.fx, self.fy = map(int, (fx, fy))
797        self.fx, self.fy = fx, fy
798
799        # get our view size
800        w = int(self.view_w)
801        h = int(self.view_h)
802        w2, h2 = w//2, h//2
803
804        # bottom-left corner of the viewport
805        x, y = fx - w2, fy - h2
806        self.viewport.x = x
807        self.viewport.y = y
808
809        self.childs_ox = x - self.view_x
810        self.childs_oy = y - self.view_y
811
812        for layer in self.layers:
813            layer.set_view(x, y, w, h, self.view_x, self.view_y)
814
815    def pixel_from_screen(self, x, y):
816        '''Look up the Layer-space pixel matching the screen-space pixel.
817        '''
818        vx, vy = self.childs_ox, self.childs_oy
819        return int(vx + x), int(vy + y)
820
821    def pixel_to_screen(self, x, y):
822        '''Look up the screen-space pixel matching the Layer-space pixel.
823        '''
824        screen_x = x-self.childs_ox
825        screen_y = y-self.childs_oy
826        return int(screen_x), int(screen_y)
827
828    def index_at(self, x, y):
829        '''Return the map index at the (screen-space) pixel position.
830        '''
831        sx, sy = self.pixel_from_screen(x, y)
832        return int(sx//self.tile_width), int(sy//self.tile_height)
833
834def load(filename, viewport):
835    return TileMap.load(filename, viewport)
836
837if __name__ == '__main__':
838    # allow image load to work
839    pygame.init()
840    pygame.display.set_mode((640, 480))
841    t = load(sys.argv[1], (0, 0))