UNPKG

26 kBJavaScriptView Raw
1"use strict";
2
3// TODO: temp
4// http://www.shikadi.net/moddingwiki/Duke_Nukem_3D
5// http://www.shikadi.net/moddingwiki/ART_Format_(Build)
6// https://fabiensanglard.net/duke3d/BUILDINF.TXT
7
8const async = require("async");
9const path = require("path-extra");
10const fs = require("fs-extra");
11const utilities = require("extra-utilities");
12const jsonFormat = require("json-format");
13const ByteBuffer = require("bytebuffer");
14const Colour = require("colour-rgba");
15const Palette = require("duke3d-palette");
16const Tile = require("./tile.js");
17
18class ArtProperties {
19 constructor() {
20 let self = this;
21
22 let _properties = {
23 verbose: true,
24 metadataFileExtension: Art.DefaultMetadataFileExtension
25 };
26
27 Object.defineProperty(self, "verbose", {
28 enumerable: true,
29 get() {
30 return _properties.verbose;
31 },
32 set(value) {
33 _properties.verbose = utilities.parseBoolean(value, false);
34 }
35 });
36
37 Object.defineProperty(self, "metadataFileExtension", {
38 enumerable: true,
39 get() {
40 return _properties.metadataFileExtension;
41 },
42 set(value) {
43 let newValue = utilities.trimString(value);
44
45 if(utilities.isEmptyString(newValue)) {
46 throw new Error("Invalid metadata file extension, expected non-empty string.");
47 }
48
49 _properties.metadataFileExtension = newValue;
50 }
51 });
52 }
53}
54
55class Art {
56 constructor(legacyTileCount, localTileStart, localTileEnd, tiles, filePath) {
57 let self = this;
58
59 let _properties = {
60 tiles: []
61 };
62
63 Object.defineProperty(self, "number", {
64 get() {
65 return Math.floor(_properties.localTileStart / Art.DefaultNumberOfTiles);
66 }
67 });
68
69 Object.defineProperty(self, "localTileStart", {
70 enumerable: true,
71 get() {
72 return _properties.localTileStart;
73 },
74 set(value) {
75 const newValue = utilities.parseInteger(value);
76
77 if(isNaN(newValue) || newValue < 0) {
78 throw new TypeError("Invalid local tile start value: " + value + ", expected positive integer.");
79 }
80
81 _properties.localTileStart = localTileStart;
82 }
83 });
84
85 Object.defineProperty(self, "localTileEnd", {
86 enumerable: true,
87 get() {
88 return _properties.localTileEnd;
89 },
90 set(value) {
91 const newValue = utilities.parseInteger(value);
92
93 if(isNaN(newValue) || newValue < 0) {
94 throw new TypeError("Invalid local tile end value: " + value + ", expected positive integer.");
95 }
96
97 _properties.localTileEnd = localTileEnd;
98 }
99 });
100
101 // note: this field is no longer used and is inaccurate
102 Object.defineProperty(self, "legacyTileCount", {
103 enumerable: true,
104 get() {
105 return _properties.legacyTileCount;
106 },
107 set(value) {
108 const newValue = utilities.parseInteger(value);
109
110 if(isNaN(newValue) || newValue < 0) {
111 throw new TypeError("Invalid tile count value: " + value + ", expected positive integer.");
112 }
113
114 _properties.legacyTileCount = newValue;
115 }
116 });
117
118 Object.defineProperty(self, "filePath", {
119 enumerable: true,
120 get() {
121 return _properties.filePath;
122 },
123 set(filePath) {
124 _properties.filePath = utilities.trimString(filePath);
125 }
126 });
127
128 Object.defineProperty(self, "tiles", {
129 enumerable: true,
130 get() {
131 return _properties.tiles;
132 },
133 set(value) {
134 if(utilities.isNonEmptyArray(value)) {
135 for(let i = 0; i < value.length; i++) {
136 if(!Art.Tile.isTile(value[i])) {
137 throw new TypeError("Invalid tile in tile list at index " + i + ".");
138 }
139 }
140
141 _properties.tiles.length = 0;
142 Array.prototype.push.apply(_properties.tiles, value);
143 }
144 }
145 });
146
147 Object.defineProperty(self, "data", {
148 enumerable: true,
149 get() {
150 return self.serialize();
151 }
152 });
153
154 self.legacyTileCount = legacyTileCount;
155 self.localTileStart = localTileStart;
156 self.localTileEnd = localTileEnd;
157 self.tiles = tiles;
158 self.filePath = filePath;
159 }
160
161 static isExtendedBy(artSubclass) {
162 if(artSubclass instanceof Object) {
163 return false;
164 }
165
166 let artSubclassPrototype = null;
167
168 if(artSubclass instanceof Function) {
169 artSubclassPrototype = artSubclass.prototype;
170 }
171 else {
172 artSubclassPrototype = artSubclass.constructor.prototype;
173 }
174
175 return artSubclassPrototype instanceof Art;
176 }
177
178 static create(tileStartOffset, numberOfTiles) {
179 const formattedTileStartOffset = utilities.parseInteger(tileStartOffset);
180
181 if(isNaN(formattedTileStartOffset) || formattedTileStartOffset < 0) {
182 formattedTileStartOffset = 0;
183 }
184
185 let formattedNumberOfTiles = utilities.parseInteger(numberOfTiles);
186
187 if(isNaN(formattedNumberOfTiles) || formattedNumberOfTiles < 0) {
188 formattedNumberOfTiles = Art.DefaultNumberOfTiles;
189 }
190
191 let tiles = [];
192
193 for(let i = 0; i < formattedNumberOfTiles; i++) {
194 tiles.push(new Art.Tile(formattedTileStartOffset + i, 0, 0, null, 0));
195 }
196
197 return new Art(0, formattedTileStartOffset, formattedTileStartOffset + formattedNumberOfTiles - 1, tiles);
198 }
199
200 getFileName() {
201 let self = this;
202
203 if(utilities.isEmptyString(self.filePath)) {
204 return null;
205 }
206
207 return utilities.getFileName(self.filePath);
208 }
209
210 getExtension() {
211 let self = this;
212
213 return utilities.getFileExtension(self.filePath);
214 }
215
216 numberOfNonEmptyTiles() {
217 let self = this;
218
219 let tileCount = 0;
220
221 for(let i = 0; i < self.tiles.length; i++) {
222 if(!self.tiles[i].isEmpty()) {
223 tileCount++;
224 }
225 }
226
227 return tileCount;
228 }
229
230 numberOfEmptyTiles() {
231 let self = this;
232
233 let tileCount = 0;
234
235 for(let i = 0; i < self.tiles.length; i++) {
236 if(self.tiles[i].isEmpty()) {
237 tileCount++;
238 }
239 }
240
241 return tileCount;
242 }
243
244 getTileByNumber(tileNumber) {
245 let self = this;
246
247 tileNumber = utilities.parseInteger(tileNumber);
248
249 if(isNaN(tileNumber)) {
250 throw new Error("Invalid tile number: " + tileNumber + ", expected valid integer value.");
251 }
252
253 const tileIndex = tileNumber - self.localTileStart;
254
255 if(tileIndex < 0 || tileIndex >= self.tiles.length) {
256 throw new Error("Tile number " + tileNumber + " is out of range, expected integer number between " + self.localTileStart + " and " + self.localTileEnd + ", inclusively.");
257 }
258
259 return self.tiles[tileIndex];
260 }
261
262 replaceTile(tile, tileNumber) {
263 const self = this;
264
265 if(!Art.Tile.isTile(tile)) {
266 throw new Error("Cannot replace tile with invalid value!");
267 }
268
269 tileNumber = utilities.parseInteger(tileNumber);
270
271 if(isNaN(tileNumber)) {
272 tileNumber = tile.number;
273 }
274
275 const tileIndex = tileNumber - self.localTileStart;
276
277 if(tileIndex < 0 || tileIndex >= self.tiles.length) {
278 throw new Error(`Cannot replace tile #${tileNumber}, number must be within range of ${self.localTileStart} and ${self.localTileEnd}`);
279 }
280
281 const newTile = tile.clone();
282 newTile.number = tileNumber;
283
284 self.tiles[tileIndex] = newTile;
285 }
286
287 clearTile(tileNumber) {
288 const self = this;
289
290 tileNumber = utilities.parseInteger(tileNumber);
291
292 if(isNaN(tileNumber)) {
293 throw new Error("Invalid tile number: " + tileNumber + ", expected valid integer value.");
294 }
295
296 const tileIndex = tileNumber - self.localTileStart;
297
298 if(tileIndex < 0 || tileIndex >= self.tiles.length) {
299 throw new Error("Tile number " + tileNumber + " is out of range, expected integer number between " + self.localTileStart + " and " + self.localTileEnd + ", inclusively.");
300 }
301
302 self.tiles[tileIndex].clear();
303 }
304
305 getTiles(includeEmpty) {
306 let self = this;
307
308 includeEmpty = utilities.parseBoolean(includeEmpty, false);
309
310 let tiles = [];
311
312 for(let i = 0; i < self.tiles.length; i++) {
313 if(self.tiles[i].isEmpty() && !includeEmpty) {
314 continue;
315 }
316
317 tiles.push(self.tiles[i]);
318 }
319
320 return tiles;
321 }
322
323 getNonEmptyTiles() {
324 let self = this;
325
326 let tiles = [];
327
328 for(let i = 0; i < self.tiles.length; i++) {
329 if(!self.tiles[i].isEmpty()) {
330 tiles.push(self.tiles[i]);
331 }
332 }
333
334 return tiles;
335 }
336
337 getEmptyTiles() {
338 let self = this;
339
340 let tiles = [];
341
342 for(let i = 0; i < self.tiles.length; i++) {
343 if(self.tiles[i].isEmpty()) {
344 tiles.push(self.tiles[i]);
345 }
346 }
347
348 return tiles;
349 }
350
351 compareTo(artFile) {
352 const self = this;
353
354 if(!Art.isArt(artFile)) {
355 throw new Error("Cannot compare to invalid art file!");
356 }
357
358 const tileComparison = {
359 new: [],
360 modified: [],
361 removed: [],
362 attributesChanged: []
363 };
364
365 for(let i = 0; i < self.tiles.length; i++) {
366 const tileA = self.tiles[i];
367 const tileB = artFile.getTileByNumber(tileA.number);
368 const tileAEmpty = tileA.isEmpty();
369 const tileBEmpty = tileB.isEmpty();
370
371 if(!tileAEmpty && tileBEmpty) {
372 tileComparison.new.push(tileA);
373 }
374 else if(tileAEmpty && !tileBEmpty) {
375 tileComparison.removed.push(tileB);
376 }
377 else if(!tileA.data.equals(tileB.data)) {
378 tileComparison.modified.push(tileA);
379 }
380 else if(!tileA.attributes.equals(tileB.attributes)) {
381 tileComparison.attributesChanged.push(tileA);
382 }
383 }
384
385 return tileComparison;
386 }
387
388 getMetadata() {
389 let self = this;
390
391 let metadata = {
392 number: self.number,
393 count: self.legacyTileCount,
394 start: self.localTileStart,
395 end: self.localTileEnd,
396 tiles: []
397 };
398
399 for(let i = 0; i < self.tiles.length; i++) {
400 metadata.tiles.push(self.tiles[i].getMetadata());
401 }
402
403 return metadata;
404 }
405
406 static formatMetadata(metadata) {
407 return utilities.formatValue(metadata, Art.MetadataFormat);
408 }
409
410 writeMetadata(filePath, overwrite, minify, fileName) {
411 let self = this;
412
413 overwrite = utilities.parseBoolean(overwrite, false);
414 minify = utilities.parseBoolean(minify, false);
415
416 if(utilities.isEmptyString(fileName)) {
417 fileName = utilities.getFileName(filePath);
418
419 if(utilities.isEmptyString(fileName)) {
420 fileName = "TILES" + utilities.addLeadingZeroes(self.number, 3) + "." + Art.metadataFileExtension;
421 }
422 }
423
424 const outputDirectory = utilities.getFilePath(filePath);
425 const outputFilePath = utilities.joinPaths(outputDirectory, fileName);
426
427 if(utilities.isNonEmptyString(outputDirectory)) {
428 fs.ensureDirSync(outputDirectory);
429 }
430
431 const metadata = self.getMetadata();
432
433 fs.writeFileSync(outputFilePath, minify ? JSON.stringify(metadata) : jsonFormat(metadata));
434
435 return outputFilePath;
436 }
437
438 static readMetadata(filePath) {
439 let self = this;
440
441 return Art.formatMetadata(JSON.parse(fs.readFileSync(filePath)));
442 }
443
444 applyMetadata(metadata) {
445 const formattedMetadata = Art.formatMetadata(metadata);
446
447// TODO: apply metadata
448 }
449
450 extractTileAtIndex(index, filePath, overwrite, palette, fileType, fileName, writeMetadata, minifyMetadata, callback) {
451 let self = this;
452
453 if(utilities.isFunction(writeMetadata)) {
454 callback = writeMetadata;
455 writeMetadata = true;
456 minifyMetadata = null;
457 }
458 else if(utilities.isFunction(minifyMetadata)) {
459 callback = minifyMetadata;
460 minifyMetadata = null;
461 }
462
463 if(!utilities.isFunction(callback)) {
464 throw new Error("Missing or invalid callback function!");
465 }
466
467 writeMetadata = utilities.parseBoolean(writeMetadata, true);
468
469 const formattedIndex = utilities.parseInteger(index);
470
471 if(isNaN(formattedIndex)) {
472 return callback(new Error("Invalid tile index: " + index));
473 }
474
475 if(formattedIndex < 0 || formattedIndex >= self.tiles.length) {
476 return callback(new Error("Tile index " + formattedIndex + " is out of range, expected value between 0 and " + (self.tiles.length - 1) + ", inclusively."))
477 }
478
479 if(writeMetadata) {
480 try {
481 self.writeMetadata(
482 utilities.getFilePath(filePath) + "/",
483 overwrite,
484 minifyMetadata
485 );
486 }
487 catch(error) {
488 return callback(error);
489 }
490 }
491
492 return self.tiles[formattedIndex].writeTo(
493 filePath,
494 overwrite,
495 palette,
496 fileType,
497 fileName,
498 function(error, filePath) {
499 if(error) {
500 return callback(error);
501 }
502
503 return callback(null, filePath, metadataFilePath);
504 }
505 );
506 }
507
508 extractTileByNumber(number, filePath, overwrite, palette, fileType, fileName, writeMetadata, minifyMetadata, callback) {
509 let self = this;
510
511 if(utilities.isFunction(writeMetadata)) {
512 callback = writeMetadata;
513 writeMetadata = true;
514 minifyMetadata = null;
515 }
516 else if(utilities.isFunction(minifyMetadata)) {
517 callback = minifyMetadata;
518 minifyMetadata = null;
519 }
520
521 if(!utilities.isFunction(callback)) {
522 throw new Error("Missing or invalid callback function!");
523 }
524
525 const formattedNumber = utilities.parseInteger(number);
526
527 if(isNaN(formattedNumber)) {
528 return callback(new Error("Invalid tile number: " + number));
529 }
530
531 const tileIndex = formattedNumber - self.localTileStart;
532
533 if(tileIndex < 0 || tileIndex >= self.tiles.length) {
534 return callback(new Error("Tile number " + formattedNumber + " is out of range, expected value between " + self.localTileStart + " and " + self.localTileEnd + ", inclusively."))
535 }
536
537 return self.extractTileAtIndex(tileIndex, filePath, overwrite, palette, fileType, fileName, writeMetadata, callback);
538 }
539
540 extractAllTiles(outputDirectory, overwrite, palette, fileType, writeMetadata, minifyMetadata, includeEmpty, callback) {
541 let self = this;
542
543 if(utilities.isFunction(writeMetadata)) {
544 callback = writeMetadata;
545 includeEmpty = false;
546 minifyMetadata = null;
547 writeMetadata = true;
548 }
549 else if(utilities.isFunction(minifyMetadata)) {
550 callback = minifyMetadata;
551 includeEmpty = false;
552 minifyMetadata = null;
553 }
554 else if(utilities.isFunction(includeEmpty)) {
555 callback = includeEmpty;
556 includeEmpty = false;
557 }
558
559 if(!utilities.isFunction(callback)) {
560 throw new Error("Missing or invalid callback function!");
561 }
562
563 return async.waterfall(
564 [
565 function(callback) {
566 if(!writeMetadata) {
567 return callback(null, null);
568 }
569
570 try {
571 const metadataFilePath = self.writeMetadata(
572 utilities.getFilePath(outputDirectory) + "/",
573 overwrite,
574 minifyMetadata
575 );
576
577 if(Art.verbose) {
578 console.log("Saved ART file #" + self.number + " metadata to file: " + metadataFilePath);
579 }
580
581 return callback(null, metadataFilePath);
582 }
583 catch(error) {
584 return callback(error);
585 }
586 },
587 function(metadataFilePath, callback) {
588 return async.concatSeries(
589 self.getTiles(includeEmpty),
590 function(tile, callback) {
591 return tile.writeTo(
592 outputDirectory,
593 overwrite,
594 palette,
595 fileType,
596 function(error, filePath) {
597 if(error) {
598 return callback(error);
599 }
600
601 if(Art.verbose) {
602 console.log("Saved tile #" + tile.number + " image to file: " + filePath);
603 }
604
605 return callback(null, filePath);
606 }
607 );
608 },
609 function(error, filePaths) {
610 if(error) {
611 return callback(error);
612 }
613
614 if(Art.verbose) {
615 console.log("Saved " + filePaths.length + " tile images from ART file #" + self.number + " to files at: " + outputDirectory);
616 }
617
618 return callback(null, filePaths, metadataFilePath);
619 }
620 );
621 },
622 ],
623 function(error, filePaths, metadataFilePath) {
624 if(error) {
625 return callback(error);
626 }
627
628 return callback(null, filePaths, metadataFilePath);
629 }
630 );
631 }
632
633 getSize() {
634 let self = this;
635
636 let size = Art.HeaderSize + (self.tiles.length * 8);
637
638 for(let i = 0; i < self.tiles.length; i++) {
639 size += self.tiles[i].getSize();
640 }
641
642 return size;
643 }
644
645 serialize() {
646 let self = this;
647
648 let artByteBuffer = new ByteBuffer(self.getSize());
649 artByteBuffer.order(true);
650
651 artByteBuffer.writeInt32(Art.Version);
652 artByteBuffer.writeInt32(self.legacyTileCount)
653 artByteBuffer.writeInt32(self.localTileStart)
654 artByteBuffer.writeInt32(self.localTileEnd);
655
656 for(let i = 0; i < self.tiles.length; i++) {
657 artByteBuffer.writeInt16(self.tiles[i].width);
658 }
659
660 for(let i = 0; i < self.tiles.length; i++) {
661 artByteBuffer.writeInt16(self.tiles[i].height);
662 }
663
664 for(let i = 0; i < self.tiles.length; i++) {
665 artByteBuffer.writeInt32(self.tiles[i].attributes.pack());
666 }
667
668 for(let i = 0; i < self.tiles.length; i++) {
669 artByteBuffer.append(self.tiles[i].data);
670 }
671
672 artByteBuffer.flip();
673
674 return artByteBuffer.toBuffer();
675 }
676
677 static deserialize(data) {
678 let self = this;
679
680 if(!Buffer.isBuffer(data)) {
681 throw new Error("Invalid data, expected buffer.");
682 }
683
684 let artByteBuffer = new ByteBuffer();
685 artByteBuffer.order(true);
686 artByteBuffer.append(data, "binary");
687 artByteBuffer.flip();
688
689 if(artByteBuffer.remaining() < Art.HeaderSize) {
690 throw new Error("Art file corrupted or invalid, missing full header data.");
691 }
692
693 const version = artByteBuffer.readInt32();
694
695 if(!Number.isInteger(version)) {
696 throw new Error("Invalid ART file version: " + version + ", expected a value of " + Art.Version + ".");
697 }
698
699 if(version !== Art.Version) {
700 throw new Error("Unsupported ART file version: " + version + ", only version " + Art.Version + " is supported.");
701 }
702
703 const legacyTileCount = artByteBuffer.readInt32();
704
705 if(!Number.isInteger(legacyTileCount) || legacyTileCount < 0) {
706 throw new Error("Invalid tile count value: " + legacyTileCount + ", expected positive integer.");
707 }
708
709 const localTileStart = artByteBuffer.readInt32();
710
711 if(!Number.isInteger(localTileStart) || localTileStart < 0) {
712 throw new Error("Invalid local tile start value: " + localTileStart + ", expected positive integer.");
713 }
714
715 const localTileEnd = artByteBuffer.readInt32();
716
717 if(!Number.isInteger(localTileEnd) || localTileEnd < 0) {
718 throw new Error("Invalid local tile end value: " + localTileEnd + ", expected positive integer.");
719 }
720
721 if(localTileEnd < localTileStart) {
722 throw new Error("Invalid local tile start / end values, start value: " + localTileStart + " should be greater than or equal to end value: " + localTileEnd + ".");
723 }
724
725 let numberOfTiles = localTileEnd - localTileStart + 1;
726
727 if(artByteBuffer.remaining() < numberOfTiles * 8) {
728 throw new Error("Art file corrupted or invalid, missing full sprite property data.");
729 }
730
731 let tileWidths = [];
732
733 for(let i = 0; i < numberOfTiles; i++) {
734 tileWidths.push(artByteBuffer.readInt16());
735 }
736
737 let tileHeights = [];
738
739 for(let i = 0; i < numberOfTiles; i++) {
740 tileHeights.push(artByteBuffer.readInt16());
741 }
742
743 let tileAttributes = [];
744
745 for(let i = 0; i < numberOfTiles; i++) {
746 tileAttributes.push(artByteBuffer.readInt32());
747 }
748
749 let tileData = [];
750
751 for(let i = 0; i < numberOfTiles; i++) {
752 const numberOfPixels = tileWidths[i] * tileHeights[i];
753
754 if(artByteBuffer.remaining() < numberOfPixels) {
755 throw new Error("Art file corrupted or invalid, missing sprite pixel data for tile #" + (localTileStart + i) + ".");
756 }
757
758 tileData.push(artByteBuffer.copy(artByteBuffer.offset, artByteBuffer.offset + numberOfPixels).toBuffer());
759 artByteBuffer.skip(numberOfPixels);
760 }
761
762 let tiles = [];
763
764 for(let i = 0; i < numberOfTiles; i++) {
765 try {
766 tiles.push(new Art.Tile(localTileStart + i, tileWidths[i], tileHeights[i], tileData[i], tileAttributes[i]));
767 }
768 catch(error) {
769 error.message = "Failed to parse tile #" + (localTileStart + i) + " from: " + error.message;
770 throw error;
771 }
772 }
773
774 return new Art(legacyTileCount, localTileStart, localTileEnd, tiles);
775 }
776
777 static readFrom(filePath) {
778 if(utilities.isEmptyString(filePath)) {
779 throw new Error("Missing or invalid art file path.");
780 }
781
782 if(Art.verbose) {
783 console.log("Reading ART file: " + filePath);
784 }
785
786 let data = null;
787
788 try {
789 data = fs.readFileSync(filePath);
790 }
791 catch(error) {
792 if(error.code === "ENOENT") {
793 throw new Error("Art file does not exist!");
794 }
795 else if(error.code === "EISDIR") {
796 throw new Error("Art file path is not a file!");
797 }
798 else {
799 throw new Error("Failed to read art file with error code: " + error.code);
800 }
801 }
802
803 let art = Art.deserialize(data);
804 art.filePath = filePath;
805
806 if(Art.verbose) {
807 console.log("Processed ART file: " + art.getFileName() + " (Start: " + art.localTileStart + ", End: " + art.localTileEnd + ", Number of Tiles: " + art.tiles.length + ", Non-Empty: " + art.numberOfNonEmptyTiles() + ")");
808 }
809
810 return art;
811 }
812
813 writeTo(filePath) {
814 let self = this;
815
816 if(utilities.isEmptyString(filePath)) {
817 throw new Error("Must specify file path to save to.");
818 }
819
820 const outputDirectory = utilities.getFilePath(filePath);
821
822 if(utilities.isNonEmptyString(outputDirectory)) {
823 fs.ensureDirSync(outputDirectory);
824 }
825
826 if(Art.verbose) {
827 console.log("Writing ART file #" + self.number + " to file: " + filePath);
828 }
829
830 fs.writeFileSync(filePath, self.serialize());
831
832 return filePath;
833 }
834
835 clone() {
836 const self = this;
837
838 return new Art(self.legacyTileCount, self.localTileStart, self.localTileEnd, self.tiles.map(function(tile) { return tile.clone(); }), self.filePath);
839 }
840
841 static isArt(value) {
842 return value instanceof Art;
843 }
844}
845
846Object.defineProperty(Art, "Version", {
847 value: 1,
848 enumerable: true
849});
850
851Object.defineProperty(Art, "DefaultNumberOfTiles", {
852 value: 256,
853 enumerable: true
854});
855
856Object.defineProperty(Art, "HeaderSize", {
857 value: 4 * 4, // 16 bytes
858 enumerable: true
859});
860
861Object.defineProperty(Art, "DefaultMetadataFileExtension", {
862 value: "JSON",
863 enumerable: true
864});
865
866Object.defineProperty(Art, "MetadataFormat", {
867 value: {
868 type: "object",
869 strict: true,
870 removeExtra: true,
871 order: true,
872 nonEmpty: true,
873 required: true,
874 format: {
875 number: {
876 type: "integer",
877 validator: function(value) {
878 return value >= 0;
879 }
880 },
881 count: {
882 type: "integer",
883 validator: function(value) {
884 return value >= 0;
885 }
886 },
887 start: {
888 type: "integer",
889 validator: function(value) {
890 return value >= 0;
891 }
892 },
893 end: {
894 type: "integer",
895 validator: function(value) {
896 return value >= 0;
897 }
898 },
899 tiles: {
900 type: "array",
901 required: true,
902 format: {
903 type: "object",
904 strict: true,
905 removeExtra: true,
906 order: true,
907 nonEmpty: true,
908 required: true,
909 format: {
910 number: {
911 type: "integer",
912 validator: function(value) {
913 return value >= 0;
914 }
915 },
916 attributes: {
917 type: "object",
918 strict: true,
919 removeExtra: true,
920 order: true,
921 nonEmpty: true,
922 required: true,
923 format: {
924 offset: {
925 type: "object",
926 strict: true,
927 removeExtra: true,
928 order: true,
929 nonEmpty: true,
930 required: true,
931 format: {
932 x: {
933 type: "integer",
934 validator: function(value) {
935 return value >= Art.Tile.Attributes.Attribute.XOffset.min && value <= Art.Tile.Attributes.Attribute.XOffset.max;
936 }
937 },
938 y: {
939 type: "integer",
940 validator: function(value) {
941 return value >= Art.Tile.Attributes.Attribute.YOffset.min && value <= Art.Tile.Attributes.Attribute.YOffset.max;
942 }
943 }
944 }
945 },
946 numberOfFrames: {
947 type: "integer",
948 validator: function(value) {
949 return value >= Art.Tile.Attributes.Attribute.NumberOfFrames.min && value <= Art.Tile.Attributes.Attribute.NumberOfFrames.max;
950 }
951 },
952 animation: {
953 type: "object",
954 strict: true,
955 removeExtra: true,
956 order: true,
957 nonEmpty: true,
958 required: true,
959 format: {
960 type: {
961 type: "string",
962 case: "title",
963 trim: true,
964 nonEmpty: true,
965 required: true,
966 validator: function(value) {
967 for(let i = 0; i < Art.Tile.AnimationType.Types.length; i++) {
968 if(value === Art.Tile.AnimationType.Types[i].name) {
969 return true;
970 }
971 }
972
973 return false;
974 }
975 },
976 speed: {
977 type: "integer",
978 validator: function(value) {
979 return value >= Art.Tile.Attributes.Attribute.AnimationSpeed.min && value <= Art.Tile.Attributes.Attribute.AnimationSpeed.max;
980 }
981 }
982 }
983 },
984 extra: {
985 type: "integer",
986 validator: function(value) {
987 return value >= Art.Tile.Attributes.Attribute.Extra.min && value <= Art.Tile.Attributes.Attribute.Extra.max;
988 }
989 }
990 }
991 }
992 }
993 }
994 }
995 }
996 },
997 enumerable: true
998});
999
1000Object.defineProperty(Art, "properties", {
1001 value: new ArtProperties(),
1002 enumerable: false
1003});
1004
1005Object.defineProperty(Art, "verbose", {
1006 enumerable: true,
1007 get() {
1008 return Art.properties.verbose;
1009 },
1010 set(value) {
1011 Art.properties.verbose = value;
1012 }
1013});
1014
1015Object.defineProperty(Art, "metadataFileExtension", {
1016 enumerable: true,
1017 get() {
1018 return Art.properties.metadataFileExtension;
1019 },
1020 set(value) {
1021 Art.properties.metadataFileExtension = value;
1022 }
1023});
1024
1025Object.defineProperty(Art, "Colour", {
1026 value: Colour,
1027 enumerable: true
1028});
1029
1030Object.defineProperty(Art, "Palette", {
1031 value: Palette,
1032 enumerable: true
1033});
1034
1035Object.defineProperty(Art, "Tile", {
1036 value: Tile,
1037 enumerable: true
1038});
1039
1040module.exports = Art;