├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── dub.sdl
├── dub.selections.json
├── rebuild_examples.sh
├── src
└── dtiled
│ ├── algorithm.d
│ ├── coords.d
│ ├── data.d
│ ├── grid.d
│ ├── map.d
│ └── package.d
├── tests
├── data.d
└── resources
│ ├── flipped_objects.json
│ ├── flipped_objects.tmx
│ ├── flipped_tiles.json
│ ├── flipped_tiles.tmx
│ ├── numbers.png
│ ├── objects.json
│ ├── objects.tmx
│ ├── spaced-tiles.png
│ ├── spaced_tiles.tmx
│ ├── terrain.png
│ ├── tiles.json
│ └── tiles.tmx
└── todo.txt
/.gitignore:
--------------------------------------------------------------------------------
1 | .dub
2 | docs.json
3 | __dummy.html
4 | *.o
5 | *.obj
6 | *.a
7 | *__unittest__
8 | docs/
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 rcorre
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | SETCOMPILER=
2 | ifdef DC
3 | SETCOMPILER="--compiler=$(DC)"
4 | endif
5 |
6 | all: debug
7 |
8 | debug:
9 | @dub build $(SETCOMPILER) --build=debug --quiet
10 |
11 | release:
12 | @dub build $(SETCOMPILER) release --quiet
13 |
14 | test:
15 | @dub test $(SETCOMPILER)
16 |
17 | .PHONY: docs
18 | docs:
19 | @dub build $(SETCOMPILER) --build=ddox --quiet
20 |
21 | clean:
22 | @dub clean
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | DTiled: D language Tiled map parser.
2 | ===
3 |
4 | [](https://gitter.im/rcorre/dtiled)
5 | [](http://code.dlang.org/packages/dtiled)
6 |
7 | Do you like tilemap-based games?
8 | How about the D programming language?
9 | Would you like to make a tilemapped game in D?
10 |
11 | If so, you'll probably need to implement a lot of commonly-needed tilemap
12 | functionality, like mapping between screen coordinates and grid coordinates,
13 | iterating through groups of tiles, loading a map from a file, and more.
14 |
15 | I've spent enough time re-implementing or copying my tilemap logic between game
16 | projects that I decided to factor it out into a library.
17 | Here's a quick overview of what dtiled can do:
18 |
19 | ```d
20 | // row/col coordinates are explicitly specified to avoid nasty bugs
21 | tilemap.tileAt(RowCol(2,3));
22 |
23 | // convert between 'grid' (row/column) and 'pixel' (x/y) coordinates
24 | auto tileUnderMouse = tilemap.tileAtPoint(myGameEngine.mousePos);
25 | auto pos = tilemap.tileCenter(RowCol(3,2)).as!MyVectorType;
26 |
27 | // no nested for loops! Conveniently foreach over all tiles in a map:
28 | foreach(coord, ref tile ; tilemap) {
29 | tile.awesomeness += 10; // if your tiles are structs, use ref to modify them
30 | }
31 |
32 | // need a range? Use `allTiles` or `allCoords`.
33 | auto tileRange = tilemap.allTiles.filter!(x => someCondition(x));
34 |
35 | // finding the neighbors of a tile is an oft-needed task
36 | auto adjacent = tilemap.adjacentTiles(RowCol(3,3));
37 | auto surrounding = tilemap.adjacentTiles(RowCol(3,3), Diagonals.yes);
38 |
39 | // use masks for grabbing tiles in some pattern
40 | uint[3][3] newWallShape = [
41 | [ 1, 1, 1 ],
42 | [ 0, 1, 0 ],
43 | [ 0, 1, 0 ],
44 | ];
45 |
46 | bool blocked = tilemap
47 | .maskTilesAround(RowCol(5,5), newWallShape)
48 | .any!(x => x.obstructed);
49 |
50 | // load data for maps created with the Tiled map editor
51 | auto mapData = MapData.load("map.json");
52 | foreach(gid ; mapData.getLayer("ground").data) { ... }
53 | ```
54 |
55 | To see the full suite of features offered by dtiled, check out the
56 | [docs](http://rcorre.github.io/dtiled/index.html).
57 |
58 | If you're more of the 'see things in action' type, check out the
59 | [Demo](https://github.com/rcorre/dtiled-example). It uses either
60 | [Allegro](http://code.dlang.org/packages/allegro)
61 | or
62 | [DGame](http://code.dlang.org/packages/dgame)
63 | to pop up a window and render a pretty tilemap you can play around with.
64 |
65 | If you're still here, keep on reading for a 'crash course' in using dtiled.
66 | I'll try to keep it terse, but there's a fair amount to cover here, so bear with
67 | me.
68 |
69 | # Picking a game engine
70 | First things first, dtiled is not a game engine. It cares only about tiles and
71 | maps, and knows nothing about rendering or input events. I expect you already
72 | have an engine of choice for your game. dtiled strives to integrate with your
73 | engine's functionality rather than replace it.
74 |
75 | If you don't have an engine, here are a few options:
76 | - [Allegro](http://code.dlang.org/packages/allegro)
77 | - [SDL](http://code.dlang.org/packages/derelict-sdl2)
78 | - [SFML](http://code.dlang.org/packages/dsfml)
79 | - [DGame](http://code.dlang.org/packages/dgame)
80 |
81 | The following examples assume you or your engine provide:
82 |
83 | - `Vector2!T`: A point with numeric `x` and `y` components
84 | - `Rect2!T`: A rectangle with numeric `x`, `y`, `width`, and `height` components
85 | - `drawBitmapRegion(bmp, pos, rect)`: draw a rectangular subsection `rect` of a
86 | bitmap/spritesheet `bmp` to the screen at Vector position `pos`
87 | - `getMousePos()`: Get the current mouse position as a Vector.
88 |
89 | # Defining our Tile
90 | We'll need to define a type that represents a single tile within the tilemap.
91 |
92 | ```d
93 | struct Tile { Rect2!int spriteRect; }
94 | ```
95 |
96 | Well, that was simple. For now, our tile just defines the rectangular region of
97 | a bitmap that should be used to render this tile. That bitmap will be our 'tile
98 | atlas'; something like this:
99 |
100 |
101 |
102 | # Loading a map
103 | Unless your map is procedurally generated, you will probably want to create map
104 | files with some application and load them in your game.
105 |
106 | DTiled can help you load maps created with [Tiled](http://mapeditor.org), a
107 | popular open-source tilemap editor.
108 |
109 | Create your map in Tiled, export it to json using the in-editor menu or the
110 | `--export-map` command-line switch, and use `dtiled.data` to interpret the json
111 | map file. Currently DTiled only supports Tiled's JSON format. Support for tmx
112 | and csv may be added, but the json format should provide all necessary
113 | information.
114 |
115 | For now, lets say your map has a single tile layer named 'ground'.
116 | Lets take the data in that file and build an in-game map structure.
117 |
118 | ```d
119 | auto loadMap(string path) {
120 | auto mapData = MapData.load(path);
121 |
122 | auto buildTile(TiledGid gid) {
123 | // each GID uniquely maps to a single tile within a single tileset
124 | // in our case, this will always return our only have a single tileset
125 | // if you use more than one tileset, this will choose the appropriate
126 | // tileset based on the GID.
127 | auto tileset = mapData.getTileset(gid);
128 |
129 | // find which region in the 'tile atlas' this GID is mapped to.
130 | // this will be the `region` argument to our `drawBitmapRegion` function
131 | auto region = Rect2!int(tileset.tileOffsetX(gid),
132 | tileset.tileOffsetY(gid),
133 | tileset.tileWidth,
134 | tileset.tileHeight);
135 |
136 | return Tile(region);
137 | }
138 |
139 | auto tiles = mapData
140 | .getLayer("ground") // grab the layer named ground
141 | .data // iterate over the GIDs in that layer
142 | .map!(x => buildTile(x)) // build a Tile based on the GID
143 | .chunks(mapData.numCols) // chunk into rows
144 | .map!(x => x.array) // create an array from each row
145 | .array; // create an array of all the row arrays
146 |
147 | // our map wraps the 2D tile array, also storing information about tile size.
148 | return OrthoMap!Tile(tiles, mapData.tileWidth, mapData.tileHeight);
149 | }
150 | ```
151 |
152 | `OrthoMap!T` is a type dtiled provides to represent an 'Orthogonal' map. Later
153 | versions of dtiled may also support isometric and hexagonal maps, but this will
154 | do for our needs.
155 |
156 | # Dealing with on-screen positions
157 | Now that we know which region of our tile atlas each tile should be drawn with
158 | (the `Tile.spriteRect` field we populated when loading the map), rendering the
159 | map just requires us to know which position each tile should be rendered to the
160 | screen at. Fortunately, our `OrthoMap` can translate grid coordinates to
161 | on-screen positions:
162 |
163 | ```d
164 | Bitmap tileAtlas; // assume we load our tile sheet at some point
165 |
166 | void drawMap(OrthoMap!Tile tileMap) {
167 | foreach(coord, tile ; tileMap) {
168 | // you could use tileCenter to get the offset of the tile's center instead
169 | auto topLeft = tileMap.tileOffset(coord).as!(Vector2!int);
170 | drawBitmap(tileAtlas, topLeft, tile.spriteRect);
171 | }
172 | }
173 | ```
174 |
175 | Remember that `Vector2!T` is a type I am assuming you or your game library
176 | provides. `tileOffset` (and `tileCenter`) return a simple (x,y) tuple, and
177 | `dtiled.coords` provides a helper `as!T` to convert it to a vector type of your
178 | choice.
179 |
180 | We can also go the other way and find out which coordinate or tile lies at a
181 | given screen position. This is useful for, say, figuring out which tile is under
182 | the player's mouse:
183 |
184 | ```d
185 | auto tileUnderMouse = tileMap.tileAtPoint(mousePos);
186 | // or
187 | auto coordUnderMouse = tileMap.coordAtPoint(mousePos);
188 | ```
189 |
190 | Just as `tileOffset` returns a general 'vector-ish' type `tileAtPoint` will
191 | accept anything vector-like as an argument (anything that has a numeric `x` or
192 | `y` component). DTiled tries not to make too many assumptions about the types
193 | you want to use.
194 |
195 | # The Grid
196 | A grid is a thin wrapper around a 2D array that enforces `RowCol`-based access
197 | and provides grid-related functionality.
198 |
199 | The `OrthoMap` we created earlier supports all of this as it is a wrapper around
200 | a `RectGrid`, but you can apply these same functions to any 2D array by wrapping
201 | it with `rectGrid`.
202 |
203 | The simplest grid operation is to access a tile by its coordinate:
204 |
205 | ```d
206 | auto tile = tileMap.tileAt(RowCol(2,3)); // grab the tile at row 2, column 3
207 | ```
208 |
209 | Now, you may be wondering how `grid.tileAt(RowCol(r,c))` is any different from
210 | `grid[r][c]`. The answer is, its not. At least, not until you get a bit tired
211 | and type `grid[c][r]`. The use of `RowCol` as an index throughout dtiled strives
212 | to avoid these annoying mistakes. As a bonus, `RowCol` is a nice way to pass
213 | around coordinate pairs and provides a few other benefits:
214 |
215 | ```d
216 | RowCol(2,3).south(5) // (7,3)
217 | RowCol(2,3).south.east // (3,4)
218 | RowCol(2,3).adjacent // [ (1,3), (2,2), (2,4), (3,3) ]
219 | RowCol(0,0).span(RowCol(2,2)) // [ (0,0), (0,1), (1,0), (1,1) ]
220 | ```
221 |
222 | Here are some other useful things you can do with a grid:
223 |
224 | ```d
225 | auto neighbors = grid.adjacentTiles(RowCol(2,3));
226 | auto surrounding = grid.adjacentTiles(RowCol(2,3), Diagonals.yes);
227 | auto coords = grid.adjacentCoords(RowCol(2,3)); // coords instead of tiles
228 | ```
229 |
230 | Nice, but still pretty standard fare. What if you need to select tiles with a
231 | bit more finesse?
232 |
233 | Suppose you are making a game where the player can place walls of various
234 | shapes. The player wants to place an 'L' shaped wall at the coordinate (5,3),
235 | and you need to know if every tile the wall would cover is currently empty:
236 |
237 | ```d
238 | uint[3][3] mask = [
239 | [ 0, 1, 0 ]
240 | [ 0, 1, 1 ]
241 | [ 0, 0, 0 ]
242 | ];
243 |
244 | auto tilesUnderWall = grid.maskTilesAround(RowCol(5,3), mask);
245 | bool canPlaceWall = tilesUnderWall.all!(x => !x.hasObstruction);
246 | ```
247 |
248 | `OrthoMap` supports all of this functionality as it is a wrapper around a
249 | `RectGrid`.
250 |
251 | # Algorithms
252 | Most of the above was pretty mundane, so lets break out `dtiled.algorithm`.
253 |
254 | Suppose your map has some walls on it, represented by a `hasWall` field on your
255 | `Tile`. You want to know if the player is standing in a 'room' entirely enclosed
256 | by walls:
257 |
258 | ```d
259 | auto room = map.enclosedTiles!(x => x.hasWall)(playerCoord);
260 | if (!room.empty) // player is in a room
261 | ```
262 |
263 | A more general function is `floodTiles`, which returns a range that lazily
264 | flood-fills tiles meeting a certain condition. By contrast, `enclosedTiles` is
265 | evaluated eagerly to determine if the area is totally enclosed.
266 |
267 | gs sometimes it is useful to get coordinates instead of tiles, most functions
268 | that yield tiles have a counterpart that yields coordinates. Instead of
269 | `floodTiles` and `enclosedTiles`, you could use `floodCoords` and
270 | `enclosedCoords`.
271 |
--------------------------------------------------------------------------------
/dub.sdl:
--------------------------------------------------------------------------------
1 | name "dtiled"
2 | description "Parse Tiled map files"
3 | copyright "Copyright © 2015, rcorre"
4 | license "MIT"
5 | authors "rcorre"
6 |
7 | dependency "jsonizer" version="~>0.7.7"
8 |
9 | configuration "library" {
10 | targetType "library"
11 | }
12 |
13 | configuration "unittest" {
14 | targetType "library"
15 | importPaths "src" "tests"
16 | sourcePaths "src" "tests"
17 | }
18 |
--------------------------------------------------------------------------------
/dub.selections.json:
--------------------------------------------------------------------------------
1 | {
2 | "fileVersion": 1,
3 | "versions": {
4 | "jsonizer": "0.7.7"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/rebuild_examples.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | MAPDIR="./tests/resources"
4 |
5 | for file in $MAPDIR/*.tmx;
6 | do
7 | target="${file%.tmx}.json"
8 | echo "$file -> $target"
9 | tiled --export-map $file $target
10 | done
11 |
--------------------------------------------------------------------------------
/src/dtiled/algorithm.d:
--------------------------------------------------------------------------------
1 | /**
2 | * Provides various useful operations on a tile grid.
3 | */
4 | module dtiled.algorithm;
5 |
6 | import std.range;
7 | import std.typecons : Tuple;
8 | import std.algorithm;
9 | import std.container : Array, SList;
10 | import dtiled.coords : RowCol, Diagonals;
11 | import dtiled.grid;
12 |
13 | /// Same as enclosedTiles, but return coords instead of tiles
14 | auto enclosedCoords(alias isWall, T)(T grid, RowCol origin, Diagonals diags = Diagonals.no)
15 | if (is(typeof(isWall(grid.tileAt(RowCol(0,0)))) : bool))
16 | {
17 | // track whether we have hit the edge of the map
18 | bool hitEdge;
19 |
20 | // keep a flag for each tile to mark which have been visited
21 | Array!bool visited;
22 | visited.length = grid.numRows * grid.numCols;
23 |
24 | // visited[] index for a (row,col) pair
25 | auto coordToIdx(RowCol coord) {
26 | return coord.row * grid.numCols + coord.col;
27 | }
28 |
29 | // row/col coord for a visited[] index
30 | auto idxToCoord(size_t idx) {
31 | return RowCol(idx / grid.numCols, idx % grid.numCols);
32 | }
33 |
34 | bool outOfBounds(RowCol coord) {
35 | return coord.row < 0 || coord.col < 0 || coord.row >= grid.numRows || coord.col >= grid.numCols;
36 | }
37 |
38 | void flood(RowCol coord) {
39 | auto idx = coordToIdx(coord);
40 | hitEdge = hitEdge || outOfBounds(coord);
41 |
42 | // break this recursive branch if we hit an edge or a visited or invalid tile.
43 | if (hitEdge || visited[idx] || isWall(grid.tileAt(coord))) return;
44 |
45 | visited[idx] = true;
46 |
47 | // recurse into neighboring tiles
48 | foreach(neighbor ; coord.adjacent(diags)) flood(neighbor);
49 | }
50 |
51 | // start the flood at the origin tile
52 | flood(origin);
53 |
54 | return visited[]
55 | .enumerate // pair each bool with an index
56 | .filter!(pair => pair.value) // keep only the visited nodes
57 | .map!(pair => idxToCoord(pair.index)) // grab the tile for each visited node
58 | .take(hitEdge ? 0 : size_t.max); // empty range if edge of map was touched
59 | }
60 |
61 | /**
62 | * Find an area of tiles enclosed by 'walls'.
63 | *
64 | * Params:
65 | * isWall = predicate which returns true if a tile should be considered a 'wall'
66 | * grid = grid of tiles to find enclosed area in
67 | * origin = tile that may be part of an enclosed region
68 | * diags = if yes, an area is not considered enclosed if there is a diagonal opening.
69 | *
70 | * Returns: a range of tiles in the enclosure (empty if origin is not part of an enclosed region)
71 | */
72 | auto enclosedTiles(alias isWall, T)(T grid, RowCol origin, Diagonals diags = Diagonals.no)
73 | if (is(typeof(isWall(grid.tileAt(RowCol(0,0)))) : bool))
74 | {
75 | return enclosedCoords!isWall(grid, origin, diags).map!(x => grid.tileAt(x));
76 | }
77 |
78 | ///
79 | unittest {
80 | import std.array;
81 | import std.algorithm : equal;
82 |
83 | // let the 'X's represent 'walls', and the other letters 'open' areas we'd link to identify
84 | auto tiles = rectGrid([
85 | // 0 1 2 3 4 5 <-col| row
86 | [ 'X', 'X', 'X', 'X', 'X', 'X' ], // 0
87 | [ 'X', 'a', 'a', 'X', 'b', 'X' ], // 1
88 | [ 'X', 'a', 'a', 'X', 'b', 'X' ], // 2
89 | [ 'X', 'X', 'X', 'X', 'X', 'X' ], // 3
90 | [ 'd', 'd', 'd', 'X', 'c', 'X' ], // 4
91 | [ 'd', 'd', 'd', 'X', 'X', 'c' ], // 5
92 | ]);
93 |
94 | static bool isWall(char c) { return c == 'X'; }
95 |
96 | // starting on a wall should return an empty result
97 | assert(tiles.enclosedTiles!isWall(RowCol(0, 0)).empty);
98 |
99 | // all tiles in the [1,1] -> [2,2] area should find the 'a' room
100 | assert(tiles.enclosedTiles!isWall(RowCol(1, 1)).equal(['a', 'a', 'a', 'a']));
101 | assert(tiles.enclosedTiles!isWall(RowCol(1, 2)).equal(['a', 'a', 'a', 'a']));
102 | assert(tiles.enclosedTiles!isWall(RowCol(2, 1)).equal(['a', 'a', 'a', 'a']));
103 | assert(tiles.enclosedTiles!isWall(RowCol(2, 2)).equal(['a', 'a', 'a', 'a']));
104 |
105 | // get the two-tile 'b' room at [1,4] -> [2,4]
106 | assert(tiles.enclosedTiles!isWall(RowCol(1, 4)).equal(['b', 'b']));
107 | assert(tiles.enclosedTiles!isWall(RowCol(2, 4)).equal(['b', 'b']));
108 |
109 | // get the single tile 'c' room at 4,4
110 | assert(tiles.enclosedTiles!isWall(RowCol(4, 4)).equal(['c']));
111 | // if we require that diagonals be blocked too, 'c' is not an enclosure
112 | assert(tiles.enclosedTiles!isWall(RowCol(4, 4), Diagonals.yes).empty);
113 |
114 | // the 'd' region is not an enclosure (touches map edge)
115 | assert(tiles.enclosedTiles!isWall(RowCol(4, 1)).empty);
116 | assert(tiles.enclosedTiles!isWall(RowCol(5, 0)).empty);
117 | }
118 |
119 | /// Same as floodTiles, but return coordinates instead of the tiles at those coordinates.
120 | auto floodCoords(alias pred, T)(T grid, RowCol origin, Diagonals diags = Diagonals.no)
121 | if (is(typeof(pred(grid.tileAt(RowCol(0,0)))) : bool))
122 | {
123 | struct Result {
124 | private {
125 | T _grid;
126 | SList!RowCol _stack;
127 | Array!bool _visited;
128 |
129 | // helpers to translate between the 2D grid coordinate space and the 1D visited array
130 | bool getVisited(RowCol coord) {
131 | auto idx = coord.row * grid.numCols + coord.col;
132 | return _visited[idx];
133 | }
134 |
135 | void setVisited(RowCol coord) {
136 | auto idx = coord.row * grid.numCols + coord.col;
137 | _visited[idx] = true;
138 | }
139 |
140 | // true if front is out of bounds, already visited, or does not meet the predicate
141 | bool shouldSkipFront() {
142 | return !_grid.contains(front) || getVisited(front) || !pred(_grid.tileAt(front));
143 | }
144 | }
145 |
146 | this(T grid, RowCol origin) {
147 | _grid = grid;
148 | _visited.length = grid.numRows * grid.numCols; // one visited entry for each tile
149 |
150 | // push the first tile onto the stack only if it meets the predicate
151 | if (pred(grid.tileAt(origin))) {
152 | _stack.insertFront(origin);
153 | }
154 | }
155 |
156 | @property auto front() { return _stack.front; }
157 | @property bool empty() { return _stack.empty; }
158 |
159 | void popFront() {
160 | // copy the current coord before we pop it
161 | auto coord = front;
162 |
163 | // mark that the current coord was visited and pop it
164 | setVisited(coord);
165 | _stack.removeFront();
166 |
167 | // push neighboring coords onto the stack
168 | foreach(neighbor ; coord.adjacent(diags)) { _stack.insert(neighbor); }
169 |
170 | // keep popping until stack is empty or we get a floodable coord
171 | while (!_stack.empty && shouldSkipFront()) { _stack.removeFront(); }
172 | }
173 | }
174 |
175 | return Result(grid, origin);
176 | }
177 |
178 | /**
179 | * Returns a range that iterates through tiles based on a flood filling algorithm.
180 | *
181 | * Params:
182 | * pred = predicate that returns true if the flood should progress through a given tile.
183 | * grid = grid to apply flood to.
184 | * origin = coordinate at which to begin flood.
185 | * diags = by default, flood only progresses to directly adjacent tiles.
186 | * Diagonals.yes causes the flood to progress across diagonals too.
187 | */
188 | auto floodTiles(alias pred, T)(T grid, RowCol origin, Diagonals diags = Diagonals.no)
189 | if (is(typeof(pred(grid.tileAt(RowCol(0,0)))) : bool))
190 | {
191 | return floodCoords!pred(grid, origin, diags).map!(x => grid.tileAt(x));
192 | }
193 |
194 | ///
195 | unittest {
196 | import std.array;
197 | import std.algorithm : equal;
198 |
199 | // let the 'X's represent 'walls', and the other letters 'open' areas we'd link to identify
200 | auto grid = rectGrid([
201 | // 0 1 2 3 4 5 <-col| row
202 | [ 'X', 'X', 'X', 'X', 'X', 'X' ], // 0
203 | [ 'X', 'a', 'a', 'X', 'b', 'X' ], // 1
204 | [ 'X', 'a', 'a', 'X', 'b', 'X' ], // 2
205 | [ 'X', 'X', 'X', 'X', 'X', 'c' ], // 3
206 | [ 'd', 'd', 'd', 'X', 'c', 'X' ], // 4
207 | [ 'd', 'd', 'd', 'X', 'X', 'X' ], // 5
208 | ]);
209 |
210 | // starting on a wall should return an empty result
211 | assert(grid.floodTiles!(x => x == 'a')(RowCol(0,0)).empty);
212 | assert(grid.floodTiles!(x => x == 'a')(RowCol(3,3)).empty);
213 |
214 | // flood the 'a' room
215 | assert(grid.floodTiles!(x => x == 'a')(RowCol(1,1)).equal(['a', 'a', 'a', 'a']));
216 | assert(grid.floodTiles!(x => x == 'a')(RowCol(1,2)).equal(['a', 'a', 'a', 'a']));
217 | assert(grid.floodTiles!(x => x == 'a')(RowCol(2,1)).equal(['a', 'a', 'a', 'a']));
218 | assert(grid.floodTiles!(x => x == 'a')(RowCol(2,2)).equal(['a', 'a', 'a', 'a']));
219 |
220 | // flood the 'a' room, but asking for a 'b'
221 | assert(grid.floodTiles!(x => x == 'b')(RowCol(2,2)).empty);
222 |
223 | // flood the 'b' room
224 | assert(grid.floodTiles!(x => x == 'b')(RowCol(1,4)).equal(['b', 'b']));
225 |
226 | // flood the 'c' room
227 | assert(grid.floodTiles!(x => x == 'c')(RowCol(4,4)).equal(['c']));
228 |
229 | // flood the 'd' room
230 | assert(grid.floodTiles!(x => x == 'd')(RowCol(4,1)).equal(['d', 'd', 'd', 'd', 'd', 'd']));
231 |
232 | // flood the 'b' and 'c' rooms, moving through diagonals
233 | assert(grid.floodTiles!(x => x == 'b' || x == 'c')(RowCol(4,4), Diagonals.yes)
234 | .equal(['c', 'c', 'b', 'b']));
235 | }
236 |
--------------------------------------------------------------------------------
/src/dtiled/coords.d:
--------------------------------------------------------------------------------
1 | /**
2 | * This module helps handle coordinate systems within a map.
3 | *
4 | * When dealing with a grid, do you ever forget whether the row or column is the first index?
5 | *
6 | * Me too.
7 | *
8 | * For this reason, all functions dealing with grid coordinates take a RowCol argument.
9 | * This makes it abundantly clear that the map is indexed in row-major order.
10 | * Furthormore, it prevents confusion between **grid** coordinates and **pixel** coordinates.
11 | *
12 | * A 'pixel' coordinate refers to an (x,y) location in 'pixel' space.
13 | * The units used by 'pixel' coords are the same as used in MapData tilewidth and tileheight.
14 | *
15 | * Within dtiled, pixel locations are represented by a PixelCoord.
16 | * However, you may already be using a game library that provides some 'Vector' implementation
17 | * used to represent positions.
18 | * You can pass any such type to dtiled functions expecting a pixel coordinate so long as it
19 | * satisfies isPixelCoord.
20 | */
21 | module dtiled.coords;
22 |
23 | import std.conv : to;
24 | import std.math : abs, sgn;
25 | import std.range : iota, only, take, chain;
26 | import std.traits : Signed;
27 | import std.format : format;
28 | import std.typecons : Tuple, Flag;
29 | import std.algorithm : map, canFind, cartesianProduct;
30 |
31 | /* This is the type used for map coordinates.
32 | * Use the platform's size_t as it will be used to index arrays.
33 | * However, make it signed to represent 'virtual' coordinates outside the maps
34 | * bounds.
35 | * This is useful for representing, for example, a mouse that is positioned
36 | * to the left of the leftmost tile in a map.
37 | */
38 | private alias coord_t = Signed!size_t;
39 |
40 | /// Whether to consider diagonals adjacent in situations dealing with the concept of adjacency.
41 | alias Diagonals = Flag!"Diagonals";
42 |
43 | /// Represents a discrete location within the map grid.
44 | struct RowCol {
45 | coord_t row, col;
46 |
47 | /// Construct a row column pair
48 | @nogc
49 | this(coord_t row, coord_t col) {
50 | this.row = row;
51 | this.col = col;
52 | }
53 |
54 | /// Get a string representation of the coordinate, useful for debugging
55 | @property {
56 | string toString() {
57 | return "(%d,%d)".format(row, col);
58 | }
59 |
60 | /**
61 | * Get a coordinate above this coordinate
62 | * Params:
63 | * dist = distance in number of tiles
64 | */
65 | auto north(int dist = 1) { return RowCol(row - dist, col); }
66 |
67 | /**
68 | * Get a coordinate below this coordinate
69 | * Params:
70 | * dist = distance in number of tiles
71 | */
72 | auto south(int dist = 1) { return RowCol(row + dist, col); }
73 |
74 | /**
75 | * Get a coordinate to the left of this coordinate
76 | * Params:
77 | * dist = distance in number of tiles
78 | */
79 | auto west(int dist = 1) { return RowCol(row, col - dist); }
80 |
81 | /**
82 | * Get a coordinate to the right of this coordinate
83 | * Params:
84 | * dist = distance in number of tiles
85 | */
86 | auto east(int dist = 1) { return RowCol(row, col + dist); }
87 |
88 | /**
89 | * Return a range containing the coords adjacent to this coord.
90 | *
91 | * The returned coords are ordered from north to south, west to east.
92 | *
93 | * Params:
94 | * diagonal = if no, include coords to the north, south, east, and west only.
95 | * if yes, additionaly include northwest, northeast, southwest, and southeast.
96 | */
97 | auto adjacent(Diagonals diagonal = Diagonals.no) {
98 | // the 'take' statements are used to conditionally include the diagonal coords
99 | return chain(
100 | (this.north.west).only.take(diagonal ? 1 : 0),
101 | (this.north).only,
102 | (this.north.east).only.take(diagonal ? 1 : 0),
103 | (this.west ).only,
104 | (this.east ).only,
105 | (this.south.west).only.take(diagonal ? 1 : 0),
106 | (this.south).only,
107 | (this.south.east).only.take(diagonal ? 1 : 0));
108 | }
109 | }
110 |
111 | /// convenient access of nearby coordinates
112 | unittest {
113 | import std.algorithm : equal;
114 |
115 | assert(RowCol(1,1).north == RowCol(0,1));
116 | assert(RowCol(1,1).south == RowCol(2,1));
117 | assert(RowCol(1,1).east == RowCol(1,2));
118 | assert(RowCol(1,1).west == RowCol(1,0));
119 |
120 | assert(RowCol(1,1).south(5) == RowCol(6,1));
121 | assert(RowCol(1,1).south(2).east(5) == RowCol(3,6));
122 |
123 | assert(RowCol(1,1).adjacent.equal([
124 | RowCol(0,1),
125 | RowCol(1,0),
126 | RowCol(1,2),
127 | RowCol(2,1)
128 | ]));
129 |
130 | assert(RowCol(1,1).adjacent(Diagonals.yes).equal([
131 | RowCol(0,0),
132 | RowCol(0,1),
133 | RowCol(0,2),
134 | RowCol(1,0),
135 | RowCol(1,2),
136 | RowCol(2,0),
137 | RowCol(2,1),
138 | RowCol(2,2)
139 | ]));
140 | }
141 |
142 | /// Add, subtract, multiply, or divide one coordinate from another.
143 | @nogc
144 | RowCol opBinary(string op)(RowCol rhs) const
145 | if (op == "+" || op == "-" || op == "*" || op == "/")
146 | {
147 | return mixin(q{RowCol(this.row %s rhs.row, this.col %s rhs.col)}.format(op, op));
148 | }
149 |
150 | /// Binary operations can be performed between coordinates.
151 | unittest {
152 | assert(RowCol(1, 2) + RowCol(4, 1) == RowCol( 5, 3));
153 | assert(RowCol(4, 2) - RowCol(6, 1) == RowCol(-2, 1));
154 | assert(RowCol(4, 2) * RowCol(2, -3) == RowCol( 8, -6));
155 | assert(RowCol(8, 4) / RowCol(2, -4) == RowCol( 4, -1));
156 | }
157 |
158 | /// A coordinate can be multiplied or divided by an integer.
159 | @nogc
160 | RowCol opBinary(string op, T : coord_t)(T rhs) const
161 | if (op == "*" || op == "/")
162 | {
163 | return mixin(q{RowCol(this.row %s rhs, this.col %s rhs)}.format(op, op));
164 | }
165 |
166 | /// Multiply/divide a coord by a constant
167 | unittest {
168 | assert(RowCol(1, 2) * -4 == RowCol(-4, -8));
169 | assert(RowCol(4, -6) / 2 == RowCol(2, -3));
170 | }
171 |
172 | /// Add, subtract, multiply, or divide one coordinate from another in place.
173 | @nogc
174 | void opOpAssign(string op, T)(T rhs)
175 | if (is(typeof(this.opBinary!op(rhs))))
176 | {
177 | this = this.opBinary!op(rhs);
178 | }
179 |
180 | unittest {
181 | auto rc = RowCol(1, 2);
182 | rc += RowCol(4, 1);
183 | assert(rc == RowCol(5, 3));
184 | rc -= RowCol(2, -1);
185 | assert(rc == RowCol(3, 4));
186 | rc *= RowCol(-2, 4);
187 | assert(rc == RowCol(-6, 16));
188 | rc /= RowCol(-3, 2);
189 | assert(rc == RowCol(2, 8));
190 | rc *= 2;
191 | assert(rc == RowCol(4, 16));
192 | rc /= 4;
193 | assert(rc == RowCol(1, 4));
194 | }
195 | }
196 |
197 | /**
198 | * Enumerate all row/col pairs spanning the rectangle bounded by the corners start and end.
199 | *
200 | * The order of enumeration is determined as follows:
201 | * Enumerate all columns in a row before moving to the next row.
202 | * If start.row >= end.row, enumerate rows in increasing order, otherwise enumerate in decreasing.
203 | * If start.col >= end.col, enumerate cols in increasing order, otherwise enumerate in decreasing.
204 | *
205 | * Params:
206 | * bound = Determines whether each bound is "[" (inclusive) or ")" (exclusive).
207 | * The default of "[)" includes start but excludes end.
208 | * See_Also: std.random.uniform.
209 | * start = RowCol pair to start enumeration from, $(B inclusive)
210 | * end = RowCol pair to end enumeration at, $(B exclusive)
211 | */
212 | auto span(string bound = "[)")(RowCol start, RowCol end) {
213 | enum validBounds = [ "()", "(]", "[)", "[]" ];
214 | static assert(validBounds.canFind(bound),
215 | bound ~ " is an invalid span bound. Try one of " ~ validBounds.toString);
216 |
217 | // direction to increment the rows/cols (1 or -1)
218 | auto colInc = sgn(end.col - start.col);
219 | auto rowInc = sgn(end.row - start.row);
220 |
221 | colInc = (colInc == 0) ? 1 : colInc;
222 | rowInc = (rowInc == 0) ? 1 : rowInc;
223 |
224 | // iota is inclusive on the lower bound; adjust if we want exclusive
225 | static if (bound[0] == '(') start += RowCol(rowInc, colInc);
226 |
227 | // iota is exclusive on the upper bound; adjust if we want inclusive
228 | static if (bound[1] == ']') end += RowCol(rowInc, colInc);
229 |
230 | auto colRange = iota(start.col, end.col, colInc);
231 | auto rowRange = iota(start.row, end.row, rowInc);
232 |
233 | return rowRange.cartesianProduct(colRange).map!(x => RowCol(x[0], x[1]));
234 | }
235 |
236 | ///
237 | unittest {
238 | import std.algorithm : equal;
239 |
240 | assert(RowCol(0,0).span(RowCol(2,3)).equal([
241 | RowCol(0,0), RowCol(0,1), RowCol(0,2),
242 | RowCol(1,0), RowCol(1,1), RowCol(1,2)]));
243 |
244 | assert(RowCol(2,2).span(RowCol(0,0)).equal([
245 | RowCol(2,2), RowCol(2,1),
246 | RowCol(1,2), RowCol(1,1)]));
247 |
248 | assert(RowCol(2,2).span(RowCol(1,3)).equal([RowCol(2,2)]));
249 |
250 | assert(RowCol(2,2).span(RowCol(3,1)).equal([RowCol(2,2)]));
251 |
252 | // as the upper bound of span is exclusive, both of these are empty (span over 0 columns):
253 | assert(RowCol(2,2).span(RowCol(2,2)).empty);
254 | assert(RowCol(2,2).span(RowCol(5,2)).empty);
255 | }
256 |
257 | /// You can control whether the bounds are inclusive or exclusive
258 | unittest {
259 | import std.algorithm : equal;
260 | assert(RowCol(2,2).span!"[]"(RowCol(2,2)).equal([ RowCol(2,2) ]));
261 |
262 | assert(RowCol(2,2).span!"[]"(RowCol(2,5)).equal(
263 | [ RowCol(2,2), RowCol(2,3), RowCol(2,4), RowCol(2,5) ]));
264 |
265 | assert(RowCol(5,2).span!"[]"(RowCol(2,2)).equal(
266 | [ RowCol(5,2), RowCol(4,2), RowCol(3,2), RowCol(2,2) ]));
267 |
268 | assert(RowCol(2,2).span!"[]"(RowCol(0,0)).equal([
269 | RowCol(2,2), RowCol(2,1), RowCol(2,0),
270 | RowCol(1,2), RowCol(1,1), RowCol(1,0),
271 | RowCol(0,2), RowCol(0,1), RowCol(0,0)]));
272 |
273 | assert(RowCol(2,2).span!"(]"(RowCol(3,3)).equal([ RowCol(3,3) ]));
274 |
275 | assert(RowCol(2,2).span!"()"(RowCol(3,3)).empty);
276 | }
277 |
278 | /// ditto
279 | auto span(RowCol start, coord_t endRow, coord_t endCol) {
280 | return span(start, RowCol(endRow, endCol));
281 | }
282 |
283 | unittest {
284 | import std.algorithm : equal;
285 |
286 | assert(RowCol(0,0).span(2,3).equal([
287 | RowCol(0,0), RowCol(0,1), RowCol(0,2),
288 | RowCol(1,0), RowCol(1,1), RowCol(1,2)]));
289 | }
290 |
291 | /// Represents a location in continuous 2D space.
292 | alias PixelCoord = Tuple!(float, "x", float, "y");
293 |
294 | /// True if T is a type that can represent a location in terms of pixels.
295 | enum isPixelCoord(T) = is(typeof(T.x) : real) &&
296 | is(typeof(T.y) : real) &&
297 | is(T == struct); // must be a struct/tuple
298 |
299 | ///
300 | unittest {
301 | // PixelCoord is dtiled's vector representation within pixel coordinate space.
302 | static assert(isPixelCoord!PixelCoord);
303 |
304 | // as a user, you may choose any (x,y) numeric pair to use as a pixel coordinate
305 | struct MyVector(T) { T x, y; }
306 |
307 | static assert(isPixelCoord!(MyVector!int));
308 | static assert(isPixelCoord!(MyVector!uint));
309 | static assert(isPixelCoord!(MyVector!float));
310 | static assert(isPixelCoord!(MyVector!double));
311 | static assert(isPixelCoord!(MyVector!real));
312 |
313 | // To avoid confusion, grid coordinates are distinct from pixel coordinates
314 | static assert(!isPixelCoord!RowCol);
315 | }
316 |
317 | /// Convert a PixelCoord to a user-defined (x,y) numeric pair.
318 | T as(T)(PixelCoord pos) if (isPixelCoord!T) {
319 | T t;
320 | t.x = pos.x.to!(typeof(t.x));
321 | t.y = pos.y.to!(typeof(t.y));
322 | return t;
323 | }
324 |
325 | /// Convert dtiled's pixel-space coordinates to your own types:
326 | unittest {
327 | // your own representation may be a struct
328 | struct MyVector(T) { T x, y; }
329 |
330 | assert(PixelCoord(5, 10).as!(MyVector!double) == MyVector!double(5, 10));
331 | assert(PixelCoord(5.5, 10.2).as!(MyVector!int) == MyVector!int(5, 10));
332 |
333 | // or it may be a tuple
334 | alias MyPoint(T) = Tuple!(T, "x", T, "y");
335 |
336 | assert(PixelCoord(5, 10).as!(MyPoint!double) == MyPoint!double(5, 10));
337 | assert(PixelCoord(5.5, 10.2).as!(MyPoint!int) == MyPoint!int(5, 10));
338 |
339 | // std.conv.to is used internally, so it should detect overflow
340 | import std.conv : ConvOverflowException;
341 | import std.exception : assertThrown;
342 | assertThrown!ConvOverflowException(PixelCoord(-1, -1).as!(MyVector!ulong));
343 | }
344 |
345 | /**
346 | * Return the manhattan distance between two tile coordinates.
347 | * For two coordinates a and b, this is defined as abs(a.row - b.row) + abs(a.col - b.col)
348 | */
349 | @nogc
350 | auto manhattan(RowCol a, RowCol b) {
351 | return abs(a.row - b.row) + abs(a.col - b.col);
352 | }
353 |
354 | unittest {
355 | assert(manhattan(RowCol(0,0), RowCol(2,2)) == 4);
356 | assert(manhattan(RowCol(2,2), RowCol(2,2)) == 0);
357 | assert(manhattan(RowCol(-2,-2), RowCol(-2,-2)) == 0);
358 | assert(manhattan(RowCol(4,-2), RowCol(2,2)) == 6);
359 | assert(manhattan(RowCol(4,-2), RowCol(-2,-2)) == 6);
360 | }
361 |
--------------------------------------------------------------------------------
/src/dtiled/data.d:
--------------------------------------------------------------------------------
1 | /**
2 | * Read and write data for Tiled maps.
3 | * Currently only supports JSON format.
4 | *
5 | * Authors: rcorre
6 | * License: MIT
7 | * Copyright: Copyright © 2015, Ryan Roden-Corrent
8 | */
9 | module dtiled.data;
10 |
11 | import std.conv : to;
12 | import std.file : exists;
13 | import std.range : empty, front, retro;
14 | import std.string : format;
15 | import std.algorithm : find;
16 | import std.exception : enforce;
17 | import jsonizer;
18 |
19 | /**
20 | * Underlying type used to represent Tiles Global IDentifiers.
21 | * Note that a GID of 0 is used to indicate the abscence of a tile.
22 | */
23 | alias TiledGid = uint;
24 |
25 | /// Flags set by Tiled in the guid field. Used to indicate mirroring and rotation.
26 | enum TiledFlag : TiledGid {
27 | none = 0x00000000, /// Tile is not flipped
28 | flipDiagonal = 0x20000000, /// Tile is flipped diagonally
29 | flipVertical = 0x40000000, /// Tile is flipped vertically (over x axis)
30 | flipHorizontal = 0x80000000, /// Tile is flipped horizontally (over y axis)
31 | all = flipHorizontal | flipVertical | flipDiagonal, /// bitwise `or` of all tile flags.
32 | }
33 |
34 | ///
35 | unittest {
36 | // this is the GID for a tile with tileset index 21 that was flipped horizontally
37 | TiledGid gid = 2147483669;
38 | // clearing the flip flags yields a gid that should map to a tileset index
39 | assert((gid & ~TiledFlag.all) == 21);
40 | // it is flipped horizontally
41 | assert(gid & TiledFlag.flipHorizontal);
42 | assert(!(gid & TiledFlag.flipVertical));
43 | assert(!(gid & TiledFlag.flipDiagonal));
44 | }
45 |
46 | /// Top-level Tiled structure - encapsulates all data in the map file.
47 | struct MapData {
48 | mixin JsonizeMe;
49 |
50 | /* Types */
51 | /// Map orientation.
52 | enum Orientation {
53 | orthogonal, /// rectangular orthogonal map
54 | isometric, /// diamond-shaped isometric map
55 | staggered /// rough rectangular isometric map
56 | }
57 |
58 | /** The order in which tiles on tile layers are rendered.
59 | * From the docs:
60 | * Valid values are right-down (the default), right-up, left-down and left-up.
61 | * In all cases, the map is drawn row-by-row.
62 | * (since 0.10, but only supported for orthogonal maps at the moment)
63 | */
64 | enum RenderOrder : string {
65 | rightDown = "right-down", /// left-to-right, top-to-bottom
66 | rightUp = "right-up", /// left-to-right, bottom-to-top
67 | leftDown = "left-down", /// right-to-left, top-to-bottom
68 | leftUp = "left-up" /// right-to-left, bottom-to-top
69 | }
70 |
71 | /* Data */
72 | @jsonize(Jsonize.yes) {
73 | @jsonize("width") int numCols; /// Number of tile columns
74 | @jsonize("height") int numRows; /// Number of tile rows
75 | @jsonize("tilewidth") int tileWidth; /// General grid size. Individual tiles sizes may differ.
76 | @jsonize("tileheight") int tileHeight; /// ditto
77 | Orientation orientation; /// Orthogonal, isometric, or staggered
78 | LayerData[] layers; /// All map layers (tiles and objects)
79 | TilesetData[] tilesets; /// All tile sets defined in this map
80 | }
81 |
82 | @jsonize(Jsonize.opt) {
83 | @jsonize("backgroundcolor") string backgroundColor; /// Hex-formatted background color (#RRGGBB)
84 | @jsonize("renderorder") string renderOrder; /// Rendering direction (orthogonal only)
85 | @jsonize("nextobjectid") int nextObjectId; /// Global counter across all objects
86 | string[string] properties; /// Key-value property pairs on map
87 | }
88 |
89 | /* Functions */
90 | /** Load a Tiled map from a JSON file.
91 | * Throws if no file is found at that path or if the parsing fails.
92 | * Params:
93 | * path = filesystem path to a JSON map file exported by Tiled
94 | * Returns: The parsed map data
95 | */
96 | static auto load(string path) {
97 | enforce(path.exists, "No map file found at " ~ path);
98 | auto map = readJSON!MapData(path);
99 |
100 | // Tiled should export Tilesets in order of increasing GID.
101 | // Double check this in debug mode, as things will break if this invariant doesn't hold.
102 | debug {
103 | import std.algorithm : isSorted;
104 | assert(map.tilesets.isSorted!((a,b) => a.firstGid < b.firstGid),
105 | "TileSets are not sorted by GID!");
106 | }
107 |
108 | return map;
109 | }
110 |
111 | /** Save a Tiled map to a JSON file.
112 | * Params:
113 | * path = file destination; parent directory must already exist
114 | */
115 | void save(string path) {
116 | // Tilemaps must be exported sorted in order of firstGid
117 | debug {
118 | import std.algorithm : isSorted;
119 | assert(tilesets.isSorted!((a,b) => a.firstGid < b.firstGid),
120 | "TileSets are not sorted by GID!");
121 | }
122 |
123 | path.writeJSON(this);
124 | }
125 |
126 | /** Fetch a map layer by its name. No check for layers with duplicate names is performed.
127 | * Throws if no layer has a matching name (case-sensitive).
128 | * Params:
129 | * name = name of layer to find
130 | * Returns: Layer matching name
131 | */
132 | auto getLayer(string name) {
133 | auto r = layers.find!(x => x.name == name);
134 | enforce(!r.empty, "Could not find layer named %s".format(name));
135 | return r.front;
136 | }
137 |
138 | /** Fetch a tileset by its name. No check for layers with duplicate names is performed.
139 | * Throws if no tileset has a matching name (case-sensitive).
140 | * Params:
141 | * name = name of tileset to find
142 | * Returns: Tileset matching name
143 | */
144 | auto getTileset(string name) {
145 | auto r = tilesets.find!(x => x.name == name);
146 | enforce(!r.empty, "Could not find tileset named %s".format(name));
147 | return r.front;
148 | }
149 |
150 | /** Fetch the tileset containing the tile a given GID.
151 | * Throws if the gid is out of range for all tilesets
152 | * Params:
153 | * gid = gid of tile to find tileset for
154 | * Returns: Tileset containing the given gid
155 | */
156 | auto getTileset(TiledGid gid) {
157 | gid = gid.cleanGid;
158 | // search in reverse order, want the highest firstGid <= the given gid
159 | auto r = tilesets.retro.find!(x => x.firstGid <= gid);
160 | enforce(!r.empty, "GID %d is out of range for all tilesets".format(gid));
161 | return r.front;
162 | }
163 |
164 | ///
165 | unittest {
166 | MapData map;
167 | map.tilesets ~= TilesetData();
168 | map.tilesets[0].firstGid = 1;
169 | map.tilesets ~= TilesetData();
170 | map.tilesets[1].firstGid = 5;
171 | map.tilesets ~= TilesetData();
172 | map.tilesets[2].firstGid = 12;
173 |
174 | assert(map.getTileset(1) == map.tilesets[0]);
175 | assert(map.getTileset(3) == map.tilesets[0]);
176 | assert(map.getTileset(5) == map.tilesets[1]);
177 | assert(map.getTileset(9) == map.tilesets[1]);
178 | assert(map.getTileset(15) == map.tilesets[2]);
179 | }
180 | }
181 |
182 | /** A layer of tiles within the map.
183 | *
184 | * A Map layer could be one of:
185 | * Tile Layer: data is an array of guids that each map to some tile from a TilesetData
186 | * Object Group: objects is a set of entities that are not necessarily tied to the grid
187 | * Image Layer: This layer is a static image (e.g. a backdrop)
188 | */
189 | struct LayerData {
190 | mixin JsonizeMe;
191 |
192 | /// Identifies what kind of information a layer contains.
193 | enum Type {
194 | tilelayer, /// One tileset index for every tile in the layer
195 | objectgroup, /// One or more ObjectData
196 | imagelayer /// TODO: try actually creating one of these
197 | }
198 |
199 | @jsonize(Jsonize.yes) {
200 | @jsonize("width") int numCols; /// Number of tile columns. Identical to map width in Tiled Qt.
201 | @jsonize("height") int numRows; /// Number of tile rows. Identical to map height in Tiled Qt.
202 | string name; /// Name assigned to this layer
203 | Type type; /// Category (tile, object, or image)
204 | bool visible; /// whether layer is shown or hidden in editor
205 | int x; /// Horizontal layer offset. Always 0 in Tiled Qt.
206 | int y; /// Vertical layer offset. Always 0 in Tiled Qt.
207 | }
208 |
209 | // These entries exist only on object layers
210 | @jsonize(Jsonize.opt) {
211 | TiledGid[] data; /// An array of tile GIDs. Only for `tilelayer`
212 | ObjectData[] objects; /// An array of objects. Only on `objectgroup` layers.
213 | string[string] properties; /// Optional key-value properties for this layer
214 | float opacity; /// Visual opacity of all tiles in this layer
215 | @jsonize("draworder") string drawOrder; /// Not documented by tiled, but may appear in JSON.
216 | }
217 |
218 | @property {
219 | /// get the row corresponding to a position in the $(D data) or $(D objects) array.
220 | auto idxToRow(size_t idx) { return idx / numCols; }
221 |
222 | ///
223 | unittest {
224 | LayerData layer;
225 | layer.numCols = 3;
226 | layer.numRows = 2;
227 |
228 | assert(layer.idxToRow(0) == 0);
229 | assert(layer.idxToRow(1) == 0);
230 | assert(layer.idxToRow(2) == 0);
231 | assert(layer.idxToRow(3) == 1);
232 | assert(layer.idxToRow(4) == 1);
233 | assert(layer.idxToRow(5) == 1);
234 | }
235 |
236 | /// get the column corresponding to a position in the $(D data) or $(D objects) array.
237 | auto idxToCol(size_t idx) { return idx % numCols; }
238 |
239 | ///
240 | unittest {
241 | LayerData layer;
242 | layer.numCols = 3;
243 | layer.numRows = 2;
244 |
245 | assert(layer.idxToCol(0) == 0);
246 | assert(layer.idxToCol(1) == 1);
247 | assert(layer.idxToCol(2) == 2);
248 | assert(layer.idxToCol(3) == 0);
249 | assert(layer.idxToCol(4) == 1);
250 | assert(layer.idxToCol(5) == 2);
251 | }
252 | }
253 | }
254 |
255 | /** Represents an entity in an object layer.
256 | *
257 | * Objects are not necessarily grid-aligned, but rather have a position specified in pixel coords.
258 | * Each object instance can have a `name`, `type`, and set of `properties` defined in the editor.
259 | */
260 | struct ObjectData {
261 | mixin JsonizeMe;
262 | @jsonize(Jsonize.yes) {
263 | int id; /// Incremental id - unique across all objects
264 | int width; /// Width in pixels. Ignored if using a gid.
265 | int height; /// Height in pixels. Ignored if using a gid.
266 | string name; /// Name assigned to this object instance
267 | string type; /// User-defined string 'type' assigned to this object instance
268 | string[string] properties; /// Optional properties defined on this instance
269 | bool visible; /// Whether object is shown.
270 | int x; /// x coordinate in pixels
271 | int y; /// y coordinate in pixels
272 | float rotation; /// Angle in degrees clockwise
273 | }
274 |
275 | @jsonize(Jsonize.opt) {
276 | TiledGid gid; /// Identifies a tile in a tileset if this object is represented by a tile
277 | }
278 | }
279 |
280 | /**
281 | * A TilesetData maps GIDs (Global IDentifiers) to tiles.
282 | *
283 | * Each tileset has a range of GIDs that map to the tiles it contains.
284 | * This range starts at `firstGid` and extends to the `firstGid` of the next tileset.
285 | * The index of a tile within a tileset is given by tile.gid - tileset.firstGid.
286 | * A tileset uses its `image` as a 'tile atlas' and may specify per-tile `properties`.
287 | */
288 | struct TilesetData {
289 | mixin JsonizeMe;
290 | @jsonize(Jsonize.yes) {
291 | string name; /// Name given to this tileset
292 | string image; /// Image used for tiles in this set
293 | int margin; /// Buffer between image edge and tiles (in pixels)
294 | int spacing; /// Spacing between tiles in image (in pixels)
295 | string[string] properties; /// Properties assigned to this tileset
296 | @jsonize("firstgid") TiledGid firstGid; /// The GID that maps to the first tile in this set
297 | @jsonize("tilewidth") int tileWidth; /// Maximum width of tiles in this set
298 | @jsonize("tileheight") int tileHeight; /// Maximum height of tiles in this set
299 | @jsonize("imagewidth") int imageWidth; /// Width of source image in pixels
300 | @jsonize("imageheight") int imageHeight; /// Height of source image in pixels
301 | }
302 |
303 | @jsonize(Jsonize.opt) {
304 | /** Optional per-tile properties, indexed by the relative ID as a string.
305 | *
306 | * $(RED Note:) The ID is $(B not) the same as the GID. The ID is calculated relative to the
307 | * firstgid of the tileset the tile belongs to.
308 | * For example, if a tile has GID 25 and belongs to the tileset with firstgid = 10, then its
309 | * properties are given by $(D tileset.tileproperties["15"]).
310 | *
311 | * A tile with no special properties will not have an index here.
312 | * If no tiles have special properties, this field is not populated at all.
313 | */
314 | string[string][string] tileproperties;
315 | }
316 |
317 | @property {
318 | /// Number of tile rows in the tileset
319 | int numRows() { return (imageHeight - margin * 2) / (tileHeight + spacing); }
320 |
321 | /// Number of tile rows in the tileset
322 | int numCols() { return (imageWidth - margin * 2) / (tileWidth + spacing); }
323 |
324 | /// Total number of tiles defined in the tileset
325 | int numTiles() { return numRows * numCols; }
326 | }
327 |
328 | /**
329 | * Find the grid position of a tile within this tileset.
330 | *
331 | * Throws if $(D gid) is out of range for this tileset.
332 | * Params:
333 | * gid = GID of tile. Does not need to be cleaned of flags.
334 | * Returns: 0-indexed row of tile
335 | */
336 | int tileRow(TiledGid gid) {
337 | return getIdx(gid) / numCols;
338 | }
339 |
340 | /**
341 | * Find the grid position of a tile within this tileset.
342 | *
343 | * Throws if $(D gid) is out of range for this tileset.
344 | * Params:
345 | * gid = GID of tile. Does not need to be cleaned of flags.
346 | * Returns: 0-indexed column of tile
347 | */
348 | int tileCol(TiledGid gid) {
349 | return getIdx(gid) % numCols;
350 | }
351 |
352 | /**
353 | * Find the pixel position of a tile within this tileset.
354 | *
355 | * Throws if $(D gid) is out of range for this tileset.
356 | * Params:
357 | * gid = GID of tile. Does not need to be cleaned of flags.
358 | * Returns: space between left side of image and left side of tile (pixels)
359 | */
360 | int tileOffsetX(TiledGid gid) {
361 | return margin + tileCol(gid) * (tileWidth + spacing);
362 | }
363 |
364 | /**
365 | * Find the pixel position of a tile within this tileset.
366 | *
367 | * Throws if $(D gid) is out of range for this tileset.
368 | * Params:
369 | * gid = GID of tile. Does not need to be cleaned of flags.
370 | * Returns: space between top side of image and top side of tile (pixels)
371 | */
372 | int tileOffsetY(TiledGid gid) {
373 | return margin + tileRow(gid) * (tileHeight + spacing);
374 | }
375 |
376 | /**
377 | * Find the properties defined for a tile in this tileset.
378 | *
379 | * Throws if $(D gid) is out of range for this tileset.
380 | * Params:
381 | * gid = GID of tile. Does not need to be cleaned of flags.
382 | * Returns: AA of key-value property pairs, or $(D null) if no properties defined for this tile.
383 | */
384 | string[string] tileProperties(TiledGid gid) {
385 | auto id = cleanGid(gid) - firstGid; // indexed by relative ID, not GID
386 | auto res = id.to!string in tileproperties;
387 | return res ? *res : null;
388 | }
389 |
390 | // clean the gid, adjust it to an index within this tileset, and throw if out of range
391 | private auto getIdx(TiledGid gid) {
392 | gid = gid.cleanGid;
393 | auto idx = gid - firstGid;
394 |
395 | enforce(idx >= 0 && idx < numTiles,
396 | "GID %d out of range [%d,%d] for tileset %s"
397 | .format( gid, firstGid, firstGid + numTiles - 1, name));
398 |
399 | return idx;
400 | }
401 | }
402 |
403 | unittest {
404 | // 3 rows, 3 columns
405 | TilesetData tileset;
406 | tileset.firstGid = 4;
407 | tileset.tileWidth = tileset.tileHeight = 32;
408 | tileset.imageWidth = tileset.imageHeight = 96;
409 | tileset.tileproperties = [ "2": ["a": "b"], "3": ["c": "d"] ];
410 |
411 | void test(TiledGid gid, int row, int col, int x, int y, string[string] props) {
412 | assert(tileset.tileRow(gid) == row , "row mismatch gid=%d".format(gid));
413 | assert(tileset.tileCol(gid) == col , "col mismatch gid=%d".format(gid));
414 | assert(tileset.tileOffsetX(gid) == x , "x mismatch gid=%d".format(gid));
415 | assert(tileset.tileOffsetY(gid) == y , "y mismatch gid=%d".format(gid));
416 | assert(tileset.tileProperties(gid) == props, "props mismatch gid=%d".format(gid));
417 | }
418 |
419 | // gid , row , col , x , y , props
420 | test(4 , 0 , 0 , 0 , 0 , null);
421 | test(5 , 0 , 1 , 32 , 0 , null);
422 | test(6 , 0 , 2 , 64 , 0 , ["a": "b"]);
423 | test(7 , 1 , 0 , 0 , 32 , ["c": "d"]);
424 | test(8 , 1 , 1 , 32 , 32 , null);
425 | test(9 , 1 , 2 , 64 , 32 , null);
426 | test(10 , 2 , 0 , 0 , 64 , null);
427 | test(11 , 2 , 1 , 32 , 64 , null);
428 | test(12 , 2 , 2 , 64 , 64 , null);
429 | }
430 |
431 | /**
432 | * Clear the TiledFlag portion of a GID, leaving just the tile id.
433 | * Params:
434 | * gid = GID to clean
435 | * Returns: A GID with the flag bits zeroed out
436 | */
437 | TiledGid cleanGid(TiledGid gid) {
438 | return gid & ~TiledFlag.all;
439 | }
440 |
441 | ///
442 | unittest {
443 | // normal tile, no flags
444 | TiledGid gid = 0x00000002;
445 | assert(gid.cleanGid == gid);
446 |
447 | // normal tile, no flags
448 | gid = 0x80000002; // tile with id 2 flipped horizontally
449 | assert(gid.cleanGid == 0x2);
450 | assert(gid & TiledFlag.flipHorizontal);
451 | }
452 |
--------------------------------------------------------------------------------
/src/dtiled/grid.d:
--------------------------------------------------------------------------------
1 | /**
2 | * A grid wraps a 2D array to provide specialized grid-based functionality.
3 | *
4 | * Currently, the only grid type is RectGrid, which can serve as the base for an orthogonal or isometric map.
5 | * HexGrid may be added in a later version to support hexagonal maps.
6 | *
7 | * Authors: rcorre
8 | * License: MIT
9 | * Copyright: Copyright © 2015, Ryan Roden-Corrent
10 | */
11 | module dtiled.grid;
12 |
13 | import std.range : only, chain, takeNone, hasLength;
14 | import std.range : isInputRange, isForwardRange, isBidirectionalRange;
15 | import std.format : format;
16 | import std.algorithm : all, map, filter;
17 | import std.exception : enforce;
18 | import dtiled.coords;
19 |
20 | /// True if T is a static or dynamic array type.
21 | enum isArray2D(T) = is(typeof(T.init[0][0])) && // 2D random access
22 | is(typeof(T.init.length) : size_t) && // stores count of rows
23 | is(typeof(T.init[0].length) : size_t); // stores count of columns
24 |
25 | ///
26 | unittest {
27 | import std.container : Array;
28 |
29 | static assert(isArray2D!(int[][]));
30 | static assert(isArray2D!(char[3][5]));
31 | static assert(isArray2D!(Array!(Array!int)));
32 | }
33 |
34 | /// Convenience function to wrap a RectGrid around a 2D array.
35 | auto rectGrid(T)(T tiles) if (isArray2D!T) { return RectGrid!T(tiles); }
36 |
37 | ///
38 | unittest {
39 | auto dynamicArray = [
40 | [1,2,3],
41 | [4,5,6]
42 | ];
43 | auto dynamicGrid = rectGrid(dynamicArray);
44 | assert(dynamicGrid.numRows == 2 && dynamicGrid.numCols == 3);
45 | static assert(is(dynamicGrid.TileType == int));
46 |
47 | char[3][2] staticArray = [
48 | [ 'a', 'a', 'a' ],
49 | [ 'a', 'a', 'a' ],
50 | ];
51 | auto staticGrid = rectGrid(staticArray);
52 | assert(staticGrid.numRows == 2 && staticGrid.numCols == 3);
53 | static assert(is(staticGrid.TileType == char));
54 | }
55 |
56 | /// A grid of rectangular tiles. Wraps a 2D array to provide grid-based access to tiles.
57 | struct RectGrid(T) if (isArray2D!T) {
58 | private T _tiles;
59 |
60 | /// The type used to represent a tile in this grid
61 | alias TileType = typeof(_tiles[0][0]);
62 |
63 | /// Construct a grid from a 2D tile array. See rectGrid for a constructor with type inference.
64 | this(T tiles) {
65 | assertNotJagged(tiles, "RectGrid cannot be a constructed from a jagged array");
66 | _tiles = tiles;
67 | }
68 |
69 | /// Number of columns along a grid's x axis.
70 | auto numCols() { return _tiles[0].length; }
71 |
72 | ///
73 | unittest {
74 | auto grid = rectGrid([
75 | [ 0, 0, 0, 0 ],
76 | [ 0, 0, 0, 0 ],
77 | ]);
78 |
79 | assert(grid.numCols == 4);
80 | }
81 |
82 | /// Number of rows along a grid's y axis.
83 | auto numRows() { return _tiles.length; }
84 |
85 | unittest {
86 | auto grid = rectGrid([
87 | [ 0, 0, 0, 0 ],
88 | [ 0, 0, 0, 0 ],
89 | ]);
90 |
91 | assert(grid.numRows == 2);
92 | }
93 |
94 | /// The total number of tiles in a grid.
95 | auto numTiles() { return this.numRows * this.numCols; }
96 |
97 | unittest {
98 | auto grid = rectGrid([
99 | [ 0, 0, 0, 0 ],
100 | [ 0, 0, 0, 0 ],
101 | ]);
102 |
103 | assert(grid.numTiles == 8);
104 | }
105 |
106 | /**
107 | * True if the grid coordinate is within the grid bounds.
108 | */
109 | bool contains(RowCol coord) {
110 | return
111 | coord.row >= 0 &&
112 | coord.col >= 0 &&
113 | coord.row < this.numRows &&
114 | coord.col < this.numCols;
115 | }
116 |
117 | ///
118 | unittest {
119 | // 5x3 map
120 | auto grid = rectGrid([
121 | //0 1 2 3 4 col row
122 | [ 0, 0, 0, 0, 0 ], // 0
123 | [ 0, 0, 0, 0, 0 ], // 1
124 | [ 0, 0, 0, 0, 0 ], // 2
125 | ]);
126 |
127 | assert( grid.contains(RowCol(0 , 0))); // top left
128 | assert( grid.contains(RowCol(2 , 4))); // bottom right
129 | assert( grid.contains(RowCol(1 , 2))); // center
130 | assert(!grid.contains(RowCol(0 , 5))); // beyond right border
131 | assert(!grid.contains(RowCol(3 , 0))); // beyond bottom border
132 | assert(!grid.contains(RowCol(0 ,-1))); // beyond left border
133 | assert(!grid.contains(RowCol(-1, 0))); // beyond top border
134 | }
135 |
136 | /**
137 | * Get the tile at a given position in the grid.
138 | * The coord must be in bounds.
139 | *
140 | * Params:
141 | * coord = a row/column pair identifying a point in the tile grid.
142 | */
143 | ref auto tileAt(RowCol coord) {
144 | assert(this.contains(coord), "coord %s not in bounds".format(coord));
145 | return _tiles[coord.row][coord.col];
146 | }
147 |
148 | ///
149 | unittest {
150 | auto grid = rectGrid([
151 | [ 00, 01, 02, 03, 04 ],
152 | [ 10, 11, 12, 13, 14 ],
153 | [ 20, 21, 22, 23, 24 ],
154 | ]);
155 |
156 | assert(grid.tileAt(RowCol(0, 0)) == 00); // top left tile
157 | assert(grid.tileAt(RowCol(2, 4)) == 24); // bottom right tile
158 | assert(grid.tileAt(RowCol(1, 1)) == 11); // one down/right from the top left
159 |
160 | // tileAt returns a reference:
161 | grid.tileAt(RowCol(2,2)) = 99;
162 | assert(grid.tileAt(RowCol(2,2)) == 99);
163 | }
164 |
165 | /**
166 | * Access tiles via a range of coords. Tiles are returned by ref.
167 | *
168 | * Params:
169 | * coords = A range that yields coords.
170 | */
171 | ref auto tilesAt(R)(R coords) if (isInputRange!R && is(typeof(coords.front) : RowCol)) {
172 | alias GridType = typeof(this);
173 |
174 | struct Result {
175 | private GridType _grid;
176 | private R _coords;
177 |
178 | ref auto front() { return _grid.tileAt(_coords.front); }
179 | bool empty() { return _coords.empty(); }
180 | void popFront() { _coords.popFront(); }
181 |
182 | static if (isForwardRange!R) {
183 | auto save() { return Result(_grid, _coords.save); }
184 | }
185 |
186 | static if (isBidirectionalRange!R) {
187 | auto back() { return _grid.tileAt(_coords.back); }
188 | void popBack() { _coords.popBack(); }
189 | }
190 | }
191 |
192 | return Result(this, coords);
193 | }
194 |
195 | unittest {
196 | import std.range;
197 | import std.algorithm : equal;
198 |
199 | auto grid = rectGrid([
200 | [ 00, 01, 02, 03, 04 ],
201 | [ 10, 11, 12, 13, 14 ],
202 | [ 20, 21, 22, 23, 24 ],
203 | ]);
204 |
205 | auto r1 = chain(RowCol(0,0).only, RowCol(2,2).only, RowCol(2,0).only);
206 | assert(grid.tilesAt(r1).equal([00, 22, 20]));
207 |
208 | auto r2 = grid.allCoords.filter!(x => x.row > 1 && x.col > 2);
209 | auto tiles = grid.tilesAt(r2);
210 | assert(tiles.equal([23, 24]));
211 | assert(tiles.equal([23, 24]));
212 | }
213 |
214 | /**
215 | * Get a range that iterates through every coordinate in the grid.
216 | */
217 | auto allCoords() {
218 | return RowCol(0,0).span(RowCol(this.numRows, this.numCols));
219 | }
220 |
221 | /// Use allCoords to apply range-oriented functions to the coords in the grid.
222 | unittest {
223 | import std.algorithm;
224 |
225 | auto myGrid = rectGrid([
226 | [ 00, 01, 02, 03, 04 ],
227 | [ 10, 11, 12, 13, 14 ],
228 | [ 20, 21, 22, 23, 24 ],
229 | ]);
230 |
231 | auto coords = myGrid.allCoords
232 | .filter!(x => x.col > 3)
233 | .map!(x => x.row * 10 + x.col);
234 |
235 | assert(coords.equal([04, 14, 24]));
236 | }
237 |
238 | /**
239 | * Get a range that iterates through every tile in the grid.
240 | */
241 | auto allTiles() {
242 | return tilesAt(this.allCoords);
243 | }
244 |
245 | /// Use allTiles to apply range-oriented functions to the tiles in the grid.
246 | unittest {
247 | import std.algorithm;
248 |
249 | auto myGrid = rectGrid([
250 | [ 00, 01, 02, 03, 04 ],
251 | [ 10, 11, 12, 13, 14 ],
252 | [ 20, 21, 22, 23, 24 ],
253 | ]);
254 |
255 | assert(myGrid.allTiles.filter!(x => x > 22).equal([23, 24]));
256 |
257 | // use ref with allTiles to apply modifications
258 | foreach(ref tile ; myGrid.allTiles.filter!(x => x < 10)) {
259 | tile += 10;
260 | }
261 |
262 | assert(myGrid.tileAt(RowCol(0,0)) == 10);
263 | }
264 |
265 | /// Foreach over every tile in the grid. Supports `ref`.
266 | int opApply(int delegate(ref TileType) fn) {
267 | int res = 0;
268 |
269 | foreach(coord ; this.allCoords) {
270 | res = fn(this.tileAt(coord));
271 | if (res) break;
272 | }
273 |
274 | return res;
275 | }
276 |
277 | /// foreach with coords
278 | unittest {
279 | auto myGrid = rectGrid([
280 | [ 00, 01, 02, 03, 04 ],
281 | [ 10, 11, 12, 13, 14 ],
282 | [ 20, 21, 22, 23, 24 ],
283 | ]);
284 |
285 | int[] actual;
286 | foreach(tile ; myGrid) { actual ~= tile; }
287 |
288 | assert(actual == [
289 | 00, 01, 02, 03, 04,
290 | 10, 11, 12, 13, 14,
291 | 20, 21, 22, 23, 24]);
292 | }
293 |
294 | /// Foreach over every (coord,tile) pair in the grid. Supports `ref`.
295 | int opApply(int delegate(RowCol, ref TileType) fn) {
296 | int res = 0;
297 |
298 | foreach(coord ; this.allCoords) {
299 | res = fn(coord, this.tileAt(coord));
300 | if (res) break;
301 | }
302 |
303 | return res;
304 | }
305 |
306 | /// foreach with coords
307 | unittest {
308 | auto myGrid = rectGrid([
309 | [ 00, 01, 02, 03, 04 ],
310 | [ 10, 11, 12, 13, 14 ],
311 | [ 20, 21, 22, 23, 24 ],
312 | ]);
313 |
314 | foreach(coord, tile ; myGrid) {
315 | assert(tile == coord.row * 10 + coord.col);
316 | }
317 | }
318 |
319 | /**
320 | * Same as maskTiles, but return coords instead of tiles.
321 | *
322 | * Params:
323 | * offset = map coordinate on which to align the top-left corner of the mask.
324 | * mask = a rectangular array of true/false values indicating which tiles to take.
325 | * each true value takes the tile at that grid coordinate.
326 | * the mask should be in row major order (indexed as mask[row][col]).
327 | */
328 | auto maskCoords(T)(RowCol offset, in T mask) if (isValidMask!T) {
329 | assertNotJagged(mask, "mask cannot be a jagged array");
330 |
331 | return RowCol(0,0).span(arraySize2D(mask))
332 | .filter!(x => mask[x.row][x.col]) // remove elements that are 0 in the mask
333 | .map!(x => x + offset) // add the offset to get the corresponding map coord
334 | .filter!(x => this.contains(x)); // remove coords outside of bounds
335 | }
336 |
337 | /**
338 | * Select specific tiles from this slice based on a mask.
339 | *
340 | * The upper left corner of the mask is positioned at the given offset.
341 | * Each map tile that is overlaid with a 'true' value is included in the result.
342 | * The mask is allowed to extend out of bounds - out of bounds coordinates are ignored
343 | *
344 | * Params:
345 | * offset = map coordinate on which to align the top-left corner of the mask.
346 | * mask = a rectangular array of true/false values indicating which tiles to take.
347 | * each true value takes the tile at that grid coordinate.
348 | * the mask should be in row major order (indexed as mask[row][col]).
349 | *
350 | * Examples:
351 | * Suppose you are making a strategy game, and an attack hits all tiles in a cross pattern.
352 | * This attack is used on the tile at row 2, column 3.
353 | * You want to check each tile that was affected to see if any unit was hit:
354 | * --------------
355 | * // cross pattern
356 | * ubyte[][] attackPattern = [
357 | * [0,1,0]
358 | * [1,1,1]
359 | * [0,1,0]
360 | * ];
361 | *
362 | * // get tiles contained by a cross pattern centered at (2,3)
363 | * auto tilesHit = map.maskTilesAround((RowCol(2, 3), attackPattern));
364 | *
365 | * // now do something with those tiles
366 | * auto unitsHit = tilesHit.map!(tile => tile.unitOnTile).filter!(unit => unit !is null);
367 | * foreach(unit ; unitsHit) unit.applySomeEffect;
368 | * --------------
369 | */
370 | auto maskTiles(T)(RowCol offset, in T mask) if (isValidMask!T) {
371 | return this.maskCoords(mask).map!(x => this.tileAt(x));
372 | }
373 |
374 | /**
375 | * Same as maskCoords, but centered.
376 | *
377 | * Params:
378 | * center = map coord on which to position the center of the mask.
379 | * if the mask has an even side length, rounds down to compute the 'center'
380 | * mask = a rectangular array of true/false values indicating which tiles to take.
381 | * each true value takes the tile at that grid coordinate.
382 | * the mask should be in row major order (indexed as mask[row][col]).
383 | */
384 | auto maskCoordsAround(T)(RowCol center, in T mask) if (isValidMask!T) {
385 | assertNotJagged(mask, "mask");
386 |
387 | auto offset = center - RowCol(mask.length / 2, mask[0].length / 2);
388 |
389 | return this.maskCoords(offset, mask);
390 | }
391 |
392 | /**
393 | * Same as maskTiles, but centered.
394 | *
395 | * Params:
396 | * center = map coord on which to position the center of the mask.
397 | * if the mask has an even side length, rounds down to compute the 'center'
398 | * mask = a rectangular array of true/false values indicating which tiles to take.
399 | * each true value takes the tile at that grid coordinate.
400 | * the mask should be in row major order (indexed as mask[row][col]).
401 | */
402 | auto maskTilesAround(T)(RowCol center, in T mask) if (isValidMask!T) {
403 | return this.maskCoordsAround(center, mask).map!(x => this.tileAt(x));
404 | }
405 |
406 | /// More masking examples:
407 | unittest {
408 | import std.array : empty;
409 | import std.algorithm : equal;
410 |
411 | auto myGrid = rectGrid([
412 | [ 00, 01, 02, 03, 04 ],
413 | [ 10, 11, 12, 13, 14 ],
414 | [ 20, 21, 22, 23, 24 ],
415 | ]);
416 |
417 | uint[3][3] mask1 = [
418 | [ 1, 1, 1 ],
419 | [ 0, 0, 0 ],
420 | [ 0, 0, 0 ],
421 | ];
422 | assert(myGrid.maskTilesAround(RowCol(0,0), mask1).empty);
423 | assert(myGrid.maskTilesAround(RowCol(1,1), mask1).equal([00, 01, 02]));
424 | assert(myGrid.maskTilesAround(RowCol(2,1), mask1).equal([10, 11, 12]));
425 | assert(myGrid.maskTilesAround(RowCol(2,4), mask1).equal([13, 14]));
426 |
427 | auto mask2 = [
428 | [ 0, 0, 1 ],
429 | [ 0, 0, 1 ],
430 | [ 1, 1, 1 ],
431 | ];
432 | assert(myGrid.maskTilesAround(RowCol(0,0), mask2).equal([01, 10, 11]));
433 | assert(myGrid.maskTilesAround(RowCol(1,2), mask2).equal([03, 13, 21, 22, 23]));
434 | assert(myGrid.maskTilesAround(RowCol(2,4), mask2).empty);
435 |
436 | auto mask3 = [
437 | [ 0 , 0 , 1 , 0 , 0 ],
438 | [ 1 , 0 , 1 , 0 , 1 ],
439 | [ 0 , 0 , 0 , 0 , 0 ],
440 | ];
441 | assert(myGrid.maskTilesAround(RowCol(0,0), mask3).equal([00, 02]));
442 | assert(myGrid.maskTilesAround(RowCol(1,2), mask3).equal([02, 10, 12, 14]));
443 |
444 | auto mask4 = [
445 | [ 1 , 1 , 1 , 0 , 1 ],
446 | ];
447 | assert(myGrid.maskTilesAround(RowCol(0,0), mask4).equal([00, 02]));
448 | assert(myGrid.maskTilesAround(RowCol(2,2), mask4).equal([20, 21, 22, 24]));
449 |
450 | auto mask5 = [
451 | [ 1 ],
452 | [ 1 ],
453 | [ 0 ],
454 | [ 1 ],
455 | [ 1 ],
456 | ];
457 | assert(myGrid.maskTilesAround(RowCol(0,4), mask5).equal([14, 24]));
458 | assert(myGrid.maskTilesAround(RowCol(1,1), mask5).equal([01, 21]));
459 | }
460 |
461 | /**
462 | * Return all tiles adjacent to the tile at the given coord (not including the tile itself).
463 | *
464 | * Params:
465 | * coord = grid location of center tile.
466 | * diagonals = if no, include tiles to the north, south, east, and west only.
467 | * if yes, also include northwest, northeast, southwest, and southeast.
468 | */
469 | auto adjacentTiles(RowCol coord, Diagonals diagonals = Diagonals.no) {
470 | return coord.adjacent(diagonals)
471 | .filter!(x => this.contains(x))
472 | .map!(x => this.tileAt(x));
473 | }
474 |
475 | ///
476 | unittest {
477 | import std.algorithm : equal;
478 | auto myGrid = rectGrid([
479 | [ 00, 01, 02, 03, 04 ],
480 | [ 10, 11, 12, 13, 14 ],
481 | [ 20, 21, 22, 23, 24 ],
482 | ]);
483 |
484 | assert(myGrid.adjacentTiles(RowCol(0,0)).equal([01, 10]));
485 | assert(myGrid.adjacentTiles(RowCol(1,1)).equal([01, 10, 12, 21]));
486 | assert(myGrid.adjacentTiles(RowCol(2,2)).equal([12, 21, 23]));
487 | assert(myGrid.adjacentTiles(RowCol(2,4)).equal([14, 23]));
488 |
489 | assert(myGrid.adjacentTiles(RowCol(0,0), Diagonals.yes)
490 | .equal([01, 10, 11]));
491 | assert(myGrid.adjacentTiles(RowCol(1,1), Diagonals.yes)
492 | .equal([00, 01, 02, 10, 12, 20, 21, 22]));
493 | assert(myGrid.adjacentTiles(RowCol(2,2), Diagonals.yes)
494 | .equal([11, 12, 13, 21, 23]));
495 | assert(myGrid.adjacentTiles(RowCol(2,4), Diagonals.yes)
496 | .equal([13, 14, 23]));
497 | }
498 | }
499 |
500 | // NOTE: declared outside of struct due to issues with alias parameters on templated structs.
501 | // See https://issues.dlang.org/show_bug.cgi?id=11098
502 | /**
503 | * Generate a mask from a region of tiles based on a condition.
504 | *
505 | * For each tile in the grid, sets the corresponding element of mask to the result of fn(tile).
506 | * If a coordinate is out of bounds (e.g. if you are generating a mask from a slice that extends
507 | * over the map border) the mask value is the init value of the mask's element type.
508 | *
509 | * Params:
510 | * fn = function that generates a mask entry from a tile
511 | * grid = grid to generate mask from
512 | * offset = map coord from which to start the top-left corner of the mask
513 | * mask = rectangular array to populate with generated mask values.
514 | * must match the size of the grid
515 | */
516 | void createMask(alias fn, T, U)(T grid, RowCol offset, ref U mask)
517 | if(__traits(compiles, { mask[0][0] = fn(grid.tileAt(RowCol(0,0))); }))
518 | {
519 | assertNotJagged(mask, "mask");
520 |
521 | foreach(coord ; RowCol(0,0).span(arraySize2D(mask))) {
522 | auto mapCoord = coord + offset;
523 |
524 | mask[coord.row][coord.col] = (grid.contains(mapCoord)) ?
525 | fn(grid.tileAt(mapCoord)) : // in bounds, apply fn to generate mask value
526 | typeof(mask[0][0]).init; // out of bounds, use default value
527 | }
528 | }
529 |
530 | /**
531 | * Same as createMask, but specify the offset of the mask's center rather than the top-left corner.
532 | *
533 | * Params:
534 | * fn = function that generates a mask entry from a tile
535 | * grid = grid to generate mask from
536 | * center = center position around which to generate mask
537 | * mask = rectangular array to populate with generated mask values.
538 | * must match the size of the grid
539 | */
540 | void createMaskAround(alias fn, T, U)(T grid, RowCol center, ref U mask)
541 | if(__traits(compiles, { mask[0][0] = fn(grid.tileAt(RowCol(0,0))); }))
542 | {
543 | assertNotJagged(mask, "mask");
544 |
545 | auto offset = center - RowCol(mask.length / 2, mask[0].length / 2);
546 | grid.createMask!fn(offset, mask);
547 | }
548 |
549 | ///
550 | unittest {
551 | auto myGrid = rectGrid([
552 | [ 00, 01, 02, 03, 04 ],
553 | [ 10, 11, 12, 13, 14 ],
554 | [ 20, 21, 22, 23, 24 ],
555 | ]);
556 |
557 | uint[3][3] mask;
558 |
559 | myGrid.createMaskAround!(tile => tile > 10)(RowCol(1,1), mask);
560 |
561 | assert(mask == [
562 | [0, 0, 0],
563 | [0, 1, 1],
564 | [1, 1, 1],
565 | ]);
566 |
567 | myGrid.createMaskAround!(tile => tile < 24)(RowCol(2,4), mask);
568 |
569 | assert(mask == [
570 | [1, 1, 0],
571 | [1, 0, 0],
572 | [0, 0, 0],
573 | ]);
574 | }
575 |
576 | private:
577 | // assertion helper for input array args
578 | void assertNotJagged(T)(in T array, string msg) {
579 | assert(array[].all!(x => x.length == array[0].length), msg);
580 | }
581 |
582 | // get a RowCol representing the size of a 2D array (assumed non-jagged).
583 | auto arraySize2D(T)(in T array) {
584 | return RowCol(array.length, array[0].length);
585 | }
586 |
587 | enum isValidMask(T) = is(typeof(cast(bool) T.init[0][0])) && // must have boolean elements
588 | is(typeof(T.init.length) : size_t) && // must have row count
589 | is(typeof(T.init[0].length) : size_t); // must have column count
590 |
591 | unittest {
592 | static assert(isValidMask!(int[][]));
593 | static assert(isValidMask!(uint[][3]));
594 | static assert(isValidMask!(uint[3][]));
595 | static assert(isValidMask!(uint[3][3]));
596 |
597 | static assert(!isValidMask!int);
598 | static assert(!isValidMask!(int[]));
599 | }
600 |
--------------------------------------------------------------------------------
/src/dtiled/map.d:
--------------------------------------------------------------------------------
1 | /**
2 | * A map is essentially a grid with additional information about tile positions and sizes.
3 | *
4 | * Currently, the only map type is `OrthoMap`, but `IsoMap` and `HexMap` may be added in later
5 | * versions.
6 | *
7 | * An `OrthoMap` represents a map of rectangular (usually square) tiles that are arranged
8 | * orthogonally. In other words, all tiles in a row are at the same y corrdinate, and all tiles in
9 | * a column are at the same x coordinate (as opposed to an Isometric map, where there is an offset).
10 | *
11 | * An `OrthoMap` provides all of the functionality as `RectGrid`.
12 | * It also stores the size of tiles and provides functions to translate between 'grid coordinates'
13 | * (row/column) and 'screen coordinates' (x/y pixel positions).
14 | *
15 | * Authors: rcorre
16 | * License: MIT
17 | * Copyright: Copyright © 2015, Ryan Roden-Corrent
18 | */
19 | module dtiled.map;
20 |
21 | import std.format;
22 | import dtiled.coords;
23 | import dtiled.grid;
24 |
25 | // need a test here to kickstart the unit tests inside OrthoMap!T
26 | unittest {
27 | auto map = OrthoMap!int([[1]], 32, 32);
28 | }
29 |
30 | /**
31 | * Generic Tile Map structure that uses a single layer of tiles in an orthogonal grid.
32 | *
33 | * This provides a 'flat' representation of multiple tile and object layers.
34 | * T can be whatever type you would like to use to represent a single tile within the map.
35 | *
36 | * An OrthoMap supports all the operations of dtiled.grid for working with RowCol coordinates.
37 | * Additionally, it stores information about tile size for operations in pixel coordinate space.
38 | */
39 | struct OrthoMap(Tile) {
40 | /// The underlying tile grid structure, surfaced with alias this.
41 | RectGrid!(Tile[][]) grid;
42 | alias grid this;
43 |
44 | private {
45 | int _tileWidth;
46 | int _tileHeight;
47 | }
48 |
49 | /**
50 | * Construct an orthogonal tilemap from a rectangular (non-jagged) grid of tiles.
51 | *
52 | * Params:
53 | * tiles = tiles arranged in **row major** order, indexed as tiles[row][col].
54 | * tileWidth = width of each tile in pixels
55 | * tileHeight = height of each tile in pixels
56 | */
57 | this(Tile[][] tiles, int tileWidth, int tileHeight) {
58 | this(rectGrid(tiles), tileWidth, tileHeight);
59 | }
60 |
61 | /// ditto
62 | this(RectGrid!(Tile[][]) grid, int tileWidth, int tileHeight) {
63 | _tileWidth = tileWidth;
64 | _tileHeight = tileHeight;
65 |
66 | this.grid = grid;
67 | }
68 |
69 | @property {
70 | /// Width of each tile in pixels
71 | auto tileWidth() { return _tileWidth; }
72 | /// Height of each tile in pixels
73 | auto tileHeight() { return _tileHeight; }
74 | }
75 |
76 | /**
77 | * Get the grid location corresponding to a given pixel coordinate.
78 | *
79 | * If the point is out of map bounds, the returned coord will also be out of bounds.
80 | * Use the containsPoint method to check if a point is in bounds.
81 | */
82 | auto coordAtPoint(T)(T pos) if (isPixelCoord!T) {
83 | import std.math : floor;
84 | import std.traits : isFloatingPoint, Select;
85 |
86 | /* if T is not floating, cast to float for operation.
87 | * this ensures that an integral value below zero is rounded more negative,
88 | * so anything even slightly out of bounds gets a negative coord.
89 | */
90 | alias F = Select!(isFloatingPoint!T, T, float);
91 |
92 | // we need to cast the result back to the integral coordinate type
93 | alias coord_t = typeof(RowCol.row);
94 |
95 | return RowCol(cast(coord_t) floor(pos.y / cast(F) tileHeight),
96 | cast(coord_t) floor(pos.x / cast(F) tileWidth));
97 | }
98 |
99 | ///
100 | unittest {
101 | struct Vec { float x, y; }
102 |
103 | // 5x3 map, rows from 0 to 4, cols from 0 to 2
104 | auto tiles = [
105 | [ 00, 01, 02, 03, 04, ],
106 | [ 10, 11, 12, 13, 14, ],
107 | [ 20, 21, 22, 23, 24, ],
108 | ];
109 | auto map = OrthoMap!int(tiles, 32, 32);
110 |
111 | assert(map.coordAtPoint(Vec(0 , 0 )) == RowCol(0 , 0 ));
112 | assert(map.coordAtPoint(Vec(16 , 16 )) == RowCol(0 , 0 ));
113 | assert(map.coordAtPoint(Vec(32 , 0 )) == RowCol(0 , 1 ));
114 | assert(map.coordAtPoint(Vec(0 , 45 )) == RowCol(1 , 0 ));
115 | assert(map.coordAtPoint(Vec(105 , 170)) == RowCol(5 , 3 ));
116 | assert(map.coordAtPoint(Vec(-10 , 0 )) == RowCol(0 , -1));
117 | assert(map.coordAtPoint(Vec(-32 , -33)) == RowCol(-2 , -1));
118 | }
119 |
120 | // test with an int pixel coord type
121 | unittest {
122 | struct Vec { int x, y; }
123 |
124 | // 5x3 map, rows from 0 to 4, cols from 0 to 2
125 | auto tiles = [
126 | [ 00, 01, 02, 03, 04, ],
127 | [ 10, 11, 12, 13, 14, ],
128 | [ 20, 21, 22, 23, 24, ],
129 | ];
130 | auto map = OrthoMap!int(tiles, 32, 32);
131 |
132 | assert(map.coordAtPoint(Vec(0 , 0 )) == RowCol(0 , 0 ));
133 | assert(map.coordAtPoint(Vec(16 , 16 )) == RowCol(0 , 0 ));
134 | assert(map.coordAtPoint(Vec(32 , 0 )) == RowCol(0 , 1 ));
135 | assert(map.coordAtPoint(Vec(0 , 45 )) == RowCol(1 , 0 ));
136 | assert(map.coordAtPoint(Vec(105 , 170)) == RowCol(5 , 3 ));
137 | assert(map.coordAtPoint(Vec(-10 , 0 )) == RowCol(0 , -1));
138 | assert(map.coordAtPoint(Vec(-32 , -33)) == RowCol(-2 , -1));
139 | }
140 |
141 | /**
142 | * True if the pixel position is within the map bounds.
143 | */
144 | bool containsPoint(T)(T pos) if (isPixelCoord!T) {
145 | return grid.contains(coordAtPoint(pos));
146 | }
147 |
148 | ///
149 | unittest {
150 | // 3x5 map, pixel bounds are [0, 0, 160, 96] (32*3 = 96, 32*5 = 160)
151 | auto grid = [
152 | [ 00, 01, 02, 03, 04, ],
153 | [ 10, 11, 12, 13, 14, ],
154 | [ 20, 21, 22, 23, 24, ],
155 | ];
156 | auto map = OrthoMap!int(grid, 32, 32);
157 |
158 | assert( map.containsPoint(PixelCoord( 0, 0))); // top left
159 | assert( map.containsPoint(PixelCoord( 159, 95))); // bottom right
160 | assert( map.containsPoint(PixelCoord( 80, 48))); // center
161 | assert(!map.containsPoint(PixelCoord( 0, 96))); // beyond right border
162 | assert(!map.containsPoint(PixelCoord( 160, 0))); // beyond bottom border
163 | assert(!map.containsPoint(PixelCoord( 0, -0.5))); // beyond left border
164 | assert(!map.containsPoint(PixelCoord(-0.5, 0))); // beyond top border
165 | }
166 |
167 | /**
168 | * Get the tile at a given pixel position on the map. Throws if out of bounds.
169 | * Params:
170 | * T = any pixel-positional point (see isPixelCoord).
171 | * pos = pixel location in 2D space
172 | */
173 | ref Tile tileAtPoint(T)(T pos) if (isPixelCoord!T) {
174 | assert(containsPoint(pos), "position %d,%d out of map bounds: ".format(pos.x, pos.y));
175 | return grid.tileAt(coordAtPoint(pos));
176 | }
177 |
178 | ///
179 | unittest {
180 | auto grid = [
181 | [ 00, 01, 02, 03, 04, ],
182 | [ 10, 11, 12, 13, 14, ],
183 | [ 20, 21, 22, 23, 24, ],
184 | ];
185 |
186 | auto map = OrthoMap!int(grid, 32, 32);
187 |
188 | assert(map.tileAtPoint(PixelCoord( 0, 0)) == 00); // corner of top left tile
189 | assert(map.tileAtPoint(PixelCoord( 16, 30)) == 00); // inside top left tile
190 | assert(map.tileAtPoint(PixelCoord(149, 95)) == 24); // inside bottom right tile
191 | }
192 |
193 | /**
194 | * Get the pixel offset of the top-left corner of the tile at the given coord.
195 | *
196 | * Params:
197 | * coord = grid location of tile.
198 | */
199 | PixelCoord tileOffset(RowCol coord) {
200 | return PixelCoord(coord.col * tileWidth,
201 | coord.row * tileHeight);
202 | }
203 |
204 | ///
205 | unittest {
206 | // 2 rows, 3 cols, 32x64 tiles
207 | auto grid = [
208 | [ 00, 01, 02, ],
209 | [ 10, 11, 12, ],
210 | ];
211 | auto myMap = OrthoMap!int(grid, 32, 64);
212 |
213 | assert(myMap.tileOffset(RowCol(0, 0)) == PixelCoord(0, 0));
214 | assert(myMap.tileOffset(RowCol(1, 2)) == PixelCoord(64, 64));
215 | }
216 |
217 | /**
218 | * Get the pixel offset of the center of the tile at the given coord.
219 | *
220 | * Params:
221 | * coord = grid location of tile.
222 | */
223 | PixelCoord tileCenter(RowCol coord) {
224 | return PixelCoord(coord.col * tileWidth + tileWidth / 2,
225 | coord.row * tileHeight + tileHeight / 2);
226 | }
227 |
228 | ///
229 | unittest {
230 | // 2 rows, 3 cols, 32x64 tiles
231 | auto grid = [
232 | [ 00, 01, 02, ],
233 | [ 10, 11, 12, ],
234 | ];
235 | auto myMap = OrthoMap!int(grid, 32, 64);
236 |
237 | assert(myMap.tileCenter(RowCol(0, 0)) == PixelCoord(16, 32));
238 | assert(myMap.tileCenter(RowCol(1, 2)) == PixelCoord(80, 96));
239 | }
240 | }
241 |
242 | /// Foreach over every tile in the map
243 | unittest {
244 | import std.algorithm : equal;
245 |
246 | auto grid = [
247 | [ 00, 01, 02, ],
248 | [ 10, 11, 12, ],
249 | ];
250 | auto myMap = OrthoMap!int(grid, 32, 64);
251 |
252 | int[] result;
253 |
254 | foreach(tile ; myMap) result ~= tile;
255 |
256 | assert(result.equal([ 00, 01, 02, 10, 11, 12 ]));
257 | }
258 |
259 | /// Use ref with foreach to modify tiles
260 | unittest {
261 | auto grid = [
262 | [ 00, 01, 02, ],
263 | [ 10, 11, 12, ],
264 | ];
265 | auto myMap = OrthoMap!int(grid, 32, 64);
266 |
267 | foreach(ref tile ; myMap) tile += 30;
268 |
269 | assert(myMap.tileAt(RowCol(1,1)) == 41);
270 | }
271 |
272 | /// Foreach over every (coord, tile) pair in the map
273 | unittest {
274 | import std.algorithm : equal;
275 |
276 | auto grid = [
277 | [ 00, 01, 02, ],
278 | [ 10, 11, 12, ],
279 | ];
280 | auto myMap = OrthoMap!int(grid, 32, 64);
281 |
282 |
283 | foreach(coord, tile ; myMap) assert(myMap.tileAt(coord) == tile);
284 | }
285 |
--------------------------------------------------------------------------------
/src/dtiled/package.d:
--------------------------------------------------------------------------------
1 | module dtiled;
2 |
3 | public import dtiled.map;
4 | public import dtiled.data;
5 | public import dtiled.grid;
6 | public import dtiled.coords;
7 | public import dtiled.algorithm;
8 |
--------------------------------------------------------------------------------
/tests/data.d:
--------------------------------------------------------------------------------
1 | module tests.data;
2 |
3 | import std.conv;
4 | import std.range;
5 | import std.algorithm;
6 | import std.path : buildPath, setExtension;
7 | import std.exception : assertThrown;
8 | import dtiled.data;
9 |
10 | enum testPath(string name) = "tests".buildPath("resources", name).setExtension("json");
11 |
12 | // expected gids for the test terrain layer
13 | enum terrainGids = [1, 2, 1, 2, 3, 1, 3, 1, 2, 2, 3, 3, 4, 4, 4, 1];
14 | enum flippedTerrainGids = [1, 2, 2, 1, 3, 1, 1, 3, 4, 4, 1, 4, 2, 2, 3, 3];
15 |
16 | /// Load a map containing a single tile layer
17 | unittest {
18 | // load map
19 | auto map = MapData.load(testPath!"tiles");
20 |
21 | // general fields
22 | assert(map.numRows == 4);
23 | assert(map.numCols == 4);
24 | assert(map.tileWidth == 32);
25 | assert(map.tileHeight == 32);
26 | assert(map.renderOrder == MapData.RenderOrder.rightDown);
27 | assert(map.orientation == MapData.Orientation.orthogonal);
28 | assert(map.backgroundColor == "#656667");
29 |
30 | // user defined properties
31 | assert(map.properties["mapProperty1"] == "one");
32 | assert(map.properties["mapProperty2"] == "two");
33 |
34 | // this map should have a single tile layer
35 | assert(map.layers.length == 1);
36 |
37 | auto tiles = map.layers[0];
38 | assert(tiles.name == "terrain");
39 | assert(tiles.data == terrainGids);
40 | assert(tiles.numRows == 4);
41 | assert(tiles.numCols == 4);
42 | assert(tiles.opacity == 1f);
43 | assert(tiles.type == LayerData.Type.tilelayer);
44 | assert(tiles.visible);
45 | assert(tiles.x == 0);
46 | assert(tiles.y == 0);
47 |
48 | // getLayer should return layers[0]
49 | assert(map.getLayer("terrain") == tiles);
50 |
51 | // this map should have a single tile set
52 | assert(map.tilesets.length == 1); auto tileset = map.tilesets[0];
53 | // fields
54 | assert(tileset.name == "terrain");
55 | assert(tileset.firstGid == 1);
56 | assert(tileset.imageHeight == 64);
57 | assert(tileset.imageWidth == 64);
58 | assert(tileset.margin == 0);
59 | assert(tileset.tileHeight == 32);
60 | assert(tileset.tileWidth == 32);
61 | assert(tileset.spacing == 0);
62 | // properties
63 | assert(tileset.numRows == 2);
64 | assert(tileset.numCols == 2);
65 | assert(tileset.numTiles == 4);
66 |
67 | // getTileset should return tilesets[0]
68 | assert(map.getTileset("terrain") == tileset);
69 | }
70 |
71 | /// Load a map containing an object layer
72 | unittest {
73 | import std.string : format;
74 |
75 | // load map
76 | auto map = MapData.load(testPath!"objects");
77 |
78 | // Layer 1 is an object layer in the test map
79 | auto layer = map.layers[1];
80 | assert(layer.name == "things");
81 | assert(layer.type == LayerData.Type.objectgroup);
82 | assert(layer.drawOrder == "topdown");
83 |
84 | // Tileset 1 is the tileset used for the objects
85 | auto tileset = map.tilesets[1];
86 | assert(tileset.name == "numbers");
87 | auto objects = layer.objects;
88 |
89 | // helper to check an object in the test data
90 | void checkObject(int num) {
91 | string name = "number%d".format(num);
92 | auto found = objects.find!(x => x.name == name);
93 | assert(!found.empty, "no object with name " ~ name);
94 | auto obj = found.front;
95 |
96 | assert(obj.gid == tileset.firstGid + num - 1); // number1 is the zeroth tile, ect.
97 | assert(obj.type == (num % 2 == 0 ? "even" : "odd")); // just an arbitrarily picked type
98 | //assert(obj.properties["half"].to!int == num / 2 ));
99 | assert(obj.rotation == 0);
100 | assert(obj.visible);
101 | }
102 |
103 | checkObject(1);
104 | checkObject(2);
105 | checkObject(3);
106 | checkObject(4);
107 | }
108 |
109 | /// Load a map containing flipped (mirrored) tiles.
110 | unittest {
111 | import std.algorithm : map, equal;
112 |
113 | // load map
114 | auto tileMap = MapData.load(testPath!"flipped_tiles");
115 |
116 | // this map should have a single tile layer
117 | assert(tileMap.layers.length == 1);
118 | auto layer = tileMap.layers[0];
119 |
120 | // clear special bits to get actual gid
121 | auto gids = layer.data.map!(gid => gid & ~TiledFlag.all);
122 | // with the special bits cleared, the gids should be the same as in the original map
123 | assert(gids.equal(flippedTerrainGids));
124 |
125 | // isolate special bits to get flipped state
126 | auto flags = layer.data.map!(gid => gid & TiledFlag.all);
127 |
128 | with(TiledFlag) {
129 | enum N = none;
130 | enum H = flipHorizontal;
131 | enum V = flipVertical;
132 | enum D = H | V;
133 |
134 | enum flippedState = [
135 | N, N, H, H,
136 | N, N, H, H,
137 | V, V, D, D,
138 | V, V, D, D,
139 | ];
140 |
141 | assert(flags.equal(flippedState));
142 | }
143 | }
144 |
145 | /// Load a map containing flipped (mirrored) objects.
146 | unittest {
147 | import std.conv;
148 | import std.string : format;
149 |
150 | // load map
151 | auto map = MapData.load(testPath!"flipped_objects");
152 |
153 | // Layer 1 is an object layer in the test map
154 | auto layer = map.layers[1];
155 |
156 | // Tileset 1 is the tileset used for the objects
157 | auto tileset = map.tilesets[1];
158 | assert(tileset.name == "numbers");
159 | auto objects = layer.objects;
160 |
161 | // helper to check an object in the test data
162 | void checkObject(int num, TiledFlag expectedFlags) {
163 | string name = "number%d".format(num);
164 | auto found = objects.find!(x => x.name == name);
165 | assert(!found.empty, "no object with name " ~ name);
166 | auto obj = found.front;
167 |
168 | auto gid = obj.gid & ~TiledFlag.all;
169 | auto flags = obj.gid & TiledFlag.all;
170 | assert(gid == tileset.firstGid + num - 1); // number1 is the zeroth tile, ect.
171 | assert(flags == expectedFlags,
172 | "tile %d: expected flag %s, got %s".format(num, expectedFlags, cast(TiledFlag) flags));
173 | }
174 |
175 | checkObject(1, TiledFlag.none);
176 | checkObject(2, TiledFlag.flipVertical);
177 | checkObject(3, TiledFlag.flipHorizontal);
178 | checkObject(4, TiledFlag.flipHorizontal | TiledFlag.flipVertical);
179 | }
180 |
--------------------------------------------------------------------------------
/tests/resources/flipped_objects.json:
--------------------------------------------------------------------------------
1 | { "backgroundcolor":"#656667",
2 | "height":4,
3 | "layers":[
4 | {
5 | "data":[1, 2, 1, 2, 3, 1, 3, 1, 2, 2, 3, 3, 4, 4, 4, 1],
6 | "height":4,
7 | "name":"terrain",
8 | "opacity":1,
9 | "properties":
10 | {
11 | "tileLayerProp":"1"
12 | },
13 | "type":"tilelayer",
14 | "visible":true,
15 | "width":4,
16 | "x":0,
17 | "y":0
18 | },
19 | {
20 | "draworder":"topdown",
21 | "height":0,
22 | "name":"things",
23 | "objects":[
24 | {
25 | "gid":5,
26 | "height":0,
27 | "id":1,
28 | "name":"number1",
29 | "properties":
30 | {
31 | "isPrime":"false"
32 | },
33 | "rotation":0,
34 | "type":"odd",
35 | "visible":true,
36 | "width":0,
37 | "x":32,
38 | "y":32
39 | },
40 | {
41 | "gid":1073741830,
42 | "height":0,
43 | "id":2,
44 | "name":"number2",
45 | "properties":
46 | {
47 | "isPrime":"true"
48 | },
49 | "rotation":0,
50 | "type":"even",
51 | "visible":true,
52 | "width":0,
53 | "x":64,
54 | "y":64
55 | },
56 | {
57 | "gid":2147483655,
58 | "height":0,
59 | "id":3,
60 | "name":"number3",
61 | "properties":
62 | {
63 | "isPrime":"true"
64 | },
65 | "rotation":0,
66 | "type":"odd",
67 | "visible":true,
68 | "width":0,
69 | "x":32,
70 | "y":96
71 | },
72 | {
73 | "gid":3221225480,
74 | "height":0,
75 | "id":4,
76 | "name":"number4",
77 | "properties":
78 | {
79 | "isPrime":"false"
80 | },
81 | "rotation":0,
82 | "type":"even",
83 | "visible":true,
84 | "width":0,
85 | "x":64,
86 | "y":128
87 | }],
88 | "opacity":1,
89 | "properties":
90 | {
91 | "objectLayerProp":"1"
92 | },
93 | "type":"objectgroup",
94 | "visible":true,
95 | "width":0,
96 | "x":0,
97 | "y":0
98 | }],
99 | "nextobjectid":6,
100 | "orientation":"orthogonal",
101 | "properties":
102 | {
103 | "mapProperty1":"one",
104 | "mapProperty2":"two"
105 | },
106 | "renderorder":"right-down",
107 | "tileheight":32,
108 | "tilesets":[
109 | {
110 | "firstgid":1,
111 | "image":"terrain.png",
112 | "imageheight":64,
113 | "imagewidth":64,
114 | "margin":0,
115 | "name":"terrain",
116 | "properties":
117 | {
118 |
119 | },
120 | "spacing":0,
121 | "tileheight":32,
122 | "tilewidth":32
123 | },
124 | {
125 | "firstgid":5,
126 | "image":"numbers.png",
127 | "imageheight":64,
128 | "imagewidth":64,
129 | "margin":0,
130 | "name":"numbers",
131 | "properties":
132 | {
133 |
134 | },
135 | "spacing":0,
136 | "tileheight":32,
137 | "tilewidth":32
138 | }],
139 | "tilewidth":32,
140 | "version":1,
141 | "width":4
142 | }
--------------------------------------------------------------------------------
/tests/resources/flipped_objects.tmx:
--------------------------------------------------------------------------------
1 |
2 |
62 |
--------------------------------------------------------------------------------
/tests/resources/flipped_tiles.json:
--------------------------------------------------------------------------------
1 | { "backgroundcolor":"#656667",
2 | "height":4,
3 | "layers":[
4 | {
5 | "data":[1, 2, 2147483650, 2147483649, 3, 1, 2147483649, 2147483651, 1073741828, 1073741828, 3221225473, 3221225476, 1073741826, 1073741826, 3221225475, 3221225475],
6 | "height":4,
7 | "name":"terrain",
8 | "opacity":1,
9 | "properties":
10 | {
11 | "tileLayerProp":"1"
12 | },
13 | "type":"tilelayer",
14 | "visible":true,
15 | "width":4,
16 | "x":0,
17 | "y":0
18 | }],
19 | "nextobjectid":1,
20 | "orientation":"orthogonal",
21 | "properties":
22 | {
23 | "mapProperty1":"one",
24 | "mapProperty2":"two"
25 | },
26 | "renderorder":"right-down",
27 | "tileheight":32,
28 | "tilesets":[
29 | {
30 | "firstgid":1,
31 | "image":"terrain.png",
32 | "imageheight":64,
33 | "imagewidth":64,
34 | "margin":0,
35 | "name":"terrain",
36 | "properties":
37 | {
38 |
39 | },
40 | "spacing":0,
41 | "tileheight":32,
42 | "tilewidth":32
43 | }],
44 | "tilewidth":32,
45 | "version":1,
46 | "width":4
47 | }
--------------------------------------------------------------------------------
/tests/resources/flipped_tiles.tmx:
--------------------------------------------------------------------------------
1 |
2 |
34 |
--------------------------------------------------------------------------------
/tests/resources/numbers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rcorre/dtiled/a39c1066b050929630044f1d8426230a984ef4ac/tests/resources/numbers.png
--------------------------------------------------------------------------------
/tests/resources/objects.json:
--------------------------------------------------------------------------------
1 | { "backgroundcolor":"#656667",
2 | "height":4,
3 | "layers":[
4 | {
5 | "data":[1, 2, 1, 2, 3, 1, 3, 1, 2, 2, 3, 3, 4, 4, 4, 1],
6 | "height":4,
7 | "name":"terrain",
8 | "opacity":1,
9 | "properties":
10 | {
11 | "tileLayerProp":"1"
12 | },
13 | "type":"tilelayer",
14 | "visible":true,
15 | "width":4,
16 | "x":0,
17 | "y":0
18 | },
19 | {
20 | "draworder":"topdown",
21 | "height":0,
22 | "name":"things",
23 | "objects":[
24 | {
25 | "gid":5,
26 | "height":0,
27 | "id":1,
28 | "name":"number1",
29 | "properties":
30 | {
31 | "isPrime":"false"
32 | },
33 | "rotation":0,
34 | "type":"odd",
35 | "visible":true,
36 | "width":0,
37 | "x":32,
38 | "y":32
39 | },
40 | {
41 | "gid":6,
42 | "height":0,
43 | "id":2,
44 | "name":"number2",
45 | "properties":
46 | {
47 | "isPrime":"true"
48 | },
49 | "rotation":0,
50 | "type":"even",
51 | "visible":true,
52 | "width":0,
53 | "x":64,
54 | "y":64
55 | },
56 | {
57 | "gid":7,
58 | "height":0,
59 | "id":3,
60 | "name":"number3",
61 | "properties":
62 | {
63 | "isPrime":"true"
64 | },
65 | "rotation":0,
66 | "type":"odd",
67 | "visible":true,
68 | "width":0,
69 | "x":32,
70 | "y":96
71 | },
72 | {
73 | "gid":8,
74 | "height":0,
75 | "id":4,
76 | "name":"number4",
77 | "properties":
78 | {
79 | "isPrime":"false"
80 | },
81 | "rotation":0,
82 | "type":"even",
83 | "visible":true,
84 | "width":0,
85 | "x":64,
86 | "y":128
87 | }],
88 | "opacity":1,
89 | "properties":
90 | {
91 | "objectLayerProp":"1"
92 | },
93 | "type":"objectgroup",
94 | "visible":true,
95 | "width":0,
96 | "x":0,
97 | "y":0
98 | }],
99 | "nextobjectid":6,
100 | "orientation":"orthogonal",
101 | "properties":
102 | {
103 | "mapProperty1":"one",
104 | "mapProperty2":"two"
105 | },
106 | "renderorder":"right-down",
107 | "tileheight":32,
108 | "tilesets":[
109 | {
110 | "firstgid":1,
111 | "image":"terrain.png",
112 | "imageheight":64,
113 | "imagewidth":64,
114 | "margin":0,
115 | "name":"terrain",
116 | "properties":
117 | {
118 |
119 | },
120 | "spacing":0,
121 | "tileheight":32,
122 | "tilewidth":32
123 | },
124 | {
125 | "firstgid":5,
126 | "image":"numbers.png",
127 | "imageheight":64,
128 | "imagewidth":64,
129 | "margin":0,
130 | "name":"numbers",
131 | "properties":
132 | {
133 |
134 | },
135 | "spacing":0,
136 | "tileheight":32,
137 | "tilewidth":32
138 | }],
139 | "tilewidth":32,
140 | "version":1,
141 | "width":4
142 | }
--------------------------------------------------------------------------------
/tests/resources/objects.tmx:
--------------------------------------------------------------------------------
1 |
2 |
62 |
--------------------------------------------------------------------------------
/tests/resources/spaced-tiles.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rcorre/dtiled/a39c1066b050929630044f1d8426230a984ef4ac/tests/resources/spaced-tiles.png
--------------------------------------------------------------------------------
/tests/resources/spaced_tiles.tmx:
--------------------------------------------------------------------------------
1 |
2 |
27 |
--------------------------------------------------------------------------------
/tests/resources/terrain.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rcorre/dtiled/a39c1066b050929630044f1d8426230a984ef4ac/tests/resources/terrain.png
--------------------------------------------------------------------------------
/tests/resources/tiles.json:
--------------------------------------------------------------------------------
1 | { "backgroundcolor":"#656667",
2 | "height":4,
3 | "layers":[
4 | {
5 | "data":[1, 2, 1, 2, 3, 1, 3, 1, 2, 2, 3, 3, 4, 4, 4, 1],
6 | "height":4,
7 | "name":"terrain",
8 | "opacity":1,
9 | "properties":
10 | {
11 | "tileLayerProp":"1"
12 | },
13 | "type":"tilelayer",
14 | "visible":true,
15 | "width":4,
16 | "x":0,
17 | "y":0
18 | }],
19 | "nextobjectid":1,
20 | "orientation":"orthogonal",
21 | "properties":
22 | {
23 | "mapProperty1":"one",
24 | "mapProperty2":"two"
25 | },
26 | "renderorder":"right-down",
27 | "tileheight":32,
28 | "tilesets":[
29 | {
30 | "firstgid":1,
31 | "image":"terrain.png",
32 | "imageheight":64,
33 | "imagewidth":64,
34 | "margin":0,
35 | "name":"terrain",
36 | "properties":
37 | {
38 |
39 | },
40 | "spacing":0,
41 | "tileheight":32,
42 | "tilewidth":32
43 | }],
44 | "tilewidth":32,
45 | "version":1,
46 | "width":4
47 | }
--------------------------------------------------------------------------------
/tests/resources/tiles.tmx:
--------------------------------------------------------------------------------
1 |
2 |
34 |
--------------------------------------------------------------------------------
/todo.txt:
--------------------------------------------------------------------------------
1 | Higher level range-based api
2 | Use structs instead of classes when possible
3 | Use unions for things like data/objects on maplayer
4 | Use guid property to return guid with top bits zeroed
5 | nest types: allow access to outer context for mapping guids to tile data
6 | What is the difference between flipH | flipV and flipDiagonal?
7 | Tests: show index that mismatched when asserting on ranges.
8 |
--------------------------------------------------------------------------------