Compare commits
No commits in common. "ai-dev" and "master" have entirely different histories.
@ -1,166 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
@ini_set('display_errors', '1');
|
||||
@error_reporting(E_ALL);
|
||||
@date_default_timezone_set('UTC');
|
||||
|
||||
session_start();
|
||||
require_once __DIR__ . '/../includes/rooms.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
ensure_rooms_schema();
|
||||
|
||||
$input = $_POST;
|
||||
$roomId = (int) ($input['room_id'] ?? 0);
|
||||
$action = $input['action'] ?? '';
|
||||
|
||||
if ($roomId <= 0 || $action === '') {
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid request']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$room = get_room($roomId);
|
||||
if (!$room) {
|
||||
echo json_encode(['success' => false, 'error' => 'Room not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$sessionPlayer = get_session_player($roomId);
|
||||
if (!$sessionPlayer) {
|
||||
echo json_encode(['success' => false, 'error' => 'Player not in room']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$state = $room['state'];
|
||||
$players = $state['players'] ?? [];
|
||||
$playerIndex = null;
|
||||
foreach ($players as $index => $player) {
|
||||
if ($player['token'] === $sessionPlayer['token']) {
|
||||
$playerIndex = $index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($playerIndex === null) {
|
||||
echo json_encode(['success' => false, 'error' => 'Player not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'start') {
|
||||
if (!empty($players[$playerIndex]['is_host']) && $room['status'] === 'waiting') {
|
||||
$room['status'] = 'playing';
|
||||
$state['started_at'] = now_ms();
|
||||
$room['state'] = $state;
|
||||
$room = update_room_tick($room);
|
||||
save_room_state($roomId, $room['state'], $room['status']);
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'status' => 'playing',
|
||||
'room' => [
|
||||
'id' => $room['id'],
|
||||
'name' => $room['name'],
|
||||
'status' => $room['status'],
|
||||
'max_players' => (int) $room['max_players']
|
||||
],
|
||||
'state' => $room['state'],
|
||||
'server_time' => now_ms()
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
echo json_encode(['success' => false, 'error' => 'Not allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($room['status'] !== 'playing') {
|
||||
echo json_encode(['success' => false, 'error' => 'Match not active']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'move') {
|
||||
$dir = $input['dir'] ?? '';
|
||||
$moves = [
|
||||
'up' => [0, -1],
|
||||
'down' => [0, 1],
|
||||
'left' => [-1, 0],
|
||||
'right' => [1, 0],
|
||||
];
|
||||
if (!isset($moves[$dir])) {
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid direction']);
|
||||
exit;
|
||||
}
|
||||
$player = $players[$playerIndex];
|
||||
if (!$player['alive']) {
|
||||
echo json_encode(['success' => false, 'error' => 'Player eliminated']);
|
||||
exit;
|
||||
}
|
||||
$nx = $player['x'] + $moves[$dir][0];
|
||||
$ny = $player['y'] + $moves[$dir][1];
|
||||
$tile = $state['map']['tiles'][$ny][$nx] ?? 1;
|
||||
$bombs = $state['bombs'] ?? [];
|
||||
$bombBlocked = false;
|
||||
foreach ($bombs as $bomb) {
|
||||
if ($bomb['x'] === $nx && $bomb['y'] === $ny) {
|
||||
$bombBlocked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($tile === 0 && !$bombBlocked) {
|
||||
$player['x'] = $nx;
|
||||
$player['y'] = $ny;
|
||||
if (!empty($state['powerups'])) {
|
||||
foreach ($state['powerups'] as $idx => $powerup) {
|
||||
if ($powerup['x'] === $nx && $powerup['y'] === $ny) {
|
||||
apply_powerup($player, $powerup['type']);
|
||||
unset($state['powerups'][$idx]);
|
||||
}
|
||||
}
|
||||
$state['powerups'] = array_values($state['powerups']);
|
||||
}
|
||||
$players[$playerIndex] = $player;
|
||||
}
|
||||
}
|
||||
|
||||
if ($action === 'bomb') {
|
||||
$player = $players[$playerIndex];
|
||||
if (!$player['alive']) {
|
||||
echo json_encode(['success' => false, 'error' => 'Player eliminated']);
|
||||
exit;
|
||||
}
|
||||
$bombs = $state['bombs'] ?? [];
|
||||
$owned = array_filter($bombs, fn($b) => $b['owner'] === $player['token']);
|
||||
$hasBomb = false;
|
||||
foreach ($bombs as $bomb) {
|
||||
if ($bomb['x'] === $player['x'] && $bomb['y'] === $player['y']) {
|
||||
$hasBomb = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (count($owned) < $player['bombs'] && !$hasBomb) {
|
||||
$bombs[] = [
|
||||
'x' => $player['x'],
|
||||
'y' => $player['y'],
|
||||
'owner' => $player['token'],
|
||||
'placed_at' => now_ms(),
|
||||
'blast' => $player['blast']
|
||||
];
|
||||
$state['bombs'] = $bombs;
|
||||
}
|
||||
}
|
||||
|
||||
$state['players'] = $players;
|
||||
$room['state'] = $state;
|
||||
$room = update_room_tick($room);
|
||||
save_room_state($roomId, $room['state'], $room['status']);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'room' => [
|
||||
'id' => $room['id'],
|
||||
'name' => $room['name'],
|
||||
'status' => $room['status'],
|
||||
'max_players' => (int) $room['max_players']
|
||||
],
|
||||
'state' => $room['state'],
|
||||
'server_time' => now_ms()
|
||||
]);
|
||||
@ -1,47 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
@ini_set('display_errors', '1');
|
||||
@error_reporting(E_ALL);
|
||||
@date_default_timezone_set('UTC');
|
||||
|
||||
session_start();
|
||||
require_once __DIR__ . '/../includes/rooms.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
ensure_rooms_schema();
|
||||
|
||||
$roomId = (int) ($_GET['room_id'] ?? 0);
|
||||
if ($roomId <= 0) {
|
||||
echo json_encode(['success' => false, 'error' => 'Missing room id']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$room = get_room($roomId);
|
||||
if (!$room) {
|
||||
echo json_encode(['success' => false, 'error' => 'Room not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$state = $room['state'] ?? [];
|
||||
$hasDynamicObjects = !empty($state['bombs']) || !empty($state['explosions']);
|
||||
if ($room['status'] === 'playing' || $hasDynamicObjects) {
|
||||
$room = update_room_tick($room);
|
||||
save_room_state($roomId, $room['state'], $room['status']);
|
||||
}
|
||||
|
||||
$sessionPlayer = get_session_player($roomId);
|
||||
$playerToken = $sessionPlayer['token'] ?? null;
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'room' => [
|
||||
'id' => $room['id'],
|
||||
'name' => $room['name'],
|
||||
'status' => $room['status'],
|
||||
'max_players' => (int) $room['max_players']
|
||||
],
|
||||
'state' => $room['state'],
|
||||
'player_token' => $playerToken,
|
||||
'server_time' => now_ms()
|
||||
]);
|
||||
@ -1,99 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
@ini_set('display_errors', '1');
|
||||
@error_reporting(E_ALL);
|
||||
@date_default_timezone_set('UTC');
|
||||
|
||||
session_start();
|
||||
require_once __DIR__ . '/../includes/rooms.php';
|
||||
|
||||
ensure_rooms_schema();
|
||||
|
||||
$roomId = (int) ($_GET['room_id'] ?? 0);
|
||||
if ($roomId <= 0) {
|
||||
http_response_code(400);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => false, 'error' => 'Missing room id']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$sessionPlayer = get_session_player($roomId);
|
||||
$playerToken = $sessionPlayer['token'] ?? null;
|
||||
session_write_close();
|
||||
|
||||
header('Content-Type: text/event-stream');
|
||||
header('Cache-Control: no-cache, no-store, must-revalidate');
|
||||
header('Pragma: no-cache');
|
||||
header('Expires: 0');
|
||||
header('X-Accel-Buffering: no');
|
||||
|
||||
@ini_set('zlib.output_compression', '0');
|
||||
@ini_set('output_buffering', '0');
|
||||
while (ob_get_level() > 0) {
|
||||
@ob_end_flush();
|
||||
}
|
||||
ob_implicit_flush(true);
|
||||
set_time_limit(0);
|
||||
ignore_user_abort(true);
|
||||
|
||||
$startedAt = time();
|
||||
$lastHash = '';
|
||||
|
||||
while (!connection_aborted()) {
|
||||
$room = get_room($roomId);
|
||||
if (!$room) {
|
||||
echo "event: room_closed\n";
|
||||
echo 'data: {"success":false,"error":"Room not found"}' . "\n\n";
|
||||
flush();
|
||||
break;
|
||||
}
|
||||
|
||||
$state = $room['state'] ?? [];
|
||||
$hasDynamicObjects = !empty($state['bombs']) || !empty($state['explosions']);
|
||||
if ($room['status'] === 'playing' || $hasDynamicObjects) {
|
||||
$room = update_room_tick($room);
|
||||
save_room_state((int) $room['id'], $room['state'], (string) $room['status']);
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'success' => true,
|
||||
'room' => [
|
||||
'id' => (int) $room['id'],
|
||||
'name' => $room['name'],
|
||||
'status' => $room['status'],
|
||||
'max_players' => (int) $room['max_players'],
|
||||
],
|
||||
'state' => $room['state'],
|
||||
'player_token' => $playerToken,
|
||||
'server_time' => now_ms(),
|
||||
];
|
||||
|
||||
$json = json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||||
if ($json === false) {
|
||||
break;
|
||||
}
|
||||
|
||||
$stateForHash = $payload['state'];
|
||||
unset($stateForHash['last_tick']);
|
||||
$hash = sha1(json_encode([
|
||||
'room' => $payload['room'],
|
||||
'state' => $stateForHash,
|
||||
'player_token' => $payload['player_token'],
|
||||
], JSON_UNESCAPED_UNICODE) ?: $json);
|
||||
if ($hash !== $lastHash) {
|
||||
$lastHash = $hash;
|
||||
echo "event: state\n";
|
||||
echo 'data: ' . $json . "\n\n";
|
||||
flush();
|
||||
} else {
|
||||
echo "event: ping\n";
|
||||
echo 'data: {"ts":' . now_ms() . "}\n\n";
|
||||
flush();
|
||||
}
|
||||
|
||||
usleep(($room['status'] === 'playing') ? 120000 : 350000);
|
||||
|
||||
if ((time() - $startedAt) >= 25) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -1,135 +1,161 @@
|
||||
body {
|
||||
background: #f4f5f7;
|
||||
color: #0f172a;
|
||||
font-family: 'Inter', system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
|
||||
background-size: 400% 400%;
|
||||
animation: gradient 15s ease infinite;
|
||||
color: #212529;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
|
||||
.form-control,
|
||||
.form-select {
|
||||
border-radius: 8px;
|
||||
border-color: #d1d5db;
|
||||
}
|
||||
|
||||
.form-control:focus,
|
||||
.form-select:focus {
|
||||
box-shadow: 0 0 0 0.2rem rgba(17, 24, 39, 0.15);
|
||||
border-color: #111827;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.btn-dark {
|
||||
background: #111827;
|
||||
border-color: #111827;
|
||||
}
|
||||
|
||||
.btn-dark:hover {
|
||||
background: #0b1220;
|
||||
border-color: #0b1220;
|
||||
}
|
||||
|
||||
.room-status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 16px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.room-status-grid .label {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.room-status-grid .value {
|
||||
font-weight: 600;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.game-board {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(13, 28px);
|
||||
grid-template-rows: repeat(11, 28px);
|
||||
gap: 4px;
|
||||
.main-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.game-cell {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@keyframes gradient {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
flex-direction: column;
|
||||
height: 85vh;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.2);
|
||||
backdrop-filter: blur(15px);
|
||||
-webkit-backdrop-filter: blur(15px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cell-solid {
|
||||
background: #111827;
|
||||
border-color: #111827;
|
||||
}
|
||||
|
||||
.cell-breakable {
|
||||
background: #cbd5f5;
|
||||
border-color: #94a3b8;
|
||||
}
|
||||
|
||||
.cell-powerup {
|
||||
border: 1px dashed #0f172a;
|
||||
}
|
||||
|
||||
.cell-bomb::after {
|
||||
content: '';
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #111827;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cell-explosion {
|
||||
background: #111827;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.player-chip {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 4px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 9px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.player-list-item {
|
||||
.chat-header {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.player-list-item:last-child {
|
||||
border-bottom: none;
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.message {
|
||||
max-width: 85%;
|
||||
padding: 0.85rem 1.1rem;
|
||||
border-radius: 16px;
|
||||
line-height: 1.5;
|
||||
font-size: 0.95rem;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
|
||||
animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px) scale(0.95); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||
}
|
||||
|
||||
.message.visitor {
|
||||
align-self: flex-end;
|
||||
background: linear-gradient(135deg, #212529 0%, #343a40 100%);
|
||||
color: #fff;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.message.bot {
|
||||
align-self: flex-start;
|
||||
background: #ffffff;
|
||||
color: #212529;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
.chat-input-area {
|
||||
padding: 1.25rem;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.chat-input-area form {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.chat-input-area input {
|
||||
flex: 1;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 0.75rem 1rem;
|
||||
outline: none;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.chat-input-area input:focus {
|
||||
border-color: #23a6d5;
|
||||
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2);
|
||||
}
|
||||
|
||||
.chat-input-area button {
|
||||
background: #212529;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.chat-input-area button:hover {
|
||||
background: #000;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* Background Animations */
|
||||
@ -374,4 +400,4 @@ body {
|
||||
.no-messages {
|
||||
text-align: center;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
@ -1,321 +1,39 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const roomId = window.ROOM_ID;
|
||||
if (!roomId) return;
|
||||
const chatForm = document.getElementById('chat-form');
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
const chatMessages = document.getElementById('chat-messages');
|
||||
|
||||
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 appendMessage = (text, sender) => {
|
||||
const msgDiv = document.createElement('div');
|
||||
msgDiv.classList.add('message', sender);
|
||||
msgDiv.textContent = text;
|
||||
chatMessages.appendChild(msgDiv);
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
};
|
||||
|
||||
const fetchState = async () => {
|
||||
chatForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const message = chatInput.value.trim();
|
||||
if (!message) return;
|
||||
|
||||
appendMessage(message, 'visitor');
|
||||
chatInput.value = '';
|
||||
|
||||
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 || 'Не удалось запустить матч.');
|
||||
}
|
||||
const response = await fetch('api/chat.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message })
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
// Artificial delay for realism
|
||||
setTimeout(() => {
|
||||
appendMessage(data.reply, 'bot');
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
appendMessage("Sorry, something went wrong. Please try again.", 'bot');
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
-- Bomber Rooms MVP schema (idempotent)
|
||||
CREATE TABLE IF NOT EXISTS rooms (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(120) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'waiting',
|
||||
max_players INT NOT NULL DEFAULT 4,
|
||||
state_json LONGTEXT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
@ -1,336 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
|
||||
function ensure_rooms_schema(): void {
|
||||
$sql = <<<SQL
|
||||
CREATE TABLE IF NOT EXISTS rooms (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(120) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'waiting',
|
||||
max_players INT NOT NULL DEFAULT 4,
|
||||
state_json LONGTEXT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
SQL;
|
||||
db()->exec($sql);
|
||||
}
|
||||
|
||||
function now_ms(): int {
|
||||
return (int) floor(microtime(true) * 1000);
|
||||
}
|
||||
|
||||
function random_token(): string {
|
||||
return bin2hex(random_bytes(8));
|
||||
}
|
||||
|
||||
function spawn_points(int $w, int $h): array {
|
||||
return [
|
||||
['x' => 1, 'y' => 1],
|
||||
['x' => $w - 2, 'y' => 1],
|
||||
['x' => 1, 'y' => $h - 2],
|
||||
['x' => $w - 2, 'y' => $h - 2],
|
||||
];
|
||||
}
|
||||
|
||||
function generate_map(int $w = 13, int $h = 11): array {
|
||||
$tiles = [];
|
||||
for ($y = 0; $y < $h; $y++) {
|
||||
$row = [];
|
||||
for ($x = 0; $x < $w; $x++) {
|
||||
$tile = 0;
|
||||
if ($x === 0 || $y === 0 || $x === $w - 1 || $y === $h - 1) {
|
||||
$tile = 1;
|
||||
} elseif ($x % 2 === 0 && $y % 2 === 0) {
|
||||
$tile = 1;
|
||||
} elseif (mt_rand(1, 100) <= 60) {
|
||||
$tile = 2;
|
||||
}
|
||||
$row[] = $tile;
|
||||
}
|
||||
$tiles[] = $row;
|
||||
}
|
||||
|
||||
$spawns = spawn_points($w, $h);
|
||||
foreach ($spawns as $spawn) {
|
||||
for ($dy = -1; $dy <= 1; $dy++) {
|
||||
for ($dx = -1; $dx <= 1; $dx++) {
|
||||
$sx = $spawn['x'] + $dx;
|
||||
$sy = $spawn['y'] + $dy;
|
||||
if ($sx >= 0 && $sy >= 0 && $sx < $w && $sy < $h) {
|
||||
$tiles[$sy][$sx] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'w' => $w,
|
||||
'h' => $h,
|
||||
'tiles' => $tiles
|
||||
];
|
||||
}
|
||||
|
||||
function default_state(string $playerName, string $token, bool $host = true): array {
|
||||
$map = generate_map();
|
||||
$spawns = spawn_points($map['w'], $map['h']);
|
||||
$colors = ['#111827', '#374151', '#6b7280', '#9ca3af'];
|
||||
$player = [
|
||||
'token' => $token,
|
||||
'name' => $playerName,
|
||||
'x' => $spawns[0]['x'],
|
||||
'y' => $spawns[0]['y'],
|
||||
'alive' => true,
|
||||
'is_host' => $host,
|
||||
'bombs' => 1,
|
||||
'blast' => 2,
|
||||
'speed' => 1,
|
||||
'color' => $colors[0]
|
||||
];
|
||||
|
||||
return [
|
||||
'map' => $map,
|
||||
'players' => [$player],
|
||||
'bombs' => [],
|
||||
'explosions' => [],
|
||||
'powerups' => [],
|
||||
'started_at' => null,
|
||||
'last_tick' => now_ms()
|
||||
];
|
||||
}
|
||||
|
||||
function get_room(int $roomId): ?array {
|
||||
$stmt = db()->prepare('SELECT * FROM rooms WHERE id = :id');
|
||||
$stmt->execute([':id' => $roomId]);
|
||||
$row = $stmt->fetch();
|
||||
if (!$row) {
|
||||
return null;
|
||||
}
|
||||
$row['state'] = json_decode($row['state_json'], true) ?: [];
|
||||
return $row;
|
||||
}
|
||||
|
||||
function save_room_state(int $roomId, array $state, string $status): void {
|
||||
$stmt = db()->prepare('UPDATE rooms SET state_json = :state_json, status = :status WHERE id = :id');
|
||||
$stmt->execute([
|
||||
':state_json' => json_encode($state, JSON_UNESCAPED_UNICODE),
|
||||
':status' => $status,
|
||||
':id' => $roomId
|
||||
]);
|
||||
}
|
||||
|
||||
function create_room(string $roomName, string $playerName, string $token, int $maxPlayers = 4): int {
|
||||
$state = default_state($playerName, $token, true);
|
||||
$stmt = db()->prepare('INSERT INTO rooms (name, status, max_players, state_json) VALUES (:name, :status, :max_players, :state_json)');
|
||||
$stmt->execute([
|
||||
':name' => $roomName,
|
||||
':status' => 'waiting',
|
||||
':max_players' => $maxPlayers,
|
||||
':state_json' => json_encode($state, JSON_UNESCAPED_UNICODE)
|
||||
]);
|
||||
return (int) db()->lastInsertId();
|
||||
}
|
||||
|
||||
function add_player_to_room(array $room, string $playerName, string $token): array {
|
||||
$state = $room['state'];
|
||||
$players = $state['players'] ?? [];
|
||||
$maxPlayers = (int) $room['max_players'];
|
||||
if (count($players) >= $maxPlayers) {
|
||||
throw new RuntimeException('Room is full');
|
||||
}
|
||||
$spawns = spawn_points($state['map']['w'], $state['map']['h']);
|
||||
$colors = ['#111827', '#374151', '#6b7280', '#9ca3af'];
|
||||
$index = count($players);
|
||||
$spawn = $spawns[$index % count($spawns)];
|
||||
$player = [
|
||||
'token' => $token,
|
||||
'name' => $playerName,
|
||||
'x' => $spawn['x'],
|
||||
'y' => $spawn['y'],
|
||||
'alive' => true,
|
||||
'is_host' => false,
|
||||
'bombs' => 1,
|
||||
'blast' => 2,
|
||||
'speed' => 1,
|
||||
'color' => $colors[$index % count($colors)]
|
||||
];
|
||||
$players[] = $player;
|
||||
$state['players'] = $players;
|
||||
return $state;
|
||||
}
|
||||
|
||||
function remove_player_from_room(array $room, string $token): ?array {
|
||||
$state = $room['state'];
|
||||
$players = $state['players'] ?? [];
|
||||
$newPlayers = array_values(array_filter($players, fn($p) => $p['token'] !== $token));
|
||||
|
||||
if (count($newPlayers) === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$hostExists = false;
|
||||
foreach ($newPlayers as $p) {
|
||||
if (!empty($p['is_host'])) {
|
||||
$hostExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$hostExists) {
|
||||
$newPlayers[0]['is_host'] = true;
|
||||
}
|
||||
|
||||
$state['players'] = $newPlayers;
|
||||
$room['state'] = $state;
|
||||
return $room;
|
||||
}
|
||||
|
||||
function delete_room(int $roomId): void {
|
||||
$stmt = db()->prepare('DELETE FROM rooms WHERE id = :id');
|
||||
$stmt->execute([':id' => $roomId]);
|
||||
}
|
||||
|
||||
function cleanup_empty_rooms(): int {
|
||||
$stmt = db()->prepare('SELECT id, state_json FROM rooms');
|
||||
$stmt->execute();
|
||||
$rows = $stmt->fetchAll();
|
||||
$deleted = 0;
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$state = json_decode($row['state_json'] ?? '', true);
|
||||
$players = is_array($state) ? ($state['players'] ?? null) : null;
|
||||
$isEmpty = !is_array($players) || count($players) === 0;
|
||||
if ($isEmpty) {
|
||||
delete_room((int) $row['id']);
|
||||
$deleted++;
|
||||
}
|
||||
}
|
||||
|
||||
return $deleted;
|
||||
}
|
||||
|
||||
function get_session_player(int $roomId): ?array {
|
||||
return $_SESSION['room_players'][$roomId] ?? null;
|
||||
}
|
||||
|
||||
function set_session_player(int $roomId, string $token, string $name): void {
|
||||
if (!isset($_SESSION['room_players'])) {
|
||||
$_SESSION['room_players'] = [];
|
||||
}
|
||||
$_SESSION['room_players'][$roomId] = ['token' => $token, 'name' => $name];
|
||||
}
|
||||
|
||||
function update_room_tick(array $room): array {
|
||||
$state = $room['state'];
|
||||
$now = now_ms();
|
||||
$state['explosions'] = array_values(array_filter($state['explosions'] ?? [], function ($exp) use ($now) {
|
||||
return ($exp['until'] ?? 0) > $now;
|
||||
}));
|
||||
|
||||
$bombs = $state['bombs'] ?? [];
|
||||
$remaining = [];
|
||||
$newExplosions = [];
|
||||
foreach ($bombs as $bomb) {
|
||||
if (($bomb['placed_at'] ?? 0) + 2000 <= $now) {
|
||||
$blast = $bomb['blast'] ?? 2;
|
||||
$newExplosions = array_merge($newExplosions, compute_explosion_tiles($state, $bomb['x'], $bomb['y'], $blast));
|
||||
} else {
|
||||
$remaining[] = $bomb;
|
||||
}
|
||||
}
|
||||
$state['bombs'] = $remaining;
|
||||
|
||||
if ($newExplosions) {
|
||||
$tiles = $state['map']['tiles'];
|
||||
foreach ($newExplosions as $exp) {
|
||||
$tiles[$exp['y']][$exp['x']] = $exp['tile_after'];
|
||||
if (!empty($exp['powerup'])) {
|
||||
$state['powerups'][] = ['x' => $exp['x'], 'y' => $exp['y'], 'type' => $exp['powerup']];
|
||||
}
|
||||
$state['explosions'][] = [
|
||||
'x' => $exp['x'],
|
||||
'y' => $exp['y'],
|
||||
'until' => $now + 550
|
||||
];
|
||||
}
|
||||
$state['map']['tiles'] = $tiles;
|
||||
}
|
||||
|
||||
$players = $state['players'] ?? [];
|
||||
foreach ($players as &$player) {
|
||||
if (!$player['alive']) {
|
||||
continue;
|
||||
}
|
||||
foreach ($state['explosions'] as $exp) {
|
||||
if ($exp['x'] === $player['x'] && $exp['y'] === $player['y']) {
|
||||
$player['alive'] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($player);
|
||||
$state['players'] = $players;
|
||||
|
||||
$alive = array_filter($players, fn($p) => $p['alive']);
|
||||
if ($room['status'] === 'playing' && count($alive) <= 1) {
|
||||
$room['status'] = 'finished';
|
||||
$state['winner'] = $alive ? array_values($alive)[0]['name'] : null;
|
||||
}
|
||||
|
||||
$state['last_tick'] = $now;
|
||||
$room['state'] = $state;
|
||||
return $room;
|
||||
}
|
||||
|
||||
function compute_explosion_tiles(array $state, int $x, int $y, int $blast): array {
|
||||
$tiles = $state['map']['tiles'];
|
||||
$w = $state['map']['w'];
|
||||
$h = $state['map']['h'];
|
||||
$out = [];
|
||||
$pushTile = function (int $tx, int $ty, bool $breakableHit = false) use (&$out, $tiles) {
|
||||
$powerup = null;
|
||||
if ($breakableHit && mt_rand(1, 100) <= 20) {
|
||||
$types = ['blast', 'bombs', 'speed'];
|
||||
$powerup = $types[array_rand($types)];
|
||||
}
|
||||
$out[] = [
|
||||
'x' => $tx,
|
||||
'y' => $ty,
|
||||
'tile_after' => $breakableHit ? 0 : $tiles[$ty][$tx],
|
||||
'powerup' => $powerup
|
||||
];
|
||||
};
|
||||
|
||||
$pushTile($x, $y);
|
||||
$dirs = [[1,0],[-1,0],[0,1],[0,-1]];
|
||||
foreach ($dirs as $dir) {
|
||||
for ($i = 1; $i <= $blast; $i++) {
|
||||
$tx = $x + $dir[0] * $i;
|
||||
$ty = $y + $dir[1] * $i;
|
||||
if ($tx < 0 || $ty < 0 || $tx >= $w || $ty >= $h) {
|
||||
break;
|
||||
}
|
||||
$tile = $tiles[$ty][$tx];
|
||||
if ($tile === 1) {
|
||||
break;
|
||||
}
|
||||
if ($tile === 2) {
|
||||
$pushTile($tx, $ty, true);
|
||||
break;
|
||||
}
|
||||
$pushTile($tx, $ty);
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
function apply_powerup(array &$player, string $type): void {
|
||||
if ($type === 'blast') {
|
||||
$player['blast'] = min(5, $player['blast'] + 1);
|
||||
} elseif ($type === 'bombs') {
|
||||
$player['bombs'] = min(5, $player['bombs'] + 1);
|
||||
} elseif ($type === 'speed') {
|
||||
$player['speed'] = min(3, $player['speed'] + 1);
|
||||
}
|
||||
}
|
||||
320
index.php
320
index.php
@ -4,237 +4,147 @@ declare(strict_types=1);
|
||||
@error_reporting(E_ALL);
|
||||
@date_default_timezone_set('UTC');
|
||||
|
||||
session_start();
|
||||
require_once __DIR__ . '/includes/rooms.php';
|
||||
|
||||
ensure_rooms_schema();
|
||||
cleanup_empty_rooms();
|
||||
|
||||
$errors = [];
|
||||
$flash = $_SESSION['flash'] ?? null;
|
||||
unset($_SESSION['flash']);
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
$playerName = trim($_POST['player_name'] ?? '');
|
||||
$roomName = trim($_POST['room_name'] ?? '');
|
||||
if ($action === 'create') {
|
||||
if ($playerName === '') {
|
||||
$errors[] = 'Enter a player name to create a room.';
|
||||
}
|
||||
if ($roomName === '') {
|
||||
$roomName = 'Retro Arena';
|
||||
}
|
||||
if (!$errors) {
|
||||
$token = random_token();
|
||||
$roomId = create_room($roomName, $playerName, $token, 4);
|
||||
set_session_player($roomId, $token, $playerName);
|
||||
header('Location: room.php?id=' . $roomId);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if ($action === 'join') {
|
||||
$roomId = (int) ($_POST['room_id'] ?? 0);
|
||||
if ($roomId <= 0) {
|
||||
$errors[] = 'Select a room to join.';
|
||||
}
|
||||
if ($playerName === '') {
|
||||
$errors[] = 'Enter a player name to join.';
|
||||
}
|
||||
if (!$errors) {
|
||||
$room = get_room($roomId);
|
||||
if (!$room) {
|
||||
$errors[] = 'Room not found.';
|
||||
} else {
|
||||
try {
|
||||
$token = random_token();
|
||||
$state = add_player_to_room($room, $playerName, $token);
|
||||
save_room_state($roomId, $state, $room['status']);
|
||||
set_session_player($roomId, $token, $playerName);
|
||||
header('Location: room.php?id=' . $roomId);
|
||||
exit;
|
||||
} catch (RuntimeException $e) {
|
||||
$errors[] = $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = db()->prepare('SELECT * FROM rooms ORDER BY updated_at DESC LIMIT 20');
|
||||
$stmt->execute();
|
||||
$rooms = $stmt->fetchAll();
|
||||
|
||||
$phpVersion = PHP_VERSION;
|
||||
$now = date('Y-m-d H:i:s');
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Bomber Rooms — Online Multiplayer Lobby</title>
|
||||
<title>New Style</title>
|
||||
<?php
|
||||
// Read project preview data from environment
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
?>
|
||||
<?php if ($projectDescription): ?>
|
||||
<!-- Meta description -->
|
||||
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
||||
<!-- Open Graph meta tags -->
|
||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<!-- Twitter meta tags -->
|
||||
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<?php else: ?>
|
||||
<meta name="description" content="Лобби и комнаты для классического Bomberman: создавайте матч, подключайтесь к друзьям и запускайте игру." />
|
||||
<?php endif; ?>
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<!-- Open Graph image -->
|
||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<!-- Twitter image -->
|
||||
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<?php endif; ?>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time(); ?>" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
|
||||
animation: bg-pan 20s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
@keyframes bg-pan {
|
||||
0% { background-position: 0% 0%; }
|
||||
100% { background-position: 100% 100%; }
|
||||
}
|
||||
main {
|
||||
padding: 2rem;
|
||||
}
|
||||
.card {
|
||||
background: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.loader {
|
||||
margin: 1.25rem auto 1.25rem;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.hint {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px; height: 1px;
|
||||
padding: 0; margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap; border: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 1rem;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
code {
|
||||
background: rgba(0,0,0,0.2);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-white border-bottom small shadow-sm sticky-top">
|
||||
<div class="container">
|
||||
<a class="navbar-brand fw-semibold text-dark" href="/">Bomber Rooms</a>
|
||||
<div class="d-flex gap-2">
|
||||
<a class="btn btn-outline-dark btn-sm" href="#rooms">Комнаты</a>
|
||||
<a class="btn btn-dark btn-sm" href="#create">Создать матч</a>
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your website…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<header class="py-5 border-bottom bg-body">
|
||||
<div class="container">
|
||||
<div class="row align-items-center g-4">
|
||||
<div class="col-lg-7">
|
||||
<p class="text-uppercase text-muted small mb-2">Retro multiplayer MVP</p>
|
||||
<h1 class="display-6 fw-semibold">Классический Bomberman онлайн — лобби, комнаты, матч</h1>
|
||||
<p class="text-muted mt-3 mb-4">Создайте комнату, позовите друзей и запустите быстрый матч. MVP синхронизирует движение и бомбы через частый опрос — в следующем шаге добавим WebSocket для настоящего realtime.</p>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a class="btn btn-dark" href="#create">Запустить комнату</a>
|
||||
<a class="btn btn-outline-dark" href="#rooms">Смотреть лобби</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<div class="panel p-4">
|
||||
<h2 class="h6 text-uppercase text-muted">Статус MVP</h2>
|
||||
<ul class="list-unstyled mb-0 small">
|
||||
<li class="d-flex justify-content-between py-2 border-bottom"><span>Комнаты</span><span><?= count($rooms) ?></span></li>
|
||||
<li class="d-flex justify-content-between py-2 border-bottom"><span>Сетка</span><span>13 × 11</span></li>
|
||||
<li class="d-flex justify-content-between py-2"><span>Текущее время</span><span><?= htmlspecialchars($now) ?> UTC</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="py-5">
|
||||
<div class="container">
|
||||
<?php if ($flash): ?>
|
||||
<div class="alert alert-info"><?= htmlspecialchars($flash) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($errors): ?>
|
||||
<div class="alert alert-danger">
|
||||
<?= htmlspecialchars(implode(' ', $errors)) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div id="create" class="row g-4 mb-5">
|
||||
<div class="col-lg-6">
|
||||
<div class="panel p-4 h-100">
|
||||
<h2 class="h5 fw-semibold mb-3">Создать комнату</h2>
|
||||
<form method="post" class="vstack gap-3">
|
||||
<input type="hidden" name="action" value="create">
|
||||
<div>
|
||||
<label class="form-label small text-muted">Название комнаты</label>
|
||||
<input type="text" name="room_name" class="form-control" placeholder="Например: Retro Arena">
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label small text-muted">Ваш ник</label>
|
||||
<input type="text" name="player_name" class="form-control" placeholder="Player 1" required>
|
||||
</div>
|
||||
<button class="btn btn-dark w-100">Создать и войти</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="panel p-4 h-100">
|
||||
<h2 class="h5 fw-semibold mb-3">Как работает матч</h2>
|
||||
<ol class="small text-muted mb-0">
|
||||
<li class="mb-2">Создайте комнату и дождитесь 2–4 игроков.</li>
|
||||
<li class="mb-2">Хост запускает матч — генерируется карта.</li>
|
||||
<li class="mb-2">Двигайтесь WASD/стрелками, ставьте бомбы (Space).</li>
|
||||
<li>Выигрывает последний выживший.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section id="rooms" class="mb-5">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2 class="h5 fw-semibold mb-0">Активные комнаты</h2>
|
||||
<span class="text-muted small"><?= count($rooms) ?> комнат</span>
|
||||
</div>
|
||||
<div class="panel p-0 overflow-hidden">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Комната</th>
|
||||
<th>Статус</th>
|
||||
<th>Игроки</th>
|
||||
<th>Действие</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!$rooms): ?>
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-muted py-4">Пока нет комнат — создайте первую выше.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($rooms as $room): ?>
|
||||
<?php
|
||||
$state = json_decode($room['state_json'], true) ?: [];
|
||||
$playerCount = count($state['players'] ?? []);
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-semibold"><?= htmlspecialchars($room['name']) ?></div>
|
||||
<div class="text-muted small">#<?= (int) $room['id'] ?></div>
|
||||
</td>
|
||||
<td><span class="badge text-bg-light border"><?= htmlspecialchars($room['status']) ?></span></td>
|
||||
<td><?= $playerCount ?> / <?= (int) $room['max_players'] ?></td>
|
||||
<td>
|
||||
<form method="post" class="d-flex gap-2 align-items-center">
|
||||
<input type="hidden" name="action" value="join">
|
||||
<input type="hidden" name="room_id" value="<?= (int) $room['id'] ?>">
|
||||
<input type="text" name="player_name" class="form-control form-control-sm" placeholder="Ник" required>
|
||||
<button class="btn btn-outline-dark btn-sm">Войти</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
||||
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
||||
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="border-top py-4">
|
||||
<div class="container small text-muted d-flex justify-content-between flex-wrap gap-2">
|
||||
<span>PHP <?= htmlspecialchars($phpVersion) ?></span>
|
||||
<span>Обновлено <?= htmlspecialchars($now) ?> UTC</span>
|
||||
<a class="text-muted" href="/healthz">/healthz</a>
|
||||
</div>
|
||||
<footer>
|
||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="assets/js/main.js?v=<?= time(); ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
122
match.php
122
match.php
@ -1,122 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
@ini_set('display_errors', '1');
|
||||
@error_reporting(E_ALL);
|
||||
@date_default_timezone_set('UTC');
|
||||
|
||||
session_start();
|
||||
require_once __DIR__ . '/includes/rooms.php';
|
||||
|
||||
ensure_rooms_schema();
|
||||
|
||||
$roomId = (int) ($_GET['id'] ?? 0);
|
||||
if ($roomId <= 0) {
|
||||
http_response_code(404);
|
||||
echo 'Room not found';
|
||||
exit;
|
||||
}
|
||||
|
||||
$room = get_room($roomId);
|
||||
if (!$room) {
|
||||
http_response_code(404);
|
||||
echo 'Room not found';
|
||||
exit;
|
||||
}
|
||||
|
||||
$sessionPlayer = get_session_player($roomId);
|
||||
if (!$sessionPlayer) {
|
||||
$_SESSION['flash'] = 'Сначала войдите в комнату.';
|
||||
header('Location: /room.php?id=' . $roomId);
|
||||
exit;
|
||||
}
|
||||
|
||||
$phpVersion = PHP_VERSION;
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Матч — <?= htmlspecialchars($room['name']) ?> | Bomber Rooms</title>
|
||||
<?php
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
?>
|
||||
<?php if ($projectDescription): ?>
|
||||
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<?php endif; ?>
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<?php endif; ?>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time(); ?>" />
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-light bg-white border-bottom small shadow-sm sticky-top">
|
||||
<div class="container">
|
||||
<a class="navbar-brand fw-semibold text-dark" href="/">Bomber Rooms</a>
|
||||
<div class="d-flex gap-2">
|
||||
<a class="btn btn-outline-dark btn-sm" href="room.php?id=<?= (int) $roomId ?>">Комната</a>
|
||||
<a class="btn btn-dark btn-sm" href="/">Лобби</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="py-4">
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap gap-3 mb-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted small mb-1">Матч #<?= (int) $roomId ?></p>
|
||||
<h1 class="h4 fw-semibold mb-0"><?= htmlspecialchars($room['name']) ?></h1>
|
||||
</div>
|
||||
<div class="panel px-3 py-2 small text-muted">
|
||||
Управление: WASD / ←↑→↓, бомба — Space
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-8">
|
||||
<div class="panel p-3">
|
||||
<div id="game-board" class="game-board" aria-live="polite"></div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mt-3 small text-muted">
|
||||
<span id="match-status">Ожидание состояния...</span>
|
||||
<span>PHP <?= htmlspecialchars($phpVersion) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="panel p-4 h-100">
|
||||
<h2 class="h6 text-uppercase text-muted">Состав и статус</h2>
|
||||
<ul id="match-players" class="list-unstyled small mb-3"></ul>
|
||||
<button id="place-bomb" class="btn btn-dark w-100 mb-2">Поставить бомбу</button>
|
||||
<a class="btn btn-outline-dark w-100" href="room.php?id=<?= (int) $roomId ?>">Вернуться в комнату</a>
|
||||
<div class="mt-4 small text-muted">
|
||||
<div class="mb-1">Бонусы: увеличение взрыва, +бомбы, скорость</div>
|
||||
<div>Realtime синхронизация включена через push‑stream.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="toast-container position-fixed bottom-0 end-0 p-3">
|
||||
<div id="match-toast" class="toast align-items-center text-bg-dark border-0" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body" id="match-toast-body">Статус обновлен.</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.ROOM_ID = <?= (int) $roomId ?>;
|
||||
window.PLAYER_TOKEN = <?= json_encode($sessionPlayer['token']) ?>;
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="assets/js/main.js?v=<?= time(); ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
205
room.php
205
room.php
@ -1,205 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
@ini_set('display_errors', '1');
|
||||
@error_reporting(E_ALL);
|
||||
@date_default_timezone_set('UTC');
|
||||
|
||||
session_start();
|
||||
require_once __DIR__ . '/includes/rooms.php';
|
||||
|
||||
ensure_rooms_schema();
|
||||
|
||||
$roomId = (int) ($_GET['id'] ?? 0);
|
||||
if ($roomId <= 0) {
|
||||
http_response_code(404);
|
||||
echo 'Room not found';
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($_GET['leave'])) {
|
||||
$sessionPlayer = get_session_player($roomId);
|
||||
if ($sessionPlayer) {
|
||||
$room = get_room($roomId);
|
||||
if ($room) {
|
||||
$newRoom = remove_player_from_room($room, $sessionPlayer['token']);
|
||||
if ($newRoom === null) {
|
||||
delete_room($roomId);
|
||||
} else {
|
||||
save_room_state($roomId, $newRoom['state'], $newRoom['status']);
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($_SESSION['room_players'][$roomId]);
|
||||
header('Location: /');
|
||||
exit;
|
||||
}
|
||||
|
||||
$room = get_room($roomId);
|
||||
if (!$room) {
|
||||
http_response_code(404);
|
||||
echo 'Room not found';
|
||||
exit;
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'join') {
|
||||
$playerName = trim($_POST['player_name'] ?? '');
|
||||
if ($playerName === '') {
|
||||
$errors[] = 'Введите ник.';
|
||||
} else {
|
||||
try {
|
||||
$token = random_token();
|
||||
$state = add_player_to_room($room, $playerName, $token);
|
||||
save_room_state($roomId, $state, $room['status']);
|
||||
set_session_player($roomId, $token, $playerName);
|
||||
$room['state'] = $state;
|
||||
} catch (RuntimeException $e) {
|
||||
$errors[] = $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$sessionPlayer = get_session_player($roomId);
|
||||
$state = $room['state'];
|
||||
$players = $state['players'] ?? [];
|
||||
$isMember = false;
|
||||
$isHost = false;
|
||||
foreach ($players as $player) {
|
||||
if ($sessionPlayer && $player['token'] === $sessionPlayer['token']) {
|
||||
$isMember = true;
|
||||
$isHost = !empty($player['is_host']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$phpVersion = PHP_VERSION;
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Комната <?= htmlspecialchars($room['name']) ?> — Bomber Rooms</title>
|
||||
<?php
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
?>
|
||||
<?php if ($projectDescription): ?>
|
||||
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<?php endif; ?>
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<?php endif; ?>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time(); ?>" />
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-light bg-white border-bottom small shadow-sm sticky-top">
|
||||
<div class="container">
|
||||
<a class="navbar-brand fw-semibold text-dark" href="/">Bomber Rooms</a>
|
||||
<div class="d-flex gap-2">
|
||||
<a class="btn btn-outline-dark btn-sm" href="/">Лобби</a>
|
||||
<a class="btn btn-dark btn-sm" href="room.php?id=<?= (int) $roomId ?>&leave=1">Выйти</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="py-5">
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-start flex-wrap gap-3 mb-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted small mb-2">Комната #<?= (int) $roomId ?></p>
|
||||
<h1 class="h3 fw-semibold mb-2"><?= htmlspecialchars($room['name']) ?></h1>
|
||||
<p class="text-muted mb-0">Статус: <span class="badge text-bg-light border"><?= htmlspecialchars($room['status']) ?></span></p>
|
||||
</div>
|
||||
<div class="panel px-3 py-2 small text-muted">
|
||||
Игроков: <strong><?= count($players) ?> / <?= (int) $room['max_players'] ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($errors): ?>
|
||||
<div class="alert alert-danger"><?= htmlspecialchars(implode(' ', $errors)) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!$isMember): ?>
|
||||
<div class="panel p-4 mb-4">
|
||||
<h2 class="h5 fw-semibold mb-3">Войти в комнату</h2>
|
||||
<form method="post" class="row g-3">
|
||||
<input type="hidden" name="action" value="join">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small text-muted">Ваш ник</label>
|
||||
<input type="text" name="player_name" class="form-control" required>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button class="btn btn-dark">Войти и ожидать</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-4">
|
||||
<div class="panel p-4 h-100">
|
||||
<h2 class="h6 text-uppercase text-muted">Игроки в комнате</h2>
|
||||
<ul id="player-list" class="list-unstyled small mb-4"></ul>
|
||||
<div class="d-grid gap-2">
|
||||
<?php if ($isHost && $room['status'] === 'waiting'): ?>
|
||||
<button id="start-match" class="btn btn-dark">Запустить матч</button>
|
||||
<?php endif; ?>
|
||||
<?php if ($room['status'] === 'playing'): ?>
|
||||
<a class="btn btn-dark" href="match.php?id=<?= (int) $roomId ?>">Перейти в матч</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($room['status'] === 'finished'): ?>
|
||||
<a class="btn btn-outline-dark" href="match.php?id=<?= (int) $roomId ?>">Смотреть итог</a>
|
||||
<?php endif; ?>
|
||||
<a class="btn btn-outline-dark" href="/">Вернуться в лобби</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-8">
|
||||
<div class="panel p-4 h-100">
|
||||
<h2 class="h6 text-uppercase text-muted">Ожидание старта</h2>
|
||||
<p class="text-muted">Матч запустится после команды хоста. Статус обновляется автоматически.</p>
|
||||
<div class="room-status-grid">
|
||||
<div>
|
||||
<div class="label">Подключено</div>
|
||||
<div id="room-count" class="value">—</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">Состояние</div>
|
||||
<div id="room-state" class="value">—</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">Победитель</div>
|
||||
<div id="room-winner" class="value">—</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 small text-muted">
|
||||
<strong>Подсказка:</strong> комната обновляется в realtime через push‑stream, fallback — обычный polling.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="toast-container position-fixed bottom-0 end-0 p-3">
|
||||
<div id="room-toast" class="toast align-items-center text-bg-dark border-0" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body" id="room-toast-body">Обновление комнаты.</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.ROOM_ID = <?= (int) $roomId ?>;
|
||||
window.PLAYER_TOKEN = <?= $sessionPlayer ? json_encode($sessionPlayer['token']) : 'null' ?>;
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="assets/js/main.js?v=<?= time(); ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user