From 8ec8ac68185bd844f3b0741de124003d36c39424 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sun, 8 Dec 2019 15:46:58 +0100 Subject: [PATCH] big cleanup, more config & get args for setting some config opts; improve debug logging --- script.js | 2340 ++++++++++++++++++++++++++++------------------------- style.css | 61 +- 2 files changed, 1305 insertions(+), 1096 deletions(-) diff --git a/script.js b/script.js index b194921..03aadda 100644 --- a/script.js +++ b/script.js @@ -1,377 +1,375 @@ -!(function () { - - class Svg { - /** - * Build a node from XML - * - * @param {String} code - * @param {String|null} wrapper - wrapper element, by default no wrapper. - * @param {Object|null} wrapperOpts - opts to add to the wrapper - * @returns {Node} - */ - static fromXML(code, wrapper = null, wrapperOpts = null) { - let n = document.createElementNS("http://www.w3.org/2000/svg", wrapper || 'g'); - - if (wrapperOpts) { - for (let p in wrapperOpts) { - if (wrapperOpts.hasOwnProperty(p)) { - n.setAttribute(p, wrapperOpts[p]); - } +class Svg { + /** + * Build a node from XML + * + * @param {String} code + * @param {String|null} wrapper - wrapper element, by default no wrapper. + * @param {Object|null} wrapperOpts - opts to add to the wrapper + * @returns {Node} + */ + static fromXML(code, wrapper = null, wrapperOpts = null) { + let n = document.createElementNS("http://www.w3.org/2000/svg", wrapper || 'g'); + + if (wrapperOpts) { + for (let p in wrapperOpts) { + if (wrapperOpts.hasOwnProperty(p)) { + n.setAttribute(p, wrapperOpts[p]); } } + } - n.innerHTML = code.trim(); - if (wrapper === null) { - return n.childNodes[0]; - } else { - return n; - } + n.innerHTML = code.trim(); + if (wrapper === null) { + return n.childNodes[0]; + } else { + return n; } + } - /** - * Build a node from XML, wrapped in `` - * - * @param {String} code - * @param {Object|null} wrapperOpts - opts to add to the wrapper - * @returns {Node} - */ - static fromXMLg(code, wrapperOpts = null) { - return Svg.fromXML(code, 'g', wrapperOpts); + /** + * Build a node from XML, wrapped in `` + * + * @param {String} code + * @param {Object|null} wrapperOpts - opts to add to the wrapper + * @returns {Node} + */ + static fromXMLg(code, wrapperOpts = null) { + return Svg.fromXML(code, 'g', wrapperOpts); + } +} + +/* --------- Shared Constants --------- */ +const BOARD_SIZE = 121; + +const METAL_SEQ = ['lead', 'tin', 'iron', 'copper', 'silver', 'gold']; + +const SYMBOLS_METALS = [ + 'mercury', 'lead', 'tin', 'iron', 'copper', 'silver', 'gold' +]; + +const SYMBOLS_ALL = [ + 'salt', 'air', 'fire', 'water', 'earth', 'mercury', 'lead', + 'tin', 'iron', 'copper', 'silver', 'gold', 'mors', 'vitae' +]; + +/** + * Convert grid coordinates to gameGrid array index + * + * @param {Number} x + * @param {Number} y + * @returns {Number} + */ +function xyToGridIndex(x, y) { + return y * 11 + x +} + +/** + * Convert grid index to X, Y + * + * @param {Number} index + * @returns {{x: Number, y: Number}} + */ +function gridIndexToXy(index) { + return { + x: index % 11, + y: Math.floor(index / 11) + }; +} + +/** + * Get if a coordinate is outside the game board. + * + * @param x + * @param y + * @return {boolean|boolean} + */ +function isXyOutside(x, y) { + return x < 0 + || x > 10 + || y < 0 + || y > 10 + || (y <= 5 && x > 5 + y) + || (y > 5 && x < y - 5); +} + +/** + * Game board + * + * Orb grid coordinates: + * x - grid X coordinate (counted from the left edge of a triangle starting in the top left corner) + * y - grid Y coordinate (row) + */ +class Board { + constructor() { + this.$bg = document.getElementById('boardbg'); + this.$orbs = document.getElementById('orbs'); + this.$svg = document.getElementById('board'); + this.$root = document.getElementById('root'); + + this.TILE_W = 91; + this.TILE_H = 79; + this.SCREEN_PAD = 20; + + // Orb grid + this.grid = []; + this.tiles = []; + + for (let i = 0; i < BOARD_SIZE; i++) { + this.grid[i] = null; + this.tiles[i] = null; } + + this.onOrbClick = (index, orb) => { + // placeholder + }; + this.onTileClick = (index) => { + // placeholder + }; + + this.initOrb(); + this.initGlyphs(); + this.initTile(); + + this.buildBackground(); + + this.initAutoScaling(); } - /* --------- Shared Constants --------- */ - const BOARD_SIZE = 121; + /** + * Show all orbs for graphics debugging + */ + testGraphics() { + let o; + + this.placeOrb(0, 0, 'salt'); + this.placeOrb(1, 0, 'air'); + this.placeOrb(2, 0, 'fire'); + o = this.placeOrb(3, 0, 'water'); + o.node.classList.add('selected'); + this.placeOrb(4, 0, 'earth'); + this.placeOrb(5, 0, 'mercury'); + this.placeOrb(0, 1, 'lead'); + this.placeOrb(1, 1, 'tin'); + this.placeOrb(2, 1, 'iron'); + this.placeOrb(3, 1, 'copper'); + this.placeOrb(4, 1, 'silver'); + this.placeOrb(5, 1, 'gold'); + this.placeOrb(6, 1, 'vitae'); + this.placeOrb(0, 2, 'mors'); + + o = this.placeOrb(1, 2, 'copper'); + o.node.classList.add('disabled'); + + o = this.placeOrb(2, 2, 'copper'); + o.node.classList.add('disabled'); + + this.highlight('copper'); + } - const METAL_SEQ = ['lead', 'tin', 'iron', 'copper', 'silver', 'gold']; + /** + * Resize to window and set up window resize event handler. + */ + initAutoScaling() { + this.rescaleTimeout = null; + window.addEventListener('resize', () => { + if (this.rescaleTimeout === null) { + this.rescaleTimeout = setTimeout(() => this.rescaleCanvas(), 60) + } + }); - const SYMBOLS_METALS = [ - 'mercury', 'lead', 'tin', 'iron', 'copper', 'silver', 'gold' - ]; + this.rescaleCanvas(); + } - const SYMBOLS_ALL = [ - 'salt', 'air', 'fire', 'water', 'earth', 'mercury', 'lead', - 'tin', 'iron', 'copper', 'silver', 'gold', 'mors', 'vitae' - ]; + /** + * Resize to current window size + */ + rescaleCanvas() { + let w = window.innerWidth; + let h = window.innerHeight; + const pad = this.SCREEN_PAD; + let scaleX = w / (1066 + pad * 2); + let scaleY = h / (926 + pad * 2); + let scale = Math.min(scaleX, scaleY); + this.$root.setAttribute('transform', `translate(${w / 2},${h / 2}) scale(${scale})`); + + this.rescaleTimeout = null; + } /** - * Convert grid coordinates to gameGrid array index + * Highlight a symbol. All other highlights are removed. * - * @param {Number} x - * @param {Number} y - * @returns {Number} + * @param {String|null} symbol - symbol to highlight, null to hide highlights */ - function xyToGridIndex(x, y) { - return y * 11 + x + highlight(symbol = null) { + SYMBOLS_ALL.forEach((s) => { + this.$svg.classList.toggle(`highlight-${symbol}`, symbol === s); + }); } /** - * Convert grid index to X, Y + * Convert grid coordinates to graphic coordinates * - * @param {Number} index - * @returns {{x: Number, y: Number}} + * @param {Number} x + * @param {Number} y + * @returns {{rx: number, ry: number}} */ - function gridIndexToXy(index) { - return { - x: index % 11, - y: Math.floor(index / 11) - }; + gridXyToCoord(x, y) { + let rx = this.TILE_W * (-2.5 + x - y * 0.5); + let ry = this.TILE_H * (-5 + y); + return {rx, ry}; } /** - * Get if a coordinate is outside the game board. + * Remove an orb from the grid at the given coordinates. * - * @param x - * @param y - * @return {boolean|boolean} + * @param {Number} x - board X + * @param {Number} y - board Y */ - function isXyOutside(x, y) { - return x < 0 - || x > 10 - || y < 0 - || y > 10 - || (y <= 5 && x > 5 + y) - || (y > 5 && x < y - 5); + removeOrb(x, y) { + const index = xyToGridIndex(x, y); + this.removeOrbByIndex(index) } /** - * Game board + * Remove orb by array index * - * Orb grid coordinates: - * x - grid X coordinate (counted from the left edge of a triangle starting in the top left corner) - * y - grid Y coordinate (row) + * @param {Number} index + * @param {Boolean} errorIfEmpty */ - class Board { - constructor() { - this.$bg = document.getElementById('boardbg'); - this.$orbs = document.getElementById('orbs'); - this.$svg = document.getElementById('board'); - this.$root = document.getElementById('root'); - - this.TILE_W = 91; - this.TILE_H = 79; - this.SCREEN_PAD = 20; - - // Orb grid - this.grid = []; - this.tiles = []; - - for (let i = 0; i < BOARD_SIZE; i++) { - this.grid[i] = null; - this.tiles[i] = null; - } - - this.onOrbClick = (index, orb) => { - }; - this.onTileClick = (index) => { - }; - - this.initOrb(); - this.initGlyphs(); - this.initTile(); - - this.buildBackground(); - - this.initAutoScaling(); - } - - /** - * Show all orbs for graphics debugging - */ - testGraphics() { - let o; - - this.placeOrb(0, 0, 'salt'); - this.placeOrb(1, 0, 'air'); - this.placeOrb(2, 0, 'fire'); - o = this.placeOrb(3, 0, 'water'); - o.node.classList.add('selected'); - this.placeOrb(4, 0, 'earth'); - this.placeOrb(5, 0, 'mercury'); - this.placeOrb(0, 1, 'lead'); - this.placeOrb(1, 1, 'tin'); - this.placeOrb(2, 1, 'iron'); - this.placeOrb(3, 1, 'copper'); - this.placeOrb(4, 1, 'silver'); - this.placeOrb(5, 1, 'gold'); - this.placeOrb(6, 1, 'vitae'); - this.placeOrb(0, 2, 'mors'); - - o = this.placeOrb(1, 2, 'copper'); - o.node.classList.add('disabled'); - - o = this.placeOrb(2, 2, 'copper'); - o.node.classList.add('disabled'); - - this.highlight('copper'); - } - - /** - * Resize to window and set up window resize event handler. - */ - initAutoScaling() { - this.rescaleTimeout = null; - window.addEventListener('resize', () => { - if (this.rescaleTimeout === null) { - this.rescaleTimeout = setTimeout(() => this.rescaleCanvas(), 60) - } - }); - - this.rescaleCanvas(); - } - - /** - * Resize to current window size - */ - rescaleCanvas() { - let w = window.innerWidth; - let h = window.innerHeight; - const pad = this.SCREEN_PAD; - let scaleX = w / (1066 + pad * 2); - let scaleY = h / (926 + pad * 2); - let scale = Math.min(scaleX, scaleY); - this.$root.setAttribute('transform', `translate(${w / 2},${h / 2}) scale(${scale})`) - - this.rescaleTimeout = null; - } - - /** - * Highlight a symbol. All other highlights are removed. - * - * @param {String|null} symbol - symbol to highlight, null to hide highlights - */ - highlight(symbol = null) { - SYMBOLS_ALL.forEach((s) => { - this.$svg.classList.toggle(`highlight-${symbol}`, symbol === s); - }); + removeOrbByIndex(index, errorIfEmpty = false) { + // placeholder orb + if (typeof this.grid[index] === 'string') { + this.grid[index] = null; + return; } - /** - * Convert grid coordinates to graphic coordinates - * - * @param {Number} x - * @param {Number} y - * @returns {{rx: number, ry: number}} - */ - gridXyToCoord(x, y) { - let rx = this.TILE_W * (-2.5 + x - y * 0.5); - let ry = this.TILE_H * (-5 + y); - return {rx, ry}; - } - - /** - * Remove an orb from the grid at the given coordinates. - * - * @param {Number} x - board X - * @param {Number} y - board Y - */ - removeOrb(x, y) { - const index = xyToGridIndex(x, y); - this.removeOrbByIndex(index) - } - - /** - * Remove orb by array index - * - * @param {Number} index - * @param {Boolean} errorIfEmpty - */ - removeOrbByIndex(index, errorIfEmpty = false) { - // placeholder orb - if (typeof this.grid[index] === 'string') { - this.grid[index] = null; - return; - } - - if (this.grid[index]) { - this.$orbs.removeChild(this.grid[index].node); - this.grid[index] = null; - } else { - if (errorIfEmpty) { - throw new Error(`Position ${index} is already empty.`); - } + if (this.grid[index]) { + this.$orbs.removeChild(this.grid[index].node); + this.grid[index] = null; + } else { + if (errorIfEmpty) { + throw new Error(`Position ${index} is already empty.`); } } + } - /** - * Place an orb by array index - * - * @param {Number} index - * @param {String} symbol - * @return {{node: Node, symbol: String}} - */ - placeOrbByIndex(index, symbol) { - const {x, y} = gridIndexToXy(index); - return this.placeOrb(x, y, symbol); - } + /** + * Place an orb by array index + * + * @param {Number} index + * @param {String} symbol + * @return {{node: Node, symbol: String}} + */ + placeOrbByIndex(index, symbol) { + const {x, y} = gridIndexToXy(index); + return this.placeOrb(x, y, symbol); + } - /** - * Place an orb on the grid - * - * @param {Number} x - board X - * @param {Number} y - board Y - * @param {String} symbol - alchemical symbol name - * @returns {{node : Node, symbol: String}} - orb object - */ - placeOrb(x, y, symbol) { - const {rx, ry} = this.gridXyToCoord(x, y); - const arrayIndex = xyToGridIndex(x, y); + /** + * Place an orb on the grid + * + * @param {Number} x - board X + * @param {Number} y - board Y + * @param {String} symbol - alchemical symbol name + * @returns {{node : Node, symbol: String}} - orb object + */ + placeOrb(x, y, symbol) { + const {rx, ry} = this.gridXyToCoord(x, y); + const arrayIndex = xyToGridIndex(x, y); - this.removeOrbByIndex(arrayIndex); + this.removeOrbByIndex(arrayIndex); - let template; - if (SYMBOLS_METALS.includes(symbol)) { - template = this.metallicOrbTpl; - } else { - template = this.orbTpl; - } + let template; + if (SYMBOLS_METALS.includes(symbol)) { + template = this.metallicOrbTpl; + } else { + template = this.orbTpl; + } - let orb = template.cloneNode(true); - orb.classList.add(`symbol-${symbol}`); - orb.setAttribute('transform', `translate(${rx},${ry})`); - orb.querySelector('.orb-fill') - .setAttribute('fill', this.orbColors[symbol]); - orb.appendChild(this.symbolTpls[symbol].cloneNode(true)); + let orb = template.cloneNode(true); + orb.classList.add(`symbol-${symbol}`); + orb.setAttribute('transform', `translate(${rx},${ry})`); + orb.querySelector('.orb-fill') + .setAttribute('fill', this.orbColors[symbol]); + orb.appendChild(this.symbolTpls[symbol].cloneNode(true)); - orb.dataset.index = arrayIndex; - orb.dataset.symbol = symbol; - this.$orbs.appendChild(orb); + orb.dataset.index = arrayIndex; + orb.dataset.symbol = symbol; + this.$orbs.appendChild(orb); - let object = { - node: orb, - symbol - }; + let object = { + node: orb, + symbol + }; - orb.addEventListener('click', () => { - this.onOrbClick(arrayIndex, object); - }); + orb.addEventListener('click', () => { + this.onOrbClick(arrayIndex, object); + }); - this.grid[arrayIndex] = object; - return object; - } + this.grid[arrayIndex] = object; + return object; + } - /** - * Build board background - */ - buildBackground() { - // Background hexagon - let polygon = Svg.fromXML(` + /** + * Build board background + */ + buildBackground() { + // Background hexagon + let polygon = Svg.fromXML(` `); - this.$bg.appendChild(polygon); + this.$bg.appendChild(polygon); - // -- Tile grid -- + // -- Tile grid -- - // Grid is built in two passes - shadows must be placed first - this.buf0 = []; - this.buf1 = []; + // Grid is built in two passes - shadows must be placed first + this.buf0 = []; + this.buf1 = []; - for (let y = 0; y < 6; y++) { - for (let x = 0; x < 6 + y; x++) { - this.placeTile(x, y); - } + for (let y = 0; y < 6; y++) { + for (let x = 0; x < 6 + y; x++) { + this.placeTile(x, y); } + } - for (let y = 0; y < 5; y++) { - for (let x = 0; x < 10 - y; x++) { - this.placeTile(x + y + 1, 6 + y); - } + for (let y = 0; y < 5; y++) { + for (let x = 0; x < 10 - y; x++) { + this.placeTile(x + y + 1, 6 + y); } + } - this.buf0.forEach((elem) => { - this.$bg.appendChild(elem); - }); + this.buf0.forEach((elem) => { + this.$bg.appendChild(elem); + }); - this.buf1.forEach((elem) => { - this.$bg.appendChild(elem); - }); + this.buf1.forEach((elem) => { + this.$bg.appendChild(elem); + }); - delete this.buf0; - delete this.buf1; - } + delete this.buf0; + delete this.buf1; + } - /** - * Init the orb template - */ - initOrb() { - this.orbTpl = Svg.fromXMLg(` + /** + * Init the orb template + */ + initOrb() { + this.orbTpl = Svg.fromXMLg(` + opacity="0" /> `, { - 'class': 'orb', - }); + 'class': 'orb', + }); - let metallicEffect = Svg.fromXML(` + let metallicEffect = Svg.fromXML(` `); - this.metallicOrbTpl = this.orbTpl.cloneNode(true); - this.metallicOrbTpl.appendChild(metallicEffect); - } + this.metallicOrbTpl = this.orbTpl.cloneNode(true); + this.metallicOrbTpl.appendChild(metallicEffect); + } - /** - * Init the tile template - */ - initTile() { - const offsetX = -13.25; - const offsetY = -283.75; - const scale = 3.9; - const scaleShadow = scale + 0.1; + /** + * Init the tile template + */ + initTile() { + const offsetX = -13.25; + const offsetY = -283.75; + const scale = 3.9; + const scaleShadow = scale + 0.1; - this.tileShadowTpl = Svg.fromXMLg(` + this.tileShadowTpl = Svg.fromXMLg(` `, {class: 'tile'}); - } + } - /** - * Add a board tile to the image. - * - * @param x - * @param y - */ - placeTile(x, y) { - const {rx, ry} = this.gridXyToCoord(x, y); - - /* - // Debug circle - this.$svg.appendChild(Svg.makeNode('circle', { - r: this.TILE_W/2, - cy: ry, cx: rx, - fill: 'none', - stroke: 'black', - 'stroke-width': 2, - })); - */ - - let polygon_shadow = this.tileShadowTpl.cloneNode(true); - polygon_shadow.setAttribute('transform', `translate(${rx},${ry}),scale(1.1)`); - this.buf0.push(polygon_shadow); - - const index = xyToGridIndex(x, y); - let tile = this.tileTpl.cloneNode(true); - tile.setAttribute('transform', `translate(${rx},${ry})`); - this.buf1.push(tile); - - tile.addEventListener('click', () => { - this.onTileClick(index); - }); + /** + * Add a board tile to the image. + * + * @param x + * @param y + */ + placeTile(x, y) { + const {rx, ry} = this.gridXyToCoord(x, y); + + /* + // Debug circle + this.$svg.appendChild(Svg.makeNode('circle', { + r: this.TILE_W/2, + cy: ry, cx: rx, + fill: 'none', + stroke: 'black', + 'stroke-width': 2, + })); + */ + + let polygon_shadow = this.tileShadowTpl.cloneNode(true); + polygon_shadow.setAttribute('transform', `translate(${rx},${ry}),scale(1.1)`); + this.buf0.push(polygon_shadow); + + const index = xyToGridIndex(x, y); + let tile = this.tileTpl.cloneNode(true); + tile.setAttribute('transform', `translate(${rx},${ry})`); + this.buf1.push(tile); + + tile.addEventListener('click', () => { + this.onTileClick(index); + }); + + this.tiles[index] = tile; + } - this.tiles[index] = tile; - } + /** + * Init the orb glyph templates + */ + initGlyphs() { + let symbolPaths = { + salt: 'm 13.229166,291.52894 c -3.193365,0.056 -6.2293596,-2.11498 -7.2925669,-5.11388 -1.1013996,-2.92841 -0.2242509,-6.48596 2.1908174,-8.50166 2.5380605,-2.28864 6.6278455,-2.54558 9.4211355,-0.56925 2.346844,1.54401 3.721353,4.41788 3.400731,7.21296 -0.300944,3.62792 -3.485132,6.74361 -7.122041,6.95045 -0.19901,0.0144 -0.398548,0.0214 -0.598076,0.0214 z m -6.1435917,-8.51297 c 4.0957287,0 8.1914567,0 12.2871847,0 -0.315436,-3.07409 -3.239662,-5.6097 -6.336645,-5.4279 -2.970056,0.0389 -5.6195586,2.4852 -5.9505397,5.4279 z m 6.1435917,6.94037 c 2.96606,0.0757 5.716029,-2.30671 6.114844,-5.23796 0.09389,-0.3732 -0.542301,-0.11772 -0.794471,-0.19272 -3.821322,0 -7.642643,0 -11.4639647,0 0.352809,3.02068 3.0769237,5.49713 6.1435917,5.43068 z', + air: 'm 5.1145913,291.51845 c 2.7048586,-5.16509 5.4097167,-10.33018 8.1145757,-15.49527 2.704858,5.16509 5.409717,10.33018 8.114575,15.49527 -5.409717,0 -10.819434,0 -16.2291507,0 z m 6.0387527,-7.84199 c 1.383881,0 2.767763,0 4.151644,0 -0.69194,-1.35592 -1.383881,-2.71185 -2.075821,-4.06777 -0.691941,1.35592 -1.383882,2.71185 -2.075823,4.06777 z m -3.1661517,6.18553 c 3.4946497,0 6.9892987,0 10.4839477,0 -0.789791,-1.54463 -1.579581,-3.08927 -2.369372,-4.6339 -1.915068,0 -3.830136,0 -5.745204,0 -0.7897906,1.54463 -1.5795811,3.08927 -2.3693717,4.6339 z', + fire: 'm 5.1145923,291.51845 c 2.7048582,-5.16509 5.4097167,-10.33018 8.1145747,-15.49527 2.704858,5.16509 5.409716,10.33018 8.114574,15.49527 -5.409716,0 -10.819432,0 -16.2291487,0 z m 2.872601,-1.65646 c 3.4946487,0 6.9892977,0 10.4839467,0 -1.747324,-3.41777 -3.494649,-6.83553 -5.241973,-10.2533 -1.747325,3.41777 -3.4946491,6.83553 -5.2419737,10.2533 z', + water: 'm 13.229169,291.51845 c -2.704859,-5.16509 -5.409717,-10.33018 -8.1145755,-15.49527 5.4097155,0 10.8194315,0 16.2291465,0 -2.704857,5.16509 -5.409714,10.33018 -8.114571,15.49527 z m 0,-3.58551 c 1.747323,-3.41777 3.494647,-6.83553 5.24197,-10.2533 -3.494648,0 -6.989296,0 -10.4839445,0 1.7473248,3.41777 3.4946495,6.83553 5.2419745,10.2533 z', + earth: 'm 13.229167,291.51845 c -2.704858,-5.16509 -5.409717,-10.33018 -8.1145755,-15.49527 5.4097165,0 10.8194335,0 16.2291505,0 -2.704858,5.16509 -5.409717,10.33018 -8.114575,15.49527 z m -2.872601,-9.2049 c 1.915068,0 3.830135,0 5.745203,0 0.789791,-1.54464 1.579581,-3.08927 2.369372,-4.63391 -3.494649,0 -6.989298,0 -10.4839475,0 0.7897908,1.54464 1.5795817,3.08927 2.3693725,4.63391 z m 2.872601,5.61939 c 0.691941,-1.35592 1.383881,-2.71185 2.075822,-4.06777 -1.383881,0 -2.767762,0 -4.151643,0 0.69194,1.35592 1.383881,2.71185 2.075821,4.06777 z', + mercury: 'm 15.732407,279.01472 c 1.457188,0.91755 2.327793,2.6639 2.118676,4.38195 -0.130888,2.01739 -1.899178,3.73377 -3.900128,3.89053 0,0.71327 0,1.42654 0,2.13981 0.737153,-0.007 1.474306,-0.0132 2.211459,-0.0198 0,0.34126 0,0.68253 0,1.02379 -0.750804,0.003 -1.501607,0.007 -2.252411,0.01 0.0034,0.68938 0.0068,1.37875 0.01024,2.06813 -0.440244,0 -0.880489,0 -1.320733,0 -0.0034,-0.69279 -0.0068,-1.38558 -0.01024,-2.07837 -0.689374,0 -1.378747,0 -2.068121,0 0,-0.33444 0,-0.66888 0,-1.00332 0.689374,0 1.378747,0 2.068121,0 -0.0068,-0.71327 -0.01365,-1.42654 -0.02047,-2.13981 -2.15006,-0.13324 -4.015907,-2.07311 -3.9813417,-4.24183 -0.084702,-1.60894 0.7922593,-3.17204 2.1384617,-4.03065 -1.3321084,-0.8479 -2.2089181,-2.39019 -2.1397915,-3.98265 0.4846095,0 0.969219,0 1.4538285,0 -0.129447,1.801 1.552959,3.41833 3.338775,3.2762 1.681768,-0.0229 3.155505,-1.58904 3.039642,-3.2762 0.48461,0 0.969221,0 1.453831,0 0.06599,1.601 -0.803406,3.12723 -2.139798,3.98222 z m -2.303602,0.71667 c -1.758778,-0.16425 -3.4619579,1.37986 -3.388854,3.16361 -0.083477,1.71263 1.535734,3.14586 3.204567,3.07148 1.719365,0.0703 3.342464,-1.48587 3.170211,-3.23621 -0.02747,-1.57245 -1.423299,-2.948 -2.985924,-2.99888 z', + lead: 'm 9.5126894,279.50182 c -0.3617507,0 -0.7235013,0 -1.085252,0 0,-0.51532 0,-1.03065 0,-1.54597 0.3617507,0 0.7235013,0 1.085252,0 0,-0.6143 0,-1.2286 0,-1.8429 0.5187366,0 1.0374736,0 1.5562106,0 0,0.6143 0,1.2286 0,1.8429 0.607468,0 1.214936,0 1.822404,0 0,0.51532 0,1.03065 0,1.54597 -0.607468,0 -1.214936,0 -1.822404,0 0,1.32073 0,2.64145 0,3.96218 1.537514,-1.62806 4.417116,-1.42308 5.808997,0.28791 1.184192,1.31365 1.566419,3.41483 0.604294,4.96063 -0.519367,0.80942 -0.90468,1.73924 -0.884648,2.71614 -0.515324,0 -1.030647,0 -1.545971,0 -0.04659,-1.19876 0.527295,-2.27553 1.100009,-3.28428 0.92482,-1.59336 -0.125612,-3.92412 -1.965862,-4.19999 -1.635761,-0.31517 -3.124345,1.21479 -3.11374,2.80432 -0.0071,1.55992 -0.0011,3.11998 -0.0031,4.67995 -0.518737,0 -1.037474,0 -1.5562106,0 0,-3.97562 0,-7.95124 0,-11.92686 z', + tin: 'm 11.836769,285.99296 c 1.581694,0.0451 3.054161,-1.39068 2.989562,-2.99956 0.06903,-1.76826 -1.701886,-3.28981 -3.441524,-2.95861 -1.4247698,0.18442 -2.5932003,1.53599 -2.5376015,2.97841 -0.5119117,0 -1.0238233,0 -1.535735,0 -0.085695,-2.3801 2.0195976,-4.57675 4.4081085,-4.55471 2.311495,-0.14461 4.495576,1.78385 4.652567,4.08999 0.127445,1.23443 -0.250024,2.52397 -1.095332,3.44405 0.774693,0 1.549385,0 2.324078,0 0,-3.2933 0,-6.58661 0,-9.87991 0.515324,0 1.030647,0 1.545971,0 0,5.10547 0,10.21093 0,15.3164 -0.515324,0 -1.030647,0 -1.545971,0 0,-1.29684 0,-2.59368 0,-3.89052 -3.429807,0 -6.859614,0 -10.2894215,0 0,-0.51532 0,-1.03065 0,-1.54597 1.5084328,1.4e-4 3.0168655,2.9e-4 4.5252985,4.3e-4 z', + iron: 'm 11.237833,281.77434 c -2.1129725,-0.0765 -4.0685065,1.7834 -4.0429529,3.90895 -0.1246246,1.8905 1.3843212,3.64089 3.2341329,3.92585 1.98814,0.41339 4.242048,-0.80512 4.735939,-2.82798 0.42126,-1.59401 -0.247897,-3.38375 -1.626517,-4.29444 -0.666348,-0.47624 -1.483823,-0.71835 -2.300602,-0.71238 z m 4.218149,8.01654 c -2.219729,2.26802 -6.2673049,2.23092 -8.4717123,-0.0411 -2.1829952,-2.03247 -2.1495359,-5.81571 -0.085686,-7.91896 1.2047257,-1.38003 3.0857918,-2.10369 4.9042713,-1.84319 1.04112,0.0976 2.126139,0.37802 2.915975,1.10076 1.232001,-1.18763 2.464001,-2.37526 3.696002,-3.56289 -1.266128,0 -2.532257,0 -3.798385,0 0.477784,-0.48461 0.955568,-0.96921 1.433352,-1.45382 1.685895,0 3.371789,0 5.057684,0 0,1.67906 0,3.35812 0,5.03718 -0.488023,0.48803 -0.976045,0.97606 -1.464068,1.46409 0,-1.27296 0,-2.54591 0,-3.81887 -1.221761,1.18763 -2.443522,2.37526 -3.665283,3.56289 1.692564,2.15746 1.51732,5.5939 -0.52215,7.47392 z', + copper: 'm 13.229167,276.61943 c -2.112981,-0.0765 -4.0685271,1.78337 -4.04295,3.90894 -0.1277359,1.92476 1.447325,3.68809 3.331873,3.93182 2.025344,0.379 4.29148,-0.9459 4.680309,-3.02816 0.350755,-1.61444 -0.419466,-3.36035 -1.832442,-4.20966 -0.633357,-0.40672 -1.386458,-0.60708 -2.13679,-0.60294 z m -4.1567215,8.05748 c -2.3050816,-2.02524 -2.2795847,-5.96633 -0.1160579,-8.08364 2.0292494,-2.23826 5.8431584,-2.42533 8.0888084,-0.41068 1.604759,1.29217 2.357262,3.46519 1.988157,5.48009 -0.363367,2.37123 -2.511902,4.28305 -4.882744,4.48852 0,0.90097 0,1.80194 0,2.70291 0.928266,0 1.856531,0 2.784797,0 0,0.42318 0,0.84635 0,1.26953 -0.928266,0 -1.856531,0 -2.784797,0 0,0.87708 0,1.75415 0,2.63123 -0.614294,0 -1.228589,0 -1.842883,0 0,-0.87708 0,-1.75415 0,-2.63123 -0.928266,0 -1.856532,0 -2.7847978,0 0,-0.42318 0,-0.84635 0,-1.26953 0.9282658,0 1.8565318,0 2.7847978,0 0,-0.90438 0,-1.80876 0,-2.71314 -1.207048,-0.10173 -2.3584982,-0.63012 -3.2352805,-1.46406 z', + silver: 'm 9.9755063,275.73179 c -0.6014253,0.18082 -1.8508344,-0.12736 -1.9078968,0.74722 0.1176844,0.4442 0.7067667,0.30018 1.0195284,0.4996 2.4102181,0.84689 4.3760261,3.03219 4.6611691,5.61268 0.329015,2.2322 -0.292857,4.61262 -1.845188,6.27899 -0.950816,1.0625 -2.3118271,1.60982 -3.6314341,2.04088 -0.3010461,0.22955 -0.1186419,0.78 0.2735733,0.75652 2.3443258,0.46012 4.8961488,-0.14011 6.7090618,-1.72152 1.916707,-1.49245 3.236515,-3.83656 3.14282,-6.30499 -0.01445,-1.96756 -0.864897,-3.88727 -2.285751,-5.24448 -1.577433,-1.67996 -3.811286,-2.7316 -6.1358827,-2.6649 z m 2.7602927,2.03153 c 1.072404,0.39734 1.870102,1.27809 2.623097,2.10375 1.308542,1.47534 1.847166,3.59612 1.292761,5.50102 -0.534285,1.8245 -1.854209,3.45004 -3.574828,4.26864 -0.191682,0.0234 0.647922,-0.682 0.817217,-1.00958 1.833102,-2.3961 2.027928,-5.83879 0.732872,-8.5234 -0.452679,-0.90448 -1.108696,-1.7015 -1.891119,-2.34043 z', + gold: 'm 13.219492,275.7189 c -2.986621,0.01 -5.9729442,1.75519 -7.2157492,4.51399 -1.4447671,2.86838 -0.9743467,6.60206 1.2502049,8.94534 1.6486642,1.87365 4.2016723,2.85934 6.6817843,2.61607 3.746213,-0.22926 7.026658,-3.46775 7.307955,-7.20938 0.345627,-3.27431 -1.477563,-6.74322 -4.539965,-8.07025 -1.086444,-0.51129 -2.281439,-0.79866 -3.48423,-0.79577 z m 0,1.55427 c 2.485623,0.002 4.974789,1.48839 5.934079,3.82276 1.125698,2.44571 0.57943,5.58664 -1.449826,7.40023 -2.35696,2.37064 -6.611547,2.37064 -8.9685065,0 -2.1169053,-1.88824 -2.608931,-5.21179 -1.3089579,-7.69696 1.0280707,-2.17935 3.4218714,-3.51551 5.7932114,-3.52603 z m 0.0066,4.19203 c -1.370356,-0.0494 -2.523547,1.30386 -2.277873,2.64791 0.150011,1.3618 1.658854,2.29772 2.951679,1.86852 1.391568,-0.36367 2.080533,-2.14843 1.33192,-3.36846 -0.389128,-0.716 -1.20151,-1.14581 -2.005726,-1.14797 z', + mors: 'm 14.482436,292.72274 -0.358077,-0.35808 v -2.86461 h 2.864614 v -1.79038 h -2.864614 v -3.93885 h 5.371152 l -6.445382,-8.95192 -6.0873063,8.95192 h 5.3711513 v 3.93885 H 9.4693607 v 1.79038 h 2.8646133 v 2.86461 l -0.358076,0.35808 z M 16.27282,282.33852 H 9.8274367 l 3.2226923,-4.65501 z', + vitae: 'm 11.975898,274.8189 0.358077,0.35808 v 2.86461 H 9.4693607 v 1.79038 h 2.8646143 v 3.93885 H 6.9628227 l 6.4453823,8.95192 6.087306,-8.95192 H 14.12436 v -3.93885 h 2.864613 v -1.79038 H 14.12436 v -2.86461 l 0.358076,-0.35808 z m -1.790384,10.38422 h 6.445383 l -3.222692,4.65501 z', + }; - /** - * Init the orb glyph templates - */ - initGlyphs() { - let symbolPaths = { - salt: 'm 13.229166,291.52894 c -3.193365,0.056 -6.2293596,-2.11498 -7.2925669,-5.11388 -1.1013996,-2.92841 -0.2242509,-6.48596 2.1908174,-8.50166 2.5380605,-2.28864 6.6278455,-2.54558 9.4211355,-0.56925 2.346844,1.54401 3.721353,4.41788 3.400731,7.21296 -0.300944,3.62792 -3.485132,6.74361 -7.122041,6.95045 -0.19901,0.0144 -0.398548,0.0214 -0.598076,0.0214 z m -6.1435917,-8.51297 c 4.0957287,0 8.1914567,0 12.2871847,0 -0.315436,-3.07409 -3.239662,-5.6097 -6.336645,-5.4279 -2.970056,0.0389 -5.6195586,2.4852 -5.9505397,5.4279 z m 6.1435917,6.94037 c 2.96606,0.0757 5.716029,-2.30671 6.114844,-5.23796 0.09389,-0.3732 -0.542301,-0.11772 -0.794471,-0.19272 -3.821322,0 -7.642643,0 -11.4639647,0 0.352809,3.02068 3.0769237,5.49713 6.1435917,5.43068 z', - air: 'm 5.1145913,291.51845 c 2.7048586,-5.16509 5.4097167,-10.33018 8.1145757,-15.49527 2.704858,5.16509 5.409717,10.33018 8.114575,15.49527 -5.409717,0 -10.819434,0 -16.2291507,0 z m 6.0387527,-7.84199 c 1.383881,0 2.767763,0 4.151644,0 -0.69194,-1.35592 -1.383881,-2.71185 -2.075821,-4.06777 -0.691941,1.35592 -1.383882,2.71185 -2.075823,4.06777 z m -3.1661517,6.18553 c 3.4946497,0 6.9892987,0 10.4839477,0 -0.789791,-1.54463 -1.579581,-3.08927 -2.369372,-4.6339 -1.915068,0 -3.830136,0 -5.745204,0 -0.7897906,1.54463 -1.5795811,3.08927 -2.3693717,4.6339 z', - fire: 'm 5.1145923,291.51845 c 2.7048582,-5.16509 5.4097167,-10.33018 8.1145747,-15.49527 2.704858,5.16509 5.409716,10.33018 8.114574,15.49527 -5.409716,0 -10.819432,0 -16.2291487,0 z m 2.872601,-1.65646 c 3.4946487,0 6.9892977,0 10.4839467,0 -1.747324,-3.41777 -3.494649,-6.83553 -5.241973,-10.2533 -1.747325,3.41777 -3.4946491,6.83553 -5.2419737,10.2533 z', - water: 'm 13.229169,291.51845 c -2.704859,-5.16509 -5.409717,-10.33018 -8.1145755,-15.49527 5.4097155,0 10.8194315,0 16.2291465,0 -2.704857,5.16509 -5.409714,10.33018 -8.114571,15.49527 z m 0,-3.58551 c 1.747323,-3.41777 3.494647,-6.83553 5.24197,-10.2533 -3.494648,0 -6.989296,0 -10.4839445,0 1.7473248,3.41777 3.4946495,6.83553 5.2419745,10.2533 z', - earth: 'm 13.229167,291.51845 c -2.704858,-5.16509 -5.409717,-10.33018 -8.1145755,-15.49527 5.4097165,0 10.8194335,0 16.2291505,0 -2.704858,5.16509 -5.409717,10.33018 -8.114575,15.49527 z m -2.872601,-9.2049 c 1.915068,0 3.830135,0 5.745203,0 0.789791,-1.54464 1.579581,-3.08927 2.369372,-4.63391 -3.494649,0 -6.989298,0 -10.4839475,0 0.7897908,1.54464 1.5795817,3.08927 2.3693725,4.63391 z m 2.872601,5.61939 c 0.691941,-1.35592 1.383881,-2.71185 2.075822,-4.06777 -1.383881,0 -2.767762,0 -4.151643,0 0.69194,1.35592 1.383881,2.71185 2.075821,4.06777 z', - mercury: 'm 15.732407,279.01472 c 1.457188,0.91755 2.327793,2.6639 2.118676,4.38195 -0.130888,2.01739 -1.899178,3.73377 -3.900128,3.89053 0,0.71327 0,1.42654 0,2.13981 0.737153,-0.007 1.474306,-0.0132 2.211459,-0.0198 0,0.34126 0,0.68253 0,1.02379 -0.750804,0.003 -1.501607,0.007 -2.252411,0.01 0.0034,0.68938 0.0068,1.37875 0.01024,2.06813 -0.440244,0 -0.880489,0 -1.320733,0 -0.0034,-0.69279 -0.0068,-1.38558 -0.01024,-2.07837 -0.689374,0 -1.378747,0 -2.068121,0 0,-0.33444 0,-0.66888 0,-1.00332 0.689374,0 1.378747,0 2.068121,0 -0.0068,-0.71327 -0.01365,-1.42654 -0.02047,-2.13981 -2.15006,-0.13324 -4.015907,-2.07311 -3.9813417,-4.24183 -0.084702,-1.60894 0.7922593,-3.17204 2.1384617,-4.03065 -1.3321084,-0.8479 -2.2089181,-2.39019 -2.1397915,-3.98265 0.4846095,0 0.969219,0 1.4538285,0 -0.129447,1.801 1.552959,3.41833 3.338775,3.2762 1.681768,-0.0229 3.155505,-1.58904 3.039642,-3.2762 0.48461,0 0.969221,0 1.453831,0 0.06599,1.601 -0.803406,3.12723 -2.139798,3.98222 z m -2.303602,0.71667 c -1.758778,-0.16425 -3.4619579,1.37986 -3.388854,3.16361 -0.083477,1.71263 1.535734,3.14586 3.204567,3.07148 1.719365,0.0703 3.342464,-1.48587 3.170211,-3.23621 -0.02747,-1.57245 -1.423299,-2.948 -2.985924,-2.99888 z', - lead: 'm 9.5126894,279.50182 c -0.3617507,0 -0.7235013,0 -1.085252,0 0,-0.51532 0,-1.03065 0,-1.54597 0.3617507,0 0.7235013,0 1.085252,0 0,-0.6143 0,-1.2286 0,-1.8429 0.5187366,0 1.0374736,0 1.5562106,0 0,0.6143 0,1.2286 0,1.8429 0.607468,0 1.214936,0 1.822404,0 0,0.51532 0,1.03065 0,1.54597 -0.607468,0 -1.214936,0 -1.822404,0 0,1.32073 0,2.64145 0,3.96218 1.537514,-1.62806 4.417116,-1.42308 5.808997,0.28791 1.184192,1.31365 1.566419,3.41483 0.604294,4.96063 -0.519367,0.80942 -0.90468,1.73924 -0.884648,2.71614 -0.515324,0 -1.030647,0 -1.545971,0 -0.04659,-1.19876 0.527295,-2.27553 1.100009,-3.28428 0.92482,-1.59336 -0.125612,-3.92412 -1.965862,-4.19999 -1.635761,-0.31517 -3.124345,1.21479 -3.11374,2.80432 -0.0071,1.55992 -0.0011,3.11998 -0.0031,4.67995 -0.518737,0 -1.037474,0 -1.5562106,0 0,-3.97562 0,-7.95124 0,-11.92686 z', - tin: 'm 11.836769,285.99296 c 1.581694,0.0451 3.054161,-1.39068 2.989562,-2.99956 0.06903,-1.76826 -1.701886,-3.28981 -3.441524,-2.95861 -1.4247698,0.18442 -2.5932003,1.53599 -2.5376015,2.97841 -0.5119117,0 -1.0238233,0 -1.535735,0 -0.085695,-2.3801 2.0195976,-4.57675 4.4081085,-4.55471 2.311495,-0.14461 4.495576,1.78385 4.652567,4.08999 0.127445,1.23443 -0.250024,2.52397 -1.095332,3.44405 0.774693,0 1.549385,0 2.324078,0 0,-3.2933 0,-6.58661 0,-9.87991 0.515324,0 1.030647,0 1.545971,0 0,5.10547 0,10.21093 0,15.3164 -0.515324,0 -1.030647,0 -1.545971,0 0,-1.29684 0,-2.59368 0,-3.89052 -3.429807,0 -6.859614,0 -10.2894215,0 0,-0.51532 0,-1.03065 0,-1.54597 1.5084328,1.4e-4 3.0168655,2.9e-4 4.5252985,4.3e-4 z', - iron: 'm 11.237833,281.77434 c -2.1129725,-0.0765 -4.0685065,1.7834 -4.0429529,3.90895 -0.1246246,1.8905 1.3843212,3.64089 3.2341329,3.92585 1.98814,0.41339 4.242048,-0.80512 4.735939,-2.82798 0.42126,-1.59401 -0.247897,-3.38375 -1.626517,-4.29444 -0.666348,-0.47624 -1.483823,-0.71835 -2.300602,-0.71238 z m 4.218149,8.01654 c -2.219729,2.26802 -6.2673049,2.23092 -8.4717123,-0.0411 -2.1829952,-2.03247 -2.1495359,-5.81571 -0.085686,-7.91896 1.2047257,-1.38003 3.0857918,-2.10369 4.9042713,-1.84319 1.04112,0.0976 2.126139,0.37802 2.915975,1.10076 1.232001,-1.18763 2.464001,-2.37526 3.696002,-3.56289 -1.266128,0 -2.532257,0 -3.798385,0 0.477784,-0.48461 0.955568,-0.96921 1.433352,-1.45382 1.685895,0 3.371789,0 5.057684,0 0,1.67906 0,3.35812 0,5.03718 -0.488023,0.48803 -0.976045,0.97606 -1.464068,1.46409 0,-1.27296 0,-2.54591 0,-3.81887 -1.221761,1.18763 -2.443522,2.37526 -3.665283,3.56289 1.692564,2.15746 1.51732,5.5939 -0.52215,7.47392 z', - copper: 'm 13.229167,276.61943 c -2.112981,-0.0765 -4.0685271,1.78337 -4.04295,3.90894 -0.1277359,1.92476 1.447325,3.68809 3.331873,3.93182 2.025344,0.379 4.29148,-0.9459 4.680309,-3.02816 0.350755,-1.61444 -0.419466,-3.36035 -1.832442,-4.20966 -0.633357,-0.40672 -1.386458,-0.60708 -2.13679,-0.60294 z m -4.1567215,8.05748 c -2.3050816,-2.02524 -2.2795847,-5.96633 -0.1160579,-8.08364 2.0292494,-2.23826 5.8431584,-2.42533 8.0888084,-0.41068 1.604759,1.29217 2.357262,3.46519 1.988157,5.48009 -0.363367,2.37123 -2.511902,4.28305 -4.882744,4.48852 0,0.90097 0,1.80194 0,2.70291 0.928266,0 1.856531,0 2.784797,0 0,0.42318 0,0.84635 0,1.26953 -0.928266,0 -1.856531,0 -2.784797,0 0,0.87708 0,1.75415 0,2.63123 -0.614294,0 -1.228589,0 -1.842883,0 0,-0.87708 0,-1.75415 0,-2.63123 -0.928266,0 -1.856532,0 -2.7847978,0 0,-0.42318 0,-0.84635 0,-1.26953 0.9282658,0 1.8565318,0 2.7847978,0 0,-0.90438 0,-1.80876 0,-2.71314 -1.207048,-0.10173 -2.3584982,-0.63012 -3.2352805,-1.46406 z', - silver: 'm 9.9755063,275.73179 c -0.6014253,0.18082 -1.8508344,-0.12736 -1.9078968,0.74722 0.1176844,0.4442 0.7067667,0.30018 1.0195284,0.4996 2.4102181,0.84689 4.3760261,3.03219 4.6611691,5.61268 0.329015,2.2322 -0.292857,4.61262 -1.845188,6.27899 -0.950816,1.0625 -2.3118271,1.60982 -3.6314341,2.04088 -0.3010461,0.22955 -0.1186419,0.78 0.2735733,0.75652 2.3443258,0.46012 4.8961488,-0.14011 6.7090618,-1.72152 1.916707,-1.49245 3.236515,-3.83656 3.14282,-6.30499 -0.01445,-1.96756 -0.864897,-3.88727 -2.285751,-5.24448 -1.577433,-1.67996 -3.811286,-2.7316 -6.1358827,-2.6649 z m 2.7602927,2.03153 c 1.072404,0.39734 1.870102,1.27809 2.623097,2.10375 1.308542,1.47534 1.847166,3.59612 1.292761,5.50102 -0.534285,1.8245 -1.854209,3.45004 -3.574828,4.26864 -0.191682,0.0234 0.647922,-0.682 0.817217,-1.00958 1.833102,-2.3961 2.027928,-5.83879 0.732872,-8.5234 -0.452679,-0.90448 -1.108696,-1.7015 -1.891119,-2.34043 z', - gold: 'm 13.219492,275.7189 c -2.986621,0.01 -5.9729442,1.75519 -7.2157492,4.51399 -1.4447671,2.86838 -0.9743467,6.60206 1.2502049,8.94534 1.6486642,1.87365 4.2016723,2.85934 6.6817843,2.61607 3.746213,-0.22926 7.026658,-3.46775 7.307955,-7.20938 0.345627,-3.27431 -1.477563,-6.74322 -4.539965,-8.07025 -1.086444,-0.51129 -2.281439,-0.79866 -3.48423,-0.79577 z m 0,1.55427 c 2.485623,0.002 4.974789,1.48839 5.934079,3.82276 1.125698,2.44571 0.57943,5.58664 -1.449826,7.40023 -2.35696,2.37064 -6.611547,2.37064 -8.9685065,0 -2.1169053,-1.88824 -2.608931,-5.21179 -1.3089579,-7.69696 1.0280707,-2.17935 3.4218714,-3.51551 5.7932114,-3.52603 z m 0.0066,4.19203 c -1.370356,-0.0494 -2.523547,1.30386 -2.277873,2.64791 0.150011,1.3618 1.658854,2.29772 2.951679,1.86852 1.391568,-0.36367 2.080533,-2.14843 1.33192,-3.36846 -0.389128,-0.716 -1.20151,-1.14581 -2.005726,-1.14797 z', - mors: 'm 14.482436,292.72274 -0.358077,-0.35808 v -2.86461 h 2.864614 v -1.79038 h -2.864614 v -3.93885 h 5.371152 l -6.445382,-8.95192 -6.0873063,8.95192 h 5.3711513 v 3.93885 H 9.4693607 v 1.79038 h 2.8646133 v 2.86461 l -0.358076,0.35808 z M 16.27282,282.33852 H 9.8274367 l 3.2226923,-4.65501 z', - vitae: 'm 11.975898,274.8189 0.358077,0.35808 v 2.86461 H 9.4693607 v 1.79038 h 2.8646143 v 3.93885 H 6.9628227 l 6.4453823,8.95192 6.087306,-8.95192 H 14.12436 v -3.93885 h 2.864613 v -1.79038 H 14.12436 v -2.86461 l 0.358076,-0.35808 z m -1.790384,10.38422 h 6.445383 l -3.222692,4.65501 z', - }; - - this.orbColors = { - salt: '#f2e7b4', - air: '#9ee0ff', - fire: '#ff540b', - water: '#0fdac3', - earth: '#99ff11', - - // TODO metals need color adjust + ideally should have different visuals - mercury: '#f2e7b4', - lead: '#728686', - tin: '#c5be9b', - iron: '#b59e97', - copper: '#f5a581', - silver: '#cfcac3', - gold: '#ffba50', - - mors: '#433c29', - vitae: '#F5A79E', - }; - - this.symbolTpls = {}; - - for (let s in symbolPaths) { - if (symbolPaths.hasOwnProperty(s)) { - let path = symbolPaths[s]; - this.symbolTpls[s] = Svg.fromXMLg(` + this.orbColors = { + salt: '#f2e7b4', + air: '#9ee0ff', + fire: '#ff540b', + water: '#0fdac3', + earth: '#99ff11', + + mercury: '#f2e7b4', + lead: '#728686', + tin: '#c5be9b', + iron: '#b59e97', + copper: '#f5a581', + silver: '#cfcac3', + gold: '#ffba50', + + mors: '#433c29', + vitae: '#F5A79E', + }; + + this.symbolTpls = {}; + + for (let s in symbolPaths) { + if (symbolPaths.hasOwnProperty(s)) { + let path = symbolPaths[s]; + this.symbolTpls[s] = Svg.fromXMLg(` `); - } } } + } - /** - * Remove all boards on the board - */ - removeAllOrbs() { - Object.keys(this.grid).forEach((n) => { - this.removeOrbByIndex(n); - }) - } + /** + * Remove all boards on the board + */ + removeAllOrbs() { + Object.keys(this.grid).forEach((n) => { + this.removeOrbByIndex(+n); + }) + } - /** - * Get orb by array index - * - * @param {Number} n - * @returns {object} grid object - */ - getOrbByIndex(n) { - return this.grid[n]; + /** + * Get orb by array index + * + * @param {Number} n + * @returns {object} grid object + */ + getOrbByIndex(n) { + return this.grid[n]; + } +} + +/** + * Random number generator + * + * Uses Mullbery32 from https://stackoverflow.com/a/47593316/2180189 + */ +class Rng { + /** + * Construct with a given or random seed + * + * @param {Number|null} seed + */ + constructor(seed = null) { + this.seed = null; + + if (seed === null) { + seed = +new Date; } + + this.setSeed(+seed); } /** - * Random number generator + * Set seed for following rolls * - * Uses Mullbery32 from https://stackoverflow.com/a/47593316/2180189 + * @param {Number} seed */ - class Rng { - /** - * Construct with a given or random seed - * - * @param {Number|null} seed - */ - constructor(seed = null) { - if (seed === null) { - seed = +new Date; + setSeed(seed) { + seed = +seed; + this.seed = seed; + this.state = seed; + } + + /** + * Get a pseudo-random number + * + * @returns {Number} + */ + next() { + let t = this.state += 0x6D2B79F5; + t = Math.imul(t ^ t >>> 15, t | 1); + t ^= t + Math.imul(t ^ t >>> 7, t | 61); + return ((t ^ t >>> 14) >>> 0) / 4294967296; + } + + /** + * Get next int, inclusive + * + * @param {Number} max + * @return {Number} + */ + nextInt(max) { + return Math.floor((max + 1) * this.next()); + } +} + +class SettingsStorage { + constructor() { + // this object is never overwritten, references are stable + this.defaults = { + log: 'info', + version: 1, + allowTemplateAugmenting: false, + retryTemplate: 30, + attemptTemplates: 50, + svgAnimations: false, + svgBlur: false, + disabledEffect: true, + }; + this.settings = Object.assign({}, this.defaults); + } + + load() { + let saved = localStorage.getItem('sigmar_settings'); + if (saved) { + let parsed; + try { + parsed = JSON.parse(saved); + + // XXX some validation / version conversion could be done here + + delete parsed.version; + + Object.assign(this.settings, parsed); + } catch (e) { + console.error("Error loading settings:", e); } - this.seed(seed); } - /** - * Set seed for following rolls - * - * @param {Number} seed - */ - seed(seed) { - this.state = seed; - } + return this.settings; + } - /** - * Get a pseudo-random number - * - * @returns {Number} - */ - next() { - let t = this.state += 0x6D2B79F5; - t = Math.imul(t ^ t >>> 15, t | 1); - t ^= t + Math.imul(t ^ t >>> 7, t | 61); - return ((t ^ t >>> 14) >>> 0) / 4294967296; - } + update(update) { + Object.assign(this.settings, update); + return this.settings; + } + + save() { + let changed = Object.entries(this.settings).reduce((acu, [k, v]) => { + if (this.defaults[k] !== v) { + acu[k] = v; + } + return acu; + }, {}); + + localStorage.setItem('sigmar_settings', JSON.stringify(changed)); + } +} - /** - * Get next int, inclusive - * - * @param {Number} max - * @return {Number} - */ - nextInt(max) { - return Math.floor((max + 1) * this.next()); +class Nav { + /** + * Replace URL in the address bar + * @param new_url + */ + static replaceUrl(new_url) { + history.replaceState(null, null, new_url); + } + + /** + * Set URL args (GET). + * + * @param args - arguments to set, or delete (when the value is null) + */ + static setUrlArgs(args) { + let url = new URL(location.href); + let query = new URLSearchParams(url.search); + for (let [k, v] of Object.entries(args)) { + if (v === null) { + query.delete(k); + } else { + query.set(k, v); + } } + url.search = query.toString(); + history.replaceState(null, null, url.href); } - class SettingsStorage { - constructor() { - // this object is never overwritten, references are stable - this.settings = { - debug: false, - version: 1, - allowTemplateAugmenting: true, - retryTemplate: 30, - attemptTemplates: 50, - animations: true, - disabledEffect: true, - }; + /** + * Get URL arguments + * + * @return {object} + */ + static getUrlArgs() { + let url = new URL(location.href); + let query = new URLSearchParams(url.search); + let params = {}; + for (const [key, value] of query) { + params[key] = value; } + return params; + } +} - load() { - let saved = localStorage.getItem('sigmar_settings'); - if (saved) { - let parsed; - try { - parsed = JSON.parse(saved); +class Game { + /** + * Init the game + */ + constructor() { + this.LOGLEVELS = ['error', 'warn', 'info', 'debug', 'trace']; - // XXX some validation / version conversion could be done here + this.settingsStore = new SettingsStorage(); + this.cfg = this.settingsStore.load(); + this.applyLogFilter(); - delete parsed.version; + this.get_opts = { + url_seed: true, + template: null, + template_flip: null, + }; - Object.assign(this.settings, parsed); - } catch (e) { - console.error("Error loading settings:", e); - } + let args = Object.assign({ + seed: null, + debug: null, + trace: null, + log: null, + pretty: null, + rnd: null, + template: null, + }, Nav.getUrlArgs()); + + this.board = new Board(); + this.rng = new Rng(); + + // Debug can be toggled via the debug=0/1 GET arg + if (args.debug !== null) { + this.setCfg({log: (!!+args.debug) ? 'debug' : 'info'}); + } + if (args.trace !== null) { + this.setCfg({log: (!!+args.trace) ? 'trace' : 'debug'}); + } + if (args.log !== null) { + this.setCfg({log: args.log}); + } + if (args.rnd !== null) { + this.get_opts.url_seed = !!!+args.rnd; + } + if (args.template !== null) { + let tpl = args.template; + this.get_opts.template = tpl; + this.get_opts.template_flip = false; + if (tpl.endsWith('_flip')) { + this.get_opts.template_flip = true; + this.get_opts.template = tpl.substring(0, tpl.length - '_flip'.length); } - - return this.settings; } - update(update) { - Object.assign(this.settings, update); - return this.settings; + // Toggle GPU intensive effects via the pretty=0/1 GET arg + if (args.pretty !== null) { + let pretty = !!+args.pretty; + this.setCfg({ + svgAnimations: pretty, + svgBlur: pretty, + }); } - save() { - localStorage.setItem('sigmar_settings', JSON.stringify(this.settings)); + this.info("Game settings:", this.cfg); + + this.layoutTemplates = { + // templates apparently all have 55 items + + //'wheel': [0,1,2,3,4,5,11,12,13,14,15,16,17,22,23,24,27,28,29,33,34,36,38,40,41,44,45,48,49,52,53,55,56,57,58,59,60,61,62,63,64,65,67,68,71,72,75,76,79,80,82,84,86,87,91,92,93,96,97,98,103,104,105,106,107,108,109,115,116,117,118,119,120], + 'beyblade': [0, 1, 2, 3, 4, 5, 12, 14, 15, 23, 26, 34, 35, 37, 38, 39, 40, 46, 47, 48, 50, 51, 52, 55, 58, 60, 61, 64, 65, 67, 68, 69, 70, 71, 73, 76, 79, 80, 83, 84, 85, 86, 87, 91, 94, 95, 97, 98, 103, 104, 105, 106, 109, 115, 120], + 'tulip': [4, 14, 15, 16, 23, 24, 25, 26, 27, 28, 34, 35, 36, 37, 39, 40, 45, 46, 47, 48, 49, 50, 51, 52, 56, 57, 59, 60, 61, 62, 63, 67, 68, 69, 70, 71, 72, 73, 74, 80, 81, 82, 83, 85, 86, 93, 94, 95, 96, 97, 105, 106, 107, 108, 109], + 'alien': [3, 4, 14, 15, 16, 22, 25, 26, 27, 28, 34, 35, 36, 37, 38, 39, 40, 41, 45, 46, 48, 49, 51, 56, 57, 58, 59, 60, 61, 62, 67, 68, 69, 70, 71, 72, 73, 74, 79, 80, 81, 82, 84, 85, 86, 94, 95, 96, 97, 98, 106, 107, 108, 109, 117], + 'cube': [1, 5, 12, 13, 14, 15, 16, 17, 23, 27, 29, 33, 34, 35, 36, 37, 38, 40, 44, 48, 49, 51, 52, 55, 56, 57, 58, 59, 60, 61, 62, 64, 68, 70, 71, 72, 73, 75, 76, 80, 82, 84, 86, 92, 94, 96, 97, 103, 104, 105, 106, 108, 118, 119, 120], + 'star': [3, 14, 15, 22, 23, 24, 25, 26, 27, 34, 35, 36, 37, 38, 39, 40, 41, 46, 47, 48, 49, 50, 51, 52, 57, 58, 59, 60, 61, 62, 63, 68, 69, 70, 71, 72, 73, 74, 79, 80, 81, 82, 83, 84, 85, 86, 93, 94, 95, 96, 97, 98, 105, 106, 117], + 'flower': [3, 11, 12, 13, 14, 15, 23, 25, 27, 28, 34, 36, 37, 38, 39, 40, 45, 46, 47, 48, 49, 50, 52, 53, 57, 58, 59, 60, 61, 62, 64, 68, 70, 71, 72, 73, 74, 75, 79, 80, 81, 82, 83, 84, 86, 92, 95, 96, 97, 98, 104, 105, 106, 107, 116], + 'windmill': [4, 11, 12, 13, 14, 15, 16, 23, 24, 25, 27, 28, 34, 37, 39, 40, 45, 46, 47, 48, 49, 50, 52, 53, 56, 57, 59, 60, 61, 63, 64, 67, 68, 70, 71, 72, 73, 74, 75, 80, 81, 83, 86, 92, 93, 95, 96, 97, 104, 105, 106, 107, 108, 109, 116], + 'propeller': [1, 2, 3, 4, 13, 14, 15, 16, 22, 25, 28, 34, 36, 37, 38, 39, 40, 41, 45, 46, 47, 48, 50, 56, 58, 60, 61, 62, 67, 68, 70, 71, 73, 74, 75, 76, 79, 80, 81, 82, 83, 84, 86, 87, 91, 92, 95, 97, 98, 103, 106, 107, 108, 109, 117], + 'garden': [0, 1, 2, 3, 4, 5, 11, 12, 13, 14, 15, 16, 17, 22, 23, 28, 29, 33, 34, 40, 41, 44, 45, 52, 53, 55, 56, 60, 64, 65, 67, 68, 75, 76, 79, 80, 86, 87, 91, 92, 97, 98, 103, 104, 105, 106, 107, 108, 109, 115, 116, 117, 118, 119, 120], + 'windmill2': [1, 12, 13, 14, 15, 16, 17, 23, 24, 26, 27, 28, 34, 35, 36, 37, 38, 40, 44, 45, 47, 50, 51, 52, 56, 57, 58, 60, 62, 63, 64, 68, 69, 70, 73, 75, 76, 80, 82, 83, 84, 85, 86, 92, 93, 94, 96, 97, 103, 104, 105, 106, 107, 108, 119], + 'bird': [2, 3, 4, 14, 15, 25, 27, 28, 29, 33, 34, 35, 36, 37, 38, 39, 40, 45, 46, 47, 48, 49, 50, 51, 57, 58, 59, 60, 61, 62, 67, 68, 70, 71, 72, 73, 74, 79, 80, 81, 82, 83, 84, 86, 87, 91, 94, 95, 96, 97, 98, 106, 107, 109, 118], + 'strider': [1, 2, 3, 4, 11, 12, 13, 14, 15, 24, 25, 26, 36, 37, 38, 47, 48, 49, 50, 53, 58, 59, 60, 61, 62, 63, 64, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 79, 80, 81, 82, 83, 84, 85, 86, 87, 91, 92, 93, 97, 98, 103, 104, 109, 116], + 'campfire': [0, 1, 2, 3, 4, 5, 11, 15, 17, 22, 23, 24, 25, 26, 27, 29, 33, 35, 39, 41, 44, 46, 51, 52, 53, 55, 57, 60, 63, 65, 67, 68, 69, 74, 76, 79, 81, 85, 87, 91, 93, 94, 95, 96, 97, 98, 103, 105, 109, 115, 116, 117, 118, 119, 120], + 'skillet': [0, 1, 2, 5, 11, 13, 17, 22, 25, 26, 27, 28, 29, 33, 37, 39, 41, 44, 45, 46, 47, 48, 49, 50, 53, 55, 57, 59, 60, 61, 65, 69, 70, 71, 72, 73, 74, 75, 76, 81, 83, 85, 87, 91, 92, 95, 96, 103, 107, 115, 116, 117, 118, 119, 120], + 'digger': [2, 3, 4, 5, 16, 17, 22, 27, 28, 29, 33, 34, 35, 36, 37, 38, 39, 40, 41, 44, 45, 46, 47, 48, 50, 51, 55, 56, 57, 58, 60, 61, 62, 67, 70, 71, 73, 79, 82, 83, 84, 87, 91, 94, 95, 96, 98, 106, 107, 108, 109, 117, 118, 119, 120], + 'chestnut': [3, 4, 12, 13, 14, 15, 16, 23, 25, 27, 28, 34, 36, 38, 39, 40, 45, 46, 47, 48, 49, 50, 52, 56, 57, 58, 59, 60, 61, 62, 64, 67, 68, 71, 72, 74, 75, 79, 80, 81, 82, 83, 84, 86, 92, 95, 96, 97, 98, 104, 105, 106, 107, 108, 109], + 'manta': [0, 5, 11, 16, 22, 23, 24, 25, 26, 27, 28, 29, 33, 34, 35, 39, 40, 44, 45, 46, 47, 51, 52, 56, 57, 58, 59, 60, 61, 62, 63, 64, 68, 69, 73, 74, 75, 76, 80, 81, 85, 86, 87, 91, 92, 93, 94, 95, 96, 97, 98, 104, 109, 115, 120], + 'pyramids': [3, 4, 14, 15, 16, 23, 25, 26, 28, 34, 35, 36, 37, 38, 39, 40, 45, 46, 47, 48, 49, 50, 51, 52, 56, 58, 59, 60, 61, 62, 67, 68, 69, 70, 71, 72, 73, 74, 79, 80, 81, 82, 83, 84, 85, 86, 94, 95, 97, 98, 105, 106, 107, 108, 109], + 'bigwheel': [0, 1, 2, 3, 4, 5, 11, 13, 17, 22, 25, 28, 29, 33, 36, 37, 38, 39, 41, 44, 45, 46, 47, 50, 53, 55, 58, 60, 62, 65, 67, 70, 73, 74, 75, 76, 79, 81, 82, 83, 84, 87, 91, 92, 95, 98, 103, 107, 109, 115, 116, 117, 118, 119, 120], + 'handshake': [0, 1, 2, 3, 4, 5, 11, 12, 13, 15, 16, 22, 23, 24, 25, 27, 33, 34, 35, 36, 38, 46, 48, 49, 50, 58, 59, 60, 61, 62, 70, 71, 72, 74, 82, 84, 85, 86, 87, 93, 95, 96, 97, 98, 104, 105, 107, 108, 109, 115, 116, 117, 118, 119, 120], + 'thinwheel': [0, 1, 2, 3, 4, 5, 11, 12, 16, 17, 22, 24, 27, 29, 33, 36, 38, 41, 44, 48, 49, 53, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 67, 71, 72, 76, 79, 82, 84, 87, 91, 93, 96, 98, 103, 104, 108, 109, 115, 116, 117, 118, 119, 120], + 'heavywheel': [12, 13, 14, 15, 16, 23, 24, 25, 27, 28, 34, 36, 37, 38, 39, 40, 45, 46, 47, 48, 49, 50, 52, 56, 57, 58, 59, 60, 61, 62, 63, 64, 68, 70, 71, 72, 73, 74, 75, 80, 81, 82, 83, 84, 86, 92, 93, 95, 96, 97, 104, 105, 106, 107, 108], + 'virus': [2, 3, 13, 14, 15, 22, 24, 26, 27, 34, 35, 36, 37, 38, 39, 40, 41, 46, 47, 48, 49, 50, 51, 57, 58, 59, 60, 61, 62, 63, 68, 69, 70, 71, 72, 73, 75, 79, 80, 82, 83, 84, 85, 86, 87, 91, 92, 93, 94, 95, 96, 97, 98, 106, 117], + 'frisbee': [0, 11, 12, 13, 14, 15, 22, 25, 26, 27, 28, 29, 33, 34, 36, 38, 39, 40, 41, 45, 46, 47, 49, 50, 53, 57, 58, 59, 60, 62, 64, 65, 68, 69, 72, 74, 75, 80, 81, 82, 83, 84, 85, 86, 92, 95, 96, 97, 104, 106, 107, 115, 116, 117, 118], + }; + + this.applySettings(); + + // Defer start to give browser time to render the background + setTimeout(() => { + this.newGame(args.seed) + }, 50); + } + + applyLogFilter() { + let index = this.LOGLEVELS.indexOf(this.cfg.log); + for (let level of this.LOGLEVELS) { + this['logging_' + level] = index >= this.LOGLEVELS.indexOf(level); } } - class Game { - /** - * Init the game - */ - constructor(seed = null) { - this.settingsStore = new SettingsStorage(); - this.cfg = this.settingsStore.load(); - this.debug("Game settings:", this.cfg); + trace(...args) { + if (this.logging_trace) console.debug(...args); + } - // TODO take seed from hash + debug(...args) { + if (this.logging_debug) console.log(...args); + } - this.board = new Board(); - if (seed === null) { - seed = +new Date(); - } + info(...args) { + if (this.logging_info) console.info(...args); + } - this.rng = new Rng(seed); - - this.layoutTemplates = { - // templates apparently all have 55 items - - //'wheel': [0,1,2,3,4,5,11,12,13,14,15,16,17,22,23,24,27,28,29,33,34,36,38,40,41,44,45,48,49,52,53,55,56,57,58,59,60,61,62,63,64,65,67,68,71,72,75,76,79,80,82,84,86,87,91,92,93,96,97,98,103,104,105,106,107,108,109,115,116,117,118,119,120], - 'beyblade': [0, 1, 2, 3, 4, 5, 12, 14, 15, 23, 26, 34, 35, 37, 38, 39, 40, 46, 47, 48, 50, 51, 52, 55, 58, 60, 61, 64, 65, 67, 68, 69, 70, 71, 73, 76, 79, 80, 83, 84, 85, 86, 87, 91, 94, 95, 97, 98, 103, 104, 105, 106, 109, 115, 120], - 'tulip': [4, 14, 15, 16, 23, 24, 25, 26, 27, 28, 34, 35, 36, 37, 39, 40, 45, 46, 47, 48, 49, 50, 51, 52, 56, 57, 59, 60, 61, 62, 63, 67, 68, 69, 70, 71, 72, 73, 74, 80, 81, 82, 83, 85, 86, 93, 94, 95, 96, 97, 105, 106, 107, 108, 109], - 'alien': [3, 4, 14, 15, 16, 22, 25, 26, 27, 28, 34, 35, 36, 37, 38, 39, 40, 41, 45, 46, 48, 49, 51, 56, 57, 58, 59, 60, 61, 62, 67, 68, 69, 70, 71, 72, 73, 74, 79, 80, 81, 82, 84, 85, 86, 94, 95, 96, 97, 98, 106, 107, 108, 109, 117], - 'cube': [1, 5, 12, 13, 14, 15, 16, 17, 23, 27, 29, 33, 34, 35, 36, 37, 38, 40, 44, 48, 49, 51, 52, 55, 56, 57, 58, 59, 60, 61, 62, 64, 68, 70, 71, 72, 73, 75, 76, 80, 82, 84, 86, 92, 94, 96, 97, 103, 104, 105, 106, 108, 118, 119, 120], - 'star': [3, 14, 15, 22, 23, 24, 25, 26, 27, 34, 35, 36, 37, 38, 39, 40, 41, 46, 47, 48, 49, 50, 51, 52, 57, 58, 59, 60, 61, 62, 63, 68, 69, 70, 71, 72, 73, 74, 79, 80, 81, 82, 83, 84, 85, 86, 93, 94, 95, 96, 97, 98, 105, 106, 117], - 'flower': [3, 11, 12, 13, 14, 15, 23, 25, 27, 28, 34, 36, 37, 38, 39, 40, 45, 46, 47, 48, 49, 50, 52, 53, 57, 58, 59, 60, 61, 62, 64, 68, 70, 71, 72, 73, 74, 75, 79, 80, 81, 82, 83, 84, 86, 92, 95, 96, 97, 98, 104, 105, 106, 107, 116], - 'windmill': [4, 11, 12, 13, 14, 15, 16, 23, 24, 25, 27, 28, 34, 37, 39, 40, 45, 46, 47, 48, 49, 50, 52, 53, 56, 57, 59, 60, 61, 63, 64, 67, 68, 70, 71, 72, 73, 74, 75, 80, 81, 83, 86, 92, 93, 95, 96, 97, 104, 105, 106, 107, 108, 109, 116], - 'propeller': [1, 2, 3, 4, 13, 14, 15, 16, 22, 25, 28, 34, 36, 37, 38, 39, 40, 41, 45, 46, 47, 48, 50, 56, 58, 60, 61, 62, 67, 68, 70, 71, 73, 74, 75, 76, 79, 80, 81, 82, 83, 84, 86, 87, 91, 92, 95, 97, 98, 103, 106, 107, 108, 109, 117], - 'garden': [0, 1, 2, 3, 4, 5, 11, 12, 13, 14, 15, 16, 17, 22, 23, 28, 29, 33, 34, 40, 41, 44, 45, 52, 53, 55, 56, 60, 64, 65, 67, 68, 75, 76, 79, 80, 86, 87, 91, 92, 97, 98, 103, 104, 105, 106, 107, 108, 109, 115, 116, 117, 118, 119, 120], - 'windmill2': [1, 12, 13, 14, 15, 16, 17, 23, 24, 26, 27, 28, 34, 35, 36, 37, 38, 40, 44, 45, 47, 50, 51, 52, 56, 57, 58, 60, 62, 63, 64, 68, 69, 70, 73, 75, 76, 80, 82, 83, 84, 85, 86, 92, 93, 94, 96, 97, 103, 104, 105, 106, 107, 108, 119], - 'bird': [2, 3, 4, 14, 15, 25, 27, 28, 29, 33, 34, 35, 36, 37, 38, 39, 40, 45, 46, 47, 48, 49, 50, 51, 57, 58, 59, 60, 61, 62, 67, 68, 70, 71, 72, 73, 74, 79, 80, 81, 82, 83, 84, 86, 87, 91, 94, 95, 96, 97, 98, 106, 107, 109, 118], - 'strider': [1, 2, 3, 4, 11, 12, 13, 14, 15, 24, 25, 26, 36, 37, 38, 47, 48, 49, 50, 53, 58, 59, 60, 61, 62, 63, 64, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 79, 80, 81, 82, 83, 84, 85, 86, 87, 91, 92, 93, 97, 98, 103, 104, 109, 116], - 'campfire': [0, 1, 2, 3, 4, 5, 11, 15, 17, 22, 23, 24, 25, 26, 27, 29, 33, 35, 39, 41, 44, 46, 51, 52, 53, 55, 57, 60, 63, 65, 67, 68, 69, 74, 76, 79, 81, 85, 87, 91, 93, 94, 95, 96, 97, 98, 103, 105, 109, 115, 116, 117, 118, 119, 120], - 'skillet': [0, 1, 2, 5, 11, 13, 17, 22, 25, 26, 27, 28, 29, 33, 37, 39, 41, 44, 45, 46, 47, 48, 49, 50, 53, 55, 57, 59, 60, 61, 65, 69, 70, 71, 72, 73, 74, 75, 76, 81, 83, 85, 87, 91, 92, 95, 96, 103, 107, 115, 116, 117, 118, 119, 120], - 'digger': [2, 3, 4, 5, 16, 17, 22, 27, 28, 29, 33, 34, 35, 36, 37, 38, 39, 40, 41, 44, 45, 46, 47, 48, 50, 51, 55, 56, 57, 58, 60, 61, 62, 67, 70, 71, 73, 79, 82, 83, 84, 87, 91, 94, 95, 96, 98, 106, 107, 108, 109, 117, 118, 119, 120], - 'chestnut': [3, 4, 12, 13, 14, 15, 16, 23, 25, 27, 28, 34, 36, 38, 39, 40, 45, 46, 47, 48, 49, 50, 52, 56, 57, 58, 59, 60, 61, 62, 64, 67, 68, 71, 72, 74, 75, 79, 80, 81, 82, 83, 84, 86, 92, 95, 96, 97, 98, 104, 105, 106, 107, 108, 109], - 'manta': [0, 5, 11, 16, 22, 23, 24, 25, 26, 27, 28, 29, 33, 34, 35, 39, 40, 44, 45, 46, 47, 51, 52, 56, 57, 58, 59, 60, 61, 62, 63, 64, 68, 69, 73, 74, 75, 76, 80, 81, 85, 86, 87, 91, 92, 93, 94, 95, 96, 97, 98, 104, 109, 115, 120], - 'pyramids': [3, 4, 14, 15, 16, 23, 25, 26, 28, 34, 35, 36, 37, 38, 39, 40, 45, 46, 47, 48, 49, 50, 51, 52, 56, 58, 59, 60, 61, 62, 67, 68, 69, 70, 71, 72, 73, 74, 79, 80, 81, 82, 83, 84, 85, 86, 94, 95, 97, 98, 105, 106, 107, 108, 109], - 'bigwheel': [0, 1, 2, 3, 4, 5, 11, 13, 17, 22, 25, 28, 29, 33, 36, 37, 38, 39, 41, 44, 45, 46, 47, 50, 53, 55, 58, 60, 62, 65, 67, 70, 73, 74, 75, 76, 79, 81, 82, 83, 84, 87, 91, 92, 95, 98, 103, 107, 109, 115, 116, 117, 118, 119, 120], - 'handshake': [0, 1, 2, 3, 4, 5, 11, 12, 13, 15, 16, 22, 23, 24, 25, 27, 33, 34, 35, 36, 38, 46, 48, 49, 50, 58, 59, 60, 61, 62, 70, 71, 72, 74, 82, 84, 85, 86, 87, 93, 95, 96, 97, 98, 104, 105, 107, 108, 109, 115, 116, 117, 118, 119, 120], - 'thinwheel': [0, 1, 2, 3, 4, 5, 11, 12, 16, 17, 22, 24, 27, 29, 33, 36, 38, 41, 44, 48, 49, 53, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 67, 71, 72, 76, 79, 82, 84, 87, 91, 93, 96, 98, 103, 104, 108, 109, 115, 116, 117, 118, 119, 120], - 'heavywheel': [12, 13, 14, 15, 16, 23, 24, 25, 27, 28, 34, 36, 37, 38, 39, 40, 45, 46, 47, 48, 49, 50, 52, 56, 57, 58, 59, 60, 61, 62, 63, 64, 68, 70, 71, 72, 73, 74, 75, 80, 81, 82, 83, 84, 86, 92, 93, 95, 96, 97, 104, 105, 106, 107, 108], - 'virus': [2, 3, 13, 14, 15, 22, 24, 26, 27, 34, 35, 36, 37, 38, 39, 40, 41, 46, 47, 48, 49, 50, 51, 57, 58, 59, 60, 61, 62, 63, 68, 69, 70, 71, 72, 73, 75, 79, 80, 82, 83, 84, 85, 86, 87, 91, 92, 93, 94, 95, 96, 97, 98, 106, 117], - 'frisbee': [0, 11, 12, 13, 14, 15, 22, 25, 26, 27, 28, 29, 33, 34, 36, 38, 39, 40, 41, 45, 46, 47, 49, 50, 53, 57, 58, 59, 60, 62, 64, 65, 68, 69, 72, 74, 75, 80, 81, 82, 83, 84, 85, 86, 92, 95, 96, 97, 104, 106, 107, 115, 116, 117, 118], - }; - - this.applySettings(); - - setTimeout(() => this.newGame(), 100); - } + warn(...args) { + if (this.logging_warn) console.warn(...args); + } - debug(...args) { - if (this.cfg.debug) console.log(...args); - } + error(...args) { + if (this.logging_error) console.error(...args); + } - info(...args) { - console.info(...args); - } + applySettings() { + this.board.$svg.classList.toggle('cfg-no-anim', !this.cfg.svgAnimations); + this.board.$svg.classList.toggle('cfg-no-fade-disabled', !this.cfg.disabledEffect); + this.board.$svg.classList.toggle('cfg-no-blur', !this.cfg.svgBlur); + this.applyLogFilter(); + } - warn(...args) { - console.warn(...args); - } + setCfg(update) { + this.settingsStore.update(update); + this.applySettings(); + this.settingsStore.save(); + } - error(...args) { - console.error(...args); - } + getCfg(key) { + return this.cfg[key]; + } - applySettings() { - this.board.$svg.classList.toggle('cfg-no-anim', !this.cfg.animations); - this.board.$svg.classList.toggle('cfg-no-fade-disabled', !this.cfg.disabledEffect); - } + /** + * Show a selected template, for debug + * + * @param {String} name - template name + * @param {boolean} flip - flip horizontally + */ + showTemplate(name, flip = false) { + this.board.removeAllOrbs(); - setCfg(update) { - this.settingsStore.update(update); - this.applySettings(); - this.settingsStore.save(); + for (let n of this.getTemplate(name, flip).positions) { + this.board.placeOrbByIndex(n, 'lead'); } + } - getCfg(key) { - return this.cfg[key]; + /** + * Show a random template, for debug + */ + showRandomTemplate() { + this.board.removeAllOrbs(); + for (let n of this.getRandomTemplate().positions) { + this.board.placeOrbByIndex(n, 'lead'); } + } - /** - * Show a selected template, for debug - * - * @param {String} name - template name - * @param {boolean} flip - flip horizontally - */ - showTemplate(name, flip = false) { - this.board.removeAllOrbs(); - - this.getTemplate(name, flip).forEach((n) => { - this.board.placeOrbByIndex(n, 'lead'); - }) - } + /** + * Get a template - a sequence of numbers that are allowed as orb positions + * + * @param {String} name + * @param {boolean} flipped + * @returns {{name: String, flipped: Boolean, positions: Number[]}} + */ + getTemplate(name, flipped) { + let positions = this.layoutTemplates[name].slice(0); // this slice takes a copy so the array is not corrupted by later manipulations - /** - * Show a random template, for debug - */ - showRandomTemplate() { - this.board.removeAllOrbs(); - this.getRandomTemplate().forEach((n) => { - this.board.placeOrbByIndex(n, 'lead'); - }) + if (flipped) { + positions = this.flipTemplate(positions); } - /** - * Get a template - a sequence of numbers that are allowed as orb positions - * - * @param {String} name - * @param {boolean} flipped - * @returns {number[]} - */ - getTemplate(name, flipped) { - let tpl = this.layoutTemplates[name].slice(0); // this slice takes a copy so the array is not corrupted by later manipulations - - if (flipped) { - tpl = this.flipTemplate(tpl); - } + return { + basename: name, + name: name+(flipped?'_flip':''), + flipped, + positions, + }; + } - return tpl; + /** + * Get a random and randomly flipped template + * + * @returns {{name: String, flipped: Boolean, positions: Number[]}} + */ + getRandomTemplate() { + let names = Object.keys(this.layoutTemplates); + let name = names[Math.floor(this.rng.next() * names.length)]; + let flipped = this.rng.next() > 0.5; + let tpl = this.getTemplate(name, flipped); + + // 60 (center) must be included to place gold + if (!tpl.positions.includes(60)) { + throw Error(`Template "${name}", flip=${+flipped}, lacks 60.`); } - /** - * Get a random and randomly flipped template - * - * @return Number[] - template indices; this is a clone, free to modify - */ - getRandomTemplate() { - let names = Object.keys(this.layoutTemplates); - let name = names[Math.floor(this.rng.next() * names.length)]; - let flipped = this.rng.next() > 0.5; - let tpl = this.getTemplate(name, flipped); - - // 60 (center) must be included to place gold - if (!tpl.includes(60)) { - throw Error(`Template "${name}", flip=${+flipped}, lacks 60.`); - } + return tpl; + } - console.info(`Selected board layout template "${name}", flipped=${+flipped}`); + /** + * Flip a template array. + * + * The array is modified in place! + * + * @param {Number[]} tpl + * @returns {Number[]} + */ + flipTemplate(tpl) { + return tpl + .sort((a, b) => a - b) + .map((n) => { + let {x, y} = gridIndexToXy(n); + return xyToGridIndex(5 + y - x, y); + }) + .sort((a, b) => a - b); + } - return tpl; - } + /** + * Print array of all occupied orbs as a layout template + * + * @returns {Number[]} + */ + toTemplate() { + return Object.keys(game.board.grid) + .map((x) => +x) + .sort((a, b) => a - b); + } - /** - * Flip a template array. - * - * The array is modified in place! - * - * @param {Number[]} tpl - * @returns {Number[]} - */ - flipTemplate(tpl) { - return tpl - .sort((a, b) => a - b) - .map((n) => { - let {x, y} = gridIndexToXy(n); - return xyToGridIndex(5 + y - x, y); - }) - .sort((a, b) => a - b); - } + /** + * Run a template editor + * + * - click on tiles to toggle orbs + * - call `game.showTemplate('wheel')` to show an existing template on the board + * - call `JSON.stringify(game.toTemplate())` to print the current template array to console + */ + templateBuilder() { + this.board.removeAllOrbs(); - /** - * Print array of all occupied orbs as a layout template - * - * @returns {Number[]} - */ - toTemplate() { - return Object.keys(game.board.grid) - .map((x) => +x) - .sort((a, b) => a - b); - } + this.board.onTileClick = (n) => { + let symbol = 'lead'; + this.board.placeOrbByIndex(n, symbol); + }; - /** - * Run a template editor - * - * - click on tiles to toggle orbs - * - call `game.showTemplate('wheel')` to show an existing template on the board - * - call `JSON.stringify(game.toTemplate())` to print the current template array to console - */ - templateBuilder() { - this.board.removeAllOrbs(); - - this.board.onTileClick = (n) => { - let symbol = 'lead'; - this.board.placeOrbByIndex(n, symbol); - }; + this.board.onOrbClick = (n, orb) => { + this.board.removeOrbByIndex(n) + }; + } - this.board.onOrbClick = (n, orb) => { - this.board.removeOrbByIndex(n) - }; - } + isAvailable(n) { + return this.getNeighbours(n).freeSequence >= 3; + } - isAvailable(n) { - return this.getNeighbours(n).freeSequence >= 3; + getNeighbours(n) { + let {x, y} = gridIndexToXy(n); + + let freeSpaces = [ + isXyOutside(x - 1, y) || !this.board.grid[n - 1], + isXyOutside(x - 1, y - 1) || !this.board.grid[n - 12], + isXyOutside(x, y - 1) || !this.board.grid[n - 11], + isXyOutside(x + 1, y) || !this.board.grid[n + 1], + isXyOutside(x + 1, y + 1) || !this.board.grid[n + 12], + isXyOutside(x, y + 1) || !this.board.grid[n + 11], + ]; + + let nOccupied = 0; + for (let i = 0; i < 6; i++) { + if (!freeSpaces[i]) { + nOccupied++; + } } - getNeighbours(n) { - let {x, y} = gridIndexToXy(n); - - let freeSpaces = [ - isXyOutside(x - 1, y) || !this.board.grid[n - 1], - isXyOutside(x - 1, y - 1) || !this.board.grid[n - 12], - isXyOutside(x, y - 1) || !this.board.grid[n - 11], - isXyOutside(x + 1, y) || !this.board.grid[n + 1], - isXyOutside(x + 1, y + 1) || !this.board.grid[n + 12], - isXyOutside(x, y + 1) || !this.board.grid[n + 11], - ]; - - let nOccupied = 0; - for (let i = 0; i < 6; i++) { - if (!freeSpaces[i]) { - nOccupied++; + let freeSequence = 0; + let maxFreeSequence = 0; + for (let i = 0; i < 12; i++) { + if (freeSpaces[i % 6]) { + freeSequence++; + if (freeSequence >= 6) { + maxFreeSequence = freeSequence; + break; } - } - - let freeSequence = 0; - let maxFreeSequence = 0; - for (let i = 0; i < 12; i++) { - if (freeSpaces[i % 6]) { - freeSequence++; - if (freeSequence >= 6) { - maxFreeSequence = freeSequence; - break; - } - if (freeSequence > maxFreeSequence) { - maxFreeSequence = freeSequence; - } - } else { - freeSequence = 0; + if (freeSequence > maxFreeSequence) { + maxFreeSequence = freeSequence; } + } else { + freeSequence = 0; } - - return {neighbours: nOccupied, freeSequence: maxFreeSequence}; } - placeOrbs(template) { - this.board.removeAllOrbs(); + return {neighbours: nOccupied, freeSequence: maxFreeSequence}; + } - let allowedTable = []; - let outsideTemplate = []; - for (let i = 0; i < BOARD_SIZE; i++) { - const allo = template.includes(i); - allowedTable.push(allo); + placeOrbs(template) { + let tilesAdded = 0; - let {x, y} = gridIndexToXy(i); - if (!allo && !isXyOutside(x, y)) { - outsideTemplate.push(i); - } + this.board.removeAllOrbs(); - // Highlight pattern shape + let allowedTable = []; + let outsideTemplate = []; + for (let i = 0; i < BOARD_SIZE; i++) { + const allo = template.includes(i); + allowedTable.push(allo); - // if (this.board.tiles[i]) { - // if (allo) { - // this.board.tiles[i].setAttribute('opacity', 1) - // } else { - // this.board.tiles[i].setAttribute('opacity', 0.6) - // } - // } + let {x, y} = gridIndexToXy(i); + if (!allo && !isXyOutside(x, y)) { + outsideTemplate.push(i); } - const place = (n, symbol) => { - this.debug(`Place ${n} <- ${symbol}`); - if (!allowedTable[n]) throw Error(`Position ${n} not allowed by template`); - if (this.board.grid[n]) throw Error(`Position ${n} is occupied by ${this.board.grid[n]}`); - // we use a hack to speed up generation here - SVG is not altered until we have a solution - this.board.grid[n] = symbol; - }; + // Highlight pattern shape - const unplace = (n) => { - this.debug(`Unplace ${n}`); - this.board.grid[n] = null; - }; + // if (this.board.tiles[i]) { + // if (allo) { + // this.board.tiles[i].setAttribute('opacity', 1) + // } else { + // this.board.tiles[i].setAttribute('opacity', 0.6) + // } + // } + } - const findAvailableIndexWithNeighbours = (count, except = null) => { - let candidates = []; - for (let i = 0; i < template.length; i++) { - const n = template[i]; - if (except && except.includes(n)) continue; + const place = (n, symbol) => { + this.trace(`Place ${n} <- ${symbol}`); + if (!allowedTable[n]) throw Error(`Position ${n} not allowed by template`); + if (this.board.grid[n]) throw Error(`Position ${n} is occupied by ${this.board.grid[n]}`); + // we use a hack to speed up generation here - SVG is not altered until we have a solution + this.board.grid[n] = symbol; + }; - if (!this.board.grid[n]) { - const neigh = this.getNeighbours(n); - if (neigh.neighbours === count && neigh.freeSequence >= 3) { - candidates.push(n); - } + const unplace = (n) => { + this.trace(`Unplace ${n}`); + this.board.grid[n] = null; + }; + + const findAvailableIndexWithNeighbours = (count, except = null) => { + let candidates = []; + for (let n of template) { + if (except && except.includes(n)) continue; + + if (!this.board.grid[n]) { + const neigh = this.getNeighbours(n); + if (neigh.neighbours === count && neigh.freeSequence >= 3) { + candidates.push(n); } } + } - if (candidates.length) { - return this.arrayChoose(candidates) - } else { - return false; - } - }; + if (candidates.length) { + return this.arrayChoose(candidates) + } else { + return false; + } + }; - const findAvailableIndex = (except = null) => { - for (let i = 6; i >= 0; i--) { - const n = findAvailableIndexWithNeighbours(i, except); - if (n !== false) return n; - } + const findAvailableIndex = (except = null) => { + for (let i = 6; i >= 0; i--) { + const n = findAvailableIndexWithNeighbours(i, except); + if (n !== false) return n; + } - // this corrupts the template, but makes the likelihood of quickly finding a valid solution much higher. - if (template.length !== BOARD_SIZE && this.cfg.allowTemplateAugmenting) { - // Prefer tile with more neighbours to make the game harder - let candidates = []; - outsideTemplate.forEach((n) => { - if (!allowedTable[n] && this.isAvailable(n)) { - const neigh = this.getNeighbours(n); - if (!candidates[neigh.neighbours]) { - candidates[neigh.neighbours] = [n]; - } else { - candidates[neigh.neighbours].push(n); - } + // this corrupts the template, but makes the likelihood of quickly finding a valid solution much higher. + if (template.length !== BOARD_SIZE && this.cfg.allowTemplateAugmenting) { + // Prefer tile with more neighbours to make the game harder + let candidates = []; + outsideTemplate.forEach((n) => { + if (!allowedTable[n] && this.isAvailable(n)) { + const neigh = this.getNeighbours(n); + if (!candidates[neigh.neighbours]) { + candidates[neigh.neighbours] = [n]; + } else { + candidates[neigh.neighbours].push(n); } - }); + } + }); - for (let i = 6; i >= 0; i--) { - if (!candidates[i]) continue; - let toAdd = this.arrayChoose(candidates[i]); - allowedTable[toAdd] = true; - outsideTemplate.splice(outsideTemplate.indexOf(toAdd), 1); + for (let i = 6; i >= 0; i--) { + if (!candidates[i]) continue; + let toAdd = this.arrayChoose(candidates[i]); + allowedTable[toAdd] = true; + outsideTemplate.splice(outsideTemplate.indexOf(toAdd), 1); - template.push(toAdd); + template.push(toAdd); + tilesAdded++; - this.warn(`Adding extra tile to template: ${toAdd}`); - return toAdd; - } + this.warn(`Adding extra tile to template: ${toAdd}`); + return toAdd; } + } - throw Error("Failed to find available tile"); - }; + throw Error("Failed to find available tile"); + }; - place(60, 'gold'); + place(60, 'gold'); - const toPlace = this.buildPlacementList(); + const toPlace = this.buildPlacementList(); - let solution = []; - while (toPlace.length > 0) { - this.debug('placing a pair.'); + let solution = [ + ['gold', 60], + ]; + while (toPlace.length > 0) { + this.trace('placing a pair.'); - let symbol1 = toPlace.pop(); - let index1 = findAvailableIndex(); - place(index1, symbol1); + let symbol1 = toPlace.pop(); + let index1 = findAvailableIndex(); + place(index1, symbol1); + solution.push([symbol1, index1]); - let symbol2 = toPlace.pop(); - let index2 = findAvailableIndex(); - place(index2, symbol2); + let symbol2 = toPlace.pop(); + let index2 = findAvailableIndex(); + place(index2, symbol2); - if (!this.isAvailable(index1)) { - this.debug(`Deadlock, trying to work around it - ${index1}, ${index2}`); + if (!this.isAvailable(index1)) { + this.debug(`Deadlock, trying to work around it - ${index1}, ${index2}`); - unplace(index2); - let except = [index2]; + unplace(index2); + let except = [index2]; - let suc = false; - for (let i = 0; i < 5; i++) { - this.debug(`try #${i + 1}`); - let index = findAvailableIndex(except); - place(index, symbol2); + let suc = false; + for (let i = 0; i < 5; i++) { + this.trace(`try #${i + 1}`); + let index = findAvailableIndex(except); + place(index, symbol2); - if (this.isAvailable(index1)) { - suc = true; - break; - } else { - unplace(index); - except.push(index); - } - } - - if (!suc) { - throw new Error("Solution contains a deadlock."); + if (this.isAvailable(index1)) { + suc = true; + index2 = index; + break; + } else { + unplace(index); + except.push(index); } } - solution.push([symbol1, index1]); - solution.push([symbol2, index2]); + if (!suc) { + throw new Error("Solution contains a deadlock."); + } } - // Show the solution for debug + // index2 is updated in the fixing loop + solution.push([symbol2, index2]); + } - solution.reverse(); - this.info("Found a valid board!"); + // Show the solution for debug - solution.forEach((a) => { - let p = gridIndexToXy(a[1]); - a[1] = `${p.x} × ${p.y}`; - }); + solution.reverse(); + this.info("Found a valid board!"); - this.debug('Solution: ', solution); - } + solution.forEach((a) => { + let p = gridIndexToXy(a[1]); + a[1] = `${p.x}×${p.y}`; + }); - /** - * Shuffle an array. - * The array is shuffled in place. - * - * @return the array - */ - arrayShuffle(a) { - let j, x, i; - for (i = a.length - 1; i > 0; i--) { - j = this.rng.nextInt(i); - x = a[i]; - a[i] = a[j]; - a[j] = x; - } - return a; + this.debug('Solution:', solution); + + return { + solution, + tilesAdded: tilesAdded, + }; + } + + /** + * Shuffle an array. + * The array is shuffled in place. + * + * @return the array + */ + arrayShuffle(a) { + let j, x, i; + for (i = a.length - 1; i > 0; i--) { + j = this.rng.nextInt(i); + x = a[i]; + a[i] = a[j]; + a[j] = x; } + return a; + } + + /** + * Count orbs in the game board + * + * @return {number} + */ + countOrbs() { + return this.board.grid.reduce((acu, x) => acu + (x !== null), 0); + } + + /** + * Choose a random emember of an array + * + * @param array + * @return {number} + */ + arrayChoose(array) { + return array[Math.floor(this.rng.next() * array.length)]; + } - /** - * Count orbs in the game board - * - * @return {number} - */ - countOrbs() { - let n = 0; - // todo use reduce - this.board.grid.forEach((x) => { - if (x !== null) { - n++; + buildPlacementList() { + let toPlace = [ + ['air', 'air'], + ['air', 'air'], + ['air', 'air'], + ['air', 'air'], + + ['fire', 'fire'], + ['fire', 'fire'], + ['fire', 'fire'], + ['fire', 'fire'], + + ['water', 'water'], + ['water', 'water'], + ['water', 'water'], + ['water', 'water'], + + ['earth', 'earth'], + ['earth', 'earth'], + ['earth', 'earth'], + ['earth', 'earth'], + ]; + + let newSaltedPairs = []; + const nsalted = this.rng.nextInt(2); + for (let i = 0; i < nsalted; i++) { + while (true) { + const n = this.rng.nextInt(toPlace.length - 1); + if (toPlace[n][1] !== 'salt') { + this.trace(`Pairing ${toPlace[n][1]} with salt.`); + newSaltedPairs.push([toPlace[n][1], 'salt']); + toPlace[n][1] = 'salt'; + break; } - }); - return n; + } + } + toPlace = toPlace.concat(newSaltedPairs); + // if we have some salt pairs left + for (let i = 0; i < 2 - nsalted; i++) { + toPlace.push(['salt', 'salt']); } - /** - * Choose a random emember of an array - * - * @param array - * @return {number} - */ - arrayChoose(array) { - return array[Math.floor(this.rng.next() * array.length)]; + // these are always paired like this, and don't support salt + toPlace = toPlace.concat([ + ['mors', 'vitae'], + ['mors', 'vitae'], + ['mors', 'vitae'], + ['mors', 'vitae'], + ]); + + // shuffle the pairs that have random order (i.e. not metals) + this.arrayShuffle(toPlace); + + // the order here is actually significant, so let's pay attention... + const metals = [ + ['lead', 'mercury'], + ['tin', 'mercury'], + ['iron', 'mercury'], + ['copper', 'mercury'], + ['silver', 'mercury'], + ]; + let mPos = []; + for (let i = 0; i < metals.length; i++) { + let x; + // find a unique position + do { + x = this.rng.nextInt(toPlace.length + i); + } while (mPos.includes(x)); + mPos.push(x) } + mPos.sort((a, b) => a - b); + this.trace('Metal positions ', mPos); + // inject them into the array + metals.forEach((pair, i) => { + toPlace.splice(mPos[i] + i, 0, pair); + }); + + this.debug('Placement order (last first):', toPlace); + + return toPlace.reduce((a, c) => { + a.push(c[0]); + a.push(c[1]); + return a; + }, []); + } - buildPlacementList() { - let toPlace = [ - ['air', 'air'], - ['air', 'air'], - ['air', 'air'], - ['air', 'air'], - - ['fire', 'fire'], - ['fire', 'fire'], - ['fire', 'fire'], - ['fire', 'fire'], - - ['water', 'water'], - ['water', 'water'], - ['water', 'water'], - ['water', 'water'], - - ['earth', 'earth'], - ['earth', 'earth'], - ['earth', 'earth'], - ['earth', 'earth'], - ]; - - let newSaltedPairs = []; - const nsalted = this.rng.nextInt(2); - for (let i = 0; i < nsalted; i++) { - while (true) { - const n = this.rng.nextInt(toPlace.length - 1); - if (toPlace[n][1] !== 'salt') { - this.debug(`Pairing ${toPlace[n][1]} with salt.`); - newSaltedPairs.push([toPlace[n][1], 'salt']); - toPlace[n][1] = 'salt'; - break; - } - } + /** + * Convert the strings in the board array to actual SVG orbs (strings are a placeholder to speed up board solving) + */ + renderPreparedBoard() { + for (let n = 0; n < BOARD_SIZE; n++) { + if (this.board.grid[n] !== null) { + const symbol = this.board.grid[n]; + this.board.grid[n] = null; + this.board.placeOrbByIndex(n, symbol); } - toPlace = toPlace.concat(newSaltedPairs); - // if we have some salt pairs left - for (let i = 0; i < 2 - nsalted; i++) { - toPlace.push(['salt', 'salt']); + } + } + + /** + * Update orb availability status (includes effects) + */ + updateOrbDisabledStatus() { + for (let n = 0; n < BOARD_SIZE; n++) { + if (this.board.grid[n]) { + const ava = this.isAvailableAtPlaytime(n); + this.board.grid[n].node.classList.toggle('disabled', !ava); } + } + } - // these are always paired like this, and don't support salt - toPlace = toPlace.concat([ - ['mors', 'vitae'], - ['mors', 'vitae'], - ['mors', 'vitae'], - ['mors', 'vitae'], - ]); - - // shuffle the pairs that have random order (i.e. not metals) - this.arrayShuffle(toPlace); - - // the order here is actually significant, so let's pay attention... - const metals = [ - ['lead', 'mercury'], - ['tin', 'mercury'], - ['iron', 'mercury'], - ['copper', 'mercury'], - ['silver', 'mercury'], - ]; - let mPos = []; - for (let i = 0; i < metals.length; i++) { - let x; - // find a unique position - do { - x = this.rng.nextInt(toPlace.length + i); - } while (mPos.includes(x)); - mPos.push(x) + /** + * Check if a tile is available at play-time (checking unlocked metals) + * + * @param n + * @return {Boolean} + */ + isAvailableAtPlaytime(n) { + let ava = this.isAvailable(n); + + const sym = this.board.grid[n].symbol; + if (METAL_SEQ.includes(sym)) { + if (sym !== this.nextMetal) { + ava = false; } - mPos.sort((a, b) => a - b); - this.debug('Metal positions ', mPos); - // inject them into the array - metals.forEach((pair, i) => { - toPlace.splice(mPos[i] + i, 0, pair); - }); + } - return toPlace.reduce((a, c) => { - a.push(c[0]); - a.push(c[1]); - return a; - }, []); + return ava; + } + + /** + * Handle orb click + * + * @param n + * @param orb + */ + inGameBoardClick(n, orb) { + this.debug(`Clicked orb ${n}: ${orb.symbol}`); + + if (!this.isAvailableAtPlaytime(n)) { + this.debug(`Orb is blocked`); + return; } - /** - * Convert the strings in the board array to actual SVG orbs (strings are a placeholder to speed up board solving) - */ - renderPreparedBoard() { - for (let n = 0; n < BOARD_SIZE; n++) { - if (this.board.grid[n] !== null) { - const symbol = this.board.grid[n]; - this.board.grid[n] = null; - this.board.placeOrbByIndex(n, symbol); + let wantRefresh = false; + + if (orb.symbol === 'gold') { + // gold has no pairing + this.debug(`Removing gold.`); + this.board.removeOrbByIndex(n); + this.selectedOrb = null; + wantRefresh = true; + } else if (this.selectedOrb === null) { + this.debug(`Select orb`); + // first selection + this.selectedOrb = {n, orb}; + orb.node.classList.add('selected'); + } else { + if (this.selectedOrb.n === n) { + this.debug(`Unselect orb`); + // orb clicked twice + orb.node.classList.remove('selected'); + this.selectedOrb = null; + } else { + this.debug(`Second selection, try to match`); + + // second orb in a pair + const otherSymbol = this.selectedOrb.orb.symbol; + + if (this.getPairSymbols(orb.symbol).includes(otherSymbol)) { + this.debug(`Match confirmed, removing ${this.selectedOrb.n} (${this.selectedOrb.orb.symbol} + ${n} (${orb.symbol}`); + + // compatible pair clicked + this.board.removeOrbByIndex(n); + this.board.removeOrbByIndex(this.selectedOrb.n); + + if ([orb.symbol, otherSymbol].includes(this.nextMetal)) { + this.debug("Advance metal transmutation sequence."); + this.advanceMetal(); + } + + this.selectedOrb = null; + + wantRefresh = true; + } else { + this.debug("No match, start new pair selection."); + + // Bad selection, select it as the first orb. + this.selectedOrb.orb.node.classList.remove('selected'); + this.selectedOrb = {n, orb}; + orb.node.classList.add('selected'); } } } - /** - * Update orb availability status (includes effects) - */ - updateOrbDisabledStatus() { - for (let n = 0; n < BOARD_SIZE; n++) { - if (this.board.grid[n]) { - const ava = this.isAvailableAtPlaytime(n); - this.board.grid[n].node.classList.toggle('disabled', !ava); - } + if (wantRefresh) { + if (this.countOrbs() === 0) { + this.info("Good work!"); } + + this.updateOrbDisabledStatus(); } + } - /** - * Check if a tile is available at play-time (checking unlocked metals) - * - * @param n - * @return {Boolean} - */ - isAvailableAtPlaytime(n) { - let ava = this.isAvailable(n); - - const sym = this.board.grid[n].symbol; - if (METAL_SEQ.includes(sym)) { - if (sym !== this.nextMetal) { - ava = false; - } - } + newGame(seed) { + if (seed !== null) { + this.rng.setSeed(seed); + } - return ava; + this.info("RNG seed is: " + this.rng.seed); + + if (this.get_opts.url_seed) { + // Place seed in the navbar for bookmarking / sharing + Nav.setUrlArgs({ + seed: this.rng.seed, + }); } - /** - * Handle orb click - * - * @param n - * @param orb - */ - ingameBoardClick(n, orb) { - if (!this.isAvailableAtPlaytime(n)) return; + this.board.onTileClick = (n) => { + this.debug(n, gridIndexToXy(n)); + }; - let wantRefresh = false; + this.selectedOrb = null; - if (orb.symbol === 'gold') { - // gold has no pairing - this.board.removeOrbByIndex(n); - this.selectedOrb = null; - wantRefresh = true; - } else if (this.selectedOrb === null) { - // first selection - this.selectedOrb = {n, orb}; - orb.node.classList.add('selected'); - } else { - if (this.selectedOrb.n === n) { - // orb clicked twice - orb.node.classList.remove('selected'); - this.selectedOrb = null; - } else { - // second orb in a pair - const otherSymbol = this.selectedOrb.orb.symbol; + let self = this; + this.board.onOrbClick = (n, orb) => self.inGameBoardClick(n, orb); - if (this.getPairSymbols(orb.symbol).includes(otherSymbol)) { - // compatible pair clicked - this.board.removeOrbByIndex(n); - this.board.removeOrbByIndex(this.selectedOrb.n); + // retry loop, should not be needed if everything is correct + let suc = false; + let retry_count = 0; + let board_info = null; + for (let n_tpl = 0; n_tpl < this.cfg.attemptTemplates && !suc; n_tpl++) { + this.debug('RNG seed is: ' + this.rng.state); - if ([orb.symbol, otherSymbol].includes(this.nextMetal)) { - this.advanceMetal(); - } + let template; + if (n_tpl === 0 && this.get_opts.template !== null) { + template = this.getTemplate(this.get_opts.template, this.get_opts.template_flip); + } else { + template = this.getRandomTemplate(); + } - this.selectedOrb = null; + this.info(`Selected board layout template "${template.name}"`); - wantRefresh = true; + for (let n_solution = 0; n_solution < this.cfg.retryTemplate; n_solution++) { + try { + board_info = this.placeOrbs(template.positions.slice(0)); // clone + board_info.template = template; + suc = true; + break; + } catch (e) { + retry_count++; + if (this.logging_trace) { + this.error(e); } else { - // Bad selection, select it as the first orb. - this.selectedOrb.orb.node.classList.remove('selected'); - this.selectedOrb = {n, orb}; - orb.node.classList.add('selected'); + this.warn(e.message); } } } - if (wantRefresh) { - if (this.countOrbs() === 0) { - this.info("Good work!"); - } - - this.updateOrbDisabledStatus(); + if (!suc) { + this.warn(`Exhausted all retries for the template "${template.name}", getting a new one`); } } - newGame() { - this.board.onTileClick = (n) => { - this.debug(n, gridIndexToXy(n)); - }; + this.nextMetal = 'lead'; + this.renderPreparedBoard(); + this.updateOrbDisabledStatus(); - this.selectedOrb = null; + if (!suc) { + alert(`Sorry, could not find a valid board setup after ${retry_count} retries.`); + } else { + this.info( + `Board set up with ${retry_count} retries:\n` + + `teplate "${board_info.template.name}"` + + (this.cfg.allowTemplateAugmenting ? ` with ${board_info.tilesAdded} extra tiles` : '')); - let self = this; - this.board.onOrbClick = (n, orb) => self.ingameBoardClick(n, orb); - - // retry loop, should not be needed if everything is correct - let suc = false; - let numretries = 0; - const alertOnError = false; - for (let i = 0; i < this.cfg.attemptTemplates && !suc; i++) { - this.debug('RNG seed is: ' + this.rng.state); - const template = this.getRandomTemplate(); - for (let j = 0; j < this.cfg.retryTemplate; j++) { - try { - this.placeOrbs(template.slice(0)); // clone - suc = true; - break; - } catch (e) { - if (alertOnError) alert('welp'); - numretries++; - if (this.cfg.debug) { - this.error(e); - } else { - this.warn(e.message); - } + this.info('Reference solution:\n ' + board_info.solution.reduce((s, entry, i) => { + s += `${entry[0]} ${entry[1]}`; + + if (i % 2 === 1) { + s += "\n "; + } else { + if (entry[0] !== 'gold') { + s += " + "; } } - if (!suc) { - this.warn("Exhausted all retries for the template, getting a new one"); - } - } - - this.nextMetal = 'lead'; - this.renderPreparedBoard(); - this.updateOrbDisabledStatus(); - if (!suc) { - alert(`Sorry, could not find a valid board setup after ${numretries} retries.`); - } else { - this.info(`Found valid solution (with ${numretries} retries)`); - } + return s; + }, '')); } + } - getPairSymbols(first) { - return { - 'salt': ['salt', 'air', 'fire', 'water', 'earth'], - 'air': ['salt', 'air'], - 'fire': ['salt', 'fire'], - 'water': ['salt', 'water'], - 'earth': ['salt', 'earth'], - 'mercury': [this.nextMetal], - 'lead': ['mercury'], - 'tin': ['mercury'], - 'iron': ['mercury'], - 'copper': ['mercury'], - 'silver': ['mercury'], - 'gold': [], - 'mors': ['vitae'], - 'vitae': ['mors'], - }[first]; - } + getPairSymbols(first) { + return { + 'salt': ['salt', 'air', 'fire', 'water', 'earth'], + 'air': ['salt', 'air'], + 'fire': ['salt', 'fire'], + 'water': ['salt', 'water'], + 'earth': ['salt', 'earth'], + 'mercury': [this.nextMetal], + 'lead': ['mercury'], + 'tin': ['mercury'], + 'iron': ['mercury'], + 'copper': ['mercury'], + 'silver': ['mercury'], + 'gold': [], + 'mors': ['vitae'], + 'vitae': ['mors'], + }[first]; + } - advanceMetal() { - if (this.nextMetal === 'gold') throw new Error("No metals to unlock beyond gold."); - this.nextMetal = METAL_SEQ[METAL_SEQ.indexOf(this.nextMetal) + 1]; - console.debug(`Next metal unlocked: ${this.nextMetal}`); - } + advanceMetal() { + if (this.nextMetal === 'gold') throw new Error("No metals to unlock beyond gold."); + this.nextMetal = METAL_SEQ[METAL_SEQ.indexOf(this.nextMetal) + 1]; + console.debug(`Next metal unlocked: ${this.nextMetal}`); } +} - /* Start */ +/* Start */ - window.game = new Game(); +window.game = new Game(); -})(); diff --git a/style.css b/style.css index 2eec271..b15bbfe 100644 --- a/style.css +++ b/style.css @@ -1,8 +1,8 @@ -*,*:before,*:after { +*, *::before, *::after { box-sizing: border-box; } -html,body { +html, body { background:black; color:white; } @@ -22,6 +22,7 @@ html,body { opacity: 0.6; } +/* Highlighting */ .highlight-salt .symbol-salt .orb-fill, .highlight-air .symbol-air .orb-fill, .highlight-fire .symbol-fire .orb-fill, @@ -40,33 +41,33 @@ html,body { stroke-width: 7px; } -.orb.disabled .orb-glow, -.orb.disabled .orb-shadow, -.orb.disabled:hover .orb-glow, -.orb.disabled:hover .orb-shadow { - opacity: 0; -} - +/* Orb is clickable */ .orb { cursor: pointer; } +/* Disabled effect */ .orb.disabled { cursor: default; opacity: 0.6; } -.cfg-no-fade-disabled .orb.disabled { - opacity: 1; +/* Disabled orb has no glow or shadow */ +.orb.disabled .orb-glow, +.orb.disabled .orb-shadow, +.orb.disabled:hover .orb-glow, +.orb.disabled:hover .orb-shadow { + opacity: 0; } -.orb-glow, -.orb-shadow { - transition: opacity linear 0.1s; +/* Blur effect */ +.orb-shadow, .orb-glow { + filter: url('#filterDropshadow'); } -.cfg-no-anim * { - transition: none !important; +/* Hover and select effects */ +.orb-glow, .orb-shadow { + transition: opacity linear 0.1s; } .orb.selected .orb-glow, @@ -78,3 +79,31 @@ html,body { .orb:hover .orb-shadow { opacity: 0; } + +/* No-anim version applies animations instantly */ +.cfg-no-anim * { + transition: none !important; +} + +/* No-blur version uses white ring around selected orbs, and has no shadow */ +.cfg-no-blur .orb-shadow, +.cfg-no-blur .orb-glow { + filter: none !important; + opacity: 0 !important; +} + +.cfg-no-blur .orb.selected .orb-fill, +.cfg-no-blur .orb:hover .orb-fill { + stroke: white; + stroke-width: 10px; + paint-order: stroke; +} + +.cfg-no-blur .orb.disabled .orb-fill { + stroke: transparent !important; +} + +/* No-disabled-fade version has all orbs at full opacity */ +.cfg-no-fade-disabled .orb.disabled { + opacity: 1; +}