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.
This commit is contained in:
parent
7e82de37c6
commit
d7b00c8bd5
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Reference in New Issue