Flatlogic Bot b3cf9df956 123
2026-01-22 16:53:25 +00:00

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();