Implement DAS/ARR for smooth horizontal piece movement

This commit is contained in:
2026-03-24 17:59:55 -07:00
parent 36965dc887
commit 3a307dbc80
11 changed files with 338 additions and 26 deletions
+25
View File
@@ -79,14 +79,39 @@ This design gives players a buffer zone to recover from garbage attacks before e
| `player-move` | Client->Server | `{ playerId, direction }` | | `player-move` | Client->Server | `{ playerId, direction }` |
| `player-rotate` | Client->Server | `{ playerId }` | | `player-rotate` | Client->Server | `{ playerId }` |
| `player-drop` | Client->Server | `{ playerId, hard }` | | `player-drop` | Client->Server | `{ playerId, hard }` |
| `player-hold` | Client->Server | `{ playerId }` |
| `zone-activate` | Client->Server | `{ playerId }` |
| `player-joined` | Server->Client | `{ player, players }` | | `player-joined` | Server->Client | `{ player, players }` |
| `player-left` | Server->Client | `{ playerId, players }` | | `player-left` | Server->Client | `{ playerId, players }` |
| `game-started` | Server->Client | `{ players, states }` | | `game-started` | Server->Client | `{ players, states }` |
| `state-update` | Server->Client | `states[]` | | `state-update` | Server->Client | `states[]` |
| `game-over` | Server->Client | `{ states }` | | `game-over` | Server->Client | `{ states }` |
### Zone Mechanic (Tetris Effect)
Zone is a time-based attack mechanic imported from Tetris Effect. When players clear lines, their **Zone meter** fills:
- **1 line** = +15 Zone meter
- **2 lines** = +25 Zone meter
- **3 lines** = +35 Zone meter
- **4 lines (Tetris)** = +50 Zone meter
**When Zone meter reaches 100:**
- Press **Z key** or **ZONE button** to activate Zone
- Gravity pauses for **20 seconds**
- Players control pieces manually but gravity is frozen
- Cleared lines **cascade to bottom** instead of disappearing
- After 20 seconds, Zone ends and sends garbage to opponents
**Zone Attack Calculation:**
- Lines cleared during Zone × 1.5 = garbage rows sent
- Example: 4 Zone lines = 6 garbage rows, 8 Zone lines = 12 garbage rows
- Garbage is distributed randomly among remaining opponents
Zone provides a high-risk, high-reward tactic for eliminating opponents with massive attacks.
### Rendering ### Rendering
- Each player gets a dynamically created board with canvas + info divs - Each player gets a dynamically created board with canvas + info divs
- `renderer.setActivePlayer()` marks current player's board as `.main`, others as `.spectator` - `renderer.setActivePlayer()` marks current player's board as `.main`, others as `.spectator`
- Zone meter shown below each player board with fill bar and status indicator
- Battle grid layout classes: `.single-player`, `.two-players`, `.multi-player` - Battle grid layout classes: `.single-player`, `.two-players`, `.multi-player`
Binary file not shown.
+45
View File
@@ -337,6 +337,38 @@ button:active {
border: 1px solid #333; border: 1px solid #333;
background: #000; background: #000;
} }
/* Zone meter styling */
.zone-info {
font-size: 0.5rem !important;
gap: 8px;
justify-content: center;
padding: 8px 5px !important;
}
.zone-meter-bar {
display: inline-block;
width: 100px;
height: 12px;
border: 1px solid #0ff;
background: #000;
position: relative;
overflow: hidden;
}
.zone-meter-fill {
display: block;
height: 100%;
background: linear-gradient(90deg, #00ff00, #ffff00, #ff6600);
width: 0%;
transition: width 0.1s ease;
}
.zone-status {
color: #fff;
font-size: 0.45rem;
}
} }
#game-header { #game-header {
@@ -554,6 +586,19 @@ button:active {
font-size: 0.9rem; font-size: 0.9rem;
} }
.touch-btn.zone-btn {
background: #8800ff;
border-color: #8800ff;
font-size: 0.9rem;
}
.touch-btn.zone-btn:disabled {
background: #440088;
border-color: #440088;
opacity: 0.5;
cursor: not-allowed;
}
/* Show touch controls on mobile only */ /* Show touch controls on mobile only */
@media (max-width: 768px) and (hover: none) { @media (max-width: 768px) and (hover: none) {
#touch-controls { #touch-controls {
+8 -3
View File
@@ -10,6 +10,9 @@
<link rel="stylesheet" href="css/style.css?v=11"> <link rel="stylesheet" href="css/style.css?v=11">
</head> </head>
<body> <body>
<!-- Background Music -->
<audio id="bg-music" volume="0.3"></audio>
<div id="app"> <div id="app">
<!-- Login Screen --> <!-- Login Screen -->
<div id="room-screen" class="screen active"> <div id="room-screen" class="screen active">
@@ -48,6 +51,7 @@
<button id="btn-rotate" class="touch-btn action-btn"></button> <button id="btn-rotate" class="touch-btn action-btn"></button>
<button id="btn-drop" class="touch-btn drop-btn"></button> <button id="btn-drop" class="touch-btn drop-btn"></button>
<button id="btn-hold" class="touch-btn hold-btn">HOLD</button> <button id="btn-hold" class="touch-btn hold-btn">HOLD</button>
<button id="btn-zone" class="touch-btn zone-btn" disabled>ZONE</button>
</div> </div>
</div> </div>
</div> </div>
@@ -62,10 +66,11 @@
</div> </div>
<script src="/socket.io/socket.io.js"></script> <script src="/socket.io/socket.io.js"></script>
<script src="js/audio.js?v=1"></script>
<script src="js/network.js?v=6"></script> <script src="js/network.js?v=6"></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=6"></script>
<script src="js/ui.js?v=6"></script> <script src="js/ui.js?v=7"></script>
<script src="js/app.js?v=5"></script> <script src="js/app.js?v=6"></script>
</body> </body>
</html> </html>
+77 -12
View File
@@ -4,6 +4,17 @@ let localGame = null;
let renderer = null; let renderer = null;
let lastTime = 0; 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 // Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// Connect to server // Connect to server
@@ -26,10 +37,18 @@ function setupNetworkListeners() {
// Show lobby screen if we're on the login screen // Show lobby screen if we're on the login screen
if (ui.screens.room.classList.contains('active')) { if (ui.screens.room.classList.contains('active')) {
ui.showScreen('lobby'); 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) // Reset lobby state when returning from game over (player is null indicates game over reset)
if (!player) { if (!player) {
ui.resetLobbyState(); ui.resetLobbyState();
// Resume lobby music after game ends
if (typeof audio !== 'undefined') {
audio.playLobby();
}
} }
}); });
@@ -43,6 +62,11 @@ function setupNetworkListeners() {
ui.showScreen('game'); ui.showScreen('game');
ui.displays.gameRoomName.textContent = 'GLOBAL LOBBY'; ui.displays.gameRoomName.textContent = 'GLOBAL LOBBY';
// Start game music - will play continuously until game ends
if (typeof audio !== 'undefined') {
audio.playGame();
}
// Clear old boards // Clear old boards
renderer.clearAll(); renderer.clearAll();
@@ -88,6 +112,11 @@ function setupNetworkListeners() {
ui.showScreen('game'); ui.showScreen('game');
ui.showSpectatorMode(); ui.showSpectatorMode();
// Start game music for spectators too
if (typeof audio !== 'undefined') {
audio.playGame();
}
// Clear old boards // Clear old boards
renderer.clearAll(); renderer.clearAll();
@@ -122,19 +151,16 @@ function setupKeyboardControls() {
if (network.isSpectator) return; if (network.isSpectator) return;
if (ui.screens.game.classList.contains('active') && localGame) { 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) { 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': case 'ArrowUp':
network.sendRotate(); network.sendRotate();
e.preventDefault(); e.preventDefault();
@@ -147,10 +173,22 @@ function setupKeyboardControls() {
network.sendHold(); network.sendHold();
e.preventDefault(); e.preventDefault();
break; 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 // Touch controls for mobile
const btnLeft = document.getElementById('btn-left'); const btnLeft = document.getElementById('btn-left');
const btnRight = document.getElementById('btn-right'); const btnRight = document.getElementById('btn-right');
@@ -158,6 +196,7 @@ function setupKeyboardControls() {
const btnRotate = document.getElementById('btn-rotate'); const btnRotate = document.getElementById('btn-rotate');
const btnDrop = document.getElementById('btn-drop'); const btnDrop = document.getElementById('btn-drop');
const btnHold = document.getElementById('btn-hold'); const btnHold = document.getElementById('btn-hold');
const btnZone = document.getElementById('btn-zone');
const handleTouch = (e, action) => { const handleTouch = (e, action) => {
e.preventDefault(); e.preventDefault();
@@ -176,6 +215,7 @@ function setupKeyboardControls() {
btnRotate.addEventListener('pointerdown', (e) => handleTouch(e, () => network.sendRotate())); btnRotate.addEventListener('pointerdown', (e) => handleTouch(e, () => network.sendRotate()));
btnDrop.addEventListener('pointerdown', (e) => handleTouch(e, () => network.sendHardDrop())); btnDrop.addEventListener('pointerdown', (e) => handleTouch(e, () => network.sendHardDrop()));
btnHold.addEventListener('pointerdown', (e) => handleTouch(e, () => network.sendHold())); btnHold.addEventListener('pointerdown', (e) => handleTouch(e, () => network.sendHold()));
btnZone.addEventListener('pointerdown', (e) => handleTouch(e, () => network.sendZoneActivate()));
// Prevent double-tap zoom // Prevent double-tap zoom
btnLeft.addEventListener('touchstart', (e) => e.preventDefault()); btnLeft.addEventListener('touchstart', (e) => e.preventDefault());
@@ -184,6 +224,7 @@ function setupKeyboardControls() {
btnRotate.addEventListener('touchstart', (e) => e.preventDefault()); btnRotate.addEventListener('touchstart', (e) => e.preventDefault());
btnDrop.addEventListener('touchstart', (e) => e.preventDefault()); btnDrop.addEventListener('touchstart', (e) => e.preventDefault());
btnHold.addEventListener('touchstart', (e) => e.preventDefault()); btnHold.addEventListener('touchstart', (e) => e.preventDefault());
btnZone.addEventListener('touchstart', (e) => e.preventDefault());
} }
function updateBattleGridLayout(playerCount) { function updateBattleGridLayout(playerCount) {
@@ -224,6 +265,8 @@ function endGame(states) {
} }
}); });
// Game music will continue during game over screen, then transition back to lobby music via resetLobbyState
// Reset spectator mode for next round // Reset spectator mode for next round
network.isSpectator = false; network.isSpectator = false;
ui.hideSpectatorMode(); ui.hideSpectatorMode();
@@ -239,6 +282,21 @@ function gameLoop(currentTime) {
const deltaTime = currentTime - lastTime; const deltaTime = currentTime - lastTime;
lastTime = currentTime; 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 // Update local game
if (localGame) { if (localGame) {
localGame.update(deltaTime); localGame.update(deltaTime);
@@ -250,6 +308,13 @@ function gameLoop(currentTime) {
renderer.renderPlayer(state.playerId, 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) // Set active player highlight (or null for spectators to show all boards equally)
renderer.setActivePlayer(network.isSpectator ? null : network.currentPlayerId); renderer.setActivePlayer(network.isSpectator ? null : network.currentPlayerId);
+34
View File
@@ -0,0 +1,34 @@
// Audio Manager - Handles background music only
class AudioManager {
constructor() {
this.bgMusic = document.getElementById('bg-music');
if (this.bgMusic) {
this.bgMusic.volume = 0.5;
}
}
playLobby() {
// No lobby music
}
playGame() {
if (!this.bgMusic) return;
this.bgMusic.src = '/audio/tetris_theme_og.mp3';
this.bgMusic.loop = true;
this.bgMusic.play().catch(e => console.log('Audio autoplay blocked:', e));
}
playGameOver() {
// No game over music
}
stop() {
if (!this.bgMusic) return;
this.bgMusic.pause();
this.bgMusic.currentTime = 0;
}
}
// Initialize audio manager
const audio = new AudioManager();
+5
View File
@@ -140,6 +140,11 @@ class NetworkManager {
this.socket.emit('player-hold', { playerId: this.currentPlayerId }); this.socket.emit('player-hold', { playerId: this.currentPlayerId });
} }
sendZoneActivate() {
if (!this.socket || !this.currentPlayerId) return;
this.socket.emit('zone-activate', { playerId: this.currentPlayerId });
}
updatePlayers(players) { updatePlayers(players) {
this.players = {}; this.players = {};
players.forEach(p => { players.forEach(p => {
+38 -8
View File
@@ -35,10 +35,10 @@ class TetrisRenderer {
nextDiv.className = 'board-info'; nextDiv.className = 'board-info';
const nextCanvas = document.createElement('canvas'); const nextCanvas = document.createElement('canvas');
nextCanvas.id = `next-${playerId}`; nextCanvas.id = `next-${playerId}`;
nextCanvas.width = 80; nextCanvas.width = 60;
nextCanvas.height = 80; nextCanvas.height = 60;
nextCanvas.style.width = '80px'; nextCanvas.style.width = '60px';
nextCanvas.style.height = '80px'; nextCanvas.style.height = '60px';
nextDiv.innerHTML = '<span>NEXT:</span>'; nextDiv.innerHTML = '<span>NEXT:</span>';
nextDiv.appendChild(nextCanvas); nextDiv.appendChild(nextCanvas);
boardDiv.appendChild(nextDiv); boardDiv.appendChild(nextDiv);
@@ -48,14 +48,21 @@ class TetrisRenderer {
holdDiv.className = 'board-info'; holdDiv.className = 'board-info';
const holdCanvas = document.createElement('canvas'); const holdCanvas = document.createElement('canvas');
holdCanvas.id = `hold-${playerId}`; holdCanvas.id = `hold-${playerId}`;
holdCanvas.width = 80; holdCanvas.width = 60;
holdCanvas.height = 80; holdCanvas.height = 60;
holdCanvas.style.width = '80px'; holdCanvas.style.width = '60px';
holdCanvas.style.height = '80px'; holdCanvas.style.height = '60px';
holdDiv.innerHTML = '<span>HOLD:</span>'; holdDiv.innerHTML = '<span>HOLD:</span>';
holdDiv.appendChild(holdCanvas); holdDiv.appendChild(holdCanvas);
boardDiv.appendChild(holdDiv); boardDiv.appendChild(holdDiv);
// Zone meter
const zoneDiv = document.createElement('div');
zoneDiv.className = 'board-info zone-info';
zoneDiv.id = `zone-${playerId}`;
zoneDiv.innerHTML = '<span>ZONE: <span class="zone-meter-bar"><span class="zone-meter-fill"></span></span> <span class="zone-status"></span></span>';
boardDiv.appendChild(zoneDiv);
this.container.appendChild(boardDiv); this.container.appendChild(boardDiv);
this.boards.set(playerId, { this.boards.set(playerId, {
@@ -63,6 +70,7 @@ class TetrisRenderer {
canvas: canvas, canvas: canvas,
nextCanvas: nextCanvas, nextCanvas: nextCanvas,
holdCanvas: holdCanvas, holdCanvas: holdCanvas,
zoneDiv: zoneDiv,
info: infoDiv, info: infoDiv,
ctx: canvas.getContext('2d'), ctx: canvas.getContext('2d'),
nextCtx: nextCanvas.getContext('2d'), nextCtx: nextCanvas.getContext('2d'),
@@ -128,6 +136,28 @@ class TetrisRenderer {
if (linesSpan) { if (linesSpan) {
linesSpan.textContent = gameState.lines; linesSpan.textContent = gameState.lines;
} }
// Update Zone meter
const zoneDiv = boardData.zoneDiv;
if (zoneDiv) {
const meterFill = zoneDiv.querySelector('.zone-meter-fill');
const statusSpan = zoneDiv.querySelector('.zone-status');
if (meterFill) {
meterFill.style.width = gameState.zoneMeter + '%';
}
if (statusSpan) {
if (gameState.zoneActive) {
statusSpan.textContent = `ACTIVE (${Math.max(0, Math.round(gameState.zoneTimeRemaining / 1000))}s)`;
statusSpan.style.color = '#00ff00';
} else if (gameState.zoneMeter >= 100) {
statusSpan.textContent = 'READY!';
statusSpan.style.color = '#ffff00';
} else {
statusSpan.textContent = '';
statusSpan.style.color = '#fff';
}
}
}
} }
// Update board state classes (preserving main/spectator/position classes) // Update board state classes (preserving main/spectator/position classes)
+5
View File
@@ -48,6 +48,11 @@ class UIManager {
showScreen(screenName) { showScreen(screenName) {
Object.values(this.screens).forEach(screen => screen.classList.remove('active')); Object.values(this.screens).forEach(screen => screen.classList.remove('active'));
this.screens[screenName].classList.add('active'); this.screens[screenName].classList.add('active');
// Play audio based on screen (but not for game/lobby - those are handled by app.js)
if (typeof audio !== 'undefined' && screenName === 'room') {
audio.stop();
}
} }
handleJoin() { handleJoin() {
+99 -3
View File
@@ -55,6 +55,11 @@ app.use(express.static(path.join(__dirname, '../public')));
* @property {number} dropCounter - Frame counter for auto-drop * @property {number} dropCounter - Frame counter for auto-drop
* @property {number} dropInterval - Milliseconds between auto-drops * @property {number} dropInterval - Milliseconds between auto-drops
* @property {object[]} garbageReceived - History of garbage received * @property {object[]} garbageReceived - History of garbage received
* @property {number} zoneMeter - Zone meter (0-100), increases by line clears
* @property {boolean} zoneActive - Whether Zone is currently active
* @property {number} zoneStartTime - Timestamp when Zone started
* @property {number} zoneLineCount - Lines cleared during current Zone
* @property {number} zoneTotalDuration - Total Zone duration in ms (20000ms = 20 seconds)
*/ */
/** /**
@@ -138,7 +143,12 @@ io.on('connection', (socket) => {
ready: false, ready: false,
dropCounter: 0, dropCounter: 0,
dropInterval: 1000, dropInterval: 1000,
garbageReceived: [] garbageReceived: [],
zoneMeter: 0,
zoneActive: false,
zoneStartTime: null,
zoneLineCount: 0,
zoneTotalDuration: 20000
}; };
lobby.players.set(socket.id, player); lobby.players.set(socket.id, player);
@@ -328,6 +338,24 @@ io.on('connection', (socket) => {
broadcastState(); broadcastState();
}); });
socket.on('zone-activate', ({ playerId }) => {
if (!lobby.gameStarted) return;
const player = lobby.players.get(playerId);
if (!player || player.eliminated || player.zoneActive) return;
// Only activate if Zone meter is full
if (player.zoneMeter < 100) return;
player.zoneActive = true;
player.zoneStartTime = Date.now();
player.zoneLineCount = 0;
player.zoneMeter = 0;
console.log(`[ZONE] ${player.name} activated Zone!`);
broadcastState();
});
socket.on('disconnect', () => { socket.on('disconnect', () => {
const player = lobby.players.get(socket.id); const player = lobby.players.get(socket.id);
if (player) { if (player) {
@@ -464,7 +492,22 @@ function lockPiece(player) {
function clearRows(player) { function clearRows(player) {
const result = clearRowsFromBoard(player.board); const result = clearRowsFromBoard(player.board);
const rowsCleared = result.rowsCleared; let rowsCleared = result.rowsCleared;
// During Zone, lines cascade to bottom instead of disappearing - reapply them
if (player.zoneActive && rowsCleared > 0) {
player.zoneLineCount += rowsCleared;
// Keep the lines on board for cascade effect (they stay, don't disappear)
player.board = result.board;
// Add cleared lines back to bottom for visual cascade
for (let i = 0; i < rowsCleared; i++) {
player.board.push(Array(BOARD_WIDTH).fill('#999999')); // Cleared line color
}
// Shift other lines up to make room
player.board = player.board.slice(-BOARD_HEIGHT);
return rowsCleared; // Still count for scoring
}
player.board = result.board; player.board = result.board;
if (rowsCleared > 0) { if (rowsCleared > 0) {
@@ -473,7 +516,12 @@ function clearRows(player) {
player.score += points[rowsCleared] * player.level; player.score += points[rowsCleared] * player.level;
player.level = Math.floor(player.lines / 10) + 1; player.level = Math.floor(player.lines / 10) + 1;
player.dropInterval = Math.max(100, 1000 - (player.level - 1) * 100); player.dropInterval = Math.max(100, 1000 - (player.level - 1) * 100);
// Fill Zone meter: different amounts based on lines cleared
const zoneMeterIncrease = [0, 15, 25, 35, 50][Math.min(4, rowsCleared)] || 0;
player.zoneMeter = Math.min(100, player.zoneMeter + zoneMeterIncrease);
} }
return rowsCleared; return rowsCleared;
} }
@@ -498,6 +546,27 @@ function sendGarbage(sender, rowsCleared) {
} }
} }
function sendZoneAttack(sender, zoneLineCount) {
// Zone attack sends bonus garbage based on lines cleared during Zone
// Formula: lines * 1.5 (rounded) = garbage rows
// Example: 4 Zone lines = 6 garbage rows, 8 Zone lines = 12 garbage rows
const totalGarbageLines = Math.round(zoneLineCount * 1.5);
const garbageLog = [];
for (let i = 0; i < totalGarbageLines; i++) {
const opponents = Array.from(lobby.players.values()).filter(p => p.id !== sender.id && !p.eliminated);
if (opponents.length === 0) break;
const target = opponents[Math.floor(Math.random() * opponents.length)];
garbageLog.push(target.name);
addGarbageToPlayer(target, `${sender.name} (ZONE)`);
}
if (garbageLog.length > 0) {
console.log(`[ZONE ATTACK] ${sender.name} sent ${totalGarbageLines} garbage rows from ${zoneLineCount} Zone lines -> targets: ${garbageLog.join(', ')}`);
}
}
function addGarbageToPlayer(player, senderName) { function addGarbageToPlayer(player, senderName) {
// Use the pure function to add garbage row // Use the pure function to add garbage row
const result = addGarbageRow(player.board, player.currentPiece); const result = addGarbageRow(player.board, player.currentPiece);
@@ -528,6 +597,10 @@ function startGame() {
player.garbageReceived = []; player.garbageReceived = [];
player.currentPiece = null; player.currentPiece = null;
player.nextPiece = null; player.nextPiece = null;
player.zoneMeter = 0;
player.zoneActive = false;
player.zoneStartTime = null;
player.zoneLineCount = 0;
lobby.playerSequenceIndex.set(player.id, 0); lobby.playerSequenceIndex.set(player.id, 0);
} }
@@ -556,6 +629,25 @@ function gameTick() {
for (const player of lobby.players.values()) { for (const player of lobby.players.values()) {
if (player.eliminated || !player.currentPiece) continue; if (player.eliminated || !player.currentPiece) continue;
// Handle Zone time management
if (player.zoneActive) {
const elapsedTime = Date.now() - player.zoneStartTime;
if (elapsedTime >= player.zoneTotalDuration) {
// Zone ended - convert zone lines to attack
player.zoneActive = false;
console.log(`[ZONE END] ${player.name} cleared ${player.zoneLineCount} lines during Zone`);
if (player.zoneLineCount > 0) {
sendZoneAttack(player, player.zoneLineCount);
}
player.zoneLineCount = 0;
} else {
// During Zone - gravity doesn't apply, let pieces sort themselves out
// Players still control manually, pieces still lock after a delay if touching stack
continue;
}
}
// Normal gravity (not in Zone)
player.dropCounter += 50; player.dropCounter += 50;
if (player.dropCounter >= player.dropInterval) { if (player.dropCounter >= player.dropInterval) {
player.dropCounter = 0; player.dropCounter = 0;
@@ -588,7 +680,11 @@ function getStates() {
level: p.level, level: p.level,
eliminated: p.eliminated, eliminated: p.eliminated,
sequenceIndex: lobby.playerSequenceIndex.get(p.id) || 0, sequenceIndex: lobby.playerSequenceIndex.get(p.id) || 0,
garbageReceived: p.garbageReceived ? JSON.parse(JSON.stringify(p.garbageReceived)) : [] garbageReceived: p.garbageReceived ? JSON.parse(JSON.stringify(p.garbageReceived)) : [],
zoneMeter: p.zoneMeter,
zoneActive: p.zoneActive,
zoneTimeRemaining: p.zoneActive ? Math.max(0, p.zoneTotalDuration - (Date.now() - p.zoneStartTime)) : 0,
zoneLineCount: p.zoneLineCount
})); }));
} }
+2
View File
@@ -45,6 +45,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.29.0", "@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0", "@babel/generator": "^7.29.0",
@@ -1306,6 +1307,7 @@
"url": "https://github.com/sponsors/ai" "url": "https://github.com/sponsors/ai"
} }
], ],
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",