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.match = null;
|
||||||
this.playfield = null;
|
this.playfield = null;
|
||||||
this.playfieldTiles = [];
|
this.playfieldTiles = [];
|
||||||
|
this.selectedColorScheme = null;
|
||||||
|
this._colorDropdownDomEl = null;
|
||||||
this.bgFill = null;
|
this.bgFill = null;
|
||||||
this.bgTex = null;
|
this.bgTex = null;
|
||||||
// Battle state
|
// Battle state
|
||||||
|
|
@ -107,6 +109,11 @@ export default class BookworkGame extends Phaser.Scene {
|
||||||
const savedId = localStorage.getItem('bookwork-playfield');
|
const savedId = localStorage.getItem('bookwork-playfield');
|
||||||
this.playfield = pfItems.find(p => p.id === savedId) ?? pfItems.find(p => p.id === pfData.default) ?? pfItems[0] ?? null;
|
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;
|
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.bgFill = this.add.rectangle(bgCx, bgCy, GAME_WIDTH, GAME_HEIGHT, 0x0f0a05).setDepth(D.bg);
|
||||||
this.applyPlayfieldBg(this.playfield);
|
this.applyPlayfieldBg(this.playfield);
|
||||||
|
|
@ -122,7 +129,17 @@ export default class BookworkGame extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
applyPlayfieldBg(pf) {
|
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) {
|
if (this.bgTex) {
|
||||||
this.bgTex.setTexture(pf.key).setDisplaySize(GAME_WIDTH, GAME_HEIGHT).setVisible(true);
|
this.bgTex.setTexture(pf.key).setDisplaySize(GAME_WIDTH, GAME_HEIGHT).setVisible(true);
|
||||||
} else {
|
} 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 ────────────────────────────────────────────────────────────────
|
// ── Textures ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
makeTextures() {
|
makeTextures() {
|
||||||
|
|
@ -181,6 +260,7 @@ export default class BookworkGame extends Phaser.Scene {
|
||||||
// ── View management ─────────────────────────────────────────────────────────
|
// ── View management ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
clearLayer() {
|
clearLayer() {
|
||||||
|
if (this._colorDropdownDomEl) { this._colorDropdownDomEl.destroy(); this._colorDropdownDomEl = null; }
|
||||||
for (const p of this.portraits) { try { p.destroy(); } catch (_) {} }
|
for (const p of this.portraits) { try { p.destroy(); } catch (_) {} }
|
||||||
this.portraits = [];
|
this.portraits = [];
|
||||||
this.turnPhase = 'idle';
|
this.turnPhase = 'idle';
|
||||||
|
|
@ -318,6 +398,9 @@ export default class BookworkGame extends Phaser.Scene {
|
||||||
this.playfield = pf;
|
this.playfield = pf;
|
||||||
localStorage.setItem('bookwork-playfield', pf.id);
|
localStorage.setItem('bookwork-playfield', pf.id);
|
||||||
this.applyPlayfieldBg(pf);
|
this.applyPlayfieldBg(pf);
|
||||||
|
if (this._colorDropdownDomEl) {
|
||||||
|
this._colorDropdownDomEl.node.style.display = pf.type === 'colored' ? 'block' : 'none';
|
||||||
|
}
|
||||||
this.playfieldTiles.forEach(t => {
|
this.playfieldTiles.forEach(t => {
|
||||||
const s = t.pf.id === pf.id;
|
const s = t.pf.id === pf.id;
|
||||||
t.bg.setStrokeStyle(s ? 4 : 2, s ? COLORS.accent : COLORS.muted);
|
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('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); });
|
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),
|
const resume = new Button(this, cx - 150, GAME_HEIGHT - 78, `Fight Level ${nextLevel}`, () => this.showIntro(nextLevel),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue