summaryrefslogtreecommitdiff
path: root/external/pytmx/util_pygame.py
blob: 7744e80ff9e53063218cf8b5ee8aefe37ddd7ab8 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
import logging
import itertools
import pytmx

logger = logging.getLogger(__name__)
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
logger.addHandler(ch)
logger.setLevel(logging.INFO)

try:
    from pygame.transform import flip, rotate
    import pygame
except ImportError:
    logger.error('cannot import pygame (is it installed?)')
    raise

__all__ = ['load_pygame', 'pygame_image_loader', 'simplify', 'build_rects']


def handle_transformation(tile, flags):
    if flags.flipped_diagonally:
        tile = flip(rotate(tile, 270), 1, 0)
    if flags.flipped_horizontally or flags.flipped_vertically:
        tile = flip(tile, flags.flipped_horizontally, flags.flipped_vertically)
    return tile


def smart_convert(original, colorkey, pixelalpha):
    """
    this method does several tests on a surface to determine the optimal
    flags and pixel format for each tile surface.

    this is done for the best rendering speeds and removes the need to
    convert() the images on your own
    """
    tile_size = original.get_size()
    threshold = 127   # the default

    # count the number of pixels in the tile that are not transparent
    px = pygame.mask.from_surface(original, threshold).count()

    # there are no transparent pixels in the image
    if px == tile_size[0] * tile_size[1]:
        tile = original.convert()

    # there are transparent pixels, and tiled set a colorkey
    elif colorkey:
        tile = original.convert()
        tile.set_colorkey(colorkey, pygame.RLEACCEL)

    # there are transparent pixels, and set for perpixel alpha
    elif pixelalpha:
        tile = original.convert_alpha()

    # there are transparent pixels, and we won't handle them
    else:
        tile = original.convert()

    return tile


def pygame_image_loader(filename, colorkey, **kwargs):
    """ pytmx image loader for pygame

    :param filename:
    :param colorkey:
    :param kwargs:
    :return:
    """
    if colorkey:
        colorkey = pygame.Color('#{0}'.format(colorkey))

    pixelalpha = kwargs.get('pixelalpha', True)
    image = pygame.image.load(filename)

    def load_image(rect=None, flags=None):
        if rect:
            try:
                tile = image.subsurface(rect)
            except ValueError:
                logger.error('Tile bounds outside bounds of tileset image')
                raise
        else:
            tile = image.copy()

        if flags:
            tile = handle_transformation(tile, flags)

        tile = smart_convert(tile, colorkey, pixelalpha)
        return tile

    return load_image


def load_pygame(filename, *args, **kwargs):
    """ Load a TMX file, images, and return a TiledMap class

    PYGAME USERS: Use me.

    this utility has 'smart' tile loading.  by default any tile without
    transparent pixels will be loaded for quick blitting.  if the tile has
    transparent pixels, then it will be loaded with per-pixel alpha.  this is
    a per-tile, per-image check.

    if a color key is specified as an argument, or in the tmx data, the
    per-pixel alpha will not be used at all. if the tileset's image has colorkey
    transparency set in Tiled, the util_pygam will return images that have their
    transparency already set.

    TL;DR:
    Don't attempt to convert() or convert_alpha() the individual tiles.  It is
    already done for you.
    """
    kwargs['image_loader'] = pygame_image_loader
    return pytmx.TiledMap(filename, *args, **kwargs)


def build_rects(tmxmap, layer, tileset=None, real_gid=None):
    """generate a set of non-overlapping rects that represents the distribution
       of the specified gid.

    useful for generating rects for use in collision detection

    Use at your own risk: this is experimental...will change in future

    GID Note: You will need to add 1 to the GID reported by Tiled.

    :param tmxmap: TiledMap object
    :param layer: int or string name of layer
    :param tileset: int or string name of tileset
    :param real_gid: Tiled GID of the tile + 1 (see note)
    :return: List of pygame Rect objects
    """
    if isinstance(tileset, int):
        try:
            tileset = tmxmap.tilesets[tileset]
        except IndexError:
            msg = "Tileset #{0} not found in map {1}."
            logger.debug(msg.format(tileset, tmxmap))
            raise IndexError

    elif isinstance(tileset, str):
        try:
            tileset = [t for t in tmxmap.tilesets if t.name == tileset].pop()
        except IndexError:
            msg = "Tileset \"{0}\" not found in map {1}."
            logger.debug(msg.format(tileset, tmxmap))
            raise ValueError

    elif tileset:
        msg = "Tileset must be either a int or string. got: {0}"
        logger.debug(msg.format(type(tileset)))
        raise TypeError

    gid = None
    if real_gid:
        try:
            gid, flags = tmxmap.map_gid(real_gid)[0]
        except IndexError:
            msg = "GID #{0} not found"
            logger.debug(msg.format(real_gid))
            raise ValueError

    if isinstance(layer, int):
        layer_data = tmxmap.get_layer_data(layer)
    elif isinstance(layer, str):
        try:
            layer = [l for l in tmxmap.layers if l.name == layer].pop()
            layer_data = layer.data
        except IndexError:
            msg = "Layer \"{0}\" not found in map {1}."
            logger.debug(msg.format(layer, tmxmap))
            raise ValueError

    p = itertools.product(range(tmxmap.width), range(tmxmap.height))
    if gid:
        points = [(x, y) for (x, y) in p if layer_data[y][x] == gid]
    else:
        points = [(x, y) for (x, y) in p if layer_data[y][x]]

    rects = simplify(points, tmxmap.tilewidth, tmxmap.tileheight)
    return rects


def simplify(all_points, tilewidth, tileheight):
    """Given a list of points, return list of rects that represent them
    kludge:

    "A kludge (or kluge) is a workaround, a quick-and-dirty solution,
    a clumsy or inelegant, yet effective, solution to a problem, typically
    using parts that are cobbled together."

    -- wikipedia

    turn a list of points into a rects
    adjacent rects will be combined.

    plain english:
        the input list must be a list of tuples that represent
        the areas to be combined into rects
        the rects will be blended together over solid groups

        so if data is something like:

        0 1 1 1 0 0 0
        0 1 1 0 0 0 0
        0 0 0 0 0 4 0
        0 0 0 0 0 4 0
        0 0 0 0 0 0 0
        0 0 1 1 1 1 1

        you'll have the 4 rects that mask the area like this:

        ..######......
        ..####........
        ..........##..
        ..........##..
        ..............
        ....##########

        pretty cool, right?

    there may be cases where the number of rectangles is not as low as possible,
    but I haven't found that it is excessively bad.  certainly much better than
    making a list of rects, one for each tile on the map!
    """
    def pick_rect(points, rects):
        ox, oy = sorted([(sum(p), p) for p in points])[0][1]
        x = ox
        y = oy
        ex = None

        while 1:
            x += 1
            if not (x, y) in points:
                if ex is None:
                    ex = x - 1

                if (ox, y + 1) in points:
                    if x == ex + 1:
                        y += 1
                        x = ox

                    else:
                        y -= 1
                        break
                else:
                    if x <= ex: y -= 1
                    break

        c_rect = pygame.Rect(ox * tilewidth, oy * tileheight,
                             (ex - ox + 1) * tilewidth,
                             (y - oy + 1) * tileheight)

        rects.append(c_rect)

        rect = pygame.Rect(ox, oy, ex - ox + 1, y - oy + 1)
        kill = [p for p in points if rect.collidepoint(p)]
        [points.remove(i) for i in kill]

        if points:
            pick_rect(points, rects)

    rect_list = []
    while all_points:
        pick_rect(all_points, rect_list)

    return rect_list