├── .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 | [![Gitter chat](https://badges.gitter.im/rcorre/dtiled.png)](https://gitter.im/rcorre/dtiled) 5 | [![Dub Package](https://img.shields.io/dub/dt/dtiled.svg)](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 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /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 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /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 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /tests/resources/spaced-tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcorre/dtiled/a39c1066b050929630044f1d8426230a984ef4ac/tests/resources/spaced-tiles.png -------------------------------------------------------------------------------- /tests/resources/spaced_tiles.tmx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /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 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------