Add spectator mode for late-joining players
- Server: Late joiners are added as spectators instead of players - Server: Send forced-spectator event only to joining spectator (not broadcast) - Server: Track spectators separately and move them to players after game ends - Client: Handle forced-spectator event to show all player boards - Client: Spectators see all boards equally without main/spectator highlighting - Client: Mobile view shows scrollable vertical list of all boards for spectators - Fix: All cleared lines are sent as garbage to each opponent (not randomized) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+63
-1
@@ -151,6 +151,44 @@ button:active {
|
|||||||
color: #0f0;
|
color: #0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.player-item .status.spectator-status {
|
||||||
|
color: #888;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Spectator list in lobby */
|
||||||
|
#spectator-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 20px 0;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#spectator-list:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#spectator-list .player-item {
|
||||||
|
background: #0a0a1a;
|
||||||
|
border: 2px solid #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
#spectator-list .player-item .status {
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Spectator mode header */
|
||||||
|
#game-room-name.spectator-mode {
|
||||||
|
color: #ff8800;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.7; }
|
||||||
|
}
|
||||||
|
|
||||||
/* Battle Grid - Focused Layout */
|
/* Battle Grid - Focused Layout */
|
||||||
#battle-grid {
|
#battle-grid {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -538,7 +576,31 @@ button:active {
|
|||||||
height: 50px !important;
|
height: 50px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-board.spectator {
|
/* Hide spectator boards on mobile when playing (has main board) */
|
||||||
|
#battle-grid:has(.main) .player-board.spectator {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* In spectator mode (no main board), show all boards in a scrollable container */
|
||||||
|
#battle-grid:not(:has(.main)) {
|
||||||
|
transform: none;
|
||||||
|
height: auto;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#battle-grid:not(:has(.main)) .player-board {
|
||||||
|
position: relative;
|
||||||
|
transform: none;
|
||||||
|
opacity: 1;
|
||||||
|
margin: 10px 0;
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#battle-grid:not(:has(.main)) .player-board canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-4
@@ -7,7 +7,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="css/style.css?v=9">
|
<link rel="stylesheet" href="css/style.css?v=10">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
<div id="lobby-screen" class="screen">
|
<div id="lobby-screen" class="screen">
|
||||||
<h2>GLOBAL LOBBY</h2>
|
<h2>GLOBAL LOBBY</h2>
|
||||||
<div id="player-list"></div>
|
<div id="player-list"></div>
|
||||||
|
<div id="spectator-list"></div>
|
||||||
<button id="ready-btn">READY</button>
|
<button id="ready-btn">READY</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -61,10 +62,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/socket.io/socket.io.js"></script>
|
<script src="/socket.io/socket.io.js"></script>
|
||||||
<script src="js/network.js?v=4"></script>
|
<script src="js/network.js?v=5"></script>
|
||||||
<script src="js/game.js?v=4"></script>
|
<script src="js/game.js?v=4"></script>
|
||||||
<script src="js/renderer.js?v=5"></script>
|
<script src="js/renderer.js?v=5"></script>
|
||||||
<script src="js/ui.js?v=4"></script>
|
<script src="js/ui.js?v=5"></script>
|
||||||
<script src="js/app.js?v=4"></script>
|
<script src="js/app.js?v=5"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+43
-2
@@ -83,10 +83,45 @@ function setupNetworkListeners() {
|
|||||||
network.setListener('game-over', (data) => {
|
network.setListener('game-over', (data) => {
|
||||||
endGame(data.states);
|
endGame(data.states);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Forced spectator - game already in progress
|
||||||
|
network.setListener('forced-spectator', ({ states, players }) => {
|
||||||
|
ui.showScreen('game');
|
||||||
|
ui.showSpectatorMode();
|
||||||
|
|
||||||
|
// Clear old boards
|
||||||
|
renderer.clearAll();
|
||||||
|
|
||||||
|
// Create boards for all players (spectator can see everyone)
|
||||||
|
states.forEach((state) => {
|
||||||
|
const player = network.getPlayer(state.playerId);
|
||||||
|
renderer.createPlayerBoard(state.playerId, player.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up battle grid layout
|
||||||
|
updateBattleGridLayout(players.length);
|
||||||
|
|
||||||
|
// Start game loop for viewing
|
||||||
|
lastTime = performance.now();
|
||||||
|
requestAnimationFrame(gameLoop);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Spectator joined
|
||||||
|
network.setListener('spectator-joined', (spectator) => {
|
||||||
|
ui.updateSpectatorList(network.getAllSpectators());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Spectator left
|
||||||
|
network.setListener('spectator-left', (spectatorId) => {
|
||||||
|
ui.updateSpectatorList(network.getAllSpectators());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupKeyboardControls() {
|
function setupKeyboardControls() {
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
|
// Spectators cannot control the game
|
||||||
|
if (network.isSpectator) return;
|
||||||
|
|
||||||
if (ui.screens.game.classList.contains('active') && localGame) {
|
if (ui.screens.game.classList.contains('active') && localGame) {
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case 'ArrowLeft':
|
case 'ArrowLeft':
|
||||||
@@ -129,6 +164,8 @@ function setupKeyboardControls() {
|
|||||||
const handleTouch = (e, action) => {
|
const handleTouch = (e, action) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
// Spectators cannot control the game
|
||||||
|
if (network.isSpectator) return;
|
||||||
if (ui.screens.game.classList.contains('active')) {
|
if (ui.screens.game.classList.contains('active')) {
|
||||||
action();
|
action();
|
||||||
}
|
}
|
||||||
@@ -185,6 +222,10 @@ function endGame(states) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Reset spectator mode for next round
|
||||||
|
network.isSpectator = false;
|
||||||
|
ui.hideSpectatorMode();
|
||||||
|
|
||||||
ui.showGameOver(winner, scores);
|
ui.showGameOver(winner, scores);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,8 +248,8 @@ function gameLoop(currentTime) {
|
|||||||
renderer.renderPlayer(state.playerId, state);
|
renderer.renderPlayer(state.playerId, state);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set active player highlight
|
// Set active player highlight (or null for spectators to show all boards equally)
|
||||||
renderer.setActivePlayer(network.currentPlayerId);
|
renderer.setActivePlayer(network.isSpectator ? null : network.currentPlayerId);
|
||||||
|
|
||||||
requestAnimationFrame(gameLoop);
|
requestAnimationFrame(gameLoop);
|
||||||
}
|
}
|
||||||
|
|||||||
+48
-2
@@ -4,14 +4,19 @@ class NetworkManager {
|
|||||||
constructor() {
|
constructor() {
|
||||||
this.socket = null;
|
this.socket = null;
|
||||||
this.currentPlayerId = null;
|
this.currentPlayerId = null;
|
||||||
|
this.isSpectator = false;
|
||||||
this.players = {};
|
this.players = {};
|
||||||
|
this.spectators = {};
|
||||||
this.gameState = {};
|
this.gameState = {};
|
||||||
this.listeners = {
|
this.listeners = {
|
||||||
onPlayerJoined: null,
|
onPlayerJoined: null,
|
||||||
onPlayerLeft: null,
|
onPlayerLeft: null,
|
||||||
onGameStarted: null,
|
onGameStarted: null,
|
||||||
onStateUpdate: null,
|
onStateUpdate: null,
|
||||||
onGameOver: null
|
onGameOver: null,
|
||||||
|
onSpectatorJoined: null,
|
||||||
|
onSpectatorLeft: null,
|
||||||
|
onForcedSpectator: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +67,33 @@ class NetworkManager {
|
|||||||
this.listeners.onGameOver(data);
|
this.listeners.onGameOver(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Forced spectator mode - game already in progress
|
||||||
|
this.socket.on('forced-spectator', ({ spectatorId, states, players }) => {
|
||||||
|
this.isSpectator = true;
|
||||||
|
this.updateStates(states);
|
||||||
|
this.updatePlayers(players);
|
||||||
|
if (this.listeners.onForcedSpectator) {
|
||||||
|
this.listeners.onForcedSpectator({ states, players });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Spectator joined
|
||||||
|
this.socket.on('spectator-joined', ({ spectator, spectators }) => {
|
||||||
|
this.updateSpectators(spectators);
|
||||||
|
if (this.listeners.onSpectatorJoined) {
|
||||||
|
this.listeners.onSpectatorJoined(spectator);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Spectator left
|
||||||
|
this.socket.on('spectator-left', ({ spectatorId, spectators }) => {
|
||||||
|
delete this.spectators[spectatorId];
|
||||||
|
this.updateSpectators(spectators);
|
||||||
|
if (this.listeners.onSpectatorLeft) {
|
||||||
|
this.listeners.onSpectatorLeft(spectatorId);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
joinLobby(playerName) {
|
joinLobby(playerName) {
|
||||||
@@ -116,6 +148,13 @@ class NetworkManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateSpectators(spectators) {
|
||||||
|
this.spectators = {};
|
||||||
|
spectators.forEach(s => {
|
||||||
|
this.spectators[s.id] = s;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getPlayer(playerId) {
|
getPlayer(playerId) {
|
||||||
return this.players[playerId];
|
return this.players[playerId];
|
||||||
}
|
}
|
||||||
@@ -132,13 +171,20 @@ class NetworkManager {
|
|||||||
return this.gameState;
|
return this.gameState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAllSpectators() {
|
||||||
|
return this.spectators;
|
||||||
|
}
|
||||||
|
|
||||||
setListener(event, callback) {
|
setListener(event, callback) {
|
||||||
const listenerMap = {
|
const listenerMap = {
|
||||||
'player-joined': 'onPlayerJoined',
|
'player-joined': 'onPlayerJoined',
|
||||||
'player-left': 'onPlayerLeft',
|
'player-left': 'onPlayerLeft',
|
||||||
'game-started': 'onGameStarted',
|
'game-started': 'onGameStarted',
|
||||||
'state-update': 'onStateUpdate',
|
'state-update': 'onStateUpdate',
|
||||||
'game-over': 'onGameOver'
|
'game-over': 'onGameOver',
|
||||||
|
'forced-spectator': 'onForcedSpectator',
|
||||||
|
'spectator-joined': 'onSpectatorJoined',
|
||||||
|
'spectator-left': 'onSpectatorLeft'
|
||||||
};
|
};
|
||||||
if (callback) {
|
if (callback) {
|
||||||
this.listeners[listenerMap[event]] = callback;
|
this.listeners[listenerMap[event]] = callback;
|
||||||
|
|||||||
@@ -323,12 +323,17 @@ class TetrisRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setActivePlayer(playerId) {
|
setActivePlayer(playerId) {
|
||||||
// Remove main class from all boards
|
// Remove main/spectator classes from all boards
|
||||||
this.boards.forEach((boardData, id) => {
|
this.boards.forEach((boardData, id) => {
|
||||||
boardData.element.classList.remove('active', 'main');
|
boardData.element.classList.remove('active', 'main');
|
||||||
boardData.element.classList.remove('spectator', 'top-left', 'top-right', 'bottom-left', 'bottom-right');
|
boardData.element.classList.remove('spectator', 'top-left', 'top-right', 'bottom-left', 'bottom-right');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If playerId is null (spectator mode), show all boards equally without highlighting
|
||||||
|
if (playerId === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Add main class to current player
|
// Add main class to current player
|
||||||
const mainBoard = this.boards.get(playerId);
|
const mainBoard = this.boards.get(playerId);
|
||||||
if (mainBoard) {
|
if (mainBoard) {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class UIManager {
|
|||||||
this.displays = {
|
this.displays = {
|
||||||
gameRoomName: document.getElementById('game-room-name'),
|
gameRoomName: document.getElementById('game-room-name'),
|
||||||
playerList: document.getElementById('player-list'),
|
playerList: document.getElementById('player-list'),
|
||||||
|
spectatorList: document.getElementById('spectator-list'),
|
||||||
battleGrid: document.getElementById('battle-grid'),
|
battleGrid: document.getElementById('battle-grid'),
|
||||||
gameStatus: document.getElementById('game-status'),
|
gameStatus: document.getElementById('game-status'),
|
||||||
winnerDisplay: document.getElementById('winner-display'),
|
winnerDisplay: document.getElementById('winner-display'),
|
||||||
@@ -90,6 +91,29 @@ class UIManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateSpectatorList(spectators) {
|
||||||
|
this.displays.spectatorList.innerHTML = '';
|
||||||
|
Object.values(spectators).forEach(spectator => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'player-item spectator';
|
||||||
|
item.innerHTML = `
|
||||||
|
<span class="name">${this.escapeHtml(spectator.name)}</span>
|
||||||
|
<span class="status">WATCHING</span>
|
||||||
|
`;
|
||||||
|
this.displays.spectatorList.appendChild(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showSpectatorMode() {
|
||||||
|
// Update UI to indicate spectator mode
|
||||||
|
this.displays.gameRoomName.textContent = 'SPECTATOR MODE - Waiting for next game';
|
||||||
|
this.displays.gameRoomName.classList.add('spectator-mode');
|
||||||
|
}
|
||||||
|
|
||||||
|
hideSpectatorMode() {
|
||||||
|
this.displays.gameRoomName.classList.remove('spectator-mode');
|
||||||
|
}
|
||||||
|
|
||||||
showMessage(message) {
|
showMessage(message) {
|
||||||
this.displays.gameStatus.textContent = message;
|
this.displays.gameStatus.textContent = message;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
+91
-10
@@ -13,6 +13,7 @@ app.use(express.static(path.join(__dirname, '../public')));
|
|||||||
// Single global lobby
|
// Single global lobby
|
||||||
const lobby = {
|
const lobby = {
|
||||||
players: new Map(),
|
players: new Map(),
|
||||||
|
spectators: new Map(),
|
||||||
gameStarted: false,
|
gameStarted: false,
|
||||||
gameInterval: null
|
gameInterval: null
|
||||||
};
|
};
|
||||||
@@ -45,6 +46,31 @@ io.on('connection', (socket) => {
|
|||||||
socket.join(LOBBY_ROOM);
|
socket.join(LOBBY_ROOM);
|
||||||
socket.data.playerName = playerName;
|
socket.data.playerName = playerName;
|
||||||
|
|
||||||
|
// If game is in progress, add as spectator instead
|
||||||
|
if (lobby.gameStarted) {
|
||||||
|
const spectator = {
|
||||||
|
id: socket.id,
|
||||||
|
name: playerName
|
||||||
|
};
|
||||||
|
lobby.spectators.set(socket.id, spectator);
|
||||||
|
|
||||||
|
// Notify all clients about new spectator
|
||||||
|
io.to(LOBBY_ROOM).emit('spectator-joined', {
|
||||||
|
spectator: { id: spectator.id, name: spectator.name },
|
||||||
|
spectators: getSpectatorsList()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send current game state to spectator only
|
||||||
|
socket.emit('forced-spectator', {
|
||||||
|
spectatorId: socket.id,
|
||||||
|
states: getStates(),
|
||||||
|
players: getPlayersList()
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`${playerName} joined as spectator (${lobby.spectators.size} spectators)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const player = {
|
const player = {
|
||||||
id: socket.id,
|
id: socket.id,
|
||||||
name: playerName,
|
name: playerName,
|
||||||
@@ -83,7 +109,7 @@ io.on('connection', (socket) => {
|
|||||||
players: getPlayersList()
|
players: getPlayersList()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if all players ready and min 2 players
|
// Check if all players ready and min 2 players (spectators don't count)
|
||||||
if (lobby.players.size >= 2 && lobby.players.size <= 8) {
|
if (lobby.players.size >= 2 && lobby.players.size <= 8) {
|
||||||
const allReady = Array.from(lobby.players.values()).every(p => p.ready);
|
const allReady = Array.from(lobby.players.values()).every(p => p.ready);
|
||||||
if (allReady) {
|
if (allReady) {
|
||||||
@@ -96,7 +122,7 @@ io.on('connection', (socket) => {
|
|||||||
if (!lobby.gameStarted) return;
|
if (!lobby.gameStarted) return;
|
||||||
|
|
||||||
const player = lobby.players.get(playerId);
|
const player = lobby.players.get(playerId);
|
||||||
if (!player || player.eliminated) return;
|
if (!player || player.eliminated || !player.currentPiece) return;
|
||||||
|
|
||||||
const newX = player.currentPiece.x + (direction === 'left' ? -1 : 1);
|
const newX = player.currentPiece.x + (direction === 'left' ? -1 : 1);
|
||||||
if (isValidPosition(player.currentPiece, newX, player.currentPiece.y, player.board)) {
|
if (isValidPosition(player.currentPiece, newX, player.currentPiece.y, player.board)) {
|
||||||
@@ -109,7 +135,7 @@ io.on('connection', (socket) => {
|
|||||||
if (!lobby.gameStarted) return;
|
if (!lobby.gameStarted) return;
|
||||||
|
|
||||||
const player = lobby.players.get(playerId);
|
const player = lobby.players.get(playerId);
|
||||||
if (!player || player.eliminated) return;
|
if (!player || player.eliminated || !player.currentPiece) return;
|
||||||
|
|
||||||
const originalShape = player.currentPiece.shape;
|
const originalShape = player.currentPiece.shape;
|
||||||
const rows = originalShape.length;
|
const rows = originalShape.length;
|
||||||
@@ -137,7 +163,7 @@ io.on('connection', (socket) => {
|
|||||||
if (!lobby.gameStarted) return;
|
if (!lobby.gameStarted) return;
|
||||||
|
|
||||||
const player = lobby.players.get(playerId);
|
const player = lobby.players.get(playerId);
|
||||||
if (!player || player.eliminated) return;
|
if (!player || player.eliminated || !player.currentPiece) return;
|
||||||
|
|
||||||
if (hard) {
|
if (hard) {
|
||||||
let dropped = 0;
|
let dropped = 0;
|
||||||
@@ -163,7 +189,7 @@ io.on('connection', (socket) => {
|
|||||||
if (!lobby.gameStarted) return;
|
if (!lobby.gameStarted) return;
|
||||||
|
|
||||||
const player = lobby.players.get(playerId);
|
const player = lobby.players.get(playerId);
|
||||||
if (!player || player.eliminated) return;
|
if (!player || player.eliminated || !player.currentPiece) return;
|
||||||
if (!player.canHold) return;
|
if (!player.canHold) return;
|
||||||
|
|
||||||
if (player.holdPiece === null) {
|
if (player.holdPiece === null) {
|
||||||
@@ -218,6 +244,19 @@ io.on('connection', (socket) => {
|
|||||||
if (lobby.gameInterval) clearInterval(lobby.gameInterval);
|
if (lobby.gameInterval) clearInterval(lobby.gameInterval);
|
||||||
lobby.gameStarted = false;
|
lobby.gameStarted = false;
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle spectator disconnect
|
||||||
|
const spectator = lobby.spectators.get(socket.id);
|
||||||
|
if (spectator) {
|
||||||
|
console.log(`${spectator.name} (spectator) disconnected`);
|
||||||
|
lobby.spectators.delete(socket.id);
|
||||||
|
|
||||||
|
io.to(LOBBY_ROOM).emit('spectator-left', {
|
||||||
|
spectatorId: socket.id,
|
||||||
|
spectators: getSpectatorsList()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -238,6 +277,13 @@ function getPlayersList() {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSpectatorsList() {
|
||||||
|
return Array.from(lobby.spectators.values()).map(s => ({
|
||||||
|
id: s.id,
|
||||||
|
name: s.name
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
function isValidPosition(piece, x, y, board, shape = null) {
|
function isValidPosition(piece, x, y, board, shape = null) {
|
||||||
const s = shape || piece.shape;
|
const s = shape || piece.shape;
|
||||||
for (let row = 0; row < s.length; row++) {
|
for (let row = 0; row < s.length; row++) {
|
||||||
@@ -245,8 +291,8 @@ function isValidPosition(piece, x, y, board, shape = null) {
|
|||||||
if (s[row][col]) {
|
if (s[row][col]) {
|
||||||
const newX = x + col;
|
const newX = x + col;
|
||||||
const newY = y + row;
|
const newY = y + row;
|
||||||
if (newX < 0 || newX >= BOARD_WIDTH || newY >= BOARD_HEIGHT) return false;
|
if (newX < 0 || newX >= BOARD_WIDTH || newY < 0 || newY >= BOARD_HEIGHT) return false;
|
||||||
if (newY >= 0 && board[newY][newX]) return false;
|
if (board[newY][newX]) return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -268,6 +314,7 @@ function getRandomPiece() {
|
|||||||
function spawnPiece(player) {
|
function spawnPiece(player) {
|
||||||
player.currentPiece = player.nextPiece;
|
player.currentPiece = player.nextPiece;
|
||||||
player.nextPiece = getRandomPiece();
|
player.nextPiece = getRandomPiece();
|
||||||
|
if (!player.currentPiece) return false;
|
||||||
return isValidPosition(player.currentPiece, player.currentPiece.x, player.currentPiece.y, player.board);
|
return isValidPosition(player.currentPiece, player.currentPiece.x, player.currentPiece.y, player.board);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,9 +370,11 @@ function sendGarbage(sender, rowsCleared) {
|
|||||||
const garbageRows = rowsCleared >= 4 ? rowsCleared : Math.max(1, rowsCleared - 1);
|
const garbageRows = rowsCleared >= 4 ? rowsCleared : Math.max(1, rowsCleared - 1);
|
||||||
const opponents = Array.from(lobby.players.values()).filter(p => p.id !== sender.id && !p.eliminated);
|
const opponents = Array.from(lobby.players.values()).filter(p => p.id !== sender.id && !p.eliminated);
|
||||||
if (opponents.length === 0) return;
|
if (opponents.length === 0) return;
|
||||||
|
// Send all garbage rows to each opponent
|
||||||
|
for (const opponent of opponents) {
|
||||||
for (let i = 0; i < garbageRows; i++) {
|
for (let i = 0; i < garbageRows; i++) {
|
||||||
const target = opponents[Math.floor(Math.random() * opponents.length)];
|
addGarbageToPlayer(opponent);
|
||||||
addGarbageToPlayer(target);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,7 +425,7 @@ function startGame() {
|
|||||||
|
|
||||||
function gameTick() {
|
function gameTick() {
|
||||||
for (const player of lobby.players.values()) {
|
for (const player of lobby.players.values()) {
|
||||||
if (player.eliminated) continue;
|
if (player.eliminated || !player.currentPiece) continue;
|
||||||
|
|
||||||
player.dropCounter += 50;
|
player.dropCounter += 50;
|
||||||
if (player.dropCounter >= player.dropInterval) {
|
if (player.dropCounter >= player.dropInterval) {
|
||||||
@@ -421,6 +470,38 @@ function checkGameOver() {
|
|||||||
lobby.gameInterval = null;
|
lobby.gameInterval = null;
|
||||||
}
|
}
|
||||||
lobby.gameStarted = false;
|
lobby.gameStarted = false;
|
||||||
|
|
||||||
|
// Move spectators to players for next round
|
||||||
|
for (const [id, spectator] of lobby.spectators.entries()) {
|
||||||
|
const player = {
|
||||||
|
id: spectator.id,
|
||||||
|
name: spectator.name,
|
||||||
|
score: 0,
|
||||||
|
lines: 0,
|
||||||
|
level: 1,
|
||||||
|
board: createEmptyBoard(),
|
||||||
|
currentPiece: null,
|
||||||
|
nextPiece: null,
|
||||||
|
eliminated: false,
|
||||||
|
ready: false,
|
||||||
|
dropCounter: 0,
|
||||||
|
dropInterval: 1000
|
||||||
|
};
|
||||||
|
lobby.players.set(id, player);
|
||||||
|
spectator.socket = io.sockets.sockets.get(id);
|
||||||
|
if (spectator.socket) {
|
||||||
|
spectator.socket.data.playerName = spectator.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lobby.spectators.clear();
|
||||||
|
|
||||||
|
// Broadcast updated player list
|
||||||
|
io.to(LOBBY_ROOM).emit('player-joined', {
|
||||||
|
player: null,
|
||||||
|
players: getPlayersList()
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Game over. Moved ${activePlayers.length === 1 ? 0 : lobby.players.size} players to next round`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user