import Phaser from 'phaser'; import data from './tiles'; import * as CONST from '../constants'; import { polygonUnion } from '../utils'; export default class artwork { constructor (options) { this.scene = options.scene; this.data = this.scene.cache.binary.get(CONST.LARGE_DAT); this.palette = this.scene.palette; this.tiles = data; this.textureSize = 4096; this.texture = this.scene.textures.createCanvas('temp', this.textureSize, this.textureSize); this.json = {}; this.canvas = null; this.polygonUnion = polygonUnion; this.parse(); this.createTexture(); } // // parse dat file into raw image data for each frame // parse () { let view = new DataView(this.data); let imageCount = view.getUint16(0x00); let img = new DataView(this.data, 2, imageCount * 10); // calculate image ids, offsets and dimensions // each image header is stored as a 10 byte chunk // only store unique images (1204 and 1183 are duplicated) for (let offset = 0; offset < imageCount * 10; offset += 10) { let id = img.getUint16(offset) - 1000; let data = {}; data.imageName = img.getUint16(offset); data.startBytes = img.getUint32(offset + 2); data.height = img.getUint16(offset + 6); data.width = img.getUint16(offset + 8); // use the offset start of the next frame to determine the end of this frame if (offset + 10 <= img.byteLength - 2) data.nextId = img.getUint16(offset + 10) - 1000; this.tiles[id].data = data; } // calculate image ending offset // separate loop so we can easily get the end byte of the following frame for (let i = 1; i < this.tiles.length; i++) { let tile = this.tiles[i]; // image block data tile.data.endBytes = (tile.data.nextId !== undefined ? this.tiles[tile.data.nextId].data.startBytes : this.data.byteLength); tile.data.size = tile.data.endBytes - tile.data.startBytes; tile.data.rawData = new DataView(this.data.slice(tile.data.startBytes, tile.data.endBytes)); tile.data.block = this.block(tile.data); tile.loaded = false; tile.animated = this.isAnimatedImage(tile.data.block); tile.frames = tile.frames || this.getFrameCount(tile.data); tile.width = tile.data.width * CONST.SCALE; tile.height = tile.data.height * CONST.SCALE; tile.rotate = tile.rotate || [tile.id, tile.id, tile.id, tile.id]; tile.hitbox = this.shape(tile.hitbox || tile.heightmap || this.tiles[256].heightmap); tile.textures = []; for (let t = 0; t <= tile.frames; t++) tile.textures.push(tile.image+'_'+t); this.tiles[i] = tile; } delete this.tiles[0]; } // // converts x/y data array to a Phaser polygon // shape (hitbox) { let polygon = []; if (hitbox.reference) hitbox = this.tiles[hitbox.reference].hitbox || this.tiles[hitbox.reference].heightmap; if (hitbox instanceof Phaser.Geom.Polygon) hitbox = { upper: hitbox.points }; // merge all sides of the shape into a single array of points let shape = [].concat( (hitbox.lower ? hitbox.lower : []), (hitbox.upper ? hitbox.upper : []), (hitbox.south ? hitbox.south : []), (hitbox.east ? hitbox.east : []), (hitbox.west ? hitbox.west : []), (hitbox.southEast ? hitbox.southEast : []), (hitbox.southWest ? hitbox.southWest : []), (hitbox.northEast ? hitbox.northEast : []), (hitbox.northWest ? hitbox.northWest : []), (hitbox.rockTop ? hitbox.rockTop : []), (hitbox.rockSouthWest ? hitbox.rockSouthWest : []), (hitbox.rockSouthEast ? hitbox.rockSouthEast : []), ); // combine into a single polygon with an exterior wall shape = polygonUnion(shape, shape); for (let i = 0; i < shape.length; i++) polygon.push(new Phaser.Geom.Point((shape[i].x), (shape[i].y))); return new Phaser.Geom.Polygon(polygon); } // // get the lowest common multiplier for all palette animation sequences // getFrameCount (image) { let frames = []; for (let y = 0; y < image.block.length; y++) for (let x = 0; x < image.block[y].pixels.length; x++) frames.push(this.palette.getFrameCountFromIndex(image.block[y].pixels[x])); if (frames.length <= 1) return 1; else return this.lcm.apply(null, frames); } // // check if image contains any palette indexes that cycle with each frame (animated) // isAnimatedImage (image) { for (var y = 0; y < image.length; y++) for (var x = 0; x < image[y].pixels.length; x++) if (this.palette.animatedIndexes.includes(image[y].pixels[x])) return true; return false; } // // processes image bytes into individual image data rows / chunks // block (image) { let offset = 0; let img = []; while (offset <= image.size) { let row = {}; row.length = image.rawData.getUint8(offset); row.more = image.rawData.getUint8(offset + 1); offset += 2; row.pixels = this.imageRow(image.rawData.buffer.slice(offset, offset + row.length)); img.push(row); if (row.more == 2) break; offset += row.length; } return img; } // // process image rows / chunks // imageRow (data) { let bytes = new DataView(data); let padding = 0; let length = 0; let extra = 0; let pixels = null; let mode = null; let image = []; let offset = 0; let header = 0; if (bytes.byteLength == 0) return image; // loop through the row chunks while (offset < bytes.byteLength - 1) { // special case for multi-chunk rows, drop first byte if zero if (bytes.getUint8(offset + 0x00) == 0x00 && offset > 0) offset++; // get chunk mode mode = bytes.getUint8(offset + 0x01); if (mode == 0x00 || mode == 0x03) { padding = bytes.getUint8(offset + 0x00); // padding pixels from the left edge length = bytes.getUint8(offset + 0x02); // pixels in the row to draw extra = bytes.getUint8(offset + 0x03); // extra bit / flag if (length == 0 && extra == 0x00) { header = 0x06; length = bytes.getUint8(offset + 0x04); extra = bytes.getUint8(offset + 0x05); pixels = new DataView(bytes.buffer.slice(offset + header, offset + header + length)); } else { header = 0x04; pixels = new DataView(bytes.buffer.slice(offset + header, offset + header + length)); } } else if (mode == 0x04) { header = 0x02; length = bytes.getUint8(offset + 0x00); pixels = new DataView(bytes.buffer.slice(offset + header, offset + header + length)); } // byte offset for the next loop offset += header + length; // save padding pixels (transparent) as null for (let i = 0; i < padding; i++) image.push(null); // save pixel data afterwards for (let i = 0; i < pixels.byteLength; i++) image.push(pixels.getUint8(i)); } return image; } createTexture () { let x = 1; let y = 1; let maxWidth = 16; let maxHeight = 8; let rowMaxY = 0; let padding = 1; let imageData = this.texture.getData(0, 0, this.textureSize, this.textureSize); let buffer = new ArrayBuffer(imageData.data.length); let buffer8 = new Uint8ClampedArray(buffer); let buffer32 = new Uint32Array(buffer); // looping 128 times here to sort tiles by size // this shuffles the smaller tiles to the front of the tilemap for (let loop = 0; loop < 128; loop++) { // loop for each tile for (let i = 1; i < this.tiles.length; i++) { let tile = this.tiles[i]; // skip tiles that were already flagged as loaded if (tile.loaded) continue; // skip anything that exceeds the current maximum if (tile.data.width > maxWidth || tile.data.height > maxHeight) continue; // loop on every frame for (let f = 0; f < tile.frames; f++) { // max tile height in this row if (tile.data.height > rowMaxY) rowMaxY = tile.data.height; // exceeds tilemap width, start a new row if (x + tile.data.width > this.textureSize) { x = 1; y += rowMaxY + padding; rowMaxY = 0; } // drop any colors? // used to foribly remove certain palette indexes // from tiles (example: traffic tiles) if (tile.importOptions && tile.importOptions.dropColor) tile.importOptions.dropColor.forEach((index, i) => { if (Number.isInteger(index)) tile.importOptions.dropColor[i] = this.palette.getColorString(index, 0); }); for (let ty = 0; ty < tile.data.block.length; ty++) { for (let tx = 0; tx < tile.data.block[ty].pixels.length; tx++) { // palette index value let index = tile.data.block[ty].pixels[tx]; // drop out specific palette colors and transparency if (tile.importOptions && tile.importOptions.dropColor && tile.importOptions.dropColor.includes(this.palette.getColorString(index, f))) index = null; // set color and canvas x/y index let color = this.palette.getColor(index, f); let cx = x + tx; let cy = y + ty; let idx = cy * this.textureSize + cx; buffer32[idx] = (color.alpha << 24) | (color.blue << 16) | (color.green << 8) | (color.red << 0); } } // add tilemap data this.json[tile.data.imageName + '_' + f] = { frame: { x: x, y: y, w: tile.data.width, h: tile.data.height }, rotated: false, trimmed: false, spriteSourceSize: { x: 0, y: 0, w: tile.data.width, h: tile.data.height }, sourceSize: { w: tile.data.width, h: tile.data.height } }; // move drawing position + padding x += tile.data.width + padding; // flag tile as loaded if the frame count matches the current frame // or if the tile has no frames if (tile.frames == f + 1 || tile.frames == 1) tile.loaded = true; } } // increase tile size next loop maxWidth = maxWidth + 4; maxHeight = maxHeight + 4; } // save buffer to texture and wrap json object imageData.data.set(buffer8); this.texture.putData(imageData, 0, 0); this.texture.refresh(); this.canvas = this.texture.getCanvas(); this.json = { frames: this.json }; // load texture atlas this.scene.textures.addAtlas(CONST.TILE_ATLAS, this.canvas, this.json); // remove temp canvas this.scene.textures.remove('temp'); // add animations for (let i = 1; i < this.tiles.length; i++) { let tile = this.tiles[i]; // set up animations if (tile.frames > 1) { this.scene.anims.create({ key: tile.data.imageName, frames: this.scene.anims.generateFrameNames(CONST.TILE_ATLAS, { prefix: tile.data.imageName + '_', start: (tile.reverseAnimation ? tile.frames : 0), end: (tile.reverseAnimation ? 0 : tile.frames) }), repeat: -1, frameRate: tile.frameRate || 2, delay: tile.animationDelay || 0 }); this.scene.anims.create({ key: tile.data.imageName+'_R', frames: this.scene.anims.generateFrameNames(CONST.TILE_ATLAS, { prefix: tile.data.imageName + '_', start: (tile.reverseAnimation ? 0 : tile.frames), end: (tile.reverseAnimation ? tile.frames : 0) }), repeat: -1, frameRate: tile.frameRate || 2, delay: tile.animationDelay || 0 }); } this.tiles[i] = tile; } } lcm (min, max) { function range (min, max) { let out = []; for (let i = min; i <= max; i++) out.push(i); return out; } function gcd (a, b) { return !b ? a : gcd(b, a % b); } function lcm (a, b) { return (a * b) / gcd(a, b); } let multiple = min; range(min, max).forEach(function(n) { multiple = lcm(multiple, n); }); return multiple; } }