1 ///Adds integration with the Tiled map editor
2 module dgt.level;
3 
4 import std.file : readText;
5 import std.json;
6 import std.path : dirName;
7 
8 import dgt.array, dgt.color, dgt.geom, dgt.texture, dgt.tilemap;
9 
10 import dgt.io;
11 
12 ///A layer of tiles represented by integer constants
13 struct TileLayer
14 {
15     string name;
16     Array!int tiles;
17     int offsetX, offsetY, widthInTiles, heightInTiles;
18     float opacity;
19     bool visible;
20 
21     @nogc nothrow:
22     pure int opIndex(in int x, in int y) const
23     {
24         return tiles[x + y * widthInTiles];
25     }
26 
27     void destroy()
28     {
29         tiles.destroy();
30     }
31 }
32 
33 ///A layer of free-floating objects
34 struct EntityLayer
35 {
36     string name;
37     Array!Entity entities;
38     int offsetX, offsetY;
39     float opacity;
40     bool visible;
41 
42     @nogc nothrow void destroy()
43     {
44         entities.destroy();
45     }
46 }
47 
48 ////A free-floating entity 
49 struct Entity
50 {
51     string name, type;
52     int x, y, width, height, rotation;
53     bool flipX, flipY;
54     Texture tex;
55     bool visible;
56 }
57 
58 private int number(in JSONValue json)
59 {
60     if(json.type == JSON_TYPE.INTEGER)
61         return cast(int)json.integer;
62     else
63         return cast(int)json.floating;
64 }
65 
66 ///A structure to load the Tiled map into
67 struct Level
68 {
69     static immutable FLIPPED_HORIZONTALLY_FLAG = 0x80000000;
70     static immutable FLIPPED_VERTICALLY_FLAG = 0x40000000;
71     static immutable FLIPPED_DIAGONALLY_FLAG = 0x20000000;
72 
73 
74     private
75     {
76         Array!Texture sourceImages;
77         Array!Texture tileImages;
78         Array!TileLayer tileLayers;
79         Array!EntityLayer entityLayers;
80     }
81 
82     ///Get the images used by the tiles and entities
83     @property const(Texture[]) images() const { return tileImages.array; }
84     ///Get the layers with tiles fixed to the grid
85     @property const(TileLayer[]) fixedTileLayers() const { return tileLayers.array; }
86     ///Get the layers with entities that can be freely moved
87     @property const(EntityLayer[]) freeEntityLayers() const { return entityLayers.array; }
88 
89     int tileWidth, tileHeight, widthInTiles, heightInTiles;
90 
91     private uint stripGID(uint gid, ref bool outFlipX, ref bool outFlipY) const
92     {
93         outFlipX = outFlipY = (gid & FLIPPED_DIAGONALLY_FLAG) != 0;
94         outFlipX = outFlipX != ((gid & FLIPPED_HORIZONTALLY_FLAG) != 0);
95         outFlipY = outFlipY != ((gid & FLIPPED_VERTICALLY_FLAG) != 0);
96         return gid & ~(FLIPPED_HORIZONTALLY_FLAG | FLIPPED_VERTICALLY_FLAG | FLIPPED_DIAGONALLY_FLAG);
97     }
98 
99     ///Load a Tiled map from a path in the filesystem
100     this(in string path, in int scale = 1)
101     {
102         sourceImages = Array!Texture(4);
103         tileImages = Array!Texture(16);
104         tileLayers = Array!TileLayer(4);
105         entityLayers = Array!EntityLayer(4);
106 
107         auto pathToMap = dirName(path);
108 
109         auto contents = parseJSON(readText(path));
110         widthInTiles = contents["width"].number;
111         heightInTiles = contents["height"].number;
112         tileWidth = contents["tilewidth"].number;
113         tileHeight = contents["tileheight"].number;
114         foreach(tileset; contents["tilesets"].array)
115         {
116             auto image = Texture(pathToMap ~ "/" ~ tileset["image"].str);
117             sourceImages.add(image);
118             int margin = tileset["margin"].number;
119             int spacing = tileset["spacing"].number;
120             int width = tileset["tilewidth"].number;
121             int height = tileset["tileheight"].number;
122             for(int y = margin; y < image.sourceHeight - margin; y += height + spacing)
123                 for(int x = margin; x < image.sourceWidth - margin; x += width + spacing)
124                     tileImages.add(image.getSlice(Rectangle(x, y, width, height)));
125         }
126 
127         foreach(layer; contents["layers"].array)
128         {
129             string name = layer["name"].str;
130             int offsetX = "offsetx" in layer ? layer["offsetx"].number : 0;
131             int offsetY = "offsety" in layer ? layer["offsety"].number : 0;
132             float opacity = layer["opacity"].type == JSON_TYPE.FLOAT ? layer["opacity"].floating : layer["opacity"].number;
133             bool visible = layer["visible"].type == JSON_TYPE.TRUE;
134             if(layer["type"].str == "tilelayer")
135             {
136                 TileLayer tlayer = TileLayer(name, Array!int(layer["data"].array.length),
137                     offsetX, offsetY,
138                     layer["width"].number,
139                     layer["height"].number,
140                     opacity, visible);
141                 foreach(tile; layer["data"].array)
142                     tlayer.tiles.add(tile.number - 1);
143                 tileLayers.add(tlayer);
144             }
145             else if(layer["type"].str == "objectgroup")
146             {
147                 EntityLayer elayer = EntityLayer(name, Array!Entity(layer["objects"].array.length),
148                     offsetX, offsetY, opacity, visible);
149                 foreach(object; layer["objects"].array)
150                 {
151                     bool flipX, flipY;
152                     uint gid = stripGID(cast(uint)object["gid"].number, flipX, flipY);
153                     elayer.entities.add(Entity(
154                         object["name"].str,
155                         object["type"].str,
156                         scale * object["x"].number,
157                         scale * object["y"].number,
158                         scale * object["width"].number,
159                         scale * object["height"].number,
160                         object["rotation"].number,
161                         flipX, flipY,
162                         tileImages[gid - 1], object["visible"].type == JSON_TYPE.TRUE
163                     ));
164                 }
165                 entityLayers.add(elayer);
166             }
167         }
168     }
169 
170     /**
171     Free all of the data in the map
172 
173     Also destroys the textures the map loads, so be careful
174     */
175     @nogc nothrow void destroy()
176     {
177         foreach(tex; sourceImages)
178             tex.destroy();
179         foreach(layer; tileLayers)
180             layer.destroy();
181         foreach(layer; entityLayers)
182             layer.destroy();
183         sourceImages.destroy();
184         tileImages.destroy();
185         tileLayers.destroy();
186         entityLayers.destroy();
187     }
188 }