2026-03-05 10:39:31 +00:00

322 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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