feat: add real-time puzzle timer and player list UI
- Persist puzzle start time in state for accurate session tracking - Display elapsed time (HH:MM:SS) and connected players in a new "Room Stats" panel - Update players list on join/left events and network sync - Refine UI button styling for consistency with main theme
This commit is contained in:
parent
e71dab9c45
commit
35f050dfeb
|
|
@ -178,6 +178,9 @@ class PuzzleScene extends Phaser.Scene {
|
||||||
update() {
|
update() {
|
||||||
if (!this._ready) return;
|
if (!this._ready) return;
|
||||||
|
|
||||||
|
// Update puzzle timer once per second
|
||||||
|
this._updateTimer();
|
||||||
|
|
||||||
// Fallback: detect when music track has finished (Phaser 3.9 'complete' event can be unreliable)
|
// Fallback: detect when music track has finished (Phaser 3.9 'complete' event can be unreliable)
|
||||||
if (this._currentMusic && this._musicStarted && !this._currentMusic.isPlaying && !this._currentMusic.isPaused) {
|
if (this._currentMusic && this._musicStarted && !this._currentMusic.isPlaying && !this._currentMusic.isPaused) {
|
||||||
this._playNextTrack();
|
this._playNextTrack();
|
||||||
|
|
@ -310,6 +313,9 @@ class PuzzleScene extends Phaser.Scene {
|
||||||
this._panLastY = 0;
|
this._panLastY = 0;
|
||||||
this._ready = true;
|
this._ready = true;
|
||||||
|
|
||||||
|
// Puzzle timer — restore from saved state or start fresh
|
||||||
|
this._startTime = (saved && saved.startTime) ? saved.startTime : Date.now();
|
||||||
|
|
||||||
// Box selection state (CTRL+drag)
|
// Box selection state (CTRL+drag)
|
||||||
this._isBoxSelecting = false;
|
this._isBoxSelecting = false;
|
||||||
this._boxStartWorld = null; // { x, y } in world coords
|
this._boxStartWorld = null; // { x, y } in world coords
|
||||||
|
|
@ -396,6 +402,7 @@ class PuzzleScene extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
this._setupNetworkListeners();
|
this._setupNetworkListeners();
|
||||||
|
this._updatePlayersList();
|
||||||
} else if (NetworkManager.connected) {
|
} else if (NetworkManager.connected) {
|
||||||
// Host — create the room on the server
|
// Host — create the room on the server
|
||||||
this._isNetworked = true;
|
this._isNetworked = true;
|
||||||
|
|
@ -412,6 +419,7 @@ class PuzzleScene extends Phaser.Scene {
|
||||||
completed: false,
|
completed: false,
|
||||||
bgKey: this._bgKey,
|
bgKey: this._bgKey,
|
||||||
bgPath: this._bgPath,
|
bgPath: this._bgPath,
|
||||||
|
startTime: this._startTime,
|
||||||
});
|
});
|
||||||
NetworkManager.createRoom(this.cfg.roomCode, state.serialize(), this.cfg.playerName);
|
NetworkManager.createRoom(this.cfg.roomCode, state.serialize(), this.cfg.playerName);
|
||||||
this._setupNetworkListeners();
|
this._setupNetworkListeners();
|
||||||
|
|
@ -548,6 +556,7 @@ class PuzzleScene extends Phaser.Scene {
|
||||||
this._playerNames.set(msg.playerId, msg.playerName);
|
this._playerNames.set(msg.playerId, msg.playerName);
|
||||||
}
|
}
|
||||||
console.log(`${msg.playerName || 'Player ' + msg.playerId} joined the room`);
|
console.log(`${msg.playerName || 'Player ' + msg.playerId} joined the room`);
|
||||||
|
this._updatePlayersList();
|
||||||
}
|
}
|
||||||
|
|
||||||
_onNetworkPlayerLeft(msg) {
|
_onNetworkPlayerLeft(msg) {
|
||||||
|
|
@ -564,6 +573,7 @@ class PuzzleScene extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
this._playerNames.delete(msg.playerId);
|
this._playerNames.delete(msg.playerId);
|
||||||
console.log(`Player ${msg.playerId} left the room`);
|
console.log(`Player ${msg.playerId} left the room`);
|
||||||
|
this._updatePlayersList();
|
||||||
}
|
}
|
||||||
|
|
||||||
_onNetworkCompleted() {
|
_onNetworkCompleted() {
|
||||||
|
|
@ -1138,6 +1148,43 @@ class PuzzleScene extends Phaser.Scene {
|
||||||
this.time.delayedCall(animate ? 1300 : 0, () => this._showDomCompletion());
|
this.time.delayedCall(animate ? 1300 : 0, () => this._showDomCompletion());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Room Stats helpers ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
_updateTimer() {
|
||||||
|
if (!this._timerEl || !this._startTime) return;
|
||||||
|
const elapsed = Math.floor((Date.now() - this._startTime) / 1000);
|
||||||
|
const h = Math.floor(elapsed / 3600);
|
||||||
|
const m = Math.floor((elapsed % 3600) / 60);
|
||||||
|
const s = elapsed % 60;
|
||||||
|
this._timerEl.textContent =
|
||||||
|
String(h).padStart(2, '0') + ':' +
|
||||||
|
String(m).padStart(2, '0') + ':' +
|
||||||
|
String(s).padStart(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
_updatePlayersList() {
|
||||||
|
if (!this._playersListEl) return;
|
||||||
|
const names = [];
|
||||||
|
// Add local player first
|
||||||
|
if (this.cfg.playerName) {
|
||||||
|
names.push(this.cfg.playerName + ' (you)');
|
||||||
|
}
|
||||||
|
// Add remote players
|
||||||
|
this._playerNames.forEach((name) => {
|
||||||
|
names.push(name);
|
||||||
|
});
|
||||||
|
if (names.length === 0) {
|
||||||
|
this._playersListEl.textContent = '\u2014';
|
||||||
|
} else {
|
||||||
|
this._playersListEl.innerHTML = '';
|
||||||
|
names.forEach(n => {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.textContent = n;
|
||||||
|
this._playersListEl.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─── DOM UI ──────────────────────────────────────────────────────────
|
// ─── DOM UI ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// ─── Music ──────────────────────────────────────────────────────────
|
// ─── Music ──────────────────────────────────────────────────────────
|
||||||
|
|
@ -1384,9 +1431,68 @@ class PuzzleScene extends Phaser.Scene {
|
||||||
this._saveState();
|
this._saveState();
|
||||||
this.scene.start('MainMenuScene');
|
this.scene.start('MainMenuScene');
|
||||||
});
|
});
|
||||||
Object.assign(backBtn.style, { top: '1%', left: '0.5%' });
|
Object.assign(backBtn.style, { top: '10px', left: '10px' });
|
||||||
this._uiLayer.appendChild(backBtn);
|
this._uiLayer.appendChild(backBtn);
|
||||||
|
|
||||||
|
// Room Stats panel — below menu button
|
||||||
|
const statsPanel = document.createElement('div');
|
||||||
|
Object.assign(statsPanel.style, {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50px',
|
||||||
|
left: '10px',
|
||||||
|
background: 'rgba(0, 0, 0, 0.55)',
|
||||||
|
padding: '10px 16px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontFamily: 'Arial, sans-serif',
|
||||||
|
color: '#ddeeff',
|
||||||
|
fontSize: '14px',
|
||||||
|
lineHeight: '1.6',
|
||||||
|
minWidth: '160px',
|
||||||
|
});
|
||||||
|
|
||||||
|
const statsTitle = document.createElement('div');
|
||||||
|
Object.assign(statsTitle.style, {
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: '15px',
|
||||||
|
marginBottom: '6px',
|
||||||
|
color: '#ddeeff',
|
||||||
|
letterSpacing: '0.05em',
|
||||||
|
});
|
||||||
|
statsTitle.textContent = 'Room Stats';
|
||||||
|
statsPanel.appendChild(statsTitle);
|
||||||
|
|
||||||
|
// Puzzle Time
|
||||||
|
const timeLabel = document.createElement('div');
|
||||||
|
Object.assign(timeLabel.style, { color: '#8899bb', fontSize: '12px', marginTop: '4px' });
|
||||||
|
timeLabel.textContent = 'Puzzle Time';
|
||||||
|
statsPanel.appendChild(timeLabel);
|
||||||
|
|
||||||
|
this._timerEl = document.createElement('div');
|
||||||
|
Object.assign(this._timerEl.style, {
|
||||||
|
color: '#ddeeff',
|
||||||
|
fontSize: '16px',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
});
|
||||||
|
this._timerEl.textContent = '00:00:00';
|
||||||
|
statsPanel.appendChild(this._timerEl);
|
||||||
|
|
||||||
|
// Players
|
||||||
|
const playersLabel = document.createElement('div');
|
||||||
|
Object.assign(playersLabel.style, { color: '#8899bb', fontSize: '12px', marginTop: '8px' });
|
||||||
|
playersLabel.textContent = 'Players';
|
||||||
|
statsPanel.appendChild(playersLabel);
|
||||||
|
|
||||||
|
this._playersListEl = document.createElement('div');
|
||||||
|
Object.assign(this._playersListEl.style, {
|
||||||
|
color: '#ddeeff',
|
||||||
|
fontSize: '13px',
|
||||||
|
});
|
||||||
|
statsPanel.appendChild(this._playersListEl);
|
||||||
|
|
||||||
|
this._uiLayer.appendChild(statsPanel);
|
||||||
|
this._updatePlayersList();
|
||||||
|
|
||||||
// Music controls — top-right
|
// Music controls — top-right
|
||||||
const musicRow = document.createElement('div');
|
const musicRow = document.createElement('div');
|
||||||
Object.assign(musicRow.style, {
|
Object.assign(musicRow.style, {
|
||||||
|
|
@ -1399,9 +1505,9 @@ class PuzzleScene extends Phaser.Scene {
|
||||||
});
|
});
|
||||||
|
|
||||||
const btnStyle = {
|
const btnStyle = {
|
||||||
background: 'rgba(0, 0, 0, 0.55)',
|
background: '#1565c0',
|
||||||
color: '#ddeeff',
|
color: '#ddeeff',
|
||||||
border: '1px solid #4477bb',
|
border: '1px solid #64b5f6',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
fontSize: '1.8vmin',
|
fontSize: '1.8vmin',
|
||||||
fontFamily: 'Arial, sans-serif',
|
fontFamily: 'Arial, sans-serif',
|
||||||
|
|
@ -1415,16 +1521,16 @@ class PuzzleScene extends Phaser.Scene {
|
||||||
skipBtn.textContent = '\u23ED'; // next track symbol
|
skipBtn.textContent = '\u23ED'; // next track symbol
|
||||||
skipBtn.title = 'Next Track';
|
skipBtn.title = 'Next Track';
|
||||||
Object.assign(skipBtn.style, btnStyle);
|
Object.assign(skipBtn.style, btnStyle);
|
||||||
skipBtn.addEventListener('mouseenter', () => { skipBtn.style.background = '#223355'; });
|
skipBtn.addEventListener('mouseenter', () => { skipBtn.style.background = '#1e88e5'; });
|
||||||
skipBtn.addEventListener('mouseleave', () => { skipBtn.style.background = 'rgba(0, 0, 0, 0.55)'; });
|
skipBtn.addEventListener('mouseleave', () => { skipBtn.style.background = '#1565c0'; });
|
||||||
skipBtn.addEventListener('click', () => this._skipTrack());
|
skipBtn.addEventListener('click', () => this._skipTrack());
|
||||||
|
|
||||||
// Mute/unmute button
|
// Mute/unmute button
|
||||||
this._muteBtn = document.createElement('button');
|
this._muteBtn = document.createElement('button');
|
||||||
this._muteBtn.title = 'Mute / Unmute';
|
this._muteBtn.title = 'Mute / Unmute';
|
||||||
Object.assign(this._muteBtn.style, btnStyle);
|
Object.assign(this._muteBtn.style, btnStyle);
|
||||||
this._muteBtn.addEventListener('mouseenter', () => { this._muteBtn.style.background = '#223355'; });
|
this._muteBtn.addEventListener('mouseenter', () => { this._muteBtn.style.background = '#1e88e5'; });
|
||||||
this._muteBtn.addEventListener('mouseleave', () => { this._muteBtn.style.background = 'rgba(0, 0, 0, 0.55)'; });
|
this._muteBtn.addEventListener('mouseleave', () => { this._muteBtn.style.background = '#1565c0'; });
|
||||||
this._muteBtn.addEventListener('click', () => {
|
this._muteBtn.addEventListener('click', () => {
|
||||||
this._toggleMute();
|
this._toggleMute();
|
||||||
this._muteBtn.textContent = this._musicMuted ? '\uD83D\uDD07' : '\uD83D\uDD0A';
|
this._muteBtn.textContent = this._musicMuted ? '\uD83D\uDD07' : '\uD83D\uDD0A';
|
||||||
|
|
@ -1517,9 +1623,9 @@ class PuzzleScene extends Phaser.Scene {
|
||||||
Object.assign(btn.style, {
|
Object.assign(btn.style, {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
padding: '0.8vmin 2vmin',
|
padding: '0.8vmin 2vmin',
|
||||||
background: '#1a2a4a',
|
background: '#1565c0',
|
||||||
color: '#ddeeff',
|
color: '#ddeeff',
|
||||||
border: '1px solid #4477bb',
|
border: '1px solid #64b5f6',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
fontSize: '1.6vmin',
|
fontSize: '1.6vmin',
|
||||||
fontFamily: 'Arial, sans-serif',
|
fontFamily: 'Arial, sans-serif',
|
||||||
|
|
@ -1528,8 +1634,8 @@ class PuzzleScene extends Phaser.Scene {
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
});
|
});
|
||||||
// Hover effect
|
// Hover effect
|
||||||
btn.addEventListener('mouseenter', () => { btn.style.background = '#223355'; btn.style.borderColor = '#66aaff'; });
|
btn.addEventListener('mouseenter', () => { btn.style.background = '#1e88e5'; btn.style.borderColor = '#64b5f6'; });
|
||||||
btn.addEventListener('mouseleave', () => { btn.style.background = '#1a2a4a'; btn.style.borderColor = '#4477bb'; });
|
btn.addEventListener('mouseleave', () => { btn.style.background = '#1565c0'; btn.style.borderColor = '#64b5f6'; });
|
||||||
btn.addEventListener('click', onClick);
|
btn.addEventListener('click', onClick);
|
||||||
return btn;
|
return btn;
|
||||||
}
|
}
|
||||||
|
|
@ -1595,6 +1701,7 @@ class PuzzleScene extends Phaser.Scene {
|
||||||
completed: this._completed || false,
|
completed: this._completed || false,
|
||||||
bgKey: this._bgKey,
|
bgKey: this._bgKey,
|
||||||
bgPath: this._bgPath,
|
bgPath: this._bgPath,
|
||||||
|
startTime: this._startTime,
|
||||||
});
|
});
|
||||||
StorageManager.save(state);
|
StorageManager.save(state);
|
||||||
StorageManager.saveCurrent(this.cfg.roomCode);
|
StorageManager.saveCurrent(this.cfg.roomCode);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
class PuzzleState {
|
class PuzzleState {
|
||||||
constructor({ imageKey, imagePath, pieceCount, roomCode, cols, rows, pieces, groups, completed, bgKey, bgPath }) {
|
constructor({ imageKey, imagePath, pieceCount, roomCode, cols, rows, pieces, groups, completed, bgKey, bgPath, startTime }) {
|
||||||
this.imageKey = imageKey;
|
this.imageKey = imageKey;
|
||||||
this.imagePath = imagePath;
|
this.imagePath = imagePath;
|
||||||
this.pieceCount = pieceCount;
|
this.pieceCount = pieceCount;
|
||||||
|
|
@ -11,6 +11,7 @@ class PuzzleState {
|
||||||
this.completed = completed || false;
|
this.completed = completed || false;
|
||||||
this.bgKey = bgKey || 'bg_dark_wood';
|
this.bgKey = bgKey || 'bg_dark_wood';
|
||||||
this.bgPath = bgPath || 'assets/images/ui/dark_wood.jpg';
|
this.bgPath = bgPath || 'assets/images/ui/dark_wood.jpg';
|
||||||
|
this.startTime = startTime || Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
serialize() {
|
serialize() {
|
||||||
|
|
@ -33,6 +34,7 @@ class PuzzleState {
|
||||||
completed: this.completed,
|
completed: this.completed,
|
||||||
bgKey: this.bgKey,
|
bgKey: this.bgKey,
|
||||||
bgPath: this.bgPath,
|
bgPath: this.bgPath,
|
||||||
|
startTime: this.startTime,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue