document.addEventListener('DOMContentLoaded', () => { const roomId = window.ROOM_ID; if (!roomId) return; const playerToken = window.PLAYER_TOKEN; const roomPage = document.getElementById('player-list'); const matchBoard = document.getElementById('game-board'); const toastEl = document.getElementById('room-toast') || document.getElementById('match-toast'); const toastBody = document.getElementById('room-toast-body') || document.getElementById('match-toast-body'); const toast = toastEl ? new bootstrap.Toast(toastEl, { delay: 2000 }) : null; let lastState = null; let lastMoveAt = 0; let roomRedirectScheduled = false; let moveInFlight = false; let queuedMoveDir = null; let pollTimer = null; let stream = null; let reconnectTimer = null; const showToast = (message) => { if (!toast || !toastBody) return; toastBody.textContent = message; toast.show(); }; const fetchState = async () => { try { const response = await fetch(`/api/room_state.php?room_id=${roomId}`, { cache: 'no-store' }); const data = await response.json(); if (!data.success) return; renderState(data); } catch (error) { console.error(error); } }; const startPolling = (intervalMs) => { if (pollTimer) return; pollTimer = setInterval(fetchState, intervalMs); }; const stopPolling = () => { if (!pollTimer) return; clearInterval(pollTimer); pollTimer = null; }; const connectRealtime = () => { if (!window.EventSource) { startPolling(matchBoard ? 200 : 900); return; } if (stream) return; stream = new EventSource(`/api/room_stream.php?room_id=${roomId}`); stream.addEventListener('state', (event) => { try { const data = JSON.parse(event.data); if (data && data.success) { renderState(data); } } catch (error) { console.error(error); } }); stream.addEventListener('open', () => { stopPolling(); }); stream.addEventListener('error', () => { if (stream) { stream.close(); stream = null; } startPolling(matchBoard ? 250 : 900); if (!reconnectTimer) { reconnectTimer = setTimeout(() => { reconnectTimer = null; connectRealtime(); }, 1500); } }); }; const sendAction = async (action, payload = {}) => { const formData = new URLSearchParams({ room_id: roomId, action, ...payload }); const response = await fetch('/api/room_action.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: formData.toString(), cache: 'no-store' }); const data = await response.json(); if (data && data.success && data.room && data.state) { renderState(data); } return data; }; const sendMoveAction = async (dir) => { if (moveInFlight) { queuedMoveDir = dir; return; } moveInFlight = true; try { await sendAction('move', { dir }); } finally { moveInFlight = false; if (queuedMoveDir) { const nextDir = queuedMoveDir; queuedMoveDir = null; sendMoveAction(nextDir); } } }; const renderState = (data) => { const { room, state } = data; if (!state) return; if (roomPage) { renderRoomPage(room, state); } if (matchBoard) { renderMatchPage(room, state); } if (lastState && lastState.room.status !== room.status) { showToast(`Статус матча: ${room.status}`); } lastState = data; }; const renderRoomPage = (room, state) => { const list = document.getElementById('player-list'); const count = document.getElementById('room-count'); const status = document.getElementById('room-state'); const winner = document.getElementById('room-winner'); list.innerHTML = ''; state.players.forEach((player) => { const li = document.createElement('li'); li.className = 'player-list-item'; const name = document.createElement('span'); name.textContent = player.name + (player.token === playerToken ? ' (вы)' : ''); const chip = document.createElement('span'); chip.className = 'player-chip'; chip.style.background = player.color || '#111827'; chip.textContent = player.alive ? 'OK' : 'KO'; li.appendChild(name); li.appendChild(chip); list.appendChild(li); }); count.textContent = `${state.players.length} / ${room.max_players}`; status.textContent = room.status; winner.textContent = state.winner || '—'; if (room.status === 'playing') { const startBtn = document.getElementById('start-match'); if (startBtn) startBtn.disabled = true; const isCurrentPlayerInRoom = !!playerToken && state.players.some((p) => p.token === playerToken); if (isCurrentPlayerInRoom && !roomRedirectScheduled) { roomRedirectScheduled = true; showToast('Матч начался. Переходим в игру...'); setTimeout(() => { window.location.href = `/match.php?id=${roomId}`; }, 600); } } }; const renderMatchPage = (room, state) => { const statusEl = document.getElementById('match-status'); const playersList = document.getElementById('match-players'); if (!matchBoard.dataset.ready) { matchBoard.style.gridTemplateColumns = `repeat(${state.map.w}, 28px)`; matchBoard.style.gridTemplateRows = `repeat(${state.map.h}, 28px)`; matchBoard.innerHTML = ''; for (let y = 0; y < state.map.h; y++) { for (let x = 0; x < state.map.w; x++) { const cell = document.createElement('div'); cell.className = 'game-cell'; cell.dataset.x = x; cell.dataset.y = y; matchBoard.appendChild(cell); } } matchBoard.dataset.ready = 'true'; } const cells = Array.from(matchBoard.children); cells.forEach((cell) => { cell.className = 'game-cell'; cell.textContent = ''; }); const bombs = state.bombs || []; const explosions = state.explosions || []; const powerups = state.powerups || []; for (let y = 0; y < state.map.h; y++) { for (let x = 0; x < state.map.w; x++) { const tile = state.map.tiles[y][x]; const cell = cells[y * state.map.w + x]; if (!cell) continue; if (tile === 1) cell.classList.add('cell-solid'); if (tile === 2) cell.classList.add('cell-breakable'); } } powerups.forEach((powerup) => { const cell = cells[powerup.y * state.map.w + powerup.x]; if (cell) cell.classList.add('cell-powerup'); }); bombs.forEach((bomb) => { const cell = cells[bomb.y * state.map.w + bomb.x]; if (cell) cell.classList.add('cell-bomb'); }); explosions.forEach((exp) => { const cell = cells[exp.y * state.map.w + exp.x]; if (cell) cell.classList.add('cell-explosion'); }); state.players.forEach((player) => { const cell = cells[player.y * state.map.w + player.x]; if (!cell) return; const chip = document.createElement('span'); chip.className = 'player-chip'; chip.style.background = player.color || '#111827'; chip.textContent = player.name.slice(0, 1).toUpperCase(); if (!player.alive) chip.style.opacity = '0.35'; cell.appendChild(chip); }); playersList.innerHTML = ''; state.players.forEach((player) => { const li = document.createElement('li'); li.className = 'player-list-item'; const name = document.createElement('span'); name.textContent = player.name + (player.token === playerToken ? ' (вы)' : ''); const chip = document.createElement('span'); chip.className = 'player-chip'; chip.style.background = player.color || '#111827'; chip.textContent = player.alive ? 'OK' : 'KO'; li.appendChild(name); li.appendChild(chip); playersList.appendChild(li); }); statusEl.textContent = room.status === 'playing' ? 'Матч идет' : room.status === 'finished' ? `Матч завершен. Победитель: ${state.winner || '—'}` : 'Ожидание старта'; const me = state.players.find((p) => p.token === playerToken); if (me && !me.alive) { showToast('Вы выбиты. Дождитесь окончания матча.'); } }; if (roomPage) { const startBtn = document.getElementById('start-match'); if (startBtn) { startBtn.addEventListener('click', async () => { const res = await sendAction('start'); if (res.success) { showToast('Матч запущен.'); setTimeout(() => { window.location.href = `/match.php?id=${roomId}`; }, 700); } else { showToast(res.error || 'Не удалось запустить матч.'); } }); } } if (matchBoard) { document.getElementById('place-bomb').addEventListener('click', () => { sendAction('bomb'); }); window.addEventListener('keydown', (event) => { const now = Date.now(); const keyMap = { ArrowUp: 'up', ArrowDown: 'down', ArrowLeft: 'left', ArrowRight: 'right', w: 'up', s: 'down', a: 'left', d: 'right' }; if (event.code === 'Space') { sendAction('bomb'); return; } const dir = keyMap[event.key]; if (dir) { const me = lastState?.state?.players?.find((p) => p.token === playerToken); const speed = Math.max(1, Number(me?.speed || 1)); const moveCooldown = Math.max(45, Math.floor(120 / speed)); if (now - lastMoveAt < moveCooldown) return; lastMoveAt = now; sendMoveAction(dir); } }); } fetchState(); connectRealtime(); if (!stream) { startPolling(matchBoard ? 250 : 900); } });