775 lines
22 KiB
JavaScript
775 lines
22 KiB
JavaScript
const canvas = document.getElementById('tetris');
|
|
const context = canvas.getContext('2d');
|
|
const nextCanvas = document.getElementById('next-piece');
|
|
const nextContext = nextCanvas.getContext('2d');
|
|
const opponentCanvas = document.getElementById('opponent-tetris');
|
|
const opponentContext = opponentCanvas ? opponentCanvas.getContext('2d') : null;
|
|
|
|
const mpSetup = document.getElementById('mp-setup');
|
|
const mpActive = document.getElementById('mp-active');
|
|
const mpStatusBar = document.getElementById('multiplayer-status-bar');
|
|
const mpStatusText = document.getElementById('multiplayer-status-text');
|
|
const activeRoomCode = document.getElementById('active-room-code');
|
|
const displayRoomCode = document.getElementById('display-room-code');
|
|
const opponentColumn = document.getElementById('opponent-column');
|
|
const debuffNotifications = document.getElementById('debuff-notifications');
|
|
const onlinePlayersList = document.getElementById('online-players-list');
|
|
const onlineCountBadge = document.getElementById('online-count');
|
|
const playerNameInput = document.getElementById('player-name');
|
|
const inviteContainer = document.getElementById('invite-container');
|
|
|
|
context.scale(20, 20);
|
|
nextContext.scale(20, 20);
|
|
if (opponentContext) opponentContext.scale(10, 10);
|
|
|
|
let isMultiplayer = false;
|
|
let roomId = null;
|
|
let playerId = localStorage.getItem('tetris_player_id') || Math.random().toString(36).substring(2, 10);
|
|
localStorage.setItem('tetris_player_id', playerId);
|
|
|
|
let roomStatus = 'waiting';
|
|
let opponentArena = null;
|
|
let opponentScore = 0;
|
|
let pollTimer = null;
|
|
let updateTimer = null;
|
|
let lastDebuffScore = 0;
|
|
let isSpeedSurge = false;
|
|
let isInputScrambled = false;
|
|
let speedSurgeTimer = null;
|
|
let scrambleTimer = null;
|
|
|
|
// Effects
|
|
let screenShake = 0;
|
|
let particles = [];
|
|
let floatingTexts = [];
|
|
|
|
class Particle {
|
|
constructor(x, y, color) {
|
|
this.x = x;
|
|
this.y = y;
|
|
this.color = color;
|
|
this.size = Math.random() * 0.15 + 0.05;
|
|
this.vx = (Math.random() - 0.5) * 0.4;
|
|
this.vy = (Math.random() - 0.5) * 0.4;
|
|
this.life = 1.0;
|
|
this.decay = Math.random() * 0.03 + 0.02;
|
|
}
|
|
update() {
|
|
this.x += this.vx;
|
|
this.y += this.vy;
|
|
this.vy += 0.01; // gravity
|
|
this.life -= this.decay;
|
|
}
|
|
draw(ctx) {
|
|
ctx.save();
|
|
ctx.globalAlpha = this.life;
|
|
ctx.fillStyle = this.color;
|
|
ctx.shadowBlur = 5;
|
|
ctx.shadowColor = this.color;
|
|
ctx.fillRect(this.x, this.y, this.size, this.size);
|
|
ctx.restore();
|
|
}
|
|
}
|
|
|
|
function addFloatingText(text, x, y, color) {
|
|
floatingTexts.push({
|
|
text, x, y, color,
|
|
life: 1.0,
|
|
decay: 0.02
|
|
});
|
|
}
|
|
|
|
function shakeScreen(intensity) {
|
|
screenShake = intensity;
|
|
}
|
|
|
|
function createExplosion(x, y, color) {
|
|
for (let i = 0; i < 15; i++) {
|
|
particles.push(new Particle(x, y, color));
|
|
}
|
|
}
|
|
|
|
function arenaSweep() {
|
|
let rowCount = 0;
|
|
outer: for (let y = arena.length - 1; y > 0; --y) {
|
|
for (let x = 0; x < arena[y].length; ++x) {
|
|
if (arena[y][x] === 0) {
|
|
continue outer;
|
|
}
|
|
}
|
|
|
|
const row = arena.splice(y, 1)[0];
|
|
arena.unshift(new Array(row.length).fill(0));
|
|
++y;
|
|
|
|
rowCount++;
|
|
|
|
row.forEach((value, x) => {
|
|
if (value !== 0) {
|
|
createExplosion(x, y - 1, colors[value]);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (rowCount > 0) {
|
|
player.score += rowCount * 10;
|
|
shakeScreen(rowCount * 0.1);
|
|
addFloatingText(`+${rowCount * 10}`, player.pos.x + 2, player.pos.y, '#fff');
|
|
}
|
|
}
|
|
|
|
function collide(arena, player) {
|
|
const [m, o] = [player.matrix, player.pos];
|
|
for (let y = 0; y < m.length; ++y) {
|
|
for (let x = 0; x < m[y].length; ++x) {
|
|
if (m[y][x] !== 0 &&
|
|
(arena[y + o.y] && arena[y + o.y][x + o.x]) !== 0) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function createMatrix(w, h) {
|
|
const matrix = [];
|
|
while (h--) {
|
|
matrix.push(new Array(w).fill(0));
|
|
}
|
|
return matrix;
|
|
}
|
|
|
|
function createPiece(type) {
|
|
if (type === 'I') {
|
|
return [
|
|
[0, 1, 0, 0],
|
|
[0, 1, 0, 0],
|
|
[0, 1, 0, 0],
|
|
[0, 1, 0, 0],
|
|
];
|
|
} else if (type === 'L') {
|
|
return [
|
|
[0, 2, 0],
|
|
[0, 2, 0],
|
|
[0, 2, 2],
|
|
];
|
|
} else if (type === 'J') {
|
|
return [
|
|
[0, 3, 0],
|
|
[0, 3, 0],
|
|
[3, 3, 0],
|
|
];
|
|
} else if (type === 'O') {
|
|
return [
|
|
[4, 4],
|
|
[4, 4],
|
|
];
|
|
} else if (type === 'Z') {
|
|
return [
|
|
[5, 5, 0],
|
|
[0, 5, 5],
|
|
[0, 0, 0],
|
|
];
|
|
} else if (type === 'S') {
|
|
return [
|
|
[0, 6, 6],
|
|
[6, 6, 0],
|
|
[0, 0, 0],
|
|
];
|
|
} else if (type === 'T') {
|
|
return [
|
|
[0, 7, 0],
|
|
[7, 7, 7],
|
|
[0, 0, 0],
|
|
];
|
|
}
|
|
}
|
|
|
|
function drawMatrix(matrix, offset, ctx) {
|
|
matrix.forEach((row, y) => {
|
|
row.forEach((value, x) => {
|
|
if (value !== 0) {
|
|
ctx.save();
|
|
ctx.fillStyle = colors[value];
|
|
ctx.shadowBlur = 10;
|
|
ctx.shadowColor = colors[value];
|
|
ctx.fillRect(x + offset.x, y + offset.y, 1, 1);
|
|
|
|
ctx.strokeStyle = 'rgba(0,0,0,0.3)';
|
|
ctx.lineWidth = 0.05;
|
|
ctx.strokeRect(x + offset.x, y + offset.y, 1, 1);
|
|
ctx.restore();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function drawBackground(ctx, w, h) {
|
|
ctx.strokeStyle = '#111';
|
|
ctx.lineWidth = 0.02;
|
|
for (let x = 0; x <= w; x++) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(x, 0);
|
|
ctx.lineTo(x, h);
|
|
ctx.stroke();
|
|
}
|
|
for (let y = 0; y <= h; y++) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, y);
|
|
ctx.lineTo(w, y);
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
|
|
function draw() {
|
|
context.fillStyle = '#000';
|
|
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
context.save();
|
|
if (screenShake > 0) {
|
|
context.translate((Math.random() - 0.5) * screenShake, (Math.random() - 0.5) * screenShake);
|
|
screenShake *= 0.9;
|
|
if (screenShake < 0.01) screenShake = 0;
|
|
}
|
|
|
|
drawBackground(context, 12, 20);
|
|
drawMatrix(arena, {x: 0, y: 0}, context);
|
|
drawMatrix(player.matrix, player.pos, context);
|
|
|
|
particles.forEach((p, i) => {
|
|
p.update();
|
|
p.draw(context);
|
|
if (p.life <= 0) particles.splice(i, 1);
|
|
});
|
|
|
|
floatingTexts.forEach((ft, i) => {
|
|
context.save();
|
|
context.globalAlpha = ft.life;
|
|
context.fillStyle = ft.color;
|
|
context.font = "0.8px 'Inter'";
|
|
context.fillText(ft.text, ft.x, ft.y);
|
|
ft.y -= 0.02;
|
|
ft.life -= ft.decay;
|
|
context.restore();
|
|
if (ft.life <= 0) floatingTexts.splice(i, 1);
|
|
});
|
|
|
|
context.restore();
|
|
|
|
nextContext.fillStyle = '#f8f9fa';
|
|
nextContext.fillRect(0, 0, nextCanvas.width, nextCanvas.height);
|
|
if (player.next) {
|
|
drawMatrix(player.next, {x: 1, y: 1}, nextContext);
|
|
}
|
|
|
|
if (isMultiplayer && opponentContext) {
|
|
opponentContext.fillStyle = '#000';
|
|
opponentContext.fillRect(0, 0, opponentCanvas.width, opponentCanvas.height);
|
|
drawBackground(opponentContext, 12, 20);
|
|
drawMatrix(opponentArena, {x: 0, y: 0}, opponentContext);
|
|
document.getElementById('opponent-score-val').innerText = opponentScore;
|
|
}
|
|
}
|
|
|
|
const colors = [
|
|
null,
|
|
'#00f0f0',
|
|
'#f0a000',
|
|
'#0000f0',
|
|
'#f0f000',
|
|
'#f00000',
|
|
'#00f000',
|
|
'#a000f0',
|
|
'#808080',
|
|
];
|
|
|
|
function merge(arena, player) {
|
|
player.matrix.forEach((row, y) => {
|
|
row.forEach((value, x) => {
|
|
if (value !== 0) {
|
|
arena[y + player.pos.y][x + player.pos.x] = value;
|
|
}
|
|
});
|
|
});
|
|
shakeScreen(0.05);
|
|
}
|
|
|
|
function rotate(matrix, dir) {
|
|
for (let y = 0; y < matrix.length; ++y) {
|
|
for (let x = 0; x < y; ++x) {
|
|
[matrix[x][y], matrix[y][x]] = [matrix[y][x], matrix[x][y]];
|
|
}
|
|
}
|
|
if (dir > 0) matrix.forEach(row => row.reverse());
|
|
else matrix.reverse();
|
|
}
|
|
|
|
function playerDrop() {
|
|
player.pos.y++;
|
|
if (collide(arena, player)) {
|
|
player.pos.y--;
|
|
merge(arena, player);
|
|
playerReset();
|
|
arenaSweep();
|
|
updateScore();
|
|
}
|
|
dropCounter = 0;
|
|
}
|
|
|
|
function playerMove(offset) {
|
|
if (isInputScrambled) {
|
|
offset *= -1;
|
|
}
|
|
|
|
player.pos.x += offset;
|
|
if (collide(arena, player)) {
|
|
player.pos.x -= offset;
|
|
}
|
|
}
|
|
|
|
function playerReset() {
|
|
const pieces = 'TJLOSZI';
|
|
player.matrix = player.next;
|
|
player.next = createPiece(pieces[pieces.length * Math.random() | 0]);
|
|
player.pos.y = 0;
|
|
player.pos.x = (arena[0].length / 2 | 0) - (player.matrix[0].length / 2 | 0);
|
|
|
|
if (collide(arena, player)) {
|
|
arena.forEach(row => row.fill(0));
|
|
player.isGameOver = true;
|
|
if (isMultiplayer) syncState();
|
|
alert('Game Over! Your final score: ' + player.score);
|
|
isPaused = true;
|
|
}
|
|
}
|
|
|
|
function playerRotate(dir) {
|
|
const pos = player.pos.x;
|
|
let offset = 1;
|
|
rotate(player.matrix, dir);
|
|
while (collide(arena, player)) {
|
|
player.pos.x += offset;
|
|
offset = -(offset + (offset > 0 ? 1 : -1));
|
|
if (offset > player.matrix[0].length) {
|
|
rotate(player.matrix, -dir);
|
|
player.pos.x = pos;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
let dropCounter = 0;
|
|
let dropInterval = 1000;
|
|
let lastTime = 0;
|
|
let isPaused = true;
|
|
|
|
function update(time = 0) {
|
|
if (isPaused) {
|
|
draw();
|
|
if (particles.length > 0 || floatingTexts.length > 0) requestAnimationFrame(update);
|
|
return;
|
|
}
|
|
|
|
const deltaTime = time - lastTime;
|
|
lastTime = time;
|
|
|
|
dropCounter += deltaTime;
|
|
|
|
let currentInterval = dropInterval;
|
|
if (isSpeedSurge) {
|
|
currentInterval /= 2;
|
|
}
|
|
|
|
if (dropCounter > currentInterval) {
|
|
playerDrop();
|
|
}
|
|
|
|
draw();
|
|
requestAnimationFrame(update);
|
|
}
|
|
|
|
function updateScore() {
|
|
document.getElementById('score-val').innerText = player.score;
|
|
document.getElementById('level-val').innerText = Math.floor(player.score / 100) + 1;
|
|
dropInterval = Math.max(100, 1000 - (Math.floor(player.score / 50) * 50));
|
|
|
|
if (isMultiplayer && player.score >= lastDebuffScore + 200) {
|
|
lastDebuffScore = Math.floor(player.score / 200) * 200;
|
|
sendRandomDebuff();
|
|
}
|
|
}
|
|
|
|
const arena = createMatrix(12, 20);
|
|
|
|
const player = {
|
|
pos: {x: 0, y: 0},
|
|
matrix: null,
|
|
next: null,
|
|
score: 0,
|
|
isGameOver: false,
|
|
};
|
|
|
|
// --- Presence & Online Players ---
|
|
|
|
async function setNickname() {
|
|
const nickname = playerNameInput.value || 'Anonymous';
|
|
await fetch('api/multiplayer.php?action=set_nickname', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ player_id: playerId, nickname: nickname })
|
|
});
|
|
}
|
|
|
|
async function updatePresence() {
|
|
await fetch('api/multiplayer.php?action=heartbeat', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ player_id: playerId })
|
|
});
|
|
}
|
|
|
|
async function fetchOnlinePlayers() {
|
|
const resp = await fetch(`api/multiplayer.php?action=get_online&player_id=${playerId}`);
|
|
const data = await resp.json();
|
|
if (data.success) {
|
|
onlineCountBadge.innerText = data.players.length;
|
|
if (data.players.length === 0) {
|
|
onlinePlayersList.innerHTML = '<p class="text-muted small p-2">No other players online</p>';
|
|
} else {
|
|
onlinePlayersList.innerHTML = data.players.map(p => `
|
|
<div class="list-group-item d-flex justify-content-between align-items-center py-2 px-1">
|
|
<span class="small"><span class="online-badge"></span>${p.nickname}</span>
|
|
<button class="btn btn-xs btn-primary py-0 px-2 small" onclick="sendInvite('${p.session_id}')" style="font-size: 10px;">Invite</button>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
}
|
|
}
|
|
|
|
async function checkInvites() {
|
|
if (isMultiplayer) return;
|
|
const resp = await fetch(`api/multiplayer.php?action=check_invites&player_id=${playerId}`);
|
|
const data = await resp.json();
|
|
if (data.success && data.invites && data.invites.length > 0) {
|
|
data.invites.forEach(inv => {
|
|
if (!document.getElementById(`invite-${inv.id}`)) {
|
|
showInvitePopup(inv);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function showInvitePopup(inv) {
|
|
const popup = document.createElement('div');
|
|
popup.id = `invite-${inv.id}`;
|
|
popup.className = 'invite-toast';
|
|
popup.innerHTML = `
|
|
<div class="fw-bold mb-1">Invitation!</div>
|
|
<div class="small mb-2"><strong>${inv.from_nickname}</strong> invited you to play.</div>
|
|
<div class="d-flex gap-2">
|
|
<button class="btn btn-sm btn-primary w-100" onclick="acceptInvite(${inv.id}, '${inv.room_code}')">Accept</button>
|
|
<button class="btn btn-sm btn-outline-secondary w-100" onclick="rejectInvite(${inv.id})">Decline</button>
|
|
</div>
|
|
`;
|
|
inviteContainer.appendChild(popup);
|
|
setTimeout(() => popup.remove(), 30000);
|
|
}
|
|
|
|
async function sendInvite(toSessionId) {
|
|
const resp = await fetch('api/multiplayer.php?action=invite', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ from_player_id: playerId, to_player_id: toSessionId })
|
|
});
|
|
const data = await resp.json();
|
|
if (data.success) {
|
|
alert('Invitation sent! Waiting for opponent...');
|
|
// The room is already created by 'invite' action.
|
|
// We just need to join it ourselves to get the roomId.
|
|
joinRoom(data.room_code);
|
|
}
|
|
}
|
|
|
|
window.acceptInvite = async (inviteId, roomCode) => {
|
|
await fetch('api/multiplayer.php?action=respond_invite', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ invite_id: inviteId, status: 'accepted' })
|
|
});
|
|
document.getElementById(`invite-${inviteId}`)?.remove();
|
|
joinRoom(roomCode);
|
|
};
|
|
|
|
window.rejectInvite = async (inviteId) => {
|
|
await fetch('api/multiplayer.php?action=respond_invite', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ invite_id: inviteId, status: 'rejected' })
|
|
});
|
|
document.getElementById(`invite-${inviteId}`)?.remove();
|
|
};
|
|
|
|
window.sendInvite = sendInvite;
|
|
|
|
playerNameInput.addEventListener('blur', setNickname);
|
|
setNickname();
|
|
setInterval(updatePresence, 10000);
|
|
setInterval(fetchOnlinePlayers, 5000);
|
|
setInterval(checkInvites, 3000);
|
|
fetchOnlinePlayers();
|
|
|
|
// --- Multiplayer Functions ---
|
|
|
|
async function createRoom() {
|
|
const resp = await fetch('api/multiplayer.php?action=create', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ player_id: playerId })
|
|
});
|
|
const data = await resp.json();
|
|
if (data.success) {
|
|
roomId = data.room_id;
|
|
startMultiplayer(data.room_code);
|
|
}
|
|
}
|
|
|
|
async function joinRoom(code) {
|
|
const resp = await fetch('api/multiplayer.php?action=join', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ player_id: playerId, room_code: code })
|
|
});
|
|
const data = await resp.json();
|
|
if (data.success) {
|
|
roomId = data.room_id;
|
|
startMultiplayer(code);
|
|
} else {
|
|
alert(data.error);
|
|
}
|
|
}
|
|
|
|
function startMultiplayer(code) {
|
|
isMultiplayer = true;
|
|
mpSetup.classList.add('d-none');
|
|
mpActive.classList.remove('d-none');
|
|
activeRoomCode.innerText = code;
|
|
displayRoomCode.innerText = code;
|
|
mpStatusBar.classList.remove('d-none');
|
|
opponentColumn.classList.remove('d-none');
|
|
|
|
if (pollTimer) clearInterval(pollTimer);
|
|
if (updateTimer) clearInterval(updateTimer);
|
|
pollTimer = setInterval(pollOpponent, 1000);
|
|
updateTimer = setInterval(syncState, 500);
|
|
}
|
|
|
|
async function syncState() {
|
|
if (!roomId) return;
|
|
await fetch('api/multiplayer.php?action=update', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
room_id: roomId,
|
|
player_id: playerId,
|
|
board: arena,
|
|
score: player.score,
|
|
is_game_over: player.isGameOver
|
|
})
|
|
});
|
|
}
|
|
|
|
async function pollOpponent() {
|
|
if (!roomId) return;
|
|
const resp = await fetch(`api/multiplayer.php?action=poll&room_id=${roomId}&player_id=${playerId}`);
|
|
const data = await resp.json();
|
|
|
|
if (data.success) {
|
|
if (data.status === 'playing') {
|
|
if (roomStatus === 'waiting') {
|
|
roomStatus = 'playing';
|
|
mpStatusText.innerText = 'Game started!';
|
|
setTimeout(() => mpStatusBar.classList.add('d-none'), 3000);
|
|
startGame();
|
|
}
|
|
}
|
|
|
|
if (data.opponent) {
|
|
opponentArena = data.opponent.board;
|
|
opponentScore = data.opponent.score;
|
|
}
|
|
|
|
if (data.debuffs && data.debuffs.length > 0) {
|
|
data.debuffs.forEach(applyDebuff);
|
|
}
|
|
}
|
|
}
|
|
|
|
function sendRandomDebuff() {
|
|
const debuffs = ['garbage', 'speed', 'scramble'];
|
|
const debuff = debuffs[Math.floor(Math.random() * debuffs.length)];
|
|
|
|
fetch('api/multiplayer.php?action=send_debuff', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
room_id: roomId,
|
|
player_id: playerId,
|
|
debuff: debuff
|
|
})
|
|
});
|
|
|
|
showNotification(`Sent debuff: ${debuff.toUpperCase()}!`, 'info');
|
|
addFloatingText(`${debuff.toUpperCase()} SENT!`, player.pos.x, player.pos.y - 2, '#00ffff');
|
|
}
|
|
|
|
function applyDebuff(type) {
|
|
if (type === 'garbage') {
|
|
showNotification('RECEIVED: Garbage Line!', 'danger');
|
|
addFloatingText('GARBAGE IN!', 5, 10, '#ff0000');
|
|
shakeScreen(0.4);
|
|
addGarbageLine();
|
|
} else if (type === 'speed') {
|
|
showNotification('RECEIVED: Speed Surge (7s)!', 'danger');
|
|
addFloatingText('SPEED UP!', 5, 10, '#ff0000');
|
|
isSpeedSurge = true;
|
|
clearTimeout(speedSurgeTimer);
|
|
speedSurgeTimer = setTimeout(() => {
|
|
isSpeedSurge = false;
|
|
showNotification('Speed normalized.', 'info');
|
|
}, 7000);
|
|
} else if (type === 'scramble') {
|
|
showNotification('RECEIVED: Input Scramble (5s)!', 'danger');
|
|
addFloatingText('SCRAMBLED!', 5, 10, '#ff0000');
|
|
isInputScrambled = true;
|
|
clearTimeout(scrambleTimer);
|
|
scrambleTimer = setTimeout(() => {
|
|
isInputScrambled = false;
|
|
showNotification('Controls normalized.', 'info');
|
|
}, 5000);
|
|
}
|
|
}
|
|
|
|
function addGarbageLine() {
|
|
const row = new Array(arena[0].length).fill(8);
|
|
const hole = Math.floor(Math.random() * row.length);
|
|
row[hole] = 0;
|
|
|
|
arena.shift();
|
|
arena.push(row);
|
|
|
|
if (collide(arena, player)) {
|
|
player.pos.y--;
|
|
if (player.pos.y < 0) {
|
|
player.pos.y = 0;
|
|
player.isGameOver = true;
|
|
syncState();
|
|
alert('Game Over by Garbage!');
|
|
}
|
|
}
|
|
}
|
|
|
|
function showNotification(message, type = 'info') {
|
|
const toast = document.createElement('div');
|
|
toast.className = `debuff-toast bg-${type}`;
|
|
toast.innerText = message;
|
|
debuffNotifications.appendChild(toast);
|
|
setTimeout(() => toast.remove(), 3000);
|
|
}
|
|
|
|
function startGame() {
|
|
arena.forEach(row => row.fill(0));
|
|
player.score = 0;
|
|
player.isGameOver = false;
|
|
lastDebuffScore = 0;
|
|
isInputScrambled = false;
|
|
isSpeedSurge = false;
|
|
particles = [];
|
|
floatingTexts = [];
|
|
updateScore();
|
|
const pieces = 'TJLOSZI';
|
|
player.next = createPiece(pieces[pieces.length * Math.random() | 0]);
|
|
playerReset();
|
|
isPaused = false;
|
|
pauseBtn.disabled = isMultiplayer;
|
|
lastTime = performance.now();
|
|
update();
|
|
}
|
|
|
|
// --- Event Listeners ---
|
|
|
|
document.addEventListener('keydown', event => {
|
|
const keysToPrevent = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' ', 'w', 'W', 'a', 'A', 's', 'S', 'd', 'D'];
|
|
if (keysToPrevent.includes(event.key)) {
|
|
event.preventDefault();
|
|
}
|
|
|
|
if (isPaused && !player.isGameOver) {
|
|
if (event.key === 'p' || event.key === 'P') {
|
|
if (!isMultiplayer) togglePause();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (event.key === 'ArrowLeft' || event.key === 'a' || event.key === 'A') {
|
|
playerMove(-1);
|
|
} else if (event.key === 'ArrowRight' || event.key === 'd' || event.key === 'D') {
|
|
playerMove(1);
|
|
} else if (event.key === 'ArrowDown' || event.key === 's' || event.key === 'S') {
|
|
playerDrop();
|
|
} else if (event.key === 'ArrowUp' || event.key === 'w' || event.key === 'W') {
|
|
playerRotate(1);
|
|
} else if (event.key === ' ') {
|
|
let dropped = 0;
|
|
while (!collide(arena, player)) {
|
|
player.pos.y++;
|
|
dropped++;
|
|
}
|
|
player.pos.y--;
|
|
if (dropped > 0) shakeScreen(0.05);
|
|
merge(arena, player);
|
|
playerReset();
|
|
arenaSweep();
|
|
updateScore();
|
|
} else if (event.key === 'p' || event.key === 'P') {
|
|
if (!isMultiplayer) togglePause();
|
|
}
|
|
});
|
|
|
|
const startBtn = document.getElementById('start-btn');
|
|
const pauseBtn = document.getElementById('pause-btn');
|
|
const createRoomBtn = document.getElementById('create-room-btn');
|
|
const joinRoomBtn = document.getElementById('join-room-btn');
|
|
const leaveRoomBtn = document.getElementById('leave-room-btn');
|
|
|
|
function togglePause() {
|
|
if (isMultiplayer) return;
|
|
isPaused = !isPaused;
|
|
pauseBtn.innerText = isPaused ? 'Resume' : 'Pause';
|
|
if (!isPaused) {
|
|
lastTime = performance.now();
|
|
update();
|
|
}
|
|
}
|
|
|
|
startBtn.addEventListener('click', () => {
|
|
if (isMultiplayer) {
|
|
alert("Wait for the game to start automatically when opponent joins.");
|
|
return;
|
|
}
|
|
startGame();
|
|
});
|
|
|
|
pauseBtn.addEventListener('click', togglePause);
|
|
|
|
createRoomBtn.addEventListener('click', createRoom);
|
|
joinRoomBtn.addEventListener('click', () => {
|
|
const code = document.getElementById('join-room-code').value.toUpperCase();
|
|
if (code) joinRoom(code);
|
|
});
|
|
|
|
leaveRoomBtn.addEventListener('click', () => {
|
|
location.reload();
|
|
});
|
|
|
|
player.next = createPiece('T');
|
|
draw(); |