322 lines
11 KiB
JavaScript
322 lines
11 KiB
JavaScript
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);
|
||
}
|
||
});
|