Initial commit: Tetris Battle Royale multiplayer game
Features: - 2-8 player multiplayer via Socket.io WebSocket - Real-time board synchronization - all players see all boards - Battle royale mechanic: clearing rows sends garbage to opponents - Classic Tetris gameplay with all 7 tetrominoes - Retro visual styling with CRT scanlines and pixel font - Automatic level progression and speed increase - Player elimination and winner announcement Files: - server/index.js: Node.js + Socket.io game server - public/js/: Frontend game logic, rendering, network, and UI - public/css/style.css: Retro Tetris styling - README.md: Setup and usage instructions - PLAN.md: Implementation plan with all phases completed
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #1a1a2e;
|
||||
color: #fff;
|
||||
font-family: 'Press Start 2P', cursive;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* CRT Scanline Effect */
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: repeating-linear-gradient(
|
||||
0deg,
|
||||
rgba(0, 0, 0, 0.15),
|
||||
rgba(0, 0, 0, 0.15) 1px,
|
||||
transparent 1px,
|
||||
transparent 2px
|
||||
);
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.screen {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
.screen.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
text-align: center;
|
||||
color: #0ff;
|
||||
text-shadow: 4px 4px 0 #ff00ff;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.2rem;
|
||||
color: #0f0;
|
||||
text-shadow: 2px 2px 0 #004400;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1rem;
|
||||
color: #ff0;
|
||||
text-shadow: 2px 2px 0 #444400;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 0.7rem;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
input {
|
||||
background: #0a0a1a;
|
||||
border: 2px solid #333;
|
||||
color: #fff;
|
||||
font-family: 'Press Start 2P', cursive;
|
||||
font-size: 0.8rem;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
border-color: #0ff;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #ff00ff;
|
||||
border: 4px solid #fff;
|
||||
color: #fff;
|
||||
font-family: 'Press Start 2P', cursive;
|
||||
font-size: 1rem;
|
||||
padding: 15px 30px;
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
transition: transform 0.1s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: scale(1.05);
|
||||
background: #0ff;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
#player-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin: 20px 0;
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.player-item {
|
||||
background: #0a0a1a;
|
||||
border: 2px solid #333;
|
||||
padding: 15px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.player-item .name {
|
||||
color: #0ff;
|
||||
}
|
||||
|
||||
.player-item .status {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.player-item .status.ready {
|
||||
color: #0f0;
|
||||
}
|
||||
|
||||
/* Battle Grid - Responsive Layout */
|
||||
#battle-grid {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
#battle-grid.grid-2x2 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
#battle-grid.grid-2x4 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.player-board {
|
||||
background: #000;
|
||||
border: 4px solid #333;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.player-board.active {
|
||||
border-color: #0ff;
|
||||
}
|
||||
|
||||
.player-board.eliminated {
|
||||
border-color: #f00;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.player-board canvas {
|
||||
display: block;
|
||||
image-rendering: pixelated;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
|
||||
.board-info {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 5px;
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
.board-info .name {
|
||||
color: #0ff;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.board-info .stats {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.next-piece-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.next-piece-container canvas {
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
#game-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 10px 20px;
|
||||
background: #0a0a1a;
|
||||
border: 2px solid #333;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#game-header span {
|
||||
font-size: 0.8rem;
|
||||
color: #0f0;
|
||||
}
|
||||
|
||||
#game-header button {
|
||||
font-size: 0.7rem;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
#game-status {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: #0a0a1a;
|
||||
border: 2px solid #333;
|
||||
text-align: center;
|
||||
font-size: 0.8rem;
|
||||
color: #ff0;
|
||||
}
|
||||
|
||||
#winner-display {
|
||||
margin: 20px 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
#final-scores {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin: 20px 0;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
#final-scores .score-item {
|
||||
background: #0a0a1a;
|
||||
border: 2px solid #333;
|
||||
padding: 10px 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#final-scores .score-item.winner {
|
||||
border-color: #0f0;
|
||||
color: #0f0;
|
||||
}
|
||||
|
||||
/* Garbage row animation */
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
25% { transform: translateX(-5px); }
|
||||
75% { transform: translateX(5px); }
|
||||
}
|
||||
|
||||
.player-board.shake {
|
||||
animation: shake 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
/* Flash effect for row clear */
|
||||
@keyframes flash {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.player-board.flash {
|
||||
animation: flash 0.3s ease-in-out;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Tetris Battle Royale</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<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 rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- Room Selection Screen -->
|
||||
<div id="room-screen" class="screen active">
|
||||
<h1>TETRIS<br>BATTLE ROYALE</h1>
|
||||
<div class="form-group">
|
||||
<label for="room-name">Room Name</label>
|
||||
<input type="text" id="room-name" placeholder="Enter room name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="player-name">Your Name</label>
|
||||
<input type="text" id="player-name" placeholder="Enter your name">
|
||||
</div>
|
||||
<button id="join-btn">JOIN ROOM</button>
|
||||
</div>
|
||||
|
||||
<!-- Lobby Screen -->
|
||||
<div id="lobby-screen" class="screen">
|
||||
<h2>Room: <span id="lobby-room-name"></span></h2>
|
||||
<div id="player-list"></div>
|
||||
<button id="ready-btn">READY</button>
|
||||
</div>
|
||||
|
||||
<!-- Game Screen -->
|
||||
<div id="game-screen" class="screen">
|
||||
<div id="game-header">
|
||||
<span id="game-room-name"></span>
|
||||
<button id="leave-btn">LEAVE</button>
|
||||
</div>
|
||||
<div id="battle-grid"></div>
|
||||
<div id="game-status"></div>
|
||||
</div>
|
||||
|
||||
<!-- Game Over Screen -->
|
||||
<div id="gameover-screen" class="screen">
|
||||
<h2>GAME OVER</h2>
|
||||
<h3 id="winner-display"></h3>
|
||||
<div id="final-scores"></div>
|
||||
<button id="back-to-lobby">BACK TO LOBBY</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script src="js/network.js"></script>
|
||||
<script src="js/game.js"></script>
|
||||
<script src="js/renderer.js"></script>
|
||||
<script src="js/ui.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,171 @@
|
||||
// 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());
|
||||
});
|
||||
|
||||
// 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 = network.currentRoom;
|
||||
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
|
||||
function setupKeyboardControls() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateBattleGridLayout(playerCount) {
|
||||
ui.displays.battleGrid.classList.remove('grid-2x2', 'grid-2x4');
|
||||
|
||||
if (playerCount <= 4) {
|
||||
ui.displays.battleGrid.classList.add('grid-2x2');
|
||||
} else {
|
||||
ui.displays.battleGrid.classList.add('grid-2x4');
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
renderer.setActivePlayer(network.currentPlayerId);
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
// Core Tetris Game Logic
|
||||
|
||||
const BOARD_WIDTH = 10;
|
||||
const BOARD_HEIGHT = 20;
|
||||
const CELL_SIZE = 24;
|
||||
|
||||
// Tetromino definitions with colors
|
||||
const TETROMINOS = {
|
||||
I: { shape: [[0,0,0,0], [1,1,1,1], [0,0,0,0], [0,0,0,0]], color: '#00ffff' },
|
||||
O: { shape: [[1,1], [1,1]], color: '#ffff00' },
|
||||
T: { shape: [[0,1,0], [1,1,1], [0,0,0]], color: '#800080' },
|
||||
S: { shape: [[0,1,1], [1,1,0], [0,0,0]], color: '#00ff00' },
|
||||
Z: { shape: [[1,1,0], [0,1,1], [0,0,0]], color: '#ff0000' },
|
||||
J: { shape: [[1,0,0], [1,1,1], [0,0,0]], color: '#0000ff' },
|
||||
L: { shape: [[0,0,1], [1,1,1], [0,0,0]], color: '#ffa500' }
|
||||
};
|
||||
|
||||
const TETROMINO_KEYS = Object.keys(TETROMINOS);
|
||||
const GARbage_COLOR = '#666666';
|
||||
|
||||
class TetrisGame {
|
||||
constructor(playerId) {
|
||||
this.playerId = playerId;
|
||||
this.board = this.createEmptyBoard();
|
||||
this.currentPiece = null;
|
||||
this.nextPiece = null;
|
||||
this.score = 0;
|
||||
this.lines = 0;
|
||||
this.level = 1;
|
||||
this.gameOver = false;
|
||||
this.eliminated = false;
|
||||
|
||||
// Game speed (milliseconds per drop)
|
||||
this.dropInterval = 1000;
|
||||
this.lastDrop = 0;
|
||||
|
||||
// Input lock for delay hits
|
||||
this.inputLocked = false;
|
||||
this.lockUntil = 0;
|
||||
}
|
||||
|
||||
createEmptyBoard() {
|
||||
return Array(BOARD_HEIGHT).fill(null).map(() => Array(BOARD_WIDTH).fill(0));
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.board = this.createEmptyBoard();
|
||||
this.score = 0;
|
||||
this.lines = 0;
|
||||
this.level = 1;
|
||||
this.gameOver = false;
|
||||
this.eliminated = false;
|
||||
this.dropInterval = 1000;
|
||||
this.currentPiece = this.spawnPiece();
|
||||
this.nextPiece = this.getRandomPiece();
|
||||
|
||||
if (!this.currentPiece) {
|
||||
this.gameOver = true;
|
||||
}
|
||||
}
|
||||
|
||||
getRandomPiece() {
|
||||
const key = TETROMINO_KEYS[Math.floor(Math.random() * TETROMINO_KEYS.length)];
|
||||
const tetromino = TETROMINOS[key];
|
||||
return {
|
||||
type: key,
|
||||
shape: JSON.parse(JSON.stringify(tetromino.shape)),
|
||||
color: tetromino.color,
|
||||
x: Math.floor(BOARD_WIDTH / 2) - Math.floor(tetromino.shape[0].length / 2),
|
||||
y: 0
|
||||
};
|
||||
}
|
||||
|
||||
spawnPiece() {
|
||||
const piece = this.nextPiece;
|
||||
this.nextPiece = this.getRandomPiece();
|
||||
|
||||
// Check if spawn position is valid
|
||||
if (!this.isValidPosition(piece.x, piece.y, piece.shape)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return piece;
|
||||
}
|
||||
|
||||
isValidPosition(x, y, shape) {
|
||||
for (let row = 0; row < shape.length; row++) {
|
||||
for (let col = 0; col < shape[row].length; col++) {
|
||||
if (shape[row][col]) {
|
||||
const newX = x + col;
|
||||
const newY = y + row;
|
||||
|
||||
// Check bounds
|
||||
if (newX < 0 || newX >= BOARD_WIDTH || newY >= BOARD_HEIGHT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check collision with locked pieces (only if on board)
|
||||
if (newY >= 0 && this.board[newY][newX]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
move(direction) {
|
||||
if (this.gameOver || this.inputLocked) return false;
|
||||
|
||||
const newX = this.currentPiece.x + (direction === 'left' ? -1 : 1);
|
||||
|
||||
if (this.isValidPosition(newX, this.currentPiece.y, this.currentPiece.shape)) {
|
||||
this.currentPiece.x = newX;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
rotate() {
|
||||
if (this.gameOver || this.inputLocked) return false;
|
||||
|
||||
const originalShape = this.currentPiece.shape;
|
||||
const rows = originalShape.length;
|
||||
const cols = originalShape[0].length;
|
||||
|
||||
// Rotate 90 degrees clockwise
|
||||
const rotated = Array(cols).fill(null).map(() => Array(rows).fill(0));
|
||||
for (let row = 0; row < rows; row++) {
|
||||
for (let col = 0; col < cols; col++) {
|
||||
rotated[col][rows - 1 - row] = originalShape[row][col];
|
||||
}
|
||||
}
|
||||
|
||||
// Try rotation with wall kicks
|
||||
const kicks = [0, -1, 1, -2, 2];
|
||||
for (const kick of kicks) {
|
||||
if (this.isValidPosition(this.currentPiece.x + kick, this.currentPiece.y, rotated)) {
|
||||
this.currentPiece.shape = rotated;
|
||||
this.currentPiece.x += kick;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
drop() {
|
||||
if (this.gameOver || this.inputLocked) return false;
|
||||
|
||||
const newY = this.currentPiece.y + 1;
|
||||
|
||||
if (this.isValidPosition(this.currentPiece.x, newY, this.currentPiece.shape)) {
|
||||
this.currentPiece.y = newY;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Lock the piece
|
||||
return this.lockPiece();
|
||||
}
|
||||
|
||||
hardDrop() {
|
||||
if (this.gameOver || this.inputLocked) return 0;
|
||||
|
||||
let dropped = 0;
|
||||
while (this.isValidPosition(this.currentPiece.x, this.currentPiece.y + 1, this.currentPiece.shape)) {
|
||||
this.currentPiece.y++;
|
||||
dropped++;
|
||||
}
|
||||
|
||||
this.lockPiece();
|
||||
return dropped;
|
||||
}
|
||||
|
||||
lockPiece() {
|
||||
if (!this.currentPiece) return false;
|
||||
|
||||
// Lock piece into board
|
||||
for (let row = 0; row < this.currentPiece.shape.length; row++) {
|
||||
for (let col = 0; col < this.currentPiece.shape[row].length; col++) {
|
||||
if (this.currentPiece.shape[row][col]) {
|
||||
const boardY = this.currentPiece.y + row;
|
||||
const boardX = this.currentPiece.x + col;
|
||||
|
||||
if (boardY >= 0 && boardY < BOARD_HEIGHT && boardX >= 0 && boardX < BOARD_WIDTH) {
|
||||
this.board[boardY][boardX] = this.currentPiece.color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for game over (piece locked above visible area)
|
||||
if (this.currentPiece.y <= 0) {
|
||||
this.gameOver = true;
|
||||
this.eliminated = true;
|
||||
}
|
||||
|
||||
// Clear completed rows
|
||||
const cleared = this.clearRows();
|
||||
|
||||
// Spawn new piece
|
||||
this.currentPiece = this.spawnPiece();
|
||||
if (!this.currentPiece) {
|
||||
this.gameOver = true;
|
||||
this.eliminated = true;
|
||||
}
|
||||
|
||||
return cleared;
|
||||
}
|
||||
|
||||
clearRows() {
|
||||
let rowsCleared = 0;
|
||||
|
||||
for (let row = BOARD_HEIGHT - 1; row >= 0; row--) {
|
||||
if (this.board[row].every(cell => cell !== 0)) {
|
||||
// Remove the row
|
||||
this.board.splice(row, 1);
|
||||
// Add empty row at top
|
||||
this.board.unshift(Array(BOARD_WIDTH).fill(0));
|
||||
rowsCleared++;
|
||||
row++; // Check same row again
|
||||
}
|
||||
}
|
||||
|
||||
if (rowsCleared > 0) {
|
||||
this.lines += rowsCleared;
|
||||
this.updateScore(rowsCleared);
|
||||
this.updateLevel();
|
||||
}
|
||||
|
||||
return rowsCleared;
|
||||
}
|
||||
|
||||
updateScore(rowsCleared) {
|
||||
const points = [0, 100, 300, 500, 800];
|
||||
this.score += points[rowsCleared] * this.level;
|
||||
}
|
||||
|
||||
updateLevel() {
|
||||
this.level = Math.floor(this.lines / 10) + 1;
|
||||
this.dropInterval = Math.max(100, 1000 - (this.level - 1) * 100);
|
||||
}
|
||||
|
||||
receiveGarbage(rows) {
|
||||
if (this.gameOver) return;
|
||||
|
||||
// Add garbage rows at bottom
|
||||
for (let i = 0; i < rows; i++) {
|
||||
// Remove top row (simulating overflow)
|
||||
this.board.pop();
|
||||
|
||||
// Add garbage row with random gap
|
||||
const garbageRow = Array(BOARD_WIDTH).fill(GARbage_COLOR);
|
||||
const gap = Math.floor(Math.random() * BOARD_WIDTH);
|
||||
garbageRow[gap] = 0;
|
||||
this.board.unshift(garbageRow);
|
||||
}
|
||||
|
||||
// Check if garbage caused game over
|
||||
for (let row = 0; row < 2; row++) {
|
||||
for (let col = 0; col < BOARD_WIDTH; col++) {
|
||||
if (this.board[row][col] !== 0) {
|
||||
this.gameOver = true;
|
||||
this.eliminated = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
applyDelayHit(duration) {
|
||||
this.inputLocked = true;
|
||||
this.lockUntil = Date.now() + duration;
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
if (this.gameOver) return;
|
||||
|
||||
// Check if input lock has expired
|
||||
if (this.inputLocked && Date.now() >= this.lockUntil) {
|
||||
this.inputLocked = false;
|
||||
}
|
||||
|
||||
// Auto drop
|
||||
if (!this.inputLocked && Date.now() - this.lastDrop >= this.dropInterval) {
|
||||
this.drop();
|
||||
this.lastDrop = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
getState() {
|
||||
return {
|
||||
playerId: this.playerId,
|
||||
board: JSON.parse(JSON.stringify(this.board)),
|
||||
currentPiece: this.currentPiece ? JSON.parse(JSON.stringify(this.currentPiece)) : null,
|
||||
nextPiece: this.nextPiece ? JSON.parse(JSON.stringify(this.nextPiece)) : null,
|
||||
score: this.score,
|
||||
lines: this.lines,
|
||||
level: this.level,
|
||||
gameOver: this.gameOver,
|
||||
eliminated: this.eliminated
|
||||
};
|
||||
}
|
||||
|
||||
loadState(state) {
|
||||
this.board = JSON.parse(JSON.stringify(state.board));
|
||||
this.currentPiece = state.currentPiece ? JSON.parse(JSON.stringify(state.currentPiece)) : null;
|
||||
this.nextPiece = state.nextPiece ? JSON.parse(JSON.stringify(state.nextPiece)) : null;
|
||||
this.score = state.score;
|
||||
this.lines = state.lines;
|
||||
this.level = state.level;
|
||||
this.gameOver = state.gameOver;
|
||||
this.eliminated = state.eliminated;
|
||||
this.dropInterval = Math.max(100, 1000 - (this.level - 1) * 100);
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in other modules
|
||||
window.TetrisGame = TetrisGame;
|
||||
window.TETROMINOS = TETROMINOS;
|
||||
window.BOARD_WIDTH = BOARD_WIDTH;
|
||||
window.BOARD_HEIGHT = BOARD_HEIGHT;
|
||||
window.CELL_SIZE = CELL_SIZE;
|
||||
window.GARBAGE_COLOR = GARbage_COLOR;
|
||||
@@ -0,0 +1,153 @@
|
||||
// Network module - Socket.io client handling
|
||||
|
||||
class NetworkManager {
|
||||
constructor() {
|
||||
this.socket = null;
|
||||
this.currentRoom = null;
|
||||
this.currentPlayerId = null;
|
||||
this.players = {};
|
||||
this.gameState = {};
|
||||
this.listeners = {
|
||||
onPlayerJoined: null,
|
||||
onPlayerLeft: null,
|
||||
onGameStarted: null,
|
||||
onStateUpdate: null,
|
||||
onGameOver: null
|
||||
};
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.socket = io();
|
||||
|
||||
this.socket.on('connect', () => {
|
||||
console.log('Connected to server');
|
||||
this.currentPlayerId = this.socket.id;
|
||||
});
|
||||
|
||||
this.socket.on('disconnect', () => {
|
||||
console.log('Disconnected from server');
|
||||
this.currentRoom = null;
|
||||
});
|
||||
|
||||
this.socket.on('player-joined', ({ player, players }) => {
|
||||
this.updatePlayers(players);
|
||||
if (this.listeners.onPlayerJoined) {
|
||||
this.listeners.onPlayerJoined(player);
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on('player-left', ({ playerId, players }) => {
|
||||
delete this.players[playerId];
|
||||
this.updatePlayers(players);
|
||||
if (this.listeners.onPlayerLeft) {
|
||||
this.listeners.onPlayerLeft(playerId);
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on('game-started', ({ players, states }) => {
|
||||
this.updatePlayers(players);
|
||||
this.updateStates(states);
|
||||
if (this.listeners.onGameStarted) {
|
||||
this.listeners.onGameStarted(players, states);
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on('state-update', (states) => {
|
||||
this.updateStates(states);
|
||||
if (this.listeners.onStateUpdate) {
|
||||
this.listeners.onStateUpdate(states);
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on('game-over', (data) => {
|
||||
if (this.listeners.onGameOver) {
|
||||
this.listeners.onGameOver(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
joinRoom(roomName, playerName) {
|
||||
if (!this.socket) return;
|
||||
|
||||
this.currentRoom = roomName;
|
||||
this.socket.emit('join-room', { roomName, playerName });
|
||||
}
|
||||
|
||||
leaveRoom() {
|
||||
if (!this.socket) return;
|
||||
|
||||
if (this.currentRoom) {
|
||||
this.socket.leave(this.currentRoom);
|
||||
}
|
||||
this.currentRoom = null;
|
||||
}
|
||||
|
||||
ready() {
|
||||
if (!this.socket) return;
|
||||
this.socket.emit('ready');
|
||||
}
|
||||
|
||||
sendMove(direction) {
|
||||
if (!this.socket || !this.currentPlayerId) return;
|
||||
this.socket.emit('player-move', { playerId: this.currentPlayerId, direction });
|
||||
}
|
||||
|
||||
sendRotate() {
|
||||
if (!this.socket || !this.currentPlayerId) return;
|
||||
this.socket.emit('player-rotate', { playerId: this.currentPlayerId });
|
||||
}
|
||||
|
||||
sendDrop() {
|
||||
if (!this.socket || !this.currentPlayerId) return;
|
||||
this.socket.emit('player-drop', { playerId: this.currentPlayerId, hard: false });
|
||||
}
|
||||
|
||||
sendHardDrop() {
|
||||
if (!this.socket || !this.currentPlayerId) return;
|
||||
this.socket.emit('player-drop', { playerId: this.currentPlayerId, hard: true });
|
||||
}
|
||||
|
||||
updatePlayers(players) {
|
||||
this.players = {};
|
||||
players.forEach(p => {
|
||||
this.players[p.id] = p;
|
||||
});
|
||||
}
|
||||
|
||||
updateStates(states) {
|
||||
states.forEach(s => {
|
||||
this.gameState[s.playerId] = s;
|
||||
});
|
||||
}
|
||||
|
||||
getPlayer(playerId) {
|
||||
return this.players[playerId];
|
||||
}
|
||||
|
||||
getGameState(playerId) {
|
||||
return this.gameState[playerId];
|
||||
}
|
||||
|
||||
getAllPlayers() {
|
||||
return this.players;
|
||||
}
|
||||
|
||||
getAllGameStates() {
|
||||
return this.gameState;
|
||||
}
|
||||
|
||||
setListener(event, callback) {
|
||||
const listenerMap = {
|
||||
'player-joined': 'onPlayerJoined',
|
||||
'player-left': 'onPlayerLeft',
|
||||
'game-started': 'onGameStarted',
|
||||
'state-update': 'onStateUpdate',
|
||||
'game-over': 'onGameOver'
|
||||
};
|
||||
if (callback) {
|
||||
this.listeners[listenerMap[event]] = callback;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const network = new NetworkManager();
|
||||
@@ -0,0 +1,264 @@
|
||||
// Canvas Renderer for Tetris
|
||||
|
||||
class TetrisRenderer {
|
||||
constructor(containerId) {
|
||||
this.container = document.getElementById(containerId);
|
||||
this.boards = new Map();
|
||||
}
|
||||
|
||||
createPlayerBoard(playerId, playerName) {
|
||||
const boardDiv = document.createElement('div');
|
||||
boardDiv.className = 'player-board';
|
||||
boardDiv.id = `board-${playerId}`;
|
||||
boardDiv.dataset.playerId = playerId;
|
||||
|
||||
// Main game canvas
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.id = `canvas-${playerId}`;
|
||||
canvas.width = BOARD_WIDTH * CELL_SIZE;
|
||||
canvas.height = BOARD_HEIGHT * CELL_SIZE;
|
||||
canvas.style.width = `${BOARD_WIDTH * CELL_SIZE}px`;
|
||||
canvas.style.height = `${BOARD_HEIGHT * CELL_SIZE}px`;
|
||||
boardDiv.appendChild(canvas);
|
||||
|
||||
// Player info
|
||||
const infoDiv = document.createElement('div');
|
||||
infoDiv.className = 'board-info';
|
||||
infoDiv.innerHTML = `
|
||||
<span class="name">${playerName}</span>
|
||||
<span class="stats">Lines: <span class="lines">0</span></span>
|
||||
`;
|
||||
boardDiv.appendChild(infoDiv);
|
||||
|
||||
// Next piece preview
|
||||
const nextDiv = document.createElement('div');
|
||||
nextDiv.className = 'board-info';
|
||||
const nextCanvas = document.createElement('canvas');
|
||||
nextCanvas.id = `next-${playerId}`;
|
||||
nextCanvas.width = 80;
|
||||
nextCanvas.height = 80;
|
||||
nextCanvas.style.width = '80px';
|
||||
nextCanvas.style.height = '80px';
|
||||
nextDiv.innerHTML = '<span>NEXT:</span>';
|
||||
nextDiv.appendChild(nextCanvas);
|
||||
boardDiv.appendChild(nextDiv);
|
||||
|
||||
this.container.appendChild(boardDiv);
|
||||
|
||||
this.boards.set(playerId, {
|
||||
element: boardDiv,
|
||||
canvas: canvas,
|
||||
nextCanvas: nextCanvas,
|
||||
info: infoDiv,
|
||||
ctx: canvas.getContext('2d'),
|
||||
nextCtx: nextCanvas.getContext('2d')
|
||||
});
|
||||
|
||||
return boardDiv;
|
||||
}
|
||||
|
||||
removePlayerBoard(playerId) {
|
||||
const board = this.boards.get(playerId);
|
||||
if (board) {
|
||||
board.element.remove();
|
||||
this.boards.delete(playerId);
|
||||
}
|
||||
}
|
||||
|
||||
renderPlayer(playerId, gameState) {
|
||||
const boardData = this.boards.get(playerId);
|
||||
if (!boardData) return;
|
||||
|
||||
const { ctx, nextCtx, element, info } = boardData;
|
||||
|
||||
// Clear canvas
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
|
||||
// Draw grid lines
|
||||
this.drawGrid(ctx);
|
||||
|
||||
// Draw locked pieces
|
||||
if (gameState && gameState.board) {
|
||||
this.drawBoard(ctx, gameState.board);
|
||||
}
|
||||
|
||||
// Draw current piece
|
||||
if (gameState && gameState.currentPiece && !gameState.gameOver) {
|
||||
this.drawPiece(ctx, gameState.currentPiece);
|
||||
}
|
||||
|
||||
// Draw next piece
|
||||
if (gameState && gameState.nextPiece) {
|
||||
this.drawNextPiece(nextCtx, gameState.nextPiece);
|
||||
}
|
||||
|
||||
// Update stats
|
||||
if (gameState) {
|
||||
const linesSpan = info.querySelector('.lines');
|
||||
if (linesSpan) {
|
||||
linesSpan.textContent = gameState.lines;
|
||||
}
|
||||
}
|
||||
|
||||
// Update board state classes
|
||||
element.classList.remove('active', 'eliminated');
|
||||
if (gameState && gameState.eliminated) {
|
||||
element.classList.add('eliminated');
|
||||
}
|
||||
}
|
||||
|
||||
drawGrid(ctx) {
|
||||
ctx.strokeStyle = '#222';
|
||||
ctx.lineWidth = 1;
|
||||
|
||||
for (let x = 0; x <= BOARD_WIDTH; x++) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x * CELL_SIZE, 0);
|
||||
ctx.lineTo(x * CELL_SIZE, BOARD_HEIGHT * CELL_SIZE);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
for (let y = 0; y <= BOARD_HEIGHT; y++) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y * CELL_SIZE);
|
||||
ctx.lineTo(BOARD_WIDTH * CELL_SIZE, y * CELL_SIZE);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
drawBoard(ctx, board) {
|
||||
for (let row = 0; row < BOARD_HEIGHT; row++) {
|
||||
for (let col = 0; col < BOARD_WIDTH; col++) {
|
||||
const color = board[row][col];
|
||||
if (color) {
|
||||
this.drawCell(ctx, col, row, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drawPiece(ctx, piece) {
|
||||
for (let row = 0; row < piece.shape.length; row++) {
|
||||
for (let col = 0; col < piece.shape[row].length; col++) {
|
||||
if (piece.shape[row][col]) {
|
||||
const x = piece.x + col;
|
||||
const y = piece.y + row;
|
||||
if (y >= 0) {
|
||||
this.drawCell(ctx, x, y, piece.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drawCell(ctx, x, y, color) {
|
||||
const px = x * CELL_SIZE;
|
||||
const py = y * CELL_SIZE;
|
||||
|
||||
// Main fill
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(px, py, CELL_SIZE, CELL_SIZE);
|
||||
|
||||
// Bevel effect
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.3)';
|
||||
ctx.fillRect(px, py, CELL_SIZE, 3);
|
||||
ctx.fillRect(px, py, 3, CELL_SIZE);
|
||||
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.3)';
|
||||
ctx.fillRect(px, py + CELL_SIZE - 3, CELL_SIZE, 3);
|
||||
ctx.fillRect(px + CELL_SIZE - 3, py, 3, CELL_SIZE);
|
||||
|
||||
// Border
|
||||
ctx.strokeStyle = 'rgba(0,0,0,0.5)';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(px, py, CELL_SIZE, CELL_SIZE);
|
||||
}
|
||||
|
||||
drawNextPiece(ctx, piece) {
|
||||
// Clear
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
|
||||
// Center the piece
|
||||
const pieceWidth = piece.shape[0].length * 20;
|
||||
const pieceHeight = piece.shape.length * 20;
|
||||
const offsetX = (ctx.canvas.width - pieceWidth) / 2;
|
||||
const offsetY = (ctx.canvas.height - pieceHeight) / 2;
|
||||
|
||||
for (let row = 0; row < piece.shape.length; row++) {
|
||||
for (let col = 0; col < piece.shape[row].length; col++) {
|
||||
if (piece.shape[row][col]) {
|
||||
const x = offsetX + col * 20;
|
||||
const y = offsetY + row * 20;
|
||||
|
||||
ctx.fillStyle = piece.color;
|
||||
ctx.fillRect(x, y, 20, 20);
|
||||
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.3)';
|
||||
ctx.fillRect(x, y, 20, 3);
|
||||
ctx.fillRect(x, y, 3, 20);
|
||||
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.3)';
|
||||
ctx.fillRect(x, y + 17, 20, 3);
|
||||
ctx.fillRect(x + 17, y, 3, 20);
|
||||
|
||||
ctx.strokeStyle = 'rgba(0,0,0,0.5)';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(x, y, 20, 20);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setActivePlayer(playerId) {
|
||||
// Remove active class from all boards
|
||||
this.boards.forEach((boardData) => {
|
||||
boardData.element.classList.remove('active');
|
||||
});
|
||||
|
||||
// Add to current player
|
||||
const boardData = this.boards.get(playerId);
|
||||
if (boardData) {
|
||||
boardData.element.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
triggerShake(playerId) {
|
||||
const boardData = this.boards.get(playerId);
|
||||
if (boardData) {
|
||||
boardData.element.classList.remove('shake');
|
||||
void boardData.element.offsetWidth; // Trigger reflow
|
||||
boardData.element.classList.add('shake');
|
||||
}
|
||||
}
|
||||
|
||||
triggerFlash(playerId) {
|
||||
const boardData = this.boards.get(playerId);
|
||||
if (boardData) {
|
||||
boardData.element.classList.remove('flash');
|
||||
void boardData.element.offsetWidth; // Trigger reflow
|
||||
boardData.element.classList.add('flash');
|
||||
}
|
||||
}
|
||||
|
||||
updatePlayerInfo(playerId, name, score, lines) {
|
||||
const boardData = this.boards.get(playerId);
|
||||
if (boardData) {
|
||||
const nameSpan = boardData.info.querySelector('.name');
|
||||
if (nameSpan) nameSpan.textContent = name;
|
||||
|
||||
const linesSpan = boardData.info.querySelector('.lines');
|
||||
if (linesSpan) linesSpan.textContent = lines;
|
||||
}
|
||||
}
|
||||
|
||||
clearAll() {
|
||||
this.boards.forEach((boardData) => {
|
||||
boardData.element.remove();
|
||||
});
|
||||
this.boards.clear();
|
||||
}
|
||||
}
|
||||
|
||||
window.TetrisRenderer = TetrisRenderer;
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
// UI Module - Handle screens and user interactions
|
||||
|
||||
class UIManager {
|
||||
constructor() {
|
||||
this.screens = {
|
||||
room: document.getElementById('room-screen'),
|
||||
lobby: document.getElementById('lobby-screen'),
|
||||
game: document.getElementById('game-screen'),
|
||||
gameover: document.getElementById('gameover-screen')
|
||||
};
|
||||
|
||||
this.inputs = {
|
||||
roomName: document.getElementById('room-name'),
|
||||
playerName: document.getElementById('player-name')
|
||||
};
|
||||
|
||||
this.buttons = {
|
||||
join: document.getElementById('join-btn'),
|
||||
ready: document.getElementById('ready-btn'),
|
||||
leave: document.getElementById('leave-btn'),
|
||||
backToLobby: document.getElementById('back-to-lobby')
|
||||
};
|
||||
|
||||
this.displays = {
|
||||
lobbyRoomName: document.getElementById('lobby-room-name'),
|
||||
gameRoomName: document.getElementById('game-room-name'),
|
||||
playerList: document.getElementById('player-list'),
|
||||
battleGrid: document.getElementById('battle-grid'),
|
||||
gameStatus: document.getElementById('game-status'),
|
||||
winnerDisplay: document.getElementById('winner-display'),
|
||||
finalScores: document.getElementById('final-scores')
|
||||
};
|
||||
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
this.buttons.join.addEventListener('click', () => this.handleJoin());
|
||||
this.buttons.ready.addEventListener('click', () => this.handleReady());
|
||||
this.buttons.leave.addEventListener('click', () => this.handleLeave());
|
||||
this.buttons.backToLobby.addEventListener('click', () => this.handleBackToLobby());
|
||||
|
||||
// Allow Enter key to submit forms
|
||||
this.inputs.roomName.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') this.handleJoin();
|
||||
});
|
||||
this.inputs.playerName.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') this.handleJoin();
|
||||
});
|
||||
}
|
||||
|
||||
showScreen(screenName) {
|
||||
Object.values(this.screens).forEach(screen => screen.classList.remove('active'));
|
||||
this.screens[screenName].classList.add('active');
|
||||
}
|
||||
|
||||
handleJoin() {
|
||||
const roomName = this.inputs.roomName.value.trim();
|
||||
const playerName = this.inputs.playerName.value.trim();
|
||||
|
||||
if (!roomName || !playerName) {
|
||||
this.showMessage('Please enter room name and your name');
|
||||
return;
|
||||
}
|
||||
|
||||
// Emit join event
|
||||
network.setListener('player-joined', () => {
|
||||
this.showScreen('lobby');
|
||||
this.displays.lobbyRoomName.textContent = roomName;
|
||||
});
|
||||
|
||||
network.joinRoom(roomName, playerName);
|
||||
}
|
||||
|
||||
handleReady() {
|
||||
network.ready();
|
||||
this.buttons.ready.textContent = 'READY!';
|
||||
this.buttons.ready.disabled = true;
|
||||
}
|
||||
|
||||
handleLeave() {
|
||||
network.leaveRoom();
|
||||
this.showScreen('room');
|
||||
}
|
||||
|
||||
handleBackToLobby() {
|
||||
this.showScreen('room');
|
||||
}
|
||||
|
||||
updatePlayerList(players) {
|
||||
this.displays.playerList.innerHTML = '';
|
||||
Object.values(players).forEach(player => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'player-item';
|
||||
const statusClass = player.ready ? 'ready' : '';
|
||||
item.innerHTML = `
|
||||
<span class="name">${this.escapeHtml(player.name)}</span>
|
||||
<span class="status ${statusClass}">${player.ready ? 'READY' : 'WAITING'}</span>
|
||||
`;
|
||||
this.displays.playerList.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
showMessage(message) {
|
||||
this.displays.gameStatus.textContent = message;
|
||||
setTimeout(() => {
|
||||
this.displays.gameStatus.textContent = '';
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
showGameOver(winner, scores) {
|
||||
this.displays.winnerDisplay.textContent = winner
|
||||
? `Winner: ${winner}!`
|
||||
: 'Game Over!';
|
||||
this.displays.winnerDisplay.style.color = winner ? '#0f0' : '#fff';
|
||||
|
||||
this.displays.finalScores.innerHTML = '';
|
||||
Object.entries(scores).forEach(([name, score], index) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'score-item' + (index === 0 ? ' winner' : '');
|
||||
item.innerHTML = `
|
||||
<span>${this.escapeHtml(name)}</span>
|
||||
<span>${score}</span>
|
||||
`;
|
||||
this.displays.finalScores.appendChild(item);
|
||||
});
|
||||
|
||||
this.showScreen('gameover');
|
||||
}
|
||||
}
|
||||
|
||||
window.ui = new UIManager();
|
||||
Reference in New Issue
Block a user