Spaces:
Running
Running
| 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; | |
| } | |
| } |