iPuzzle/js/scenes/NewPuzzleScene.js

539 lines
18 KiB
JavaScript

/* 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 — centered in the top bar area
this.add.text(960, 45, '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, 88, 1843, 88);
// Bright centre segment
sep.lineStyle(2, 0x1e88e5, 1);
sep.lineBetween(653, 88, 1267, 88);
// Diamond accent at the midpoint
sep.fillStyle(0xffb74d, 1);
sep.fillTriangle(960, 83, 966, 88, 960, 93);
sep.fillTriangle(960, 83, 954, 88, 960, 93);
// 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 ──────────────────────────────────────────────────────────
_syncOverlayToCanvas() {
if (!this._uiLayer) return;
const rect = this.sys.game.canvas.getBoundingClientRect();
Object.assign(this._uiLayer.style, {
left: rect.left + 'px',
top: rect.top + 'px',
transform: `scale(${rect.width / 1920}, ${rect.height / 1080})`,
});
}
_buildDomUI() {
this._uiLayer = document.createElement('div');
Object.assign(this._uiLayer.style, {
position: 'fixed',
width: '1920px',
height: '1080px',
transformOrigin: 'top left',
pointerEvents: 'none',
zIndex: '10',
});
document.body.appendChild(this._uiLayer);
this._syncOverlayToCanvas();
// Keep overlay aligned when the window resizes
this._onResizeScale = () => this._syncOverlayToCanvas();
this.scale.on('resize', this._onResizeScale);
window.addEventListener('resize', this._onResizeScale);
this._buildPuzzleGrid(this._uiLayer);
this._buildControlsBar(this._uiLayer);
this._refreshStartButton();
this.events.once('shutdown', () => {
if (this._onResizeScale) {
this.scale.off('resize', this._onResizeScale);
window.removeEventListener('resize', this._onResizeScale);
}
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,
});
}
}