From d7b00c8bd59212b9d6d995993dba6dfe564b4772 Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Mon, 22 Jun 2026 22:53:43 -0600 Subject: [PATCH] feat: add dynamic color scheme support for colored playfields Introduce runtime-generated canvas textures for colored playfields, allowing players to select from predefined base/accent color combinations via a UI dropdown. The selected scheme is persisted to localStorage and the dropdown is shown/hidden contextually based on the active playfield type. Includes proper DOM cleanup when clearing layers. --- public/src/games/bookwork/BookworkGame.js | 96 ++++++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/public/src/games/bookwork/BookworkGame.js b/public/src/games/bookwork/BookworkGame.js index 701e68e..5b53d27 100644 --- a/public/src/games/bookwork/BookworkGame.js +++ b/public/src/games/bookwork/BookworkGame.js @@ -50,6 +50,8 @@ export default class BookworkGame extends Phaser.Scene { this.match = null; this.playfield = null; this.playfieldTiles = []; + this.selectedColorScheme = null; + this._colorDropdownDomEl = null; this.bgFill = null; this.bgTex = null; // Battle state @@ -107,6 +109,11 @@ export default class BookworkGame extends Phaser.Scene { const savedId = localStorage.getItem('bookwork-playfield'); this.playfield = pfItems.find(p => p.id === savedId) ?? pfItems.find(p => p.id === pfData.default) ?? pfItems[0] ?? null; + // Load saved color scheme for colored playfield + const schemes = this.cache.json.get('colored-playfields')?.schemes ?? []; + const savedSchemeId = localStorage.getItem('bookwork-colorscheme'); + this.selectedColorScheme = schemes.find(s => s.id === savedSchemeId) ?? schemes[Math.floor(Math.random() * schemes.length)] ?? null; + const bgCx = GAME_WIDTH / 2, bgCy = GAME_HEIGHT / 2; this.bgFill = this.add.rectangle(bgCx, bgCy, GAME_WIDTH, GAME_HEIGHT, 0x0f0a05).setDepth(D.bg); this.applyPlayfieldBg(this.playfield); @@ -122,7 +129,17 @@ export default class BookworkGame extends Phaser.Scene { } applyPlayfieldBg(pf) { - if (pf?.key && this.textures.exists(pf.key)) { + if (pf?.type === 'colored' && this.selectedColorScheme) { + const texKey = `playfield-colored-${this.selectedColorScheme.id}`; + this.generateColoredTexture(this.selectedColorScheme, texKey); + if (this.bgTex) { + this.bgTex.setTexture(texKey).setDisplaySize(GAME_WIDTH, GAME_HEIGHT).setVisible(true); + } else { + this.bgTex = this.add.image(GAME_WIDTH / 2, GAME_HEIGHT / 2, texKey) + .setDepth(D.bg).setDisplaySize(GAME_WIDTH, GAME_HEIGHT); + } + this.bgFill.setFillStyle(0x0f0a05); + } else if (pf?.key && this.textures.exists(pf.key)) { if (this.bgTex) { this.bgTex.setTexture(pf.key).setDisplaySize(GAME_WIDTH, GAME_HEIGHT).setVisible(true); } else { @@ -137,6 +154,68 @@ export default class BookworkGame extends Phaser.Scene { } } + generateColoredTexture(scheme, texKey) { + const W = GAME_WIDTH, H = GAME_HEIGHT; + const canvas = document.createElement('canvas'); + canvas.width = W; canvas.height = H; + const ctx = canvas.getContext('2d'); + ctx.fillStyle = scheme.base; + ctx.fillRect(0, 0, W, H); + const n = scheme.accents.length; + const D2 = Math.sqrt(W * W + H * H); + const px = -H / D2, py = W / D2; + scheme.accents.forEach((hex, i) => { + const t = (i + 1) / (n + 1); + const gx = W * t, gy = H * (1 - t); + const spread = D2 * 0.55; + const grad = ctx.createLinearGradient(gx - px * spread, gy - py * spread, gx + px * spread, gy + py * spread); + grad.addColorStop(0, hex + '00'); + grad.addColorStop(0.5, hex + '80'); + grad.addColorStop(1, hex + '00'); + ctx.fillStyle = grad; + ctx.fillRect(0, 0, W, H); + }); + if (this.textures.exists(texKey)) this.textures.remove(texKey); + this.textures.addCanvas(texKey, canvas); + } + + _buildColorSchemeDropdown(x, y) { + const schemes = this.cache.json.get('colored-playfields')?.schemes ?? []; + if (!schemes.length) return; + + const wrapper = document.createElement('div'); + wrapper.style.cssText = 'text-align:center;'; + + const label = document.createElement('span'); + label.textContent = 'Color Scheme:'; + label.style.cssText = 'font-family:"Julius Sans One",sans-serif;font-size:15px;color:#9e9080;margin-right:10px;vertical-align:middle;'; + + const select = document.createElement('select'); + select.style.cssText = 'background:#1e1a12;color:#f2ead8;border:2px solid #9e9080;border-radius:6px;padding:6px 14px;font-family:"Julius Sans One",sans-serif;font-size:15px;cursor:pointer;outline:none;vertical-align:middle;'; + select.addEventListener('mouseover', () => { select.style.borderColor = '#c8a84b'; }); + select.addEventListener('mouseout', () => { select.style.borderColor = '#9e9080'; }); + + schemes.forEach(scheme => { + const opt = document.createElement('option'); + opt.value = scheme.id; + opt.textContent = scheme.name; + select.appendChild(opt); + }); + + select.value = this.selectedColorScheme?.id ?? schemes[0].id; + + select.addEventListener('change', (e) => { + const scheme = schemes.find(s => s.id === e.target.value) ?? schemes[0]; + this.selectedColorScheme = scheme; + localStorage.setItem('bookwork-colorscheme', scheme.id); + if (this.playfield?.type === 'colored') this.applyPlayfieldBg(this.playfield); + }); + + wrapper.appendChild(label); + wrapper.appendChild(select); + this._colorDropdownDomEl = this.add.dom(x, y, wrapper); + } + // ── Textures ──────────────────────────────────────────────────────────────── makeTextures() { @@ -181,6 +260,7 @@ export default class BookworkGame extends Phaser.Scene { // ── View management ───────────────────────────────────────────────────────── clearLayer() { + if (this._colorDropdownDomEl) { this._colorDropdownDomEl.destroy(); this._colorDropdownDomEl = null; } for (const p of this.portraits) { try { p.destroy(); } catch (_) {} } this.portraits = []; this.turnPhase = 'idle'; @@ -318,6 +398,9 @@ export default class BookworkGame extends Phaser.Scene { this.playfield = pf; localStorage.setItem('bookwork-playfield', pf.id); this.applyPlayfieldBg(pf); + if (this._colorDropdownDomEl) { + this._colorDropdownDomEl.node.style.display = pf.type === 'colored' ? 'block' : 'none'; + } this.playfieldTiles.forEach(t => { const s = t.pf.id === pf.id; t.bg.setStrokeStyle(s ? 4 : 2, s ? COLORS.accent : COLORS.muted); @@ -328,6 +411,17 @@ export default class BookworkGame extends Phaser.Scene { bg.on('pointerover', () => { if (this.playfield?.id !== pf.id) bg.setStrokeStyle(2, COLORS.text); }); bg.on('pointerout', () => { if (this.playfield?.id !== pf.id) bg.setStrokeStyle(2, COLORS.muted); }); }); + + // Color scheme dropdown positioned below the "Colored" tile + const coloredIdx = pfItems.findIndex(p => p.type === 'colored'); + if (coloredIdx >= 0) { + const dropX = pfLeft + coloredIdx * (PW + PGAP); + const dropY = pfY + PH / 2 + 30; + this._buildColorSchemeDropdown(dropX, dropY); + if (this.playfield?.type !== 'colored' && this._colorDropdownDomEl) { + this._colorDropdownDomEl.node.style.display = 'none'; + } + } } const resume = new Button(this, cx - 150, GAME_HEIGHT - 78, `Fight Level ${nextLevel}`, () => this.showIntro(nextLevel),