241 lines
10 KiB
PHP
241 lines
10 KiB
PHP
<?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();
|
||
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">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>Bomber Rooms — Online Multiplayer Lobby</title>
|
||
<?php
|
||
// Read project preview data from environment
|
||
$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 else: ?>
|
||
<meta name="description" content="Лобби и комнаты для классического Bomberman: создавайте матч, подключайтесь к друзьям и запускайте игру." />
|
||
<?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-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>
|
||
</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>
|
||
</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>
|
||
|
||
<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>
|