feat: add saved puzzle persistence and fullscreen support
- Introduce StorageManager API for tracking current room (`saveCurrent`, `loadCurrent`, `clearCurrent`) - Persist active puzzle state when joining/creating; restore on main menu load if incomplete - Add Continue Puzzle button that only activates when a valid saved puzzle exists - Implement Enter Fullscreen button with dynamic state handling and event listeners - Adjust logo/sub-logo animation positions for cleaner visual flow - Clear saved puzzle data on new puzzles or completion to avoid stale state
This commit is contained in:
parent
9a353dbacf
commit
8b30269b56
|
|
@ -1,4 +1,4 @@
|
|||
/* global Phaser, NetworkManager */
|
||||
/* global Phaser, NetworkManager, StorageManager */
|
||||
|
||||
class MainMenuScene extends Phaser.Scene {
|
||||
constructor() {
|
||||
|
|
@ -50,16 +50,15 @@ class MainMenuScene extends Phaser.Scene {
|
|||
// Logo drops in at 300ms
|
||||
this.tweens.add({
|
||||
targets: logo,
|
||||
y: 270,
|
||||
y: 240,
|
||||
alpha: 1,
|
||||
duration: 1200,
|
||||
delay: 300,
|
||||
ease: 'Cubic.easeOut',
|
||||
onComplete: () => {
|
||||
// Subtle bounce settle
|
||||
this.tweens.add({
|
||||
targets: logo,
|
||||
y: 280,
|
||||
y: 250,
|
||||
duration: 300,
|
||||
ease: 'Sine.easeInOut',
|
||||
});
|
||||
|
|
@ -69,16 +68,15 @@ class MainMenuScene extends Phaser.Scene {
|
|||
// Sub-logo rises in at 900ms (halfway through logo anim)
|
||||
this.tweens.add({
|
||||
targets: sublogo,
|
||||
y: 500,
|
||||
y: 460,
|
||||
alpha: 1,
|
||||
duration: 1000,
|
||||
delay: 900,
|
||||
ease: 'Cubic.easeOut',
|
||||
onComplete: () => {
|
||||
// Subtle bounce settle
|
||||
this.tweens.add({
|
||||
targets: sublogo,
|
||||
y: 490,
|
||||
y: 450,
|
||||
duration: 250,
|
||||
ease: 'Sine.easeInOut',
|
||||
});
|
||||
|
|
@ -87,8 +85,10 @@ class MainMenuScene extends Phaser.Scene {
|
|||
|
||||
// Buttons fade in at 1400ms (halfway through sub-logo anim)
|
||||
this.time.delayedCall(1400, () => {
|
||||
if (this._continueBtn) this._continueBtn.style.opacity = this._hasSavedPuzzle ? '1' : '0.4';
|
||||
if (this._newBtn) this._newBtn.style.opacity = '1';
|
||||
if (this._joinBtn) this._joinBtn.style.opacity = '1';
|
||||
if (this._fullscreenBtn) this._fullscreenBtn.style.opacity = '1';
|
||||
});
|
||||
|
||||
// Idle breathing pulse — starts after intro finishes, repeats every 10s
|
||||
|
|
@ -98,7 +98,6 @@ class MainMenuScene extends Phaser.Scene {
|
|||
}
|
||||
|
||||
_breathePulse(logo, logoBase, sublogo, sublogoBase) {
|
||||
// Logo scales up then back down
|
||||
this.tweens.add({
|
||||
targets: logo,
|
||||
scaleX: logoBase * 1.06,
|
||||
|
|
@ -107,7 +106,6 @@ class MainMenuScene extends Phaser.Scene {
|
|||
ease: 'Sine.easeInOut',
|
||||
yoyo: true,
|
||||
onComplete: () => {
|
||||
// Sub-logo follows after logo settles
|
||||
this.tweens.add({
|
||||
targets: sublogo,
|
||||
scaleX: sublogoBase * 1.06,
|
||||
|
|
@ -116,7 +114,6 @@ class MainMenuScene extends Phaser.Scene {
|
|||
ease: 'Sine.easeInOut',
|
||||
yoyo: true,
|
||||
onComplete: () => {
|
||||
// Schedule next pulse ~10s from now
|
||||
this.time.delayedCall(10000, () => {
|
||||
this._breathePulse(logo, logoBase, sublogo, sublogoBase);
|
||||
});
|
||||
|
|
@ -144,11 +141,41 @@ class MainMenuScene extends Phaser.Scene {
|
|||
});
|
||||
document.body.appendChild(this._uiLayer);
|
||||
|
||||
// Check for saved puzzle
|
||||
const saved = StorageManager.loadCurrent();
|
||||
this._hasSavedPuzzle = saved && !saved.completed;
|
||||
|
||||
// Continue Puzzle button
|
||||
this._continueBtn = this._makeDomBtn('Continue Puzzle', '#2e7d32', '#43a047', '#66bb6a', () => {
|
||||
const current = StorageManager.loadCurrent();
|
||||
if (current && !current.completed) {
|
||||
this.scene.start('PuzzleScene', {
|
||||
roomCode: current.roomCode,
|
||||
_restore: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
Object.assign(this._continueBtn.style, {
|
||||
top: '630px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
width: '340px',
|
||||
opacity: '0',
|
||||
transition: 'opacity 0.6s ease',
|
||||
});
|
||||
if (!this._hasSavedPuzzle) {
|
||||
this._continueBtn.disabled = true;
|
||||
this._continueBtn.style.cursor = 'not-allowed';
|
||||
this._continueBtn.style.pointerEvents = 'auto';
|
||||
}
|
||||
this._uiLayer.appendChild(this._continueBtn);
|
||||
|
||||
// New Puzzle button
|
||||
this._newBtn = this._makeDomBtn('New Puzzle', '#1565c0', '#1e88e5', '#64b5f6', () => {
|
||||
this.scene.start('NewPuzzleScene');
|
||||
});
|
||||
Object.assign(this._newBtn.style, {
|
||||
top: '630px',
|
||||
top: '550px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
width: '340px',
|
||||
|
|
@ -157,6 +184,7 @@ class MainMenuScene extends Phaser.Scene {
|
|||
});
|
||||
this._uiLayer.appendChild(this._newBtn);
|
||||
|
||||
// Join Puzzle button
|
||||
this._joinBtn = this._makeDomBtn('Join Puzzle', '#e65100', '#f57c00', '#ffb74d', () => {
|
||||
this._showJoinDialog();
|
||||
});
|
||||
|
|
@ -170,7 +198,46 @@ class MainMenuScene extends Phaser.Scene {
|
|||
});
|
||||
this._uiLayer.appendChild(this._joinBtn);
|
||||
|
||||
this.events.once('shutdown', () => this._destroyDomUI());
|
||||
// Enter Fullscreen button
|
||||
const isFullscreen = !!document.fullscreenElement;
|
||||
this._fullscreenBtn = this._makeDomBtn(
|
||||
isFullscreen ? 'Fullscreen Active' : 'Enter Fullscreen',
|
||||
'rgba(60,60,60,0.8)', 'rgba(80,80,80,0.9)', '#999999',
|
||||
() => {
|
||||
if (!document.fullscreenElement) {
|
||||
document.documentElement.requestFullscreen().catch(() => {});
|
||||
}
|
||||
}
|
||||
);
|
||||
Object.assign(this._fullscreenBtn.style, {
|
||||
top: '790px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
width: '340px',
|
||||
opacity: '0',
|
||||
transition: 'opacity 0.6s ease',
|
||||
});
|
||||
if (isFullscreen) {
|
||||
this._fullscreenBtn.disabled = true;
|
||||
this._fullscreenBtn.style.cursor = 'default';
|
||||
this._fullscreenBtn.style.opacity = '0.4';
|
||||
}
|
||||
this._uiLayer.appendChild(this._fullscreenBtn);
|
||||
|
||||
// Listen for fullscreen changes
|
||||
this._onFullscreenChange = () => {
|
||||
const fs = !!document.fullscreenElement;
|
||||
this._fullscreenBtn.textContent = fs ? 'Fullscreen Active' : 'Enter Fullscreen';
|
||||
this._fullscreenBtn.disabled = fs;
|
||||
this._fullscreenBtn.style.cursor = fs ? 'default' : 'pointer';
|
||||
this._fullscreenBtn.style.opacity = fs ? '0.4' : '1';
|
||||
};
|
||||
document.addEventListener('fullscreenchange', this._onFullscreenChange);
|
||||
|
||||
this.events.once('shutdown', () => {
|
||||
document.removeEventListener('fullscreenchange', this._onFullscreenChange);
|
||||
this._destroyDomUI();
|
||||
});
|
||||
}
|
||||
|
||||
_showJoinDialog() {
|
||||
|
|
@ -400,9 +467,9 @@ class MainMenuScene extends Phaser.Scene {
|
|||
whiteSpace: 'nowrap',
|
||||
textAlign: 'center',
|
||||
});
|
||||
btn.addEventListener('mouseenter', () => { btn.style.background = bgHover; });
|
||||
btn.addEventListener('mouseleave', () => { btn.style.background = bgNormal; });
|
||||
btn.addEventListener('mousedown', () => { btn.style.transform = (btn.style.transform || '') + ' scale(0.97)'; });
|
||||
btn.addEventListener('mouseenter', () => { if (!btn.disabled) btn.style.background = bgHover; });
|
||||
btn.addEventListener('mouseleave', () => { if (!btn.disabled) btn.style.background = bgNormal; });
|
||||
btn.addEventListener('mousedown', () => { if (!btn.disabled) btn.style.transform = (btn.style.transform || '') + ' scale(0.97)'; });
|
||||
btn.addEventListener('mouseup', () => { btn.style.transform = btn.style.transform.replace(' scale(0.97)', ''); });
|
||||
btn.addEventListener('click', onClick);
|
||||
return btn;
|
||||
|
|
|
|||
|
|
@ -202,6 +202,11 @@ class PuzzleScene extends Phaser.Scene {
|
|||
// ─── Scene construction ──────────────────────────────────────────────
|
||||
|
||||
_buildScene(saved) {
|
||||
// Clear old saved puzzle when starting fresh (not restoring or joining)
|
||||
if (!saved) {
|
||||
StorageManager.clearCurrent();
|
||||
}
|
||||
|
||||
const { width, height } = this.sys.game.config;
|
||||
|
||||
// Tile the background texture across the playfield, centered on the world
|
||||
|
|
@ -518,6 +523,7 @@ class PuzzleScene extends Phaser.Scene {
|
|||
_onNetworkCompleted() {
|
||||
if (!this._completed) {
|
||||
this._completed = true;
|
||||
StorageManager.clearCurrent();
|
||||
this._showCompletion(true);
|
||||
}
|
||||
}
|
||||
|
|
@ -871,6 +877,7 @@ class PuzzleScene extends Phaser.Scene {
|
|||
const gm = this._groupManager;
|
||||
if (gm.groupCount === 1 && gm.pieceCount === this._pieces.length) {
|
||||
this._completed = true;
|
||||
StorageManager.clearCurrent();
|
||||
this._showCompletion(true);
|
||||
}
|
||||
}
|
||||
|
|
@ -1301,6 +1308,7 @@ class PuzzleScene extends Phaser.Scene {
|
|||
bgPath: this._bgPath,
|
||||
});
|
||||
StorageManager.save(state);
|
||||
StorageManager.saveCurrent(this.cfg.roomCode);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -27,4 +27,20 @@ class StorageManager {
|
|||
static remove(roomCode) {
|
||||
localStorage.removeItem(StorageManager._key(roomCode));
|
||||
}
|
||||
|
||||
static saveCurrent(roomCode) {
|
||||
localStorage.setItem('ipuzzle_currentRoom', roomCode);
|
||||
}
|
||||
|
||||
static loadCurrent() {
|
||||
const roomCode = localStorage.getItem('ipuzzle_currentRoom');
|
||||
if (!roomCode) return null;
|
||||
return StorageManager.load(roomCode);
|
||||
}
|
||||
|
||||
static clearCurrent() {
|
||||
const roomCode = localStorage.getItem('ipuzzle_currentRoom');
|
||||
if (roomCode) StorageManager.remove(roomCode);
|
||||
localStorage.removeItem('ipuzzle_currentRoom');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue