/* 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' }, { pieces: 140, label: '140 Pieces' }, { pieces: 180, label: '180 Pieces' }, { pieces: 250, label: '250 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'); // Loading progress text this._loadingText = this.add.text(960, 540, 'Loading 0%...', { fontFamily: 'Arial, sans-serif', fontSize: '36px', color: '#ffffff', stroke: '#000000', strokeThickness: 4, }).setOrigin(0.5); this._onLoadProgress = (value) => { if (this._loadingText) { this._loadingText.setText(`Loading ${Math.round(value * 100)}%...`); } }; this._onLoadComplete = () => { if (this._loadingText) { this._loadingText.destroy(); this._loadingText = null; } this.load.off('progress', this._onLoadProgress); this.load.off('complete', this._onLoadComplete); }; this.load.on('progress', this._onLoadProgress); this.load.on('complete', this._onLoadComplete); } create() { this.selectedImageIdx = null; this.selectedPieces = 60; this.selectedBg = BACKGROUNDS[0]; // default to dark wood const W = this.sys.game.config.width; const H = this.sys.game.config.height; // Background if (!this.textures.exists('main_menu_bg')) { this.load.image('main_menu_bg', 'assets/images/ui/main_menu.png'); this.load.once('complete', () => this.add.image(960, 540, 'main_menu_bg').setDisplaySize(1920, 1080).setDepth(-1)); this.load.start(); } else { this.add.image(960, 540, 'main_menu_bg').setDisplaySize(1920, 1080); } // Title — fixed position (canvas is always 1920×1080) this.add.text(960, -30, 'Choose Your Puzzle', { fontFamily: 'Georgia, serif', fontSize: '58px', color: '#ffffff', stroke: '#1565c0', strokeThickness: 6, shadow: { offsetX: 0, offsetY: 0, color: '#1e88e5', 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, 0x64b5f6, 0.4); sep.lineBetween(77, -81, 1843, -81); // Bright centre segment sep.lineStyle(2, 0x1e88e5, 1); sep.lineBetween(653, -81, 1267, -81); // Diamond accent at the midpoint sep.fillStyle(0xffb74d, 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)); // Show loading text for thumbnail loading this._loadingText = this.add.text(960, 540, 'Loading 0%...', { fontFamily: 'Arial, sans-serif', fontSize: '36px', color: '#ffffff', stroke: '#000000', strokeThickness: 4, }).setOrigin(0.5).setDepth(9999); this.load.on('progress', (value) => { if (this._loadingText) { this._loadingText.setText(`Loading ${Math.round(value * 100)}%...`); } }); this.load.once('complete', () => { if (this._loadingText) { this._loadingText.destroy(); this._loadingText = null; } 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: 'rgba(10, 10, 30, 0.8)', }); 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: '#e0e0e0', 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 = '#64b5f6'; } }); 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 rgba(139, 109, 78, 0.4)', background: 'rgba(42, 28, 16, 0.88)', 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: '#e0e0e0', fontSize: '1.8vmin', fontFamily: 'Arial, sans-serif', }); nameLabel.textContent = 'Player Name'; this._nameInput = document.createElement('input'); Object.assign(this._nameInput.style, { background: 'rgba(10, 10, 30, 0.9)', color: '#ffffff', border: '2px solid #1e88e5', 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: '#90a4ae', 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: '#e0e0e0', 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; const isDefault = diff.pieces === this.selectedPieces; Object.assign(btn.style, { padding: '0.7vmin 2.5vmin', background: isDefault ? '#1565c0' : 'rgba(10, 10, 30, 0.8)', color: '#e0e0e0', border: '1px solid ' + (isDefault ? '#64b5f6' : 'rgba(100, 181, 246, 0.3)'), 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 = '#1565c0'; }); btn.addEventListener('mouseleave', () => { if (this.selectedPieces !== diff.pieces) btn.style.background = 'rgba(10, 10, 30, 0.8)'; }); 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: '#e0e0e0', 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 ? '#1565c0' : 'rgba(10, 10, 30, 0.8)', color: '#e0e0e0', border: '1px solid ' + (isDefault ? '#64b5f6' : 'rgba(100, 181, 246, 0.3)'), 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 = '#1565c0'; }); btn.addEventListener('mouseleave', () => { if (this.selectedBg.key !== bg.key) btn.style.background = 'rgba(10, 10, 30, 0.8)'; }); 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', 'rgba(10, 10, 30, 0.8)', '#c62828', '#ef5350'); backBtn.style.color = '#e0e0e0'; backBtn.style.flex = '1'; backBtn.addEventListener('click', () => this.scene.start('MainMenuScene')); this._startBtnEl = this._makeDomBtn('Start Puzzle →', 'rgba(10, 10, 30, 0.8)', 'rgba(10, 10, 30, 0.8)', 'rgba(100, 100, 100, 0.3)'); Object.assign(this._startBtnEl.style, { flex: '1', color: '#666666', cursor: 'not-allowed', }); this._startBtnEl.disabled = true; this._startBtnEl._bgNormal = 'rgba(10, 10, 30, 0.8)'; this._startBtnEl._bgHover = 'rgba(10, 10, 30, 0.8)'; 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 ? '#f57c00' : 'transparent'; card.style.boxShadow = sel ? '0 0 12px rgba(245, 124, 0, 0.5)' : ''; }); this._refreshStartButton(); } _selectDifficulty(idx, pieces) { this.selectedPieces = pieces; this._diffBtnEls.forEach(({ el, pieces: p }) => { const sel = p === pieces; el.style.background = sel ? '#1565c0' : 'rgba(10, 10, 30, 0.8)'; el.style.borderColor = sel ? '#64b5f6' : 'rgba(100, 181, 246, 0.3)'; }); this._refreshStartButton(); } _selectBackground(idx) { this.selectedBg = BACKGROUNDS[idx]; this._bgBtnEls.forEach(({ el, bg }) => { const sel = bg.key === this.selectedBg.key; el.style.background = sel ? '#1565c0' : 'rgba(10, 10, 30, 0.8)'; el.style.borderColor = sel ? '#64b5f6' : 'rgba(100, 181, 246, 0.3)'; }); } _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 ? '#2e7d32' : 'rgba(10, 10, 30, 0.8)'; this._startBtnEl._bgHover = ready ? '#43a047' : 'rgba(10, 10, 30, 0.8)'; Object.assign(this._startBtnEl.style, { background: this._startBtnEl._bgNormal, borderColor: ready ? '#66bb6a' : 'rgba(100, 100, 100, 0.3)', color: ready ? '#ffffff' : '#666666', 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: '#e0e0e0', 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, }); } }