1 module dgt.tilemap;
2 import dgt.array, dgt.geom;
3 import std.math;
4 
5 /**
6 A single tile with an arbitrary value and if the tile is solid or not
7 
8 A solid tile will indicate its square is not empty
9 */
10 struct Tile(T)
11 {
12 	T value;
13 	bool solid;
14 }
15 
16 /**
17 A fixed-size grid of tiles that can be queried
18 */
19 struct Tilemap(T)
20 {
21 	static immutable INVALID_TILE = Tile!T(T(), true);
22 
23 	private Array!(Tile!T) buffer;
24 	private int size, _width, _height;
25 
26 	@nogc nothrow public:
27     ///Create a tilemap with a given unit width and height and the units for the size of each tile square
28 	this(in int mapWidth, in int mapHeight, in int size)
29 	{
30 		this._width = mapWidth;
31 		this._height = mapHeight;
32 		this.size = size;
33         buffer = Array!(Tile!T)((width / size) * (height / size));
34 		for(size_t i = 0; i < width; i += size)
35 			for(size_t j = 0; j < height; j += size)
36 				buffer.add(Tile!T(T(), false));
37 	}
38 
39     ///Free the underlying tilemap memory
40 	void destroy()
41 	{
42 		buffer.destroy();
43 	}
44 
45 	pure:
46     ///Get a tile from a location
47 	Tile!T opIndex(in float x, in float y) const
48 	{
49 		return valid(x, y) ? buffer[cast(int)((x / size) * height / size + (y / size))] : INVALID_TILE;
50 	}
51     ///Get a tile from a location
52     Tile!T opIndex(in Vector vec) const { return this[vec.x, vec.y]; }
53 
54     ///Set a tile from a location
55 	ref Tile!T opIndexAssign(in Tile!T tile, in float x, in float y)
56 	{
57 		return buffer[cast(int)((x / size) * height / size + (y / size))] = tile;
58 	}
59     ///Set a tile from a location
60     ref Tile!T opIndexAssign(in Tile!T tile, in Vector vec) { return this[vec.x, vec.y] = tile; }
61 
62 	///Checks if a point falls within a tilemap
63     bool valid(in float x, in float y) const
64 	{
65 		return x >= 0 && y >= 0 && x < width && y < height;
66 	}
67 	///Checks if a point falls within a tilemap
68     bool valid(in Vector vec) const { return valid(vec.x, vec.y); }
69 
70     ///Checks if a point is both valid and empty
71 	bool empty(in float x, in float y) const
72 	{
73 		return !this[x, y].solid;
74 	}
75     ///Checks if a point is both valid and empty
76     bool empty(in Vector vec) const { return empty(vec.x, vec.y); }
77 
78     ///Checks if a region is both valid and empty
79 	bool empty(in float x, in float y, in float width, in float height) const
80 	{
81 		for(float i = x; i < x + width; i += size)
82 			for(float j = y; j < y + height; j += size)
83 				if(!empty(i, j))
84 					return false;
85 		return empty(x + width, y) && empty(x, y + height) && empty(x + width, y + height);
86 	}
87     ///Checks if a region is both valid and empty
88     bool empty(in Rectangle rect) const { return empty(rect.x, rect.y, rect.width, rect.height); }
89 
90     ///Determine the furthest a region can move without hitting a wall
91 	Vector slideContact(in float x, in float y, in float width, in float height, in Vector v) const
92 	{
93 		//Objects embedded in walls cannot move
94 		if(!empty(x, y, width, height))
95 			return Vector(0, 0);
96 		float tryX = x + v.x;
97 		float tryY = y + v.y;
98 		//The object can move unobstructed
99 		if(empty(tryX, tryY, width, height))
100 			return v;
101 		//The left side is embedded in a wall
102 		if(!empty(tryX, tryY) && !empty(tryX, tryY + height))
103 			tryX = (cast(int)tryX / size) * size;
104 		//The right side is embedded in a wall
105 		if(!empty(tryX + width, tryY) && !empty(tryX + width, tryY + height))
106 			tryX = (cast(int)(tryX + width) / size) * size - width;
107 		//The tpp side is embedded in a wall
108 		if(!empty(tryX, tryY) && !empty(tryX + width, tryY))
109 			tryY = (cast(int)tryY / size) * size;
110 		//The bottom side is embedded in a wall
111 		if(!empty(tryX, tryY + height) && !empty(tryX + width, tryY + height))
112 			tryY = (cast(int)(tryY + height) / size) * size - height;
113 		return Vector(tryX - x, tryY - y);
114 	}
115     ///Determine the furthest a region can move without hitting a wall
116     Vector slideContact(in Rectangle rect, in Vector vec) const 
117 	{
118 		return slideContact(rect.x, rect.y, rect.width, rect.height, vec); 
119 	}
120 
121     ///The width of the map in units
122 	@property int width() const { return _width; }
123     ///The height of the map in units
124 	@property int height() const { return _height; }
125     ///The size of a tile in units (both width and height)
126 	@property int tileSize() const { return size; }
127 }
128 
129 unittest
130 {
131     Tilemap!int map = Tilemap!int(640, 480, 32);
132     map[35, 35] = Tile!int(5, true);
133     assert(map[-1, 0].solid);
134     assert(!map[35, 0].solid);
135     assert(map[35, 35].value == 5);
136     auto moved = map.slideContact(300, 5, 32, 32, Vector(0, -10));
137     assert(moved.x == 0 && moved.y == -5);
138     moved = map.slideContact(80, 10, 16, 16, Vector(1, -20));
139     assert(moved.x == 1 && moved.y == -10);
140     moved = map.slideContact(50, 50, 10, 10, Vector(20, 30));
141     assert(moved.x == 20 && moved.y == 30);
142     moved = map.slideContact(600, 10, 30, 10, Vector(15, 10));
143     assert(moved.x == 10 && moved.y == 10);
144 	auto movedf = map.slideContact(10.0, 5, 5, 5, Vector(2, 2));
145 	assert(movedf.x == 2 && movedf.y == 2);
146 	movedf = map.slideContact(5.0, 5, 10, 10, Vector(-7.2, 0));
147 	assert(movedf.x == -5 && movedf.y == 0);
148 	movedf = map.slideContact(600, 0, 25, 10, Vector(20, 0));
149 	import dgt.io;
150 	println(movedf);
151 	assert(movedf.x == 15 && movedf.y == 0);
152 }