iPuzzle/js/scenes/NewPuzzleScene.js

465 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* global Phaser, generateRoomCode */
const DIFFICULTIES = [
{ pieces: 20, label: '20 Pieces' },
{ pieces: 40, label: '40 Pieces' },
{ pieces: 60, label: '60 Pieces' },
{ pieces: 100, label: '100 Pieces' }
];
const BACKGROUNDS = [
{ key: 'bg_dark_wood', path: 'assets/images/ui/dark_wood.jpg', label: 'Dark Wood' },
{ key: 'bg_green_felt', path: 'assets/images/ui/green_felt.jpg', label: 'Green Felt' },
];
class NewPuzzleScene extends Phaser.Scene {
constructor() {
super({ key: 'NewPuzzleScene' });
}
preload() {
// Load puzzle list and thumbnail manifest; full images are loaded by PuzzleScene
this.load.json('puzzle_list', 'assets/puzzles.json');
this.load.json('thumbnail_list', 'assets/thumbnails.json');
}
create() {
this.selectedImageIdx = null;
this.selectedPieces = null;
this.selectedBg = BACKGROUNDS[0]; // default to dark wood
const W = this.sys.game.config.width;
const H = this.sys.game.config.height;
// Background
this.add.graphics().fillStyle(0x1a1a2e, 1).fillRect(0, 0, W, H);
// Title — fixed position (canvas is always 1920×1080)
this.add.text(960, -30, 'Choose Your Puzzle', {
fontFamily: 'Georgia, serif',
fontSize: '58px',
color: '#e8f4ff',
stroke: '#1a4a88',
strokeThickness: 6,
shadow: { offsetX: 0, offsetY: 0, color: '#3388ff', blur: 28, fill: true, stroke: true }
}).setOrigin(0.5, 0.5);
// Decorative separator below the title
const sep = this.add.graphics();
// Faint full-width rule
sep.lineStyle(1, 0x223355, 0.7);
sep.lineBetween(77, -81, 1843, -81);
// Bright centre segment
sep.lineStyle(2, 0x3377bb, 1);
sep.lineBetween(653, -81, 1267, -81);
// Diamond accent at the midpoint
sep.fillStyle(0x66aaff, 1);
sep.fillTriangle(960, 100, 966, 105, 960, 110);
sep.fillTriangle(960, 100, 954, 105, 960, 110);
// Read puzzle list and thumbnail manifest from cache
this._puzzleImages = this.cache.json.get('puzzle_list');
this._thumbnails = this.cache.json.get('thumbnail_list') || [];
// Build a lookup from puzzleKey -> thumbnail entry
this._thumbByPuzzleKey = {};
this._thumbnails.forEach(t => { this._thumbByPuzzleKey[t.puzzleKey] = t; });
// Only load thumbnail textures (full images are loaded by PuzzleScene)
const toLoad = this._thumbnails.filter(t => !this.textures.exists(t.key));
if (toLoad.length > 0) {
toLoad.forEach(t => this.load.image(t.key, t.path));
this.load.once('complete', () => this._buildDomUI());
this.load.start();
} else {
this._buildDomUI();
}
}
// ─── DOM UI ──────────────────────────────────────────────────────────
_buildDomUI() {
this._uiLayer = document.createElement('div');
Object.assign(this._uiLayer.style, {
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '100vw',
height: '56.25vw',
maxHeight: '100vh',
maxWidth: '177.78vh',
pointerEvents: 'none',
zIndex: '10',
});
document.body.appendChild(this._uiLayer);
this._buildPuzzleGrid(this._uiLayer);
this._buildControlsBar(this._uiLayer);
this._refreshStartButton();
this.events.once('shutdown', () => this._destroyDomUI());
}
_buildPuzzleGrid(uiLayer) {
// Scrollable area: sits below the Phaser title (top 10%) and above the controls bar (bottom 28%)
const scrollArea = document.createElement('div');
Object.assign(scrollArea.style, {
position: 'absolute',
top: '10%',
left: '5%',
width: '90%',
height: '62%',
overflowY: 'auto',
overflowX: 'hidden',
pointerEvents: 'auto',
});
const grid = document.createElement('div');
Object.assign(grid.style, {
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: '1.5vmin',
padding: '1vmin',
});
this._cardEls = this._puzzleImages.map((img, i) => {
const card = document.createElement('div');
Object.assign(card.style, {
cursor: 'pointer',
border: '2px solid transparent',
borderRadius: '6px',
overflow: 'hidden',
transition: 'border-color 0.15s, box-shadow 0.15s',
background: '#111128',
});
const thumb = document.createElement('img');
const thumbEntry = this._thumbByPuzzleKey[img.key];
thumb.src = thumbEntry ? thumbEntry.path : img.path;
Object.assign(thumb.style, {
display: 'block',
width: '100%',
aspectRatio: '16 / 9',
objectFit: 'cover',
});
const label = document.createElement('div');
Object.assign(label.style, {
padding: '0.5vmin 0.8vmin',
color: '#8899cc',
fontSize: '1.5vmin',
fontFamily: 'Arial, sans-serif',
textAlign: 'center',
});
label.textContent = img.label;
card.appendChild(thumb);
card.appendChild(label);
card.addEventListener('mouseenter', () => {
if (this.selectedImageIdx !== i) {
card.style.borderColor = '#334466';
}
});
card.addEventListener('mouseleave', () => {
if (this.selectedImageIdx !== i) {
card.style.borderColor = 'transparent';
card.style.boxShadow = '';
}
});
card.addEventListener('click', () => this._selectImage(i));
grid.appendChild(card);
return card;
});
scrollArea.appendChild(grid);
uiLayer.appendChild(scrollArea);
}
_buildControlsBar(uiLayer) {
const bar = document.createElement('div');
Object.assign(bar.style, {
position: 'absolute',
bottom: '0',
left: '0',
width: '100%',
height: '28%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '1.5vmin',
borderTop: '1px solid #222244',
background: 'rgba(10, 10, 25, 0.7)',
pointerEvents: 'auto',
});
// "Player Name" row
const nameRow = document.createElement('div');
Object.assign(nameRow.style, {
display: 'flex',
alignItems: 'center',
gap: '1.5vmin',
});
const nameLabel = document.createElement('div');
Object.assign(nameLabel.style, {
color: '#8899cc',
fontSize: '1.8vmin',
fontFamily: 'Arial, sans-serif',
});
nameLabel.textContent = 'Player Name';
this._nameInput = document.createElement('input');
Object.assign(this._nameInput.style, {
background: '#111133',
color: '#eeeeff',
border: '2px solid #4466aa',
borderRadius: '4px',
padding: '0.5vmin 1.2vmin',
fontSize: '1.8vmin',
fontFamily: 'Arial, sans-serif',
outline: 'none',
width: '18vmin',
});
this._nameInput.maxLength = 16;
this._nameInput.placeholder = 'Your name';
this._nameInput.autocomplete = 'off';
this._nameInput.value = localStorage.getItem('ipuzzle_playerName') || '';
this._nameInput.addEventListener('input', () => {
localStorage.setItem('ipuzzle_playerName', this._nameInput.value.trim());
this._refreshStartButton();
});
const nameHint = document.createElement('div');
Object.assign(nameHint.style, {
color: '#556688',
fontSize: '1.3vmin',
fontFamily: 'Arial, sans-serif',
});
nameHint.textContent = 'Shown to other players in multiplayer';
nameRow.appendChild(nameLabel);
nameRow.appendChild(this._nameInput);
nameRow.appendChild(nameHint);
// "Difficulty" label
const diffLabel = document.createElement('div');
Object.assign(diffLabel.style, {
color: '#8899cc',
fontSize: '1.8vmin',
fontFamily: 'Arial, sans-serif',
});
diffLabel.textContent = 'Difficulty';
// Difficulty buttons row
const diffRow = document.createElement('div');
Object.assign(diffRow.style, {
display: 'flex',
gap: '2vmin',
});
this._diffBtnEls = DIFFICULTIES.map((diff, i) => {
const btn = document.createElement('button');
btn.textContent = diff.label;
Object.assign(btn.style, {
padding: '0.7vmin 2.5vmin',
background: '#111133',
color: '#ccddff',
border: '1px solid #222255',
borderRadius: '4px',
fontSize: '1.6vmin',
fontFamily: 'Arial, sans-serif',
cursor: 'pointer',
transition: 'background 0.1s, border-color 0.1s',
});
btn.addEventListener('mouseenter', () => {
if (this.selectedPieces !== diff.pieces) btn.style.background = '#1a3366';
});
btn.addEventListener('mouseleave', () => {
if (this.selectedPieces !== diff.pieces) btn.style.background = '#111133';
});
btn.addEventListener('click', () => this._selectDifficulty(i, diff.pieces));
diffRow.appendChild(btn);
return { el: btn, pieces: diff.pieces };
});
// "Background" label
const bgLabel = document.createElement('div');
Object.assign(bgLabel.style, {
color: '#8899cc',
fontSize: '1.8vmin',
fontFamily: 'Arial, sans-serif',
});
bgLabel.textContent = 'Background';
// Background buttons row
const bgRow = document.createElement('div');
Object.assign(bgRow.style, {
display: 'flex',
gap: '2vmin',
});
this._bgBtnEls = BACKGROUNDS.map((bg, i) => {
const btn = document.createElement('button');
btn.textContent = bg.label;
const isDefault = bg.key === this.selectedBg.key;
Object.assign(btn.style, {
padding: '0.7vmin 2.5vmin',
background: isDefault ? '#224488' : '#111133',
color: '#ccddff',
border: '1px solid ' + (isDefault ? '#44aaff' : '#222255'),
borderRadius: '4px',
fontSize: '1.6vmin',
fontFamily: 'Arial, sans-serif',
cursor: 'pointer',
transition: 'background 0.1s, border-color 0.1s',
});
btn.addEventListener('mouseenter', () => {
if (this.selectedBg.key !== bg.key) btn.style.background = '#1a3366';
});
btn.addEventListener('mouseleave', () => {
if (this.selectedBg.key !== bg.key) btn.style.background = '#111133';
});
btn.addEventListener('click', () => this._selectBackground(i));
bgRow.appendChild(btn);
return { el: btn, bg };
});
// Action buttons row (Back left, Start right)
const actionRow = document.createElement('div');
Object.assign(actionRow.style, {
display: 'flex',
justifyContent: 'center',
gap: '4vmin',
width: '55%',
});
const backBtn = this._makeDomBtn('← Back', '#221133', '#332244', '#443366');
backBtn.style.color = '#8899cc';
backBtn.style.flex = '1';
backBtn.addEventListener('click', () => this.scene.start('MainMenuScene'));
this._startBtnEl = this._makeDomBtn('Start Puzzle →', '#111111', '#111111', '#222222');
Object.assign(this._startBtnEl.style, {
flex: '1',
color: '#445544',
cursor: 'not-allowed',
});
this._startBtnEl.disabled = true;
this._startBtnEl._bgNormal = '#111111';
this._startBtnEl._bgHover = '#111111';
this._startBtnEl.addEventListener('click', () => this._startPuzzle());
actionRow.appendChild(backBtn);
actionRow.appendChild(this._startBtnEl);
bar.appendChild(nameRow);
bar.appendChild(diffLabel);
bar.appendChild(diffRow);
bar.appendChild(bgLabel);
bar.appendChild(bgRow);
bar.appendChild(actionRow);
uiLayer.appendChild(bar);
}
// ─── Selection ───────────────────────────────────────────────────────
_selectImage(idx) {
this.selectedImageIdx = idx;
this._cardEls.forEach((card, i) => {
const sel = i === idx;
card.style.borderColor = sel ? '#44aaff' : 'transparent';
card.style.boxShadow = sel ? '0 0 10px #44aaff88' : '';
});
this._refreshStartButton();
}
_selectDifficulty(idx, pieces) {
this.selectedPieces = pieces;
this._diffBtnEls.forEach(({ el, pieces: p }) => {
const sel = p === pieces;
el.style.background = sel ? '#224488' : '#111133';
el.style.borderColor = sel ? '#44aaff' : '#222255';
});
this._refreshStartButton();
}
_selectBackground(idx) {
this.selectedBg = BACKGROUNDS[idx];
this._bgBtnEls.forEach(({ el, bg }) => {
const sel = bg.key === this.selectedBg.key;
el.style.background = sel ? '#224488' : '#111133';
el.style.borderColor = sel ? '#44aaff' : '#222255';
});
}
_refreshStartButton() {
if (!this._startBtnEl) return;
const hasName = this._nameInput && this._nameInput.value.trim().length > 0;
const ready = this.selectedImageIdx !== null && this.selectedPieces !== null && hasName;
this._startBtnEl.disabled = !ready;
this._startBtnEl._bgNormal = ready ? '#115511' : '#111111';
this._startBtnEl._bgHover = ready ? '#227722' : '#111111';
Object.assign(this._startBtnEl.style, {
background: this._startBtnEl._bgNormal,
borderColor: ready ? '#44cc44' : '#222222',
color: ready ? '#aaffaa' : '#445544',
cursor: ready ? 'pointer' : 'not-allowed',
});
}
// ─── Helpers ─────────────────────────────────────────────────────────
_makeDomBtn(label, bgNormal, bgHover, borderColor) {
const btn = document.createElement('button');
btn.textContent = label;
btn._bgNormal = bgNormal;
btn._bgHover = bgHover;
Object.assign(btn.style, {
padding: '0.8vmin 0',
background: bgNormal,
color: '#ddeeff',
border: `1px solid ${borderColor}`,
borderRadius: '4px',
fontSize: '1.7vmin',
fontFamily: 'Arial, sans-serif',
cursor: 'pointer',
whiteSpace: 'nowrap',
textAlign: 'center',
});
btn.addEventListener('mouseenter', () => { if (!btn.disabled) btn.style.background = btn._bgHover; });
btn.addEventListener('mouseleave', () => { if (!btn.disabled) btn.style.background = btn._bgNormal; });
return btn;
}
_destroyDomUI() {
if (this._uiLayer && this._uiLayer.parentNode) {
this._uiLayer.parentNode.removeChild(this._uiLayer);
}
this._uiLayer = null;
}
// ─── Start puzzle ────────────────────────────────────────────────────
_startPuzzle() {
const name = this._nameInput ? this._nameInput.value.trim() : '';
if (this.selectedImageIdx === null || this.selectedPieces === null || !name) return;
const img = this._puzzleImages[this.selectedImageIdx];
this.scene.start('PuzzleScene', {
imageKey: img.key,
imagePath: img.path,
pieceCount: this.selectedPieces,
roomCode: generateRoomCode(),
bgKey: this.selectedBg.key,
bgPath: this.selectedBg.path,
playerName: name,
});
}
}