a0ab4ff5cd
- 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>
256 lines
7.4 KiB
JavaScript
256 lines
7.4 KiB
JavaScript
// Main Application - Ties everything together
|
|
|
|
let localGame = null;
|
|
let renderer = null;
|
|
let lastTime = 0;
|
|
|
|
// Initialize when DOM is ready
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// Connect to server
|
|
network.connect();
|
|
|
|
// Initialize renderer
|
|
renderer = new TetrisRenderer('battle-grid');
|
|
|
|
// Setup network listeners
|
|
setupNetworkListeners();
|
|
|
|
// Setup keyboard controls
|
|
setupKeyboardControls();
|
|
});
|
|
|
|
function setupNetworkListeners() {
|
|
// Player joined lobby
|
|
network.setListener('player-joined', (player) => {
|
|
ui.updatePlayerList(network.getAllPlayers());
|
|
// Show lobby screen if we're on the login screen
|
|
if (ui.screens.room.classList.contains('active')) {
|
|
ui.showScreen('lobby');
|
|
}
|
|
});
|
|
|
|
// Player left lobby
|
|
network.setListener('player-left', (playerId) => {
|
|
ui.updatePlayerList(network.getAllPlayers());
|
|
});
|
|
|
|
// Game started
|
|
network.setListener('game-started', (players, states) => {
|
|
ui.showScreen('game');
|
|
ui.displays.gameRoomName.textContent = 'GLOBAL LOBBY';
|
|
|
|
// Clear old boards
|
|
renderer.clearAll();
|
|
|
|
// Create boards for all players
|
|
states.forEach((state) => {
|
|
const player = network.getPlayer(state.playerId);
|
|
renderer.createPlayerBoard(state.playerId, player.name);
|
|
|
|
// Initialize local game for current player
|
|
if (state.playerId === network.currentPlayerId) {
|
|
localGame = new TetrisGame(state.playerId);
|
|
localGame.loadState(state);
|
|
}
|
|
});
|
|
|
|
// Set up battle grid layout
|
|
updateBattleGridLayout(players.length);
|
|
|
|
// Start game loop
|
|
lastTime = performance.now();
|
|
requestAnimationFrame(gameLoop);
|
|
});
|
|
|
|
// State update during game
|
|
network.setListener('state-update', (states) => {
|
|
// Update local game if it's our state
|
|
const localState = states.find(s => s.playerId === network.currentPlayerId);
|
|
if (localState) {
|
|
localGame.loadState(localState);
|
|
}
|
|
|
|
// Check for game over
|
|
const allStates = network.getAllGameStates();
|
|
const activePlayers = Object.values(allStates).filter(s => !s.eliminated);
|
|
|
|
if (activePlayers.length <= 1) {
|
|
endGame(allStates);
|
|
}
|
|
});
|
|
|
|
// Game over
|
|
network.setListener('game-over', (data) => {
|
|
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() {
|
|
document.addEventListener('keydown', (e) => {
|
|
// Spectators cannot control the game
|
|
if (network.isSpectator) return;
|
|
|
|
if (ui.screens.game.classList.contains('active') && localGame) {
|
|
switch (e.key) {
|
|
case 'ArrowLeft':
|
|
network.sendMove('left');
|
|
e.preventDefault();
|
|
break;
|
|
case 'ArrowRight':
|
|
network.sendMove('right');
|
|
e.preventDefault();
|
|
break;
|
|
case 'ArrowDown':
|
|
network.sendDrop();
|
|
e.preventDefault();
|
|
break;
|
|
case 'ArrowUp':
|
|
network.sendRotate();
|
|
e.preventDefault();
|
|
break;
|
|
case ' ':
|
|
network.sendHardDrop();
|
|
e.preventDefault();
|
|
break;
|
|
case 'c':
|
|
case 'C':
|
|
network.sendHold();
|
|
e.preventDefault();
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Touch controls for mobile
|
|
const btnLeft = document.getElementById('btn-left');
|
|
const btnRight = document.getElementById('btn-right');
|
|
const btnDown = document.getElementById('btn-down');
|
|
const btnRotate = document.getElementById('btn-rotate');
|
|
const btnDrop = document.getElementById('btn-drop');
|
|
const btnHold = document.getElementById('btn-hold');
|
|
|
|
const handleTouch = (e, action) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
// Spectators cannot control the game
|
|
if (network.isSpectator) return;
|
|
if (ui.screens.game.classList.contains('active')) {
|
|
action();
|
|
}
|
|
};
|
|
|
|
// Use pointerdown for better touch response
|
|
btnLeft.addEventListener('pointerdown', (e) => handleTouch(e, () => network.sendMove('left')));
|
|
btnRight.addEventListener('pointerdown', (e) => handleTouch(e, () => network.sendMove('right')));
|
|
btnDown.addEventListener('pointerdown', (e) => handleTouch(e, () => network.sendDrop()));
|
|
btnRotate.addEventListener('pointerdown', (e) => handleTouch(e, () => network.sendRotate()));
|
|
btnDrop.addEventListener('pointerdown', (e) => handleTouch(e, () => network.sendHardDrop()));
|
|
btnHold.addEventListener('pointerdown', (e) => handleTouch(e, () => network.sendHold()));
|
|
|
|
// Prevent double-tap zoom
|
|
btnLeft.addEventListener('touchstart', (e) => e.preventDefault());
|
|
btnRight.addEventListener('touchstart', (e) => e.preventDefault());
|
|
btnDown.addEventListener('touchstart', (e) => e.preventDefault());
|
|
btnRotate.addEventListener('touchstart', (e) => e.preventDefault());
|
|
btnDrop.addEventListener('touchstart', (e) => e.preventDefault());
|
|
btnHold.addEventListener('touchstart', (e) => e.preventDefault());
|
|
}
|
|
|
|
function updateBattleGridLayout(playerCount) {
|
|
ui.displays.battleGrid.classList.remove('single-player', 'two-players', 'multi-player');
|
|
|
|
if (playerCount === 1) {
|
|
ui.displays.battleGrid.classList.add('single-player');
|
|
} else if (playerCount === 2) {
|
|
ui.displays.battleGrid.classList.add('two-players');
|
|
} else {
|
|
ui.displays.battleGrid.classList.add('multi-player');
|
|
}
|
|
}
|
|
|
|
function endGame(states) {
|
|
// Find winner
|
|
const activePlayers = Object.values(states).filter(s => !s.eliminated);
|
|
const eliminatedPlayers = Object.values(states).filter(s => s.eliminated);
|
|
|
|
let winner = null;
|
|
let scores = {};
|
|
|
|
if (activePlayers.length === 1) {
|
|
const winnerState = activePlayers[0];
|
|
const winnerPlayer = network.getPlayer(winnerState.playerId);
|
|
winner = winnerPlayer.name;
|
|
}
|
|
|
|
// Build scores list
|
|
Object.values(states).forEach(state => {
|
|
const player = network.getPlayer(state.playerId);
|
|
if (player) {
|
|
scores[player.name] = state.score;
|
|
}
|
|
});
|
|
|
|
// Reset spectator mode for next round
|
|
network.isSpectator = false;
|
|
ui.hideSpectatorMode();
|
|
|
|
ui.showGameOver(winner, scores);
|
|
}
|
|
|
|
function gameLoop(currentTime) {
|
|
if (!ui.screens.game.classList.contains('active')) {
|
|
return;
|
|
}
|
|
|
|
const deltaTime = currentTime - lastTime;
|
|
lastTime = currentTime;
|
|
|
|
// Update local game
|
|
if (localGame) {
|
|
localGame.update(deltaTime);
|
|
}
|
|
|
|
// Render all players
|
|
const allStates = network.getAllGameStates();
|
|
Object.values(allStates).forEach(state => {
|
|
renderer.renderPlayer(state.playerId, state);
|
|
});
|
|
|
|
// Set active player highlight (or null for spectators to show all boards equally)
|
|
renderer.setActivePlayer(network.isSpectator ? null : network.currentPlayerId);
|
|
|
|
requestAnimationFrame(gameLoop);
|
|
}
|