39366-vm/index.php
2026-03-30 18:54:34 +00:00

266 lines
14 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . '/game_bootstrap.php';
ensureFpsMatchesTable();
$recentMatches = fetchRecentMatches(6);
$projectName = trim((string)($_SERVER['PROJECT_NAME'] ?? 'Strike Grid Arena'));
$projectDescription = trim((string)($_SERVER['PROJECT_DESCRIPTION'] ?? 'Browser FPS prototype with weapon switching, moving bots, round summaries, and stored match reports.'));
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
$buildStamp = (string)max(@filemtime(__DIR__ . '/assets/css/custom.css') ?: time(), @filemtime(__DIR__ . '/assets/js/main.js') ?: time());
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><?= htmlspecialchars($projectName) ?> | Browser FPS Arena</title>
<meta name="description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php if ($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; ?>
<meta name="theme-color" content="#0b0f14" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" href="/assets/css/custom.css?v=<?= urlencode($buildStamp) ?>">
</head>
<body class="app-shell" data-bs-theme="dark">
<nav class="navbar navbar-expand-lg border-bottom border-secondary-subtle bg-body-tertiary bg-opacity-75 sticky-top">
<div class="container py-2">
<a class="navbar-brand fw-semibold text-uppercase small-brand" href="/">Strike Grid Arena</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav" aria-controls="mainNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="mainNav">
<ul class="navbar-nav ms-auto align-items-lg-center gap-lg-2">
<li class="nav-item"><a class="nav-link" href="#loadout">Loadout</a></li>
<li class="nav-item"><a class="nav-link" href="#arena">Arena</a></li>
<li class="nav-item"><a class="nav-link" href="#history">Recent matches</a></li>
<li class="nav-item"><a class="btn btn-light btn-sm ms-lg-2" href="#arena">Play round</a></li>
</ul>
</div>
</div>
</nav>
<main>
<section class="hero-section border-bottom border-secondary-subtle">
<div class="container py-5 py-lg-6">
<div class="row g-4 align-items-center">
<div class="col-lg-7">
<p class="eyebrow mb-3">Browser FPS MVP</p>
<h1 class="display-title mb-3">Pick a gun, drop into the arena, and fight moving bots in your browser.</h1>
<p class="hero-copy mb-4">This first slice ships a full mini loop: choose a loadout, survive an active round, get a combat summary, save the result, and review past matches.</p>
<div class="d-flex flex-wrap gap-2 mb-4">
<span class="surface-chip">4 weapon profiles</span>
<span class="surface-chip">Moving bot AI</span>
<span class="surface-chip">Round summary + saved scores</span>
<span class="surface-chip">Canvas gameplay + PHP history</span>
</div>
<div class="d-flex flex-wrap gap-3">
<a class="btn btn-light" href="#loadout">Configure operator</a>
<a class="btn btn-outline-light" href="#history">View score cards</a>
</div>
</div>
<div class="col-lg-5">
<div class="surface-panel p-4">
<div class="mini-report mb-4">
<div>
<span class="mini-label">Mode</span>
<strong>Skirmish vs. bots</strong>
</div>
<div>
<span class="mini-label">Round length</span>
<strong>75 seconds</strong>
</div>
</div>
<div class="mini-report mb-4">
<div>
<span class="mini-label">Objective</span>
<strong>Clear the squad or outscore the timer</strong>
</div>
<div>
<span class="mini-label">Controls</span>
<strong>WASD • Mouse • R • Space</strong>
</div>
</div>
<div class="status-note">
<div class="status-dot"></div>
<div>
<strong>Live prototype</strong>
<p class="mb-0 text-secondary">Built as a thin MVP slice so you can test feel, weapons, and bot movement before adding bigger maps.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section id="loadout" class="section-block border-bottom border-secondary-subtle">
<div class="container py-5">
<div class="row g-4 align-items-start">
<div class="col-xl-4">
<div class="section-header sticky-lg-top top-offset">
<p class="eyebrow mb-2">Step 1</p>
<h2 class="h3 mb-3">Set your operator profile</h2>
<p class="text-secondary mb-0">Choose a call sign and weapon preset. The selected loadout feeds directly into the live arena and the saved match record.</p>
</div>
</div>
<div class="col-xl-8">
<div class="surface-panel p-4 p-lg-5">
<form id="loadout-form" class="row g-4" novalidate>
<div class="col-12 col-lg-5">
<label for="playerName" class="form-label">Call sign</label>
<input type="text" class="form-control form-control-lg" id="playerName" name="player_name" maxlength="40" placeholder="Operator Nova" value="Operator Nova" required>
<div class="form-text">Used in score history and match detail pages.</div>
</div>
<div class="col-12 col-lg-7">
<label class="form-label d-block">Weapon loadout</label>
<div class="weapon-grid" id="weaponGrid" role="radiogroup" aria-label="Weapon presets">
<button type="button" class="weapon-card active" data-weapon="carbine" aria-pressed="true">
<span class="weapon-title">VX Carbine</span>
<span class="weapon-meta">Balanced • 30 rounds • stable recoil</span>
</button>
<button type="button" class="weapon-card" data-weapon="smg" aria-pressed="false">
<span class="weapon-title">Mako SMG</span>
<span class="weapon-meta">Fast fire • close range • 36 rounds</span>
</button>
<button type="button" class="weapon-card" data-weapon="shotgun" aria-pressed="false">
<span class="weapon-title">Breach-8</span>
<span class="weapon-meta">Heavy spread • burst damage • 8 shells</span>
</button>
<button type="button" class="weapon-card" data-weapon="marksman" aria-pressed="false">
<span class="weapon-title">Atlas DMR</span>
<span class="weapon-meta">High damage • precise • 12 rounds</span>
</button>
</div>
<input type="hidden" id="weaponInput" name="weapon_key" value="carbine">
</div>
<div class="col-12 d-flex flex-wrap gap-2 align-items-center">
<button type="submit" class="btn btn-light">Start round</button>
<span class="text-secondary small">Round starts instantly in the arena panel below.</span>
</div>
</form>
</div>
</div>
</div>
</div>
</section>
<section id="arena" class="section-block border-bottom border-secondary-subtle">
<div class="container py-5">
<div class="row g-4">
<div class="col-xl-8">
<div class="surface-panel p-3 p-lg-4 h-100">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
<div>
<p class="eyebrow mb-1">Step 2</p>
<h2 class="h4 mb-0">Arena skirmish</h2>
</div>
<div class="d-flex gap-2">
<button class="btn btn-outline-light btn-sm" id="restartButton" type="button">Restart</button>
<button class="btn btn-light btn-sm" id="saveButton" type="button" disabled>Save result</button>
</div>
</div>
<canvas id="gameCanvas" width="1200" height="720" aria-label="FPS game arena" role="img" tabindex="0"></canvas>
<div class="hud-strip mt-3" id="hudStrip">
<div><span>Operator</span><strong id="hudName">Operator Nova</strong></div>
<div><span>Weapon</span><strong id="hudWeapon">VX Carbine</strong></div>
<div><span>Health</span><strong id="hudHealth">100</strong></div>
<div><span>Ammo</span><strong id="hudAmmo">30 / 120</strong></div>
<div><span>Kills</span><strong id="hudKills">0</strong></div>
<div><span>Score</span><strong id="hudScore">0</strong></div>
<div><span>Time</span><strong id="hudTime">75s</strong></div>
</div>
<div class="controls-note mt-3">
<span>Click inside the arena to lock the mouse. Use <strong>WASD</strong> to move and climb the hills, move the <strong>mouse</strong> to look <strong>up / down / left / right</strong>, use <strong>← →</strong> plus <strong>↑ ↓</strong> as keyboard fallback, <strong>hold click/space</strong> for rapid fire, <strong>R</strong> to reload, and <strong>Esc</strong> to release aim.</span>
</div>
</div>
</div>
<div class="col-xl-4">
<div class="d-grid gap-4 h-100">
<div class="surface-panel p-4">
<p class="eyebrow mb-2">Step 3</p>
<h2 class="h5 mb-3">Round summary</h2>
<div id="summaryPanel" class="summary-state empty">
<p class="summary-title">No round finished yet.</p>
<p class="summary-copy mb-0 text-secondary">Start a round to generate a combat report, then save it into recent matches.</p>
</div>
</div>
<div class="surface-panel p-4">
<h2 class="h5 mb-3">Combat cues</h2>
<ul class="list-unstyled compact-list mb-0">
<li>The arena now has climbable hills, greener grass, and a brighter island-style sky palette.</li>
<li>Human-like enemy silhouettes still strafe, dodge, retreat, and chase across the new terrain.</li>
<li>Clearing all bots awards a bonus score and marks the round as victory.</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</section>
<section id="history" class="section-block">
<div class="container py-5">
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-end gap-3 mb-4">
<div>
<p class="eyebrow mb-2">Step 4</p>
<h2 class="h3 mb-2">Recent match reports</h2>
<p class="text-secondary mb-0">Stored server-side with PHP and MariaDB so each round becomes a reviewable score card.</p>
</div>
<button class="btn btn-outline-light btn-sm" id="refreshMatchesButton" type="button">Refresh list</button>
</div>
<div class="surface-panel p-0 overflow-hidden">
<div id="matchesList" class="matches-list">
<?php if (!$recentMatches): ?>
<div class="empty-state p-5 text-center">
<h3 class="h5 mb-2">No matches saved yet</h3>
<p class="text-secondary mb-0">Finish a round and use “Save result” to create the first combat report.</p>
</div>
<?php else: ?>
<?php foreach ($recentMatches as $match): ?>
<a class="match-row" href="/match.php?id=<?= (int)$match['id'] ?>">
<div>
<strong>#<?= (int)$match['id'] ?> • <?= htmlspecialchars((string)$match['player_name']) ?></strong>
<span><?= htmlspecialchars((string)$match['weapon_name']) ?> • <?= (int)$match['kills'] ?> kills • <?= htmlspecialchars(date('M j, H:i', strtotime((string)$match['created_at']))) ?> UTC</span>
</div>
<div class="match-score-wrap">
<span class="outcome-tag <?= htmlspecialchars((string)$match['outcome']) ?>"><?= htmlspecialchars(strtoupper((string)$match['outcome'])) ?></span>
<strong><?= number_format((int)$match['score']) ?></strong>
</div>
</a>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</div>
</section>
</main>
<div class="toast-container position-fixed bottom-0 end-0 p-3">
<div id="appToast" class="toast align-items-center text-bg-dark border border-secondary-subtle" role="status" aria-live="polite" aria-atomic="true">
<div class="d-flex">
<div class="toast-body" id="toastMessage">Ready.</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
</div>
<script>
window.__FPS_BOOTSTRAP__ = {
apiUrl: '/api/fps_matches.php',
initialMatches: <?= json_encode($recentMatches, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>
};
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<script src="/assets/js/main.js?v=<?= urlencode($buildStamp) ?>" defer></script>
</body>
</html>