Implement DAS/ARR for smooth horizontal piece movement
This commit is contained in:
@@ -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-rotate` | Client->Server | `{ playerId }` |
|
||||
| `player-drop` | Client->Server | `{ playerId, hard }` |
|
||||
| `player-hold` | Client->Server | `{ playerId }` |
|
||||
| `zone-activate` | Client->Server | `{ playerId }` |
|
||||
| `player-joined` | Server->Client | `{ player, players }` |
|
||||
| `player-left` | Server->Client | `{ playerId, players }` |
|
||||
| `game-started` | Server->Client | `{ players, states }` |
|
||||
| `state-update` | 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
|
||||
|
||||
- Each player gets a dynamically created board with canvas + info divs
|
||||
- `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`
|
||||
|
||||
Binary file not shown.
@@ -337,6 +337,38 @@ button:active {
|
||||
border: 1px solid #333;
|
||||
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 {
|
||||
@@ -554,6 +586,19 @@ button:active {
|
||||
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 */
|
||||
@media (max-width: 768px) and (hover: none) {
|
||||
#touch-controls {
|
||||
|
||||
+8
-3
@@ -10,6 +10,9 @@
|
||||
<link rel="stylesheet" href="css/style.css?v=11">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Background Music -->
|
||||
<audio id="bg-music" volume="0.3"></audio>
|
||||
|
||||
<div id="app">
|
||||
<!-- Login Screen -->
|
||||
<div id="room-screen" class="screen active">
|
||||
@@ -48,6 +51,7 @@
|
||||
<button id="btn-rotate" class="touch-btn action-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-zone" class="touch-btn zone-btn" disabled>ZONE</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,10 +66,11 @@
|
||||
</div>
|
||||
|
||||
<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/game.js?v=4"></script>
|
||||
<script src="js/renderer.js?v=5"></script>
|
||||
<script src="js/ui.js?v=6"></script>
|
||||
<script src="js/app.js?v=5"></script>
|
||||
<script src="js/renderer.js?v=6"></script>
|
||||
<script src="js/ui.js?v=7"></script>
|
||||
<script src="js/app.js?v=6"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+77
-12
@@ -4,6 +4,17 @@ 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
|
||||
@@ -26,10 +37,18 @@ function setupNetworkListeners() {
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -43,6 +62,11 @@ function setupNetworkListeners() {
|
||||
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();
|
||||
|
||||
@@ -88,6 +112,11 @@ function setupNetworkListeners() {
|
||||
ui.showScreen('game');
|
||||
ui.showSpectatorMode();
|
||||
|
||||
// Start game music for spectators too
|
||||
if (typeof audio !== 'undefined') {
|
||||
audio.playGame();
|
||||
}
|
||||
|
||||
// Clear old boards
|
||||
renderer.clearAll();
|
||||
|
||||
@@ -122,19 +151,16 @@ function setupKeyboardControls() {
|
||||
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 '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();
|
||||
@@ -147,10 +173,22 @@ function setupKeyboardControls() {
|
||||
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');
|
||||
@@ -158,6 +196,7 @@ function setupKeyboardControls() {
|
||||
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();
|
||||
@@ -176,6 +215,7 @@ function setupKeyboardControls() {
|
||||
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());
|
||||
@@ -184,6 +224,7 @@ function setupKeyboardControls() {
|
||||
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) {
|
||||
@@ -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
|
||||
network.isSpectator = false;
|
||||
ui.hideSpectatorMode();
|
||||
@@ -239,6 +282,21 @@ function gameLoop(currentTime) {
|
||||
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);
|
||||
@@ -250,6 +308,13 @@ function gameLoop(currentTime) {
|
||||
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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
@@ -140,6 +140,11 @@ class NetworkManager {
|
||||
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) {
|
||||
this.players = {};
|
||||
players.forEach(p => {
|
||||
|
||||
+38
-8
@@ -35,10 +35,10 @@ class TetrisRenderer {
|
||||
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';
|
||||
nextCanvas.width = 60;
|
||||
nextCanvas.height = 60;
|
||||
nextCanvas.style.width = '60px';
|
||||
nextCanvas.style.height = '60px';
|
||||
nextDiv.innerHTML = '<span>NEXT:</span>';
|
||||
nextDiv.appendChild(nextCanvas);
|
||||
boardDiv.appendChild(nextDiv);
|
||||
@@ -48,14 +48,21 @@ class TetrisRenderer {
|
||||
holdDiv.className = 'board-info';
|
||||
const holdCanvas = document.createElement('canvas');
|
||||
holdCanvas.id = `hold-${playerId}`;
|
||||
holdCanvas.width = 80;
|
||||
holdCanvas.height = 80;
|
||||
holdCanvas.style.width = '80px';
|
||||
holdCanvas.style.height = '80px';
|
||||
holdCanvas.width = 60;
|
||||
holdCanvas.height = 60;
|
||||
holdCanvas.style.width = '60px';
|
||||
holdCanvas.style.height = '60px';
|
||||
holdDiv.innerHTML = '<span>HOLD:</span>';
|
||||
holdDiv.appendChild(holdCanvas);
|
||||
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.boards.set(playerId, {
|
||||
@@ -63,6 +70,7 @@ class TetrisRenderer {
|
||||
canvas: canvas,
|
||||
nextCanvas: nextCanvas,
|
||||
holdCanvas: holdCanvas,
|
||||
zoneDiv: zoneDiv,
|
||||
info: infoDiv,
|
||||
ctx: canvas.getContext('2d'),
|
||||
nextCtx: nextCanvas.getContext('2d'),
|
||||
@@ -128,6 +136,28 @@ class TetrisRenderer {
|
||||
if (linesSpan) {
|
||||
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)
|
||||
|
||||
@@ -48,6 +48,11 @@ class UIManager {
|
||||
showScreen(screenName) {
|
||||
Object.values(this.screens).forEach(screen => screen.classList.remove('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() {
|
||||
|
||||
+99
-3
@@ -55,6 +55,11 @@ app.use(express.static(path.join(__dirname, '../public')));
|
||||
* @property {number} dropCounter - Frame counter for auto-drop
|
||||
* @property {number} dropInterval - Milliseconds between auto-drops
|
||||
* @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,
|
||||
dropCounter: 0,
|
||||
dropInterval: 1000,
|
||||
garbageReceived: []
|
||||
garbageReceived: [],
|
||||
zoneMeter: 0,
|
||||
zoneActive: false,
|
||||
zoneStartTime: null,
|
||||
zoneLineCount: 0,
|
||||
zoneTotalDuration: 20000
|
||||
};
|
||||
|
||||
lobby.players.set(socket.id, player);
|
||||
@@ -328,6 +338,24 @@ io.on('connection', (socket) => {
|
||||
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', () => {
|
||||
const player = lobby.players.get(socket.id);
|
||||
if (player) {
|
||||
@@ -464,7 +492,22 @@ function lockPiece(player) {
|
||||
|
||||
function clearRows(player) {
|
||||
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;
|
||||
|
||||
if (rowsCleared > 0) {
|
||||
@@ -473,7 +516,12 @@ function clearRows(player) {
|
||||
player.score += points[rowsCleared] * player.level;
|
||||
player.level = Math.floor(player.lines / 10) + 1;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
// Use the pure function to add garbage row
|
||||
const result = addGarbageRow(player.board, player.currentPiece);
|
||||
@@ -528,6 +597,10 @@ function startGame() {
|
||||
player.garbageReceived = [];
|
||||
player.currentPiece = null;
|
||||
player.nextPiece = null;
|
||||
player.zoneMeter = 0;
|
||||
player.zoneActive = false;
|
||||
player.zoneStartTime = null;
|
||||
player.zoneLineCount = 0;
|
||||
lobby.playerSequenceIndex.set(player.id, 0);
|
||||
}
|
||||
|
||||
@@ -556,6 +629,25 @@ function gameTick() {
|
||||
for (const player of lobby.players.values()) {
|
||||
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;
|
||||
if (player.dropCounter >= player.dropInterval) {
|
||||
player.dropCounter = 0;
|
||||
@@ -588,7 +680,11 @@ function getStates() {
|
||||
level: p.level,
|
||||
eliminated: p.eliminated,
|
||||
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
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
Generated
+2
@@ -45,6 +45,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@@ -1306,6 +1307,7 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
|
||||
Reference in New Issue
Block a user