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: <a href="https://github.com/rcorre">rcorre</a>
16  * License: <a href="http://opensource.org/licenses/MIT">MIT</a>
17  * Copyright: Copyright © 2015, Ryan Roden-Corrent
18  */
19 module dtiled.map;
20 
21 import dtiled.coords;
22 import dtiled.grid;
23 
24 // need a test here to kickstart the unit tests inside OrthoMap!T
25 unittest {
26   auto map = OrthoMap!int([[1]], 32, 32);
27 }
28 
29 /**
30  * Generic Tile Map structure that uses a single layer of tiles in an orthogonal grid.
31  *
32  * This provides a 'flat' representation of multiple tile and object layers.
33  * T can be whatever type you would like to use to represent a single tile within the map.
34  *
35  * An OrthoMap supports all the operations of dtiled.grid for working with RowCol coordinates.
36  * Additionally, it stores information about tile size for operations in pixel coordinate space.
37  */
38 struct OrthoMap(Tile) {
39   /// The underlying tile grid structure, surfaced with alias this.
40   RectGrid!(Tile[][]) grid;
41   alias grid this;
42 
43   private {
44     int _tileWidth;
45     int _tileHeight;
46   }
47 
48   /**
49    * Construct an orthogonal tilemap from a rectangular (non-jagged) grid of tiles.
50    *
51    * Params:
52    *  tiles      = tiles arranged in **row major** order, indexed as tiles[row][col].
53    *  tileWidth  = width of each tile in pixels
54    *  tileHeight = height of each tile in pixels
55    */
56   this(Tile[][] tiles, int tileWidth, int tileHeight) {
57     this(rectGrid(tiles), tileWidth, tileHeight);
58   }
59 
60   /// ditto
61   this(RectGrid!(Tile[][]) grid, int tileWidth, int tileHeight) {
62     _tileWidth  = tileWidth;
63     _tileHeight = tileHeight;
64 
65     this.grid = grid;
66   }
67 
68   @property {
69     /// Width of each tile in pixels
70     auto tileWidth()  { return _tileWidth; }
71     /// Height of each tile in pixels
72     auto tileHeight() { return _tileHeight; }
73   }
74 
75   /**
76    * Get the grid location corresponding to a given pixel coordinate.
77    *
78    * If the point is out of map bounds, the returned coord will also be out of bounds.
79    * Use the containsPoint method to check if a point is in bounds.
80    */
81   auto coordAtPoint(T)(T pos) if (isPixelCoord!T) {
82     import std.math : floor;
83     import std.traits : isFloatingPoint, Select;
84 
85     /* if T is not floating, cast to float for operation.
86      * this ensures that an integral value below zero is rounded more negative,
87      * so anything even slightly out of bounds gets a negative coord.
88      */
89     alias F = Select!(isFloatingPoint!T, T, float);
90 
91     // we need to cast the result back to the integral coordinate type
92     alias coord_t = typeof(RowCol.row);
93 
94     return RowCol(cast(coord_t) floor(pos.y / cast(F) tileHeight),
95                   cast(coord_t) floor(pos.x / cast(F) tileWidth));
96   }
97 
98   ///
99   unittest {
100     struct Vec { float x, y; }
101 
102     // 5x3 map, rows from 0 to 4, cols from 0 to 2
103     auto tiles = [
104       [ 00, 01, 02, 03, 04, ],
105       [ 10, 11, 12, 13, 14, ],
106       [ 20, 21, 22, 23, 24, ],
107     ];
108     auto map = OrthoMap!int(tiles, 32, 32);
109 
110     assert(map.coordAtPoint(Vec(0   , 0  )) == RowCol(0  , 0 ));
111     assert(map.coordAtPoint(Vec(16  , 16 )) == RowCol(0  , 0 ));
112     assert(map.coordAtPoint(Vec(32  , 0  )) == RowCol(0  , 1 ));
113     assert(map.coordAtPoint(Vec(0   , 45 )) == RowCol(1  , 0 ));
114     assert(map.coordAtPoint(Vec(105 , 170)) == RowCol(5  , 3 ));
115     assert(map.coordAtPoint(Vec(-10 , 0  )) == RowCol(0  , -1));
116     assert(map.coordAtPoint(Vec(-32 , -33)) == RowCol(-2 , -1));
117   }
118 
119   // test with an int pixel coord type
120   unittest {
121     struct Vec { int x, y; }
122 
123     // 5x3 map, rows from 0 to 4, cols from 0 to 2
124     auto tiles = [
125       [ 00, 01, 02, 03, 04, ],
126       [ 10, 11, 12, 13, 14, ],
127       [ 20, 21, 22, 23, 24, ],
128     ];
129     auto map = OrthoMap!int(tiles, 32, 32);
130 
131     assert(map.coordAtPoint(Vec(0   , 0  )) == RowCol(0  , 0 ));
132     assert(map.coordAtPoint(Vec(16  , 16 )) == RowCol(0  , 0 ));
133     assert(map.coordAtPoint(Vec(32  , 0  )) == RowCol(0  , 1 ));
134     assert(map.coordAtPoint(Vec(0   , 45 )) == RowCol(1  , 0 ));
135     assert(map.coordAtPoint(Vec(105 , 170)) == RowCol(5  , 3 ));
136     assert(map.coordAtPoint(Vec(-10 , 0  )) == RowCol(0  , -1));
137     assert(map.coordAtPoint(Vec(-32 , -33)) == RowCol(-2 , -1));
138   }
139 
140   /**
141    * True if the pixel position is within the map bounds.
142    */
143   bool containsPoint(T)(T pos) if (isPixelCoord!T) {
144     return grid.contains(coordAtPoint(pos));
145   }
146 
147   ///
148   unittest {
149     // 3x5 map, pixel bounds are [0, 0, 160, 96] (32*3 = 96, 32*5 = 160)
150     auto grid = [
151       [ 00, 01, 02, 03, 04, ],
152       [ 10, 11, 12, 13, 14, ],
153       [ 20, 21, 22, 23, 24, ],
154     ];
155     auto map = OrthoMap!int(grid, 32, 32);
156 
157     assert( map.containsPoint(PixelCoord(   0,    0))); // top left
158     assert( map.containsPoint(PixelCoord( 159,   95))); // bottom right
159     assert( map.containsPoint(PixelCoord(  80,   48))); // center
160     assert(!map.containsPoint(PixelCoord(   0,   96))); // beyond right border
161     assert(!map.containsPoint(PixelCoord( 160,    0))); // beyond bottom border
162     assert(!map.containsPoint(PixelCoord(   0, -0.5))); // beyond left border
163     assert(!map.containsPoint(PixelCoord(-0.5,    0))); // beyond top border
164   }
165 
166   /**
167    * Get the tile at a given pixel position on the map. Throws if out of bounds.
168    * Params:
169    *  T = any pixel-positional point (see isPixelCoord).
170    *  pos = pixel location in 2D space
171    */
172   ref Tile tileAtPoint(T)(T pos) if (isPixelCoord!T) {
173     assert(containsPoint(pos), "position %d,%d out of map bounds: ".format(pos.x, pos.y));
174     return grid.tileAt(coordAtPoint(pos));
175   }
176 
177   ///
178   unittest {
179     auto grid = [
180       [ 00, 01, 02, 03, 04, ],
181       [ 10, 11, 12, 13, 14, ],
182       [ 20, 21, 22, 23, 24, ],
183     ];
184 
185     auto map = OrthoMap!int(grid, 32, 32);
186 
187     assert(map.tileAtPoint(PixelCoord(  0,  0)) == 00); // corner of top left tile
188     assert(map.tileAtPoint(PixelCoord( 16, 30)) == 00); // inside top left tile
189     assert(map.tileAtPoint(PixelCoord(149, 95)) == 24); // inside bottom right tile
190   }
191 
192   /**
193    * Get the pixel offset of the top-left corner of the tile at the given coord.
194    *
195    * Params:
196    *  coord = grid location of tile.
197    */
198   PixelCoord tileOffset(RowCol coord) {
199     return PixelCoord(coord.col * tileWidth,
200                       coord.row * tileHeight);
201   }
202 
203   ///
204   unittest {
205     // 2 rows, 3 cols, 32x64 tiles
206     auto grid = [
207       [ 00, 01, 02, ],
208       [ 10, 11, 12, ],
209     ];
210     auto myMap = OrthoMap!int(grid, 32, 64);
211 
212     assert(myMap.tileOffset(RowCol(0, 0)) == PixelCoord(0, 0));
213     assert(myMap.tileOffset(RowCol(1, 2)) == PixelCoord(64, 64));
214   }
215 
216   /**
217    * Get the pixel offset of the center of the tile at the given coord.
218    *
219    * Params:
220    *  coord = grid location of tile.
221    */
222   PixelCoord tileCenter(RowCol coord) {
223     return PixelCoord(coord.col * tileWidth  + tileWidth  / 2,
224                       coord.row * tileHeight + tileHeight / 2);
225   }
226 
227   ///
228   unittest {
229     // 2 rows, 3 cols, 32x64 tiles
230     auto grid = [
231       [ 00, 01, 02, ],
232       [ 10, 11, 12, ],
233     ];
234     auto myMap = OrthoMap!int(grid, 32, 64);
235 
236     assert(myMap.tileCenter(RowCol(0, 0)) == PixelCoord(16, 32));
237     assert(myMap.tileCenter(RowCol(1, 2)) == PixelCoord(80, 96));
238   }
239 }
240 
241 /// Foreach over every tile in the map
242 unittest {
243   import std.algorithm : equal;
244 
245   auto grid = [
246     [ 00, 01, 02, ],
247     [ 10, 11, 12, ],
248   ];
249   auto myMap = OrthoMap!int(grid, 32, 64);
250 
251   int[] result;
252 
253   foreach(tile ; myMap) result ~= tile;
254 
255   assert(result.equal([ 00, 01, 02, 10, 11, 12 ]));
256 }
257 
258 /// Use ref with foreach to modify tiles
259 unittest {
260   auto grid = [
261     [ 00, 01, 02, ],
262     [ 10, 11, 12, ],
263   ];
264   auto myMap = OrthoMap!int(grid, 32, 64);
265 
266   foreach(ref tile ; myMap) tile += 30;
267 
268   assert(myMap.tileAt(RowCol(1,1)) == 41);
269 }
270 
271 /// Foreach over every (coord, tile) pair in the map
272 unittest {
273   import std.algorithm : equal;
274 
275   auto grid = [
276     [ 00, 01, 02, ],
277     [ 10, 11, 12, ],
278   ];
279   auto myMap = OrthoMap!int(grid, 32, 64);
280 
281 
282   foreach(coord, tile ; myMap) assert(myMap.tileAt(coord) == tile);
283 }