| @@ -4,6 +4,26 @@ import NESBank from "/app/js/models/NESBank.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{ | |||
| constructor(){ | |||
| super(); | |||
| @@ -12,18 +32,49 @@ export default class NESNameTable extends ISurface{ | |||
| this.__tiles = []; | |||
| this.__attribs = []; | |||
| this.__undos = []; | |||
| this.__redos = []; | |||
| for (let i=0; i < 960; i++) | |||
| this.__tiles[i] = 0; | |||
| for (let i=0; i < 64; i++) | |||
| 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;} | |||
| set bank(b){ | |||
| if (b !== null and !(b instanceof NESBank)) | |||
| throw new TypeError("Expected a NESBank object."); | |||
| this.__bank = b; | |||
| this.emit("data_changed"); | |||
| } | |||
| get palette(){return this.__palette;} | |||
| @@ -31,13 +82,14 @@ export default class NESNameTable extends ISurface{ | |||
| if (p !== null && !(p instanceof NESPalette)) | |||
| throw new TypeError("Expected a NESPalette object."); | |||
| this.__palette = p; | |||
| this.emit("data_changed"); | |||
| } | |||
| get width(){return 256;} | |||
| 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){ | |||
| @@ -53,6 +105,7 @@ export default class NESNameTable extends ISurface{ | |||
| b.__attribs[i][3] | |||
| ]; | |||
| } | |||
| this.emit("data_changed"); | |||
| return this; | |||
| } | |||
| @@ -60,13 +113,52 @@ export default class NESNameTable extends ISurface{ | |||
| 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){ | |||
| @@ -96,9 +188,8 @@ export default class NESNameTable extends ISurface{ | |||
| var tileX = Math.floor(x / 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); | |||
| } | |||
| return {pi:pi, ci:ci}; | |||
| @@ -106,22 +197,111 @@ export default class NESNameTable extends ISurface{ | |||
| 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; | |||
| } | |||
| 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 blockY = Math.floor(y / 32); | |||
| var bIndex = (blockY * 8) + blockX; | |||
| bp.bindex = (blockY * 8) + blockX; | |||
| var palX = Math.floor(x % 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]; | |||
| } | |||
| } | |||