UNPKG

7.64 kBJavaScriptView Raw
1"use strict";
2
3const async = require("async");
4const path = require("path-extra");
5const fs = require("fs-extra");
6const utilities = require("extra-utilities");
7const ByteBuffer = require("bytebuffer");
8const Jimp = require("jimp");
9const Colour = require("colour-rgba");
10const Palette = require("duke3d-palette");
11const Art = require("./art.js");
12const TileAttributes = require("./tile-attributes");
13
14class Tile {
15 constructor(number, width, height, data, attributes, xOffset, yOffset, numberOfFrames, animationType, animationSpeed, extra) {
16 let self = this;
17
18 let _properties = { };
19
20 Object.defineProperty(self, "number", {
21 get() {
22 return _properties.number;
23 },
24 set(value) {
25 const newValue = utilities.parseInteger(value);
26
27 if(isNaN(newValue)) {
28 throw new TypeError("Invalid number value: " + value + " expected positive integer.");
29 }
30
31 _properties.number = newValue;
32 }
33 });
34
35 Object.defineProperty(self, "width", {
36 get() {
37 return _properties.width;
38 },
39 set(value) {
40 const newValue = utilities.parseInteger(value);
41
42 if(isNaN(newValue) || newValue < 0) {
43 throw new TypeError("Invalid width value: " + value + " expected positive integer.");
44 }
45
46 _properties.width = newValue;
47 }
48 });
49
50 Object.defineProperty(self, "height", {
51 get() {
52 return _properties.height;
53 },
54 set(value) {
55 const newValue = utilities.parseInteger(value);
56
57 if(isNaN(newValue) || newValue < 0) {
58 throw new TypeError("Invalid height value: " + value + " expected positive integer.");
59 }
60
61 _properties.height = newValue;
62 }
63 });
64
65 Object.defineProperty(self, "data", {
66 enumerable: true,
67 get() {
68 return _properties.data;
69 },
70 set(data) {
71 if(ByteBuffer.isByteBuffer(data)) {
72 _properties.data = data.toBuffer();
73 }
74 else if(Buffer.isBuffer(data) || Array.isArray(data) || typeof data === "string") {
75 _properties.data = Buffer.from(data);
76 }
77 else {
78 _properties.data = Buffer.alloc(0);
79 }
80
81 self.validateData();
82 }
83 });
84
85 Object.defineProperty(self, "attributes", {
86 enumerable: true,
87 get() {
88 return _properties.attributes;
89 },
90 set(value) {
91 if(Tile.Attributes.isTileAttributes(value)) {
92 _properties.attributes = value.clone();
93 }
94 else if(Number.isInteger(value)) {
95 _properties.attributes = Tile.Attributes.unpack(value);
96 }
97 else {
98 throw new TypeError("Invalid attributes value, expected integer or instance of TileAttribute.");
99 }
100 }
101 });
102
103 self.number = number;
104 self.width = width;
105 self.height = height;
106 self.data = data;
107
108 if(Tile.Attributes.isTileAttributes(attributes) || Number.isInteger(attributes)) {
109 self.attributes = attributes;
110 }
111 else {
112 self.attributes = new Tile.Attributes(xOffset, yOffset, numberOfFrames, animationType, animationSpeed, extra);
113 }
114 }
115
116 isEmpty() {
117 let self = this;
118
119 return !Buffer.isBuffer(self.data) || self.data.length === 0;
120 }
121
122 getSize() {
123 let self = this;
124
125 return Buffer.isBuffer(self.data) ? self.data.length : 0;
126 }
127
128 getMetadata() {
129 let self = this;
130
131 return {
132 number: self.number,
133 attributes: self.attributes.getMetadata()
134 }
135 }
136
137 getImage(palette, fileType) {
138 let self = this;
139
140 if(self.isEmpty()) {
141 return null;
142 }
143
144 if(!Palette.isValid(palette)) {
145 throw new Error("Invalid palette!");
146 }
147
148 fileType = Tile.FileType.getFileType(fileType);
149
150 if(!Tile.FileType.isValid(fileType)) {
151 throw new Error("Invalid file type.");
152 }
153
154 let image = new Jimp(self.width, self.height, Colour.Transparent.pack());
155
156 for(let y = 0; y < self.height; y++) {
157 for(let x = 0; x < self.width; x++) {
158 const pixelValue = self.data[(x * self.height) + y];
159
160// TODO: what if colour data array is pulled locally, probably quicker?
161 image.setPixelColour(
162 pixelValue === 255
163 ? Colour.Transparent.pack()
164 : palette.lookupPixel(
165 pixelValue,
166 0
167 ).pack(),
168 x,
169 y
170 );
171 }
172 }
173
174 return image;
175 }
176
177 clear() {
178 const self = this;
179
180 self.width = 0;
181 self.height = 0;
182 self.data = null;
183 self.attributes = 0;
184 }
185
186 writeTo(filePath, overwrite, palette, fileType, fileName, callback) {
187 let self = this;
188
189 if(utilities.isFunction(fileName)) {
190 callback = fileName;
191 fileName = null;
192 }
193
194 if(!utilities.isFunction(callback)) {
195 throw new Error("Missing or invalid callback function!");
196 }
197
198 if(self.isEmpty()) {
199 return callback(null, null);
200 }
201
202 if(!Palette.isValid(palette)) {
203 return callback(new Error("Invalid palette!"));
204 }
205
206 overwrite = utilities.parseBoolean(overwrite, false);
207
208 fileType = Tile.FileType.getFileType(fileType);
209
210 if(utilities.isEmptyString(fileName)) {
211 fileName = utilities.getFileName(filePath);
212
213 if(utilities.isEmptyString(fileName)) {
214 fileName = "TILE" + utilities.addLeadingZeroes(self.number, 4) + (Tile.FileType.isValid(fileType) ? "." + fileType.extension : "");
215 }
216 }
217
218 if(!Tile.FileType.isValid(fileType)) {
219 fileType = Tile.FileType.getFileType(utilities.getFileExtension(fileName));
220
221 if(!Tile.FileType.isValid(fileType)) {
222 return callback(new Error("Unable to determine file type."));
223 }
224 }
225
226 if(!Tile.FileType.isValid(fileType)) {
227 return callback(new Error("Invalid file type."));
228 }
229
230 const outputDirectory = utilities.getFilePath(filePath);
231 const outputFilePath = utilities.joinPaths(outputDirectory, fileName);
232
233 return async.waterfall(
234 [
235 function(callback) {
236 if(utilities.isEmptyString(outputDirectory)) {
237 return callback();
238 }
239
240 return fs.ensureDir(
241 outputDirectory,
242 function(error) {
243 if(error) {
244 return callback(error);
245 }
246
247 return callback();
248 }
249 );
250 },
251 function(callback) {
252 return fs.stat(
253 outputFilePath,
254 function(error, outputFileStats) {
255 if(utilities.isObject(error) && error.code !== "ENOENT") {
256 return callback(error);
257 }
258
259 if(utilities.isValid(outputFileStats) && !overwrite) {
260 return callback(new Error("File \"" + fileName + "\" already exists, must specify overwrite parameter."));
261 }
262
263 return callback();
264 }
265 );
266 },
267 function(callback) {
268 try {
269 return self.getImage(palette, fileType).write(
270 outputFilePath,
271 function(error) {
272 if(error) {
273 return callback(error);
274 }
275
276 return callback(null, outputFilePath);
277 }
278 );
279 }
280 catch(error) {
281 return callback(error);
282 }
283 }
284 ],
285 function(error, outputFilePath) {
286 if(error) {
287 return callback(error);
288 }
289
290 return callback(null, outputFilePath);
291 }
292 );
293 }
294
295 validateData() {
296 let self = this;
297
298 if(!Buffer.isBuffer(self.data)) {
299 throw new Error("Invalid data attribute, expected valid buffer value.");
300 }
301
302 if(self.data.length !== self.width * self.height) {
303 throw new Error("Invalid data buffer size: " + self.data.length + ", expected " + (self.width * self.height) + ".");
304 }
305 }
306
307 clone() {
308 const self = this;
309
310 return new Tile(self.number, self.width, self.height, self.data, self.attributes);
311 }
312
313 static isTile(value) {
314 return value instanceof Tile;
315 }
316}
317
318Object.defineProperty(Tile, "Attributes", {
319 value: TileAttributes,
320 enumerable: true
321});
322
323module.exports = Tile;