diff --git a/script.js b/script.js index 03aadda..696ebc2 100644 --- a/script.js +++ b/script.js @@ -113,6 +113,7 @@ class Board { // Orb grid this.grid = []; this.tiles = []; + this.buttons = {}; for (let i = 0; i < BOARD_SIZE; i++) { this.grid[i] = null; @@ -132,6 +133,8 @@ class Board { this.buildBackground(); + this.buildGUI(); + this.initAutoScaling(); } @@ -219,6 +222,19 @@ class Board { return {rx, ry}; } + /** + * Convert GUI coordinates to graphic coordinates + * + * @param {Number} x + * @param {Number} y + * @returns {{rx: number, ry: number}} + */ + guiXyToCoord(x, y) { + let rx = this.TILE_W * (-5.9 + x); + let ry = this.TILE_H * (-5.5 + y); + return {rx, ry}; + } + /** * Remove an orb from the grid at the given coordinates. * @@ -598,6 +614,36 @@ class Board { getOrbByIndex(n) { return this.grid[n]; } + + buildGUI() { + const y0 = 0.05; + const x0 = 0; + const ysp = 0.75; + const ysp2 = 0.6; + + this.buttons.randomize = this.addButton(x0, y0, 'Randomize'); + this.buttons.restart = this.addButton(x0, y0 + ysp, 'Try Again', 'disabled'); + // this.buttons.undo = this.addButton(x0, y0 + ysp*2, 'Undo'); + + const cfgy0 = 10; + + this.buttons.optFancy = this.addButton(x0, cfgy0, 'Effects:', 'config'); + this.buttons.optBlockedEffect = this.addButton(x0, cfgy0+ysp2, 'Dim Blocked:', 'config'); + this.buttons.optSloppy = this.addButton(x0, cfgy0+ysp2*2, 'Sloppy Gen:', 'config'); + } + + updateSettingsGUI(cfg) { + this.buttons.optFancy.textContent = 'Effects: '+((cfg.svgAnimations || cfg.svgBlur) ? 'On' : 'Off'); + this.buttons.optBlockedEffect.textContent = 'Dim Blocked: '+(cfg.disabledEffect ? 'On' : 'Off'); + this.buttons.optSloppy.textContent = 'Sloppy Gen: '+(cfg.allowTemplateAugmenting ? 'On' : 'Off'); + } + + addButton(x, y, text, classes='') { + let { rx, ry } = this.guiXyToCoord(x, y); + let button = Svg.fromXML(`${text}`); + this.$root.appendChild(button); + return button; + } } /** @@ -847,6 +893,7 @@ class Game { }; this.applySettings(); + this.installButtonHandlers(); // Defer start to give browser time to render the background setTimeout(() => { @@ -886,6 +933,7 @@ class Game { 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(); + this.board.updateSettingsGUI(this.cfg); } setCfg(update) { @@ -1343,6 +1391,31 @@ class Game { }, []); } + 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}`); + } + /** * Convert the strings in the board array to actual SVG orbs (strings are a placeholder to speed up board solving) */ @@ -1356,18 +1429,6 @@ class Game { } } - /** - * 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); - } - } - } - /** * Check if a tile is available at play-time (checking unlocked metals) * @@ -1457,7 +1518,55 @@ class Game { this.info("Good work!"); } - this.updateOrbDisabledStatus(); + this.updateGameGUI(); + } + } + + /** + * Add event handlers for the menu buttons + */ + installButtonHandlers() { + this.board.buttons.restart.addEventListener('click', () => { + this.newGame(this.rng.seed); + }); + + this.board.buttons.randomize.addEventListener('click', () => { + this.newGame(+new Date); + }); + + this.board.buttons.optFancy.addEventListener('click', () => { + let val = !(this.cfg.svgAnimations || this.cfg.svgBlur); + this.setCfg({ + svgAnimations: val, + svgBlur: val, + }) + }); + + this.board.buttons.optBlockedEffect.addEventListener('click', () => { + this.setCfg({ + disabledEffect: !this.cfg.disabledEffect, + }) + }); + + this.board.buttons.optSloppy.addEventListener('click', () => { + this.setCfg({ + allowTemplateAugmenting: !this.cfg.allowTemplateAugmenting, + }) + }); + } + + /** + * Update button hiding attributes, disabled orb effects, etc + */ + updateGameGUI() { + this.board.buttons.restart.classList.toggle('disabled', this.countOrbs() === 55); + + // Update orb disabled status + 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); + } } } @@ -1480,6 +1589,7 @@ class Game { }; this.selectedOrb = null; + this.nextMetal = 'lead'; let self = this; this.board.onOrbClick = (n, orb) => self.inGameBoardClick(n, orb); @@ -1521,9 +1631,8 @@ class Game { } } - this.nextMetal = 'lead'; this.renderPreparedBoard(); - this.updateOrbDisabledStatus(); + this.updateGameGUI(); if (!suc) { alert(`Sorry, could not find a valid board setup after ${retry_count} retries.`); @@ -1548,31 +1657,6 @@ class Game { }, '')); } } - - 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}`); - } } /* Start */ diff --git a/style.css b/style.css index b15bbfe..6c72461 100644 --- a/style.css +++ b/style.css @@ -3,7 +3,7 @@ } html, body { - background:black; + background: #201717; color:white; } @@ -107,3 +107,37 @@ html, body { .cfg-no-fade-disabled .orb.disabled { opacity: 1; } + +/* GUI */ +.button-text { + fill: #8d7761; + stroke: #453b30; + text-anchor: start; + text-align: left; + stroke-width: 2px; + paint-order: stroke; + font: 32px "Book Antiqua", Palatino, "Palatino Linotype", "Palatino LT STD", Georgia, serif; + cursor: pointer; + user-select: none; +} + +.button-text.config { + font-size: 26px; +} + +.button-text:hover { + fill: #e3c4a2; +} + +.button-text:active { + transform: translateY(1px); +} + +.button-text.disabled, +.button-text.disabled:hover, +.button-text.disabled:active { + cursor: default; + opacity: .5; + fill: #8d7761; + transform: none; +}