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() {
|
||||
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)
|
||||
if (this._currentMusic && this._musicStarted && !this._currentMusic.isPlaying && !this._currentMusic.isPaused) {
|
||||
this._playNextTrack();
|
||||
|
|
@ -310,6 +313,9 @@ class PuzzleScene extends Phaser.Scene {
|
|||
this._panLastY = 0;
|
||||
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)
|
||||
this._isBoxSelecting = false;
|
||||
this._boxStartWorld = null; // { x, y } in world coords
|
||||
|
|
@ -396,6 +402,7 @@ class PuzzleScene extends Phaser.Scene {
|
|||
}
|
||||
|
||||
this._setupNetworkListeners();
|
||||
this._updatePlayersList();
|
||||
} else if (NetworkManager.connected) {
|
||||
// Host — create the room on the server
|
||||
this._isNetworked = true;
|
||||
|
|
@ -412,6 +419,7 @@ class PuzzleScene extends Phaser.Scene {
|
|||
completed: false,
|
||||
bgKey: this._bgKey,
|
||||
bgPath: this._bgPath,
|
||||
startTime: this._startTime,
|
||||
});
|
||||
NetworkManager.createRoom(this.cfg.roomCode, state.serialize(), this.cfg.playerName);
|
||||
this._setupNetworkListeners();
|
||||
|
|
@ -548,6 +556,7 @@ class PuzzleScene extends Phaser.Scene {
|
|||
this._playerNames.set(msg.playerId, msg.playerName);
|
||||
}
|
||||
console.log(`${msg.playerName || 'Player ' + msg.playerId} joined the room`);
|
||||
this._updatePlayersList();
|
||||
}
|
||||
|
||||
_onNetworkPlayerLeft(msg) {
|
||||
|
|
@ -564,6 +573,7 @@ class PuzzleScene extends Phaser.Scene {
|
|||
}
|
||||
this._playerNames.delete(msg.playerId);
|
||||
console.log(`Player ${msg.playerId} left the room`);
|
||||
this._updatePlayersList();
|
||||
}
|
||||
|
||||
_onNetworkCompleted() {
|
||||
|
|
@ -1138,6 +1148,43 @@ class PuzzleScene extends Phaser.Scene {
|
|||
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 ──────────────────────────────────────────────────────────
|
||||
|
||||
// ─── Music ──────────────────────────────────────────────────────────
|
||||
|
|
@ -1384,9 +1431,68 @@ class PuzzleScene extends Phaser.Scene {
|
|||
this._saveState();
|
||||
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);
|
||||
|
||||
// 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
|
||||
const musicRow = document.createElement('div');
|
||||
Object.assign(musicRow.style, {
|
||||
|
|
@ -1399,9 +1505,9 @@ class PuzzleScene extends Phaser.Scene {
|
|||
});
|
||||
|
||||
const btnStyle = {
|
||||
background: 'rgba(0, 0, 0, 0.55)',
|
||||
background: '#1565c0',
|
||||
color: '#ddeeff',
|
||||
border: '1px solid #4477bb',
|
||||
border: '1px solid #64b5f6',
|
||||
borderRadius: '4px',
|
||||
fontSize: '1.8vmin',
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
|
|
@ -1415,16 +1521,16 @@ class PuzzleScene extends Phaser.Scene {
|
|||
skipBtn.textContent = '\u23ED'; // next track symbol
|
||||
skipBtn.title = 'Next Track';
|
||||
Object.assign(skipBtn.style, btnStyle);
|
||||
skipBtn.addEventListener('mouseenter', () => { skipBtn.style.background = '#223355'; });
|
||||
skipBtn.addEventListener('mouseleave', () => { skipBtn.style.background = 'rgba(0, 0, 0, 0.55)'; });
|
||||
skipBtn.addEventListener('mouseenter', () => { skipBtn.style.background = '#1e88e5'; });
|
||||
skipBtn.addEventListener('mouseleave', () => { skipBtn.style.background = '#1565c0'; });
|
||||
skipBtn.addEventListener('click', () => this._skipTrack());
|
||||
|
||||
// Mute/unmute button
|
||||
this._muteBtn = document.createElement('button');
|
||||
this._muteBtn.title = 'Mute / Unmute';
|
||||
Object.assign(this._muteBtn.style, btnStyle);
|
||||
this._muteBtn.addEventListener('mouseenter', () => { this._muteBtn.style.background = '#223355'; });
|
||||
this._muteBtn.addEventListener('mouseleave', () => { this._muteBtn.style.background = 'rgba(0, 0, 0, 0.55)'; });
|
||||
this._muteBtn.addEventListener('mouseenter', () => { this._muteBtn.style.background = '#1e88e5'; });
|
||||
this._muteBtn.addEventListener('mouseleave', () => { this._muteBtn.style.background = '#1565c0'; });
|
||||
this._muteBtn.addEventListener('click', () => {
|
||||
this._toggleMute();
|
||||
this._muteBtn.textContent = this._musicMuted ? '\uD83D\uDD07' : '\uD83D\uDD0A';
|
||||
|
|
@ -1517,9 +1623,9 @@ class PuzzleScene extends Phaser.Scene {
|
|||
Object.assign(btn.style, {
|
||||
position: 'absolute',
|
||||
padding: '0.8vmin 2vmin',
|
||||
background: '#1a2a4a',
|
||||
background: '#1565c0',
|
||||
color: '#ddeeff',
|
||||
border: '1px solid #4477bb',
|
||||
border: '1px solid #64b5f6',
|
||||
borderRadius: '4px',
|
||||
fontSize: '1.6vmin',
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
|
|
@ -1528,8 +1634,8 @@ class PuzzleScene extends Phaser.Scene {
|
|||
whiteSpace: 'nowrap',
|
||||
});
|
||||
// Hover effect
|
||||
btn.addEventListener('mouseenter', () => { btn.style.background = '#223355'; btn.style.borderColor = '#66aaff'; });
|
||||
btn.addEventListener('mouseleave', () => { btn.style.background = '#1a2a4a'; btn.style.borderColor = '#4477bb'; });
|
||||
btn.addEventListener('mouseenter', () => { btn.style.background = '#1e88e5'; btn.style.borderColor = '#64b5f6'; });
|
||||
btn.addEventListener('mouseleave', () => { btn.style.background = '#1565c0'; btn.style.borderColor = '#64b5f6'; });
|
||||
btn.addEventListener('click', onClick);
|
||||
return btn;
|
||||
}
|
||||
|
|
@ -1595,6 +1701,7 @@ class PuzzleScene extends Phaser.Scene {
|
|||
completed: this._completed || false,
|
||||
bgKey: this._bgKey,
|
||||
bgPath: this._bgPath,
|
||||
startTime: this._startTime,
|
||||
});
|
||||
StorageManager.save(state);
|
||||
StorageManager.saveCurrent(this.cfg.roomCode);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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.imagePath = imagePath;
|
||||
this.pieceCount = pieceCount;
|
||||
|
|
@ -11,6 +11,7 @@ class PuzzleState {
|
|||
this.completed = completed || false;
|
||||
this.bgKey = bgKey || 'bg_dark_wood';
|
||||
this.bgPath = bgPath || 'assets/images/ui/dark_wood.jpg';
|
||||
this.startTime = startTime || Date.now();
|
||||
}
|
||||
|
||||
serialize() {
|
||||
|
|
@ -33,6 +34,7 @@ class PuzzleState {
|
|||
completed: this.completed,
|
||||
bgKey: this.bgKey,
|
||||
bgPath: this.bgPath,
|
||||
startTime: this.startTime,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue