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 {
|
class MainMenuScene extends Phaser.Scene {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -50,16 +50,15 @@ class MainMenuScene extends Phaser.Scene {
|
||||||
// Logo drops in at 300ms
|
// Logo drops in at 300ms
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: logo,
|
targets: logo,
|
||||||
y: 270,
|
y: 240,
|
||||||
alpha: 1,
|
alpha: 1,
|
||||||
duration: 1200,
|
duration: 1200,
|
||||||
delay: 300,
|
delay: 300,
|
||||||
ease: 'Cubic.easeOut',
|
ease: 'Cubic.easeOut',
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
// Subtle bounce settle
|
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: logo,
|
targets: logo,
|
||||||
y: 280,
|
y: 250,
|
||||||
duration: 300,
|
duration: 300,
|
||||||
ease: 'Sine.easeInOut',
|
ease: 'Sine.easeInOut',
|
||||||
});
|
});
|
||||||
|
|
@ -69,16 +68,15 @@ class MainMenuScene extends Phaser.Scene {
|
||||||
// Sub-logo rises in at 900ms (halfway through logo anim)
|
// Sub-logo rises in at 900ms (halfway through logo anim)
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: sublogo,
|
targets: sublogo,
|
||||||
y: 500,
|
y: 460,
|
||||||
alpha: 1,
|
alpha: 1,
|
||||||
duration: 1000,
|
duration: 1000,
|
||||||
delay: 900,
|
delay: 900,
|
||||||
ease: 'Cubic.easeOut',
|
ease: 'Cubic.easeOut',
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
// Subtle bounce settle
|
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: sublogo,
|
targets: sublogo,
|
||||||
y: 490,
|
y: 450,
|
||||||
duration: 250,
|
duration: 250,
|
||||||
ease: 'Sine.easeInOut',
|
ease: 'Sine.easeInOut',
|
||||||
});
|
});
|
||||||
|
|
@ -87,8 +85,10 @@ class MainMenuScene extends Phaser.Scene {
|
||||||
|
|
||||||
// Buttons fade in at 1400ms (halfway through sub-logo anim)
|
// Buttons fade in at 1400ms (halfway through sub-logo anim)
|
||||||
this.time.delayedCall(1400, () => {
|
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._newBtn) this._newBtn.style.opacity = '1';
|
||||||
if (this._joinBtn) this._joinBtn.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
|
// Idle breathing pulse — starts after intro finishes, repeats every 10s
|
||||||
|
|
@ -98,7 +98,6 @@ class MainMenuScene extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
_breathePulse(logo, logoBase, sublogo, sublogoBase) {
|
_breathePulse(logo, logoBase, sublogo, sublogoBase) {
|
||||||
// Logo scales up then back down
|
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: logo,
|
targets: logo,
|
||||||
scaleX: logoBase * 1.06,
|
scaleX: logoBase * 1.06,
|
||||||
|
|
@ -107,7 +106,6 @@ class MainMenuScene extends Phaser.Scene {
|
||||||
ease: 'Sine.easeInOut',
|
ease: 'Sine.easeInOut',
|
||||||
yoyo: true,
|
yoyo: true,
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
// Sub-logo follows after logo settles
|
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: sublogo,
|
targets: sublogo,
|
||||||
scaleX: sublogoBase * 1.06,
|
scaleX: sublogoBase * 1.06,
|
||||||
|
|
@ -116,7 +114,6 @@ class MainMenuScene extends Phaser.Scene {
|
||||||
ease: 'Sine.easeInOut',
|
ease: 'Sine.easeInOut',
|
||||||
yoyo: true,
|
yoyo: true,
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
// Schedule next pulse ~10s from now
|
|
||||||
this.time.delayedCall(10000, () => {
|
this.time.delayedCall(10000, () => {
|
||||||
this._breathePulse(logo, logoBase, sublogo, sublogoBase);
|
this._breathePulse(logo, logoBase, sublogo, sublogoBase);
|
||||||
});
|
});
|
||||||
|
|
@ -144,11 +141,41 @@ class MainMenuScene extends Phaser.Scene {
|
||||||
});
|
});
|
||||||
document.body.appendChild(this._uiLayer);
|
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._newBtn = this._makeDomBtn('New Puzzle', '#1565c0', '#1e88e5', '#64b5f6', () => {
|
||||||
this.scene.start('NewPuzzleScene');
|
this.scene.start('NewPuzzleScene');
|
||||||
});
|
});
|
||||||
Object.assign(this._newBtn.style, {
|
Object.assign(this._newBtn.style, {
|
||||||
top: '630px',
|
top: '550px',
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translateX(-50%)',
|
transform: 'translateX(-50%)',
|
||||||
width: '340px',
|
width: '340px',
|
||||||
|
|
@ -157,6 +184,7 @@ class MainMenuScene extends Phaser.Scene {
|
||||||
});
|
});
|
||||||
this._uiLayer.appendChild(this._newBtn);
|
this._uiLayer.appendChild(this._newBtn);
|
||||||
|
|
||||||
|
// Join Puzzle button
|
||||||
this._joinBtn = this._makeDomBtn('Join Puzzle', '#e65100', '#f57c00', '#ffb74d', () => {
|
this._joinBtn = this._makeDomBtn('Join Puzzle', '#e65100', '#f57c00', '#ffb74d', () => {
|
||||||
this._showJoinDialog();
|
this._showJoinDialog();
|
||||||
});
|
});
|
||||||
|
|
@ -170,7 +198,46 @@ class MainMenuScene extends Phaser.Scene {
|
||||||
});
|
});
|
||||||
this._uiLayer.appendChild(this._joinBtn);
|
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() {
|
_showJoinDialog() {
|
||||||
|
|
@ -400,9 +467,9 @@ class MainMenuScene extends Phaser.Scene {
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
});
|
});
|
||||||
btn.addEventListener('mouseenter', () => { btn.style.background = bgHover; });
|
btn.addEventListener('mouseenter', () => { if (!btn.disabled) btn.style.background = bgHover; });
|
||||||
btn.addEventListener('mouseleave', () => { btn.style.background = bgNormal; });
|
btn.addEventListener('mouseleave', () => { if (!btn.disabled) btn.style.background = bgNormal; });
|
||||||
btn.addEventListener('mousedown', () => { btn.style.transform = (btn.style.transform || '') + ' scale(0.97)'; });
|
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('mouseup', () => { btn.style.transform = btn.style.transform.replace(' scale(0.97)', ''); });
|
||||||
btn.addEventListener('click', onClick);
|
btn.addEventListener('click', onClick);
|
||||||
return btn;
|
return btn;
|
||||||
|
|
|
||||||
|
|
@ -202,6 +202,11 @@ class PuzzleScene extends Phaser.Scene {
|
||||||
// ─── Scene construction ──────────────────────────────────────────────
|
// ─── Scene construction ──────────────────────────────────────────────
|
||||||
|
|
||||||
_buildScene(saved) {
|
_buildScene(saved) {
|
||||||
|
// Clear old saved puzzle when starting fresh (not restoring or joining)
|
||||||
|
if (!saved) {
|
||||||
|
StorageManager.clearCurrent();
|
||||||
|
}
|
||||||
|
|
||||||
const { width, height } = this.sys.game.config;
|
const { width, height } = this.sys.game.config;
|
||||||
|
|
||||||
// Tile the background texture across the playfield, centered on the world
|
// Tile the background texture across the playfield, centered on the world
|
||||||
|
|
@ -518,6 +523,7 @@ class PuzzleScene extends Phaser.Scene {
|
||||||
_onNetworkCompleted() {
|
_onNetworkCompleted() {
|
||||||
if (!this._completed) {
|
if (!this._completed) {
|
||||||
this._completed = true;
|
this._completed = true;
|
||||||
|
StorageManager.clearCurrent();
|
||||||
this._showCompletion(true);
|
this._showCompletion(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -871,6 +877,7 @@ class PuzzleScene extends Phaser.Scene {
|
||||||
const gm = this._groupManager;
|
const gm = this._groupManager;
|
||||||
if (gm.groupCount === 1 && gm.pieceCount === this._pieces.length) {
|
if (gm.groupCount === 1 && gm.pieceCount === this._pieces.length) {
|
||||||
this._completed = true;
|
this._completed = true;
|
||||||
|
StorageManager.clearCurrent();
|
||||||
this._showCompletion(true);
|
this._showCompletion(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1301,6 +1308,7 @@ class PuzzleScene extends Phaser.Scene {
|
||||||
bgPath: this._bgPath,
|
bgPath: this._bgPath,
|
||||||
});
|
});
|
||||||
StorageManager.save(state);
|
StorageManager.save(state);
|
||||||
|
StorageManager.saveCurrent(this.cfg.roomCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,4 +27,20 @@ class StorageManager {
|
||||||
static remove(roomCode) {
|
static remove(roomCode) {
|
||||||
localStorage.removeItem(StorageManager._key(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