| import NESPalette from "/app/js/models/NESPalette.js"; | import NESPalette from "/app/js/models/NESPalette.js"; | ||||
| function NumToHex(n){ | |||||
| var h = n.toString(16); | |||||
| if (h.length %2) | |||||
| h = '0' + h; | |||||
| return '$' + h; | |||||
| } | |||||
| function CompileAttribs(attrib){ | |||||
| return (attrib[3] << 6) | (attrib[2] << 4) | (attrib[1] << 2) | (attrib[0] << 0) | |||||
| } | |||||
| function DecompileAttribs(v){ | |||||
| return [ | |||||
| v & 0x00000011, | |||||
| (v & 0x00001100) >> 2, | |||||
| (v & 0x00110000) >> 4, | |||||
| (v & 0x11000000) >> 6 | |||||
| ]; | |||||
| } | |||||
| export default class NESNameTable extends ISurface{ | export default class NESNameTable extends ISurface{ | ||||
| constructor(){ | constructor(){ | ||||
| super(); | super(); | ||||
| this.__tiles = []; | this.__tiles = []; | ||||
| this.__attribs = []; | this.__attribs = []; | ||||
| this.__undos = []; | |||||
| this.__redos = []; | |||||
| for (let i=0; i < 960; i++) | for (let i=0; i < 960; i++) | ||||
| this.__tiles[i] = 0; | this.__tiles[i] = 0; | ||||
| for (let i=0; i < 64; i++) | for (let i=0; i < 64; i++) | ||||
| this.__attribs[i] = [0,0,0,0]; | this.__attribs[i] = [0,0,0,0]; | ||||
| } | } | ||||
| get base64(){ | |||||
| var b = ""; | |||||
| for (let i = 0; i < this.__tiles.length; i++) | |||||
| b += String.fromCharCode(this.__tiles[i]); | |||||
| for (let i = 0; i < this.__attribs.length; i++) | |||||
| b += String.fromCharCode(CompileAttribs(this.__attribs[i])); | |||||
| return window.btoa(b); | |||||
| } | |||||
| set base64(s){ | |||||
| var b = window.atob(s); | |||||
| var len = b.length; | |||||
| if (b.length !== 1024){ | |||||
| throw new Error("Base64 string contains invalid byte count."); | |||||
| } | |||||
| b = new Uint8Array(b.split("").map(function(c){ | |||||
| return c.charCodeAt(0); | |||||
| })); | |||||
| for (let i=0; i < b.length; i++){ | |||||
| if (i < 960){ | |||||
| this.__tiles[i] = b[i]; | |||||
| } else { | |||||
| this.__attribs[i-960] = DecompileAttribs(b[i]); | |||||
| } | |||||
| } | |||||
| this.emit("data_changed"); | |||||
| } | |||||
| get bank(){return this.__bank;} | get bank(){return this.__bank;} | ||||
| set bank(b){ | set bank(b){ | ||||
| if (b !== null and !(b instanceof NESBank)) | if (b !== null and !(b instanceof NESBank)) | ||||
| throw new TypeError("Expected a NESBank object."); | throw new TypeError("Expected a NESBank object."); | ||||
| this.__bank = b; | this.__bank = b; | ||||
| this.emit("data_changed"); | |||||
| } | } | ||||
| get palette(){return this.__palette;} | get palette(){return this.__palette;} | ||||
| if (p !== null && !(p instanceof NESPalette)) | if (p !== null && !(p instanceof NESPalette)) | ||||
| throw new TypeError("Expected a NESPalette object."); | throw new TypeError("Expected a NESPalette object."); | ||||
| this.__palette = p; | this.__palette = p; | ||||
| this.emit("data_changed"); | |||||
| } | } | ||||
| get width(){return 256;} | get width(){return 256;} | ||||
| get height(){return 240;} | get height(){return 240;} | ||||
| get length(){return 0;} | |||||
| get undos(){return 0;} | |||||
| get redos(){return 0;} | |||||
| get length(){return this.width * this.height;} | |||||
| get undos(){return this.__undos.length;} | |||||
| get redos(){return this.__redos.length;} | |||||
| copy(b){ | copy(b){ | ||||
| b.__attribs[i][3] | b.__attribs[i][3] | ||||
| ]; | ]; | ||||
| } | } | ||||
| this.emit("data_changed"); | |||||
| return this; | return this; | ||||
| } | } | ||||
| return (new NESNameTable()).clone(this); | return (new NESNameTable()).clone(this); | ||||
| } | } | ||||
| snapshot(){return this;} | |||||
| undo(){return this;} | |||||
| redo(){return this;} | |||||
| clearUndos(){return this;} | |||||
| clearRedos(){return this;} | |||||
| clearHistory(){ | |||||
| return this.clearUndos().clearRedos(); | |||||
| snapshot(){ | |||||
| if (this.__redos.length > 0) // Remove the redo history. We're adding a new snapshot. | |||||
| this.__redos = []; | |||||
| var snap = this.base64; | |||||
| if (this.__undos.length === this.__historyLength){ | |||||
| this.__undos.pop(); | |||||
| } | |||||
| this.__undos.splice(0,0,snap); | |||||
| return this; | |||||
| } | |||||
| undo(){ | |||||
| if (this.__undos.length > 0){ | |||||
| var usnap = this.__undos.splice(0, 1)[0]; | |||||
| var rsnap = this.base64; | |||||
| this.base64 = usnap; | |||||
| if (this.__redos.length === this.__historyLength){ | |||||
| this.__redos.pop(); | |||||
| } | |||||
| this.__redos.splice(0,0,rsnap); | |||||
| } | |||||
| return this; | |||||
| } | |||||
| redo(){ | |||||
| if (this.__redos.length > 0){ | |||||
| var rsnap = this.__redos.splice(0,1)[0]; | |||||
| var usnap = this.base64; | |||||
| this.base64 = rsnap; | |||||
| if (this.__undos.length === this.__historyLength){ | |||||
| this.__undos.pop(); | |||||
| } | |||||
| this.__undos.splice(0,0,usnap); | |||||
| } | |||||
| return this; | |||||
| } | |||||
| clearUndos(){ | |||||
| this.__undos = []; | |||||
| return this; | |||||
| } | |||||
| clearRedos(){ | |||||
| this.__redos = []; | |||||
| return this; | |||||
| } | } | ||||
| getColor(x, y){ | getColor(x, y){ | ||||
| var tileX = Math.floor(x / 32); | var tileX = Math.floor(x / 32); | ||||
| var tileY = Math.floor(y / 32); | var tileY = Math.floor(y / 32); | ||||
| var tileIndex = 256 + this.__tiles[(tileY * 32) + tileX]; | |||||
| ci = this.__bank.rp[tileIndex].getPixelIndex(_x, _y); | |||||
| ci = this.__bank.rp[this.__tiles[(tileY * 32) + tileX]].getPixelIndex(_x, _y); | |||||
| pi = this._PaletteFromCoords(x, y); | pi = this._PaletteFromCoords(x, y); | ||||
| } | } | ||||
| return {pi:pi, ci:ci}; | return {pi:pi, ci:ci}; | ||||
| setColorIndex(x, y, ci, pi){ | setColorIndex(x, y, ci, pi){ | ||||
| if (x < 0 || x >= this.width || y < 0 || y >= this.height) | |||||
| throw new RangeError("Coordinates are out of bounds."); | |||||
| if (pi < 0 || pi >= 4) | |||||
| throw new RangeError("Palette index is out of bounds."); | |||||
| // NOTE: This method (setColorIndex) is called by CTRLPainter, which doesn't know about painting tile | |||||
| // indicies... therefore, CTRLPainter will still call this method for NESNameTable surfaces, but all it | |||||
| // will do is change palette indicies. | |||||
| // To paint the actual tile index, however, we'll use this emit that will be watched by the CTRLNameTable class | |||||
| // and call this class's setTileIndex() method for tile painting. | |||||
| // YAY to cheating!! | |||||
| this.emit("paint_nametable"); | |||||
| // Then business as usual! | |||||
| var bp = this._GetAttribBlockPalette(x,y); | |||||
| if (this.__attribs[bp.bindex][bp.pindex] !== pi){ | |||||
| this.__attribs[bp.bindex][bp.pindex] = pi; | |||||
| this.emit("data_changed"); | |||||
| } | |||||
| return this; | return this; | ||||
| } | } | ||||
| setTileIndex(x, y, ti){ | |||||
| if (ti < 0 || ti >= 256) | |||||
| throw new RangeError("Tile index is out of bounds."); | |||||
| if (x < 0 || x >= this.width || y < 0 || y >= this.height) | |||||
| throw new RangeError("Coordinates are out of bounds."); | |||||
| var _x = Math.floor(x / 8); | |||||
| var _y = Math.floor(y / 8); | |||||
| var tindex = (_y * 32) + _x; | |||||
| if (this.__tiles[tindex] !== ti){ | |||||
| this.__tiles[tindex] = ti; | |||||
| this.emit("data_changed"); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Generates a small 6502 assembly block string containing the current nametable data. | |||||
| * @param {string} [ntname="NameTableData"] The label name under which to store the data. | |||||
| * @returns {string} | |||||
| */ | |||||
| nametable_asm(ntname="NameTableData"){ | |||||
| var s = ntname + ":"; | |||||
| for (let i=0; i < this.__tiles.length; i++){ | |||||
| if (i % 32 === 0) | |||||
| s += "\n\t.db"; | |||||
| s += " " + NumToHex(this.__tiles[i]); | |||||
| } | |||||
| return s; | |||||
| } | |||||
| /** | |||||
| * Generates a small 6502 assembly block string containing the current attribute table data. | |||||
| * @param {string} [atname="AttribTableData"] The label name under which to store the data. | |||||
| * @returns {string} | |||||
| */ | |||||
| attribtable_asm(atname="AttribTableData"){ | |||||
| var s = atname + ":"; | |||||
| for (let i=0; i < this.__attribs.length; i++){ | |||||
| if (i % 32 === 0) | |||||
| s += "\n\t.db"; | |||||
| s += " " + NumToHex(CompileAttribs(this.__attribs[i])); | |||||
| } | |||||
| return s; | |||||
| } | |||||
| /** | |||||
| * Generates a small 6502 assembly block string containing the current name and attribute table data. | |||||
| * @param {string} [ntname="NameTableData"] The label name under which to store the Nametable data. | |||||
| * @param {string} [atname="AttribTableData"] the label name under which to store the Attribute table data. | |||||
| * @returns {string} | |||||
| */ | |||||
| to_asm(ntname="NameTableData", atname="AttribTableData"){ | |||||
| return this.nametable_asm(ntname) + "\n\n" + this.attribtable_asm(atname); | |||||
| } | |||||
| _GetAttribBlockPalette(x,y){ | |||||
| var bp = {bindex:0, pindex:0}; | |||||
| _PaletteFromCoords(x,y){ | |||||
| var blockX = Math.floor(x / 32); | var blockX = Math.floor(x / 32); | ||||
| var blockY = Math.floor(y / 32); | var blockY = Math.floor(y / 32); | ||||
| var bIndex = (blockY * 8) + blockX; | |||||
| bp.bindex = (blockY * 8) + blockX; | |||||
| var palX = Math.floor(x % 16); | var palX = Math.floor(x % 16); | ||||
| var palY = Math.floor(y % 16); | var palY = Math.floor(y % 16); | ||||
| var pIndex = ((palX < 8) ? 0 : 1) + ((palY >= 8) ? 2 : 0); | |||||
| bp.pindex = ((palX < 8) ? 0 : 1) + ((palY >= 8) ? 2 : 0); | |||||
| return this.__attribs[bIndex][pIndex]; | |||||
| return bp; | |||||
| } | |||||
| _PaletteFromCoords(x,y){ | |||||
| var bp = this._GetAttribBlockPalette(x,y); | |||||
| return this.__attribs[bp.bindex][bp.pindex]; | |||||
| } | } | ||||
| } | } | ||||