1 | "use strict";
|
2 |
|
3 | const async = require("async");
|
4 | const path = require("path-extra");
|
5 | const fs = require("fs-extra");
|
6 | const utilities = require("extra-utilities");
|
7 | const ByteBuffer = require("bytebuffer");
|
8 | const Jimp = require("jimp");
|
9 | const Colour = require("colour-rgba");
|
10 | const Palette = require("duke3d-palette");
|
11 | const Art = require("./art.js");
|
12 | const TileAttributes = require("./tile-attributes");
|
13 |
|
14 | class 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 |
|
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 |
|
318 | Object.defineProperty(Tile, "Attributes", {
|
319 | value: TileAttributes,
|
320 | enumerable: true
|
321 | });
|
322 |
|
323 | module.exports = Tile;
|