Files
battle-royal-tetris/public/js/app.js
T

323 lines
9.7 KiB
JavaScript

// Main Application - Ties everything together
let localGame = null;
let renderer = null;
let lastTime = 0;
// Keyboard state for continuous movement (DAS - Delayed Auto Shift)
const keyState = {
ArrowLeft: false,
ArrowRight: false,
ArrowDown: false,
};
let dasCounter = 0;
const DAS_DELAY = 100; // ms before auto-repeat starts
const ARR_SPEED = 30; // ms between auto-repeat moves
// 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');
// Play lobby music when first entering lobby
if (typeof audio !== 'undefined') {
audio.playLobby();
}
}
// Reset lobby state when returning from game over (player is null indicates game over reset)
if (!player) {
ui.resetLobbyState();
// Resume lobby music after game ends
if (typeof audio !== 'undefined') {
audio.playLobby();
}
}
});
// 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';
// Start game music - will play continuously until game ends
if (typeof audio !== 'undefined') {
audio.playGame();
}
// 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);
}
// Game over is handled by the server via 'game-over' event
// Don't check locally to avoid premature game end
});
// 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();
// Start game music for spectators too
if (typeof audio !== 'undefined') {
audio.playGame();
}
// 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) {
// Track held keys for movement
if (keyState.hasOwnProperty(e.key)) {
keyState[e.key] = true;
dasCounter = 0; // Reset DAS counter when key pressed
e.preventDefault();
return;
}
// One-time actions
switch (e.key) {
case 'ArrowUp':
network.sendRotate();
e.preventDefault();
break;
case ' ':
network.sendHardDrop();
e.preventDefault();
break;
case 'Shift':
network.sendHold();
e.preventDefault();
break;
case 'a':
case 'A':
network.sendZoneActivate();
e.preventDefault();
break;
}
}
});
document.addEventListener('keyup', (e) => {
if (keyState.hasOwnProperty(e.key)) {
keyState[e.key] = false;
dasCounter = 0;
}
});
// 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 btnZone = document.getElementById('btn-zone');
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()));
btnZone.addEventListener('pointerdown', (e) => handleTouch(e, () => network.sendZoneActivate()));
// 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());
btnZone.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 with garbage info
Object.values(states).forEach(state => {
const player = network.getPlayer(state.playerId);
if (player) {
scores[player.name] = {
score: state.score,
garbageReceived: (state.garbageReceived || []).length,
eliminated: state.eliminated
};
}
});
// Game music will continue during game over screen, then transition back to lobby music via resetLobbyState
// 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;
// Handle DAS (Delayed Auto Shift) for continuous movement
if (!network.isSpectator && (keyState.ArrowLeft || keyState.ArrowRight || keyState.ArrowDown)) {
dasCounter += deltaTime;
// Send initial move immediately, then auto-repeat after DAS_DELAY
if (dasCounter >= DAS_DELAY) {
const autoRepeatCounter = (dasCounter - DAS_DELAY) % ARR_SPEED;
if (autoRepeatCounter < (deltaTime || 0)) {
if (keyState.ArrowLeft) network.sendMove('left');
if (keyState.ArrowRight) network.sendMove('right');
if (keyState.ArrowDown) network.sendDrop();
}
}
}
// 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);
});
// Update Zone button state for current player
const btnZone = document.getElementById('btn-zone');
const localState = allStates[network.currentPlayerId];
if (btnZone && localState) {
btnZone.disabled = localState.zoneMeter < 100 || localState.zoneActive;
}
// Set active player highlight (or null for spectators to show all boards equally)
renderer.setActivePlayer(network.isSpectator ? null : network.currentPlayerId);
requestAnimationFrame(gameLoop);
}