diff --git a/index.html b/index.html index ce237f8..daf4588 100644 --- a/index.html +++ b/index.html @@ -74,6 +74,6 @@ - + diff --git a/script.js b/script.js index c4a7024..7a7b137 100644 --- a/script.js +++ b/script.js @@ -57,7 +57,11 @@ class Board { this.SCREEN_PAD = 20; // Orb grid - this.gameGrid = {}; + this.grid = {}; + this.tiles = {}; + + this.onOrbClick = (index, orb) => {}; + this.onTileClick = (index) => {}; this.initOrb(); this.initGlyphs(); @@ -66,8 +70,12 @@ class Board { this.buildBackground(); this.initAutoScaling(); + } - // XXX test content + /** + * Show all orbs for graphics debugging + */ + testGraphics() { let o; this.placeOrb(0, 0, 'salt'); @@ -101,7 +109,6 @@ class Board { initAutoScaling() { this.rescaleTimeout = null; window.addEventListener('resize', () => { - console.log('window resize'); if (this.rescaleTimeout === null) { this.rescaleTimeout = setTimeout(() => this.rescaleCanvas(), 60) } @@ -141,19 +148,29 @@ class Board { /** * Convert grid coordinates to gameGrid array index * - * @param x - * @param y + * @param {Number} x + * @param {Number} y * @returns {Number} */ - gridXyToArrayIndex(x, y) { + xyToGridIndex(x, y) { return y * 11 + x } + /** + * Convert grid index to X, Y + * + * @param {Number} index + * @returns {{x: Number, y: Number}} + */ + gridIndexToXy(index) { + return { x : index % 11, y: Math.floor(index/11) }; + } + /** * Convert grid coordinates to graphic coordinates * - * @param x - * @param y + * @param {Number} x + * @param {Number} y * @returns {{rx: number, ry: number}} */ gridXyToCoord(x, y) { @@ -165,29 +182,51 @@ class Board { /** * Remove an orb from the grid at the given coordinates. * - * @param x - board X - * @param y - board Y + * @param {Number} x - board X + * @param {Number} y - board Y */ removeOrb(x, y) { - const arrayIndex = this.gridXyToArrayIndex(x, y); - if (this.gameGrid[arrayIndex]) { - this.$orbs.removeChild(this.gameGrid[arrayIndex].node); - this.gameGrid[arrayIndex] = null; + const index = this.xyToGridIndex(x, y); + this.removeOrbByIndex(index) + } + + /** + * Remove orb by array index + * + * @param {Number} index + */ + removeOrbByIndex(index) { + if (this.grid[index]) { + this.$orbs.removeChild(this.grid[index].node); + delete this.grid[index]; } } + /** + * Place an orb by array index + * + * @param {Number} index + * @param {String} symbol + * @return {{node: Node, symbol: String}} + */ + placeOrbByIndex(index, symbol) { + const {x, y} = this.gridIndexToXy(index); + return this.placeOrb(x, y, symbol); + } + /** * Place an orb on the grid * - * @param x - board X - * @param y - board Y - * @param symbol - alchemical symbol name - * @returns {object} - orb object + * @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 = this.xyToGridIndex(x, y); - this.removeOrb(x, y); + this.removeOrbByIndex(arrayIndex); let template; if (this.metals.includes(symbol)) { @@ -196,23 +235,27 @@ class Board { template = this.orbTpl; } - let o = template.cloneNode(true); - o.classList.add(`element-${symbol}`); - o.setAttribute('transform', `translate(${rx},${ry})`); - o.querySelector('.orb-fill') + 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]); - o.appendChild(this.symbolTpls[symbol].cloneNode(true)); + orb.appendChild(this.symbolTpls[symbol].cloneNode(true)); - this.$orbs.appendChild(o); + orb.dataset.index = arrayIndex; + orb.dataset.symbol = symbol; + this.$orbs.appendChild(orb); - const arrayIndex = this.gridXyToArrayIndex(x, y); + orb.addEventListener('click', () => { + this.onOrbClick(arrayIndex, orb); + }); let object = { - node: o, + node: orb, symbol }; - this.gameGrid[arrayIndex] = object; + this.grid[arrayIndex] = object; return object; } @@ -389,7 +432,7 @@ class Board { cx="13.229166" fill="url(#radGradSlotBg)" /> - `); + `, { class: 'tile' }); } /** @@ -416,9 +459,16 @@ class Board { polygon_shadow.setAttribute('transform', `translate(${rx},${ry}),scale(1.1)`); this.buf0.push(polygon_shadow); + const index = this.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; } /** @@ -442,7 +492,13 @@ class Board { 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.metals = ['mercury', 'lead', 'tin', 'iron', 'copper', 'silver', 'gold']; + this.symbols = [ + 'salt', 'air', 'fire', 'water', 'earth', 'mercury', 'lead', + 'tin', 'iron', 'copper', 'silver', 'gold', 'mors', 'vitae' + ]; + this.metals = [ + 'mercury', 'lead', 'tin', 'iron', 'copper', 'silver', 'gold' + ]; this.orbColors = { salt: '#f2e7b4', @@ -483,8 +539,214 @@ class Board { } } } + + /** + * 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]; + } +} + +/** + * 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) { + if (seed === null) { + seed = Math.random(); + } + this.seed(seed); + } + + /** + * Set seed for following rolls + * + * @param {Number} seed + */ + 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; + } +} + +class Game { + /** + * Init the game + */ + constructor() { + this.board = new Board(); + this.rng = new Rng(); + + this.layoutTemplates = { + '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.newGame() + } + + /** + * 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'); + }) + } + + /** + * Show a random template, for debug + */ + showRandomTemplate() { + this.board.removeAllOrbs(); + this.getRandomTemplate().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 {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 tpl; + } + + /** + * Get a random and randomly flipped template + * + * @return {{template: number[], name: string, flipped: boolean}} + */ + getRandomTemplate() { + let names = Object.keys(this.layoutTemplates); + let name = names[Math.floor(this.rng.next() * names.length)]; + let flipped = this.rng.next() > 0.5; + return { + template: this.getTemplate(name, flipped), + name, + flipped + }; + } + + /** + * Flip a template array. + * + * The array is modified in place! + * + * @param tpl + * @returns {Uint16Array} + */ + flipTemplate(tpl) { + return tpl.sort((a,b) => a-b).map((n) => { + let { x, y } = this.board.gridIndexToXy(n); + return this.board.xyToGridIndex(5 + y - x, y); + }); + } + + /** + * 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); + } + + /** + * 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) + }; + } + + newGame() { + // + } } /* Start */ -window.board = new Board(); +window.game = new Game(); diff --git a/style.css b/style.css index 6c61787..8071728 100644 --- a/style.css +++ b/style.css @@ -22,20 +22,20 @@ html,body { opacity: 0.6; } -.highlight-salt .element-salt .orb-fill, -.highlight-air .element-air .orb-fill, -.highlight-fire .element-fire .orb-fill, -.highlight-water .element-water .orb-fill, -.highlight-earth .element-earth .orb-fill, -.highlight-mercury .element-mercury .orb-fill, -.highlight-lead .element-lead .orb-fill, -.highlight-tin .element-tin .orb-fill, -.highlight-iron .element-iron .orb-fill, -.highlight-copper .element-copper .orb-fill, -.highlight-silver .element-silver .orb-fill, -.highlight-gold .element-gold .orb-fill, -.highlight-vitae .element-vitae .orb-fill, -.highlight-mors .element-mors .orb-fill { +.highlight-salt .symbol-salt .orb-fill, +.highlight-air .symbol-air .orb-fill, +.highlight-fire .symbol-fire .orb-fill, +.highlight-water .symbol-water .orb-fill, +.highlight-earth .symbol-earth .orb-fill, +.highlight-mercury .symbol-mercury .orb-fill, +.highlight-lead .symbol-lead .orb-fill, +.highlight-tin .symbol-tin .orb-fill, +.highlight-iron .symbol-iron .orb-fill, +.highlight-copper .symbol-copper .orb-fill, +.highlight-silver .symbol-silver .orb-fill, +.highlight-gold .symbol-gold .orb-fill, +.highlight-vitae .symbol-vitae .orb-fill, +.highlight-mors .symbol-mors .orb-fill { stroke: yellow; stroke-width: 7px; }