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, lround;
83     import std.traits : isFloatingPoint, Select;
84     // if T is not floating, cast to float for operation
85     alias F = Select!(isFloatingPoint!T, T, float);
86 
87     RowCol coord;
88     coord.col = floor(pos.x / cast(F) tileWidth).lround;
89     coord.row = floor(pos.y / cast(F) tileHeight).lround;
90     return coord;
91   }
92 
93   ///
94   unittest {
95     // 5x3 map, rows from 0 to 4, cols from 0 to 2
96     auto tiles = [
97       [ 00, 01, 02, 03, 04, ],
98       [ 10, 11, 12, 13, 14, ],
99       [ 20, 21, 22, 23, 24, ],
100     ];
101     auto map = OrthoMap!int(tiles, 32, 32);
102 
103     assert( map.contains(RowCol(0 , 0))); // top left
104     assert( map.contains(RowCol(2 , 4))); // bottom right
105     assert( map.contains(RowCol(1 , 3))); // center
106     assert(!map.contains(RowCol(3 , 0))); // beyond bottom border
107     assert(!map.contains(RowCol(0 , 5))); // beyond right border
108     assert(!map.contains(RowCol(0 ,-1))); // beyond left border
109     assert(!map.contains(RowCol(-1, 0))); // beyond top border
110   }
111 
112   /**
113    * True if the pixel position is within the map bounds.
114    */
115   bool containsPoint(T)(T pos) if (isPixelCoord!T) {
116     return grid.contains(coordAtPoint(pos));
117   }
118 
119   ///
120   unittest {
121     // 3x5 map, pixel bounds are [0, 0, 160, 96] (32*3 = 96, 32*5 = 160)
122     auto grid = [
123       [ 00, 01, 02, 03, 04, ],
124       [ 10, 11, 12, 13, 14, ],
125       [ 20, 21, 22, 23, 24, ],
126     ];
127     auto map = OrthoMap!int(grid, 32, 32);
128 
129     assert( map.containsPoint(PixelCoord(   0,    0))); // top left
130     assert( map.containsPoint(PixelCoord( 159,   95))); // bottom right
131     assert( map.containsPoint(PixelCoord(  80,   48))); // center
132     assert(!map.containsPoint(PixelCoord(   0,   96))); // beyond right border
133     assert(!map.containsPoint(PixelCoord( 160,    0))); // beyond bottom border
134     assert(!map.containsPoint(PixelCoord(   0, -0.5))); // beyond left border
135     assert(!map.containsPoint(PixelCoord(-0.5,    0))); // beyond top border
136   }
137 
138   /**
139    * Get the tile at a given pixel position on the map. Throws if out of bounds.
140    * Params:
141    *  T = any pixel-positional point (see isPixelCoord).
142    *  pos = pixel location in 2D space
143    */
144   ref Tile tileAtPoint(T)(T pos) if (isPixelCoord!T) {
145     assert(containsPoint(pos), "position %d,%d out of map bounds: ".format(pos.x, pos.y));
146     return grid.tileAt(coordAtPoint(pos));
147   }
148 
149   ///
150   unittest {
151     auto grid = [
152       [ 00, 01, 02, 03, 04, ],
153       [ 10, 11, 12, 13, 14, ],
154       [ 20, 21, 22, 23, 24, ],
155     ];
156 
157     auto map = OrthoMap!int(grid, 32, 32);
158 
159     assert(map.tileAtPoint(PixelCoord(  0,  0)) == 00); // corner of top left tile
160     assert(map.tileAtPoint(PixelCoord( 16, 30)) == 00); // inside top left tile
161     assert(map.tileAtPoint(PixelCoord(149, 95)) == 24); // inside bottom right tile
162   }
163 
164   /**
165    * Get the pixel offset of the top-left corner of the tile at the given coord.
166    *
167    * Params:
168    *  coord = grid location of tile.
169    */
170   PixelCoord tileOffset(RowCol coord) {
171     return PixelCoord(coord.col * tileWidth,
172                       coord.row * tileHeight);
173   }
174 
175   ///
176   unittest {
177     // 2 rows, 3 cols, 32x64 tiles
178     auto grid = [
179       [ 00, 01, 02, ],
180       [ 10, 11, 12, ],
181     ];
182     auto myMap = OrthoMap!int(grid, 32, 64);
183 
184     assert(myMap.tileOffset(RowCol(0, 0)) == PixelCoord(0, 0));
185     assert(myMap.tileOffset(RowCol(1, 2)) == PixelCoord(64, 64));
186   }
187 
188   /**
189    * Get the pixel offset of the center of the tile at the given coord.
190    *
191    * Params:
192    *  coord = grid location of tile.
193    */
194   PixelCoord tileCenter(RowCol coord) {
195     return PixelCoord(coord.col * tileWidth  + tileWidth  / 2,
196                       coord.row * tileHeight + tileHeight / 2);
197   }
198 
199   ///
200   unittest {
201     // 2 rows, 3 cols, 32x64 tiles
202     auto grid = [
203       [ 00, 01, 02, ],
204       [ 10, 11, 12, ],
205     ];
206     auto myMap = OrthoMap!int(grid, 32, 64);
207 
208     assert(myMap.tileCenter(RowCol(0, 0)) == PixelCoord(16, 32));
209     assert(myMap.tileCenter(RowCol(1, 2)) == PixelCoord(80, 96));
210   }
211 }
212 
213 /// Foreach over every tile in the map
214 unittest {
215   import std.algorithm : equal;
216 
217   auto grid = [
218     [ 00, 01, 02, ],
219     [ 10, 11, 12, ],
220   ];
221   auto myMap = OrthoMap!int(grid, 32, 64);
222 
223   int[] result;
224 
225   foreach(tile ; myMap) result ~= tile;
226 
227   assert(result.equal([ 00, 01, 02, 10, 11, 12 ]));
228 }
229 
230 /// Use ref with foreach to modify tiles
231 unittest {
232   auto grid = [
233     [ 00, 01, 02, ],
234     [ 10, 11, 12, ],
235   ];
236   auto myMap = OrthoMap!int(grid, 32, 64);
237 
238   foreach(ref tile ; myMap) tile += 30;
239 
240   assert(myMap.tileAt(RowCol(1,1)) == 41);
241 }
242 
243 /// Foreach over every (coord, tile) pair in the map
244 unittest {
245   import std.algorithm : equal;
246 
247   auto grid = [
248     [ 00, 01, 02, ],
249     [ 10, 11, 12, ],
250   ];
251   auto myMap = OrthoMap!int(grid, 32, 64);
252 
253 
254   foreach(coord, tile ; myMap) assert(myMap.tileAt(coord) == tile);
255 }