473 lines
16 KiB
JavaScript
473 lines
16 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' }
|
||
];
|
||
|
||
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 = 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));
|
||
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: '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,
|
||
});
|
||
}
|
||
}
|