From c1bb6ee77cfd0ee6de558ca28ec48b5c1ac3ef7b Mon Sep 17 00:00:00 2001 From: darcy Date: Thu, 18 Jun 2026 23:54:41 +0800 Subject: [PATCH] Build static tank battle game --- .gitignore | 1 + README.md | 41 ++++ index.html | 68 +++++++ src/game.js | 518 +++++++++++++++++++++++++++++++++++++++++++++++++ src/styles.css | 360 ++++++++++++++++++++++++++++++++++ 5 files changed, 988 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 index.html create mode 100644 src/game.js create mode 100644 src/styles.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..64bac17 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +tank-battle-*.png diff --git a/README.md b/README.md new file mode 100644 index 0000000..3cbd1ef --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# Tank Battle + +Static browser tank battle game for testing a multi-agent workflow. + +## Run + +Open `index.html` directly or visit the mapped URL: + +```text +https://splan.laidaixi.com/docs/ +``` + +No build step or server dependency is required. + +## Controls + +- Move: `WASD` or arrow keys +- Fire: `Space` +- Pause: `P` +- Start / restart: `Enter` + +## Multi-agent workflow + +- `codex`: project scaffold, integration, Git/Gitea setup, final verification +- `zcode(glm-5.2)`: gameplay logic, collision, enemy AI, win/loss state +- `qoder(qwen3.7-max)`: UI, HUD, responsive layout, visual polish + +Suggested branches: + +- `feature/scaffold-codex` +- `feature/gameplay-zcode` +- `feature/ui-qoder` + +## Acceptance checklist + +- Player moves and shoots. +- Tanks cannot pass through walls or leave the field. +- Enemy tanks move, aim, and fire. +- Bullets collide with walls and tanks. +- Score, lives, enemies, victory, defeat, pause, and restart states update correctly. +- The game works through `https://splan.laidaixi.com/docs/`. diff --git a/index.html b/index.html new file mode 100644 index 0000000..1d4aafd --- /dev/null +++ b/index.html @@ -0,0 +1,68 @@ + + + + + + 坦克大战 + + + +
+
+
+
+ +
+

Web Arcade

+

坦克大战

+
+
+
+
+ Score + 0 +
+
+ Lives + 3 +
+
+ Enemies + 0 +
+
待命
+
+
+ +
+ +
+
+

Ready

+

Defend the field

+

Press Enter or tap Start to roll out.

+
+ + +
+
+
+
+ +
+
+ + + +
+
WASDArrowMove
+
SpaceFire
+
PPause
+
EnterStart / Restart
+
+
+
+ + + + diff --git a/src/game.js b/src/game.js new file mode 100644 index 0000000..e938104 --- /dev/null +++ b/src/game.js @@ -0,0 +1,518 @@ +(() => { + "use strict"; + + function initGame() { + let canvas = document.querySelector("#game") || document.querySelector("canvas"); + if (!canvas) { + canvas = document.createElement("canvas"); + document.body.appendChild(canvas); + } + canvas.id = canvas.id || "game"; + canvas.width = 800; + canvas.height = 600; + canvas.tabIndex = 0; + canvas.style.maxWidth = "100%"; + canvas.style.display = "block"; + canvas.style.margin = canvas.style.margin || "0 auto"; + + const ctx = canvas.getContext("2d"); + + function ensureText(id, fallbackParent = document.body) { + let element = document.querySelector(`#${id}`); + if (!element) { + element = document.createElement("span"); + element.id = id; + fallbackParent.appendChild(element); + } + return element; + } + + let hud = document.querySelector("#hud"); + if (!hud) { + hud = document.createElement("div"); + hud.id = "hud"; + hud.style.cssText = + "width:800px;max-width:100%;margin:8px auto;color:#e8eadf;font:16px/1.4 system-ui,sans-serif;display:flex;gap:18px;flex-wrap:wrap;"; + hud.innerHTML = + 'Score: 0 Lives: 3 Enemies: 0'; + canvas.parentNode.insertBefore(hud, canvas.nextSibling); + } + + let overlay = document.querySelector("#overlay"); + if (!overlay) { + overlay = document.createElement("div"); + overlay.id = "overlay"; + overlay.style.cssText = + "width:800px;max-width:100%;margin:8px auto;color:#e8eadf;font:18px/1.4 system-ui,sans-serif;text-align:center;"; + overlay.innerHTML = + '

'; + hud.parentNode.insertBefore(overlay, hud.nextSibling); + } + + const scoreEl = ensureText("score", hud); + const livesEl = ensureText("lives", hud); + const enemiesEl = ensureText("enemies", hud); + const statusLabel = ensureText("statusLabel", overlay); + const statusTitle = ensureText("statusTitle", overlay); + const statusText = ensureText("statusText", overlay); + const primaryAction = document.querySelector("#primaryAction") || (() => { + const button = document.createElement("button"); + button.id = "primaryAction"; + button.type = "button"; + button.textContent = "Start"; + overlay.appendChild(button); + return button; + })(); + const statusChip = document.querySelector("#statusChip"); + const startAction = document.querySelector("#startAction"); + const pauseAction = document.querySelector("#pauseAction"); + const stopAction = document.querySelector("#stopAction"); + const endAction = document.querySelector("#endAction"); + + const WIDTH = canvas.width; + const HEIGHT = canvas.height; + const TILE = 40; + const PLAYER_SPEED = 150; + const ENEMY_SPEED = 82; + const BULLET_SPEED = 330; + const TANK_SIZE = 32; + const BULLET_SIZE = 6; + const FIRE_COOLDOWN = 0.34; + const ENEMY_FIRE_COOLDOWN = 1.35; + const DIRS = { + up: { x: 0, y: -1 }, + down: { x: 0, y: 1 }, + left: { x: -1, y: 0 }, + right: { x: 1, y: 0 }, + }; + + const keys = new Set(); + let state = "menu"; + let score = 0; + let lives = 3; + let player; + let enemies = []; + let bullets = []; + let particles = []; + let lastTime = 0; + + const walls = [ + rect(120, 80, TILE, TILE * 3), + rect(240, 120, TILE * 4, TILE), + rect(520, 80, TILE, TILE * 3), + rect(640, 160, TILE * 2, TILE), + rect(80, 320, TILE * 3, TILE), + rect(280, 280, TILE, TILE * 3), + rect(400, 360, TILE * 4, TILE), + rect(640, 360, TILE, TILE * 3), + rect(160, 480, TILE * 2, TILE), + rect(480, 500, TILE * 3, TILE), + ]; + + function rect(x, y, w, h) { + return { x, y, w, h }; + } + + function tank(x, y, color, dir = "up", team = "enemy") { + return { + x, + y, + w: TANK_SIZE, + h: TANK_SIZE, + color, + dir, + team, + fireTimer: 0, + aiTimer: 0.4 + Math.random() * 1.2, + alive: true, + }; + } + + function resetGame() { + score = 0; + lives = 3; + bullets = []; + particles = []; + player = tank(384, 536, "#77b255", "up", "player"); + enemies = [ + tank(84, 64, "#db5a42", "down"), + tank(384, 64, "#d9923b", "down"), + tank(684, 64, "#db5a42", "down"), + tank(84, 220, "#d9923b", "right"), + tank(684, 260, "#db5a42", "left"), + ]; + setState("playing"); + updateHud(); + } + + function setState(next) { + state = next; + const visible = state !== "playing"; + overlay.classList.toggle("is-hidden", !visible); + overlay.style.display = visible ? "" : "none"; + + if (state === "menu") { + if (statusChip) statusChip.textContent = "待命"; + statusLabel.textContent = "Ready"; + statusTitle.textContent = "Defend the field"; + statusText.textContent = "Press Enter or tap Start to roll out."; + primaryAction.textContent = "Start"; + if (endAction) endAction.hidden = true; + } else if (state === "paused") { + if (statusChip) statusChip.textContent = "暂停"; + statusLabel.textContent = "Paused"; + statusTitle.textContent = "Hold position"; + statusText.textContent = "Press P or tap Resume to continue."; + primaryAction.textContent = "Resume"; + if (endAction) endAction.hidden = false; + } else if (state === "ended") { + if (statusChip) statusChip.textContent = "结束"; + statusLabel.textContent = "Ended"; + statusTitle.textContent = "Mission ended"; + statusText.textContent = `Final score: ${score}. Press Enter to restart.`; + primaryAction.textContent = "Restart"; + if (endAction) endAction.hidden = true; + } else if (state === "gameOver") { + if (statusChip) statusChip.textContent = "战损"; + statusLabel.textContent = "Defeat"; + statusTitle.textContent = "Armor breached"; + statusText.textContent = `Final score: ${score}. Press Enter to restart.`; + primaryAction.textContent = "Restart"; + if (endAction) endAction.hidden = true; + } else if (state === "win") { + if (statusChip) statusChip.textContent = "胜利"; + statusLabel.textContent = "Victory"; + statusTitle.textContent = "Field secured"; + statusText.textContent = `All enemies destroyed. Score: ${score}.`; + primaryAction.textContent = "Play again"; + if (endAction) endAction.hidden = true; + } else { + if (statusChip) statusChip.textContent = "作战中"; + if (endAction) endAction.hidden = false; + } + } + + function updateHud() { + scoreEl.textContent = score; + livesEl.textContent = lives; + enemiesEl.textContent = enemies.filter((enemy) => enemy.alive).length; + } + + function startOrResume() { + if (state === "playing") return; + if (state === "paused") { + setState("playing"); + return; + } + resetGame(); + } + + function togglePause() { + if (state === "playing") setState("paused"); + else if (state === "paused") setState("playing"); + } + + function endGame() { + if (state === "playing" || state === "paused") setState("ended"); + } + + function fire(source) { + if (source.fireTimer > 0 || !source.alive) return; + const dir = DIRS[source.dir]; + bullets.push({ + x: source.x + source.w / 2 - BULLET_SIZE / 2 + dir.x * 20, + y: source.y + source.h / 2 - BULLET_SIZE / 2 + dir.y * 20, + w: BULLET_SIZE, + h: BULLET_SIZE, + vx: dir.x * BULLET_SPEED, + vy: dir.y * BULLET_SPEED, + owner: source.team, + }); + source.fireTimer = source.team === "player" ? FIRE_COOLDOWN : ENEMY_FIRE_COOLDOWN; + } + + function update(dt) { + if (state !== "playing") return; + player.fireTimer = Math.max(0, player.fireTimer - dt); + movePlayer(dt); + updateEnemies(dt); + updateBullets(dt); + updateParticles(dt); + if (enemies.every((enemy) => !enemy.alive)) setState("win"); + updateHud(); + } + + function movePlayer(dt) { + let move = null; + if (keys.has("arrowup") || keys.has("w")) move = "up"; + else if (keys.has("arrowdown") || keys.has("s")) move = "down"; + else if (keys.has("arrowleft") || keys.has("a")) move = "left"; + else if (keys.has("arrowright") || keys.has("d")) move = "right"; + if (!move) return; + player.dir = move; + moveTank(player, DIRS[move].x * PLAYER_SPEED * dt, DIRS[move].y * PLAYER_SPEED * dt); + } + + function updateEnemies(dt) { + const livingEnemies = enemies.filter((enemy) => enemy.alive); + for (const enemy of livingEnemies) { + enemy.fireTimer = Math.max(0, enemy.fireTimer - dt); + enemy.aiTimer -= dt; + + if (enemy.aiTimer <= 0) { + enemy.dir = chooseEnemyDirection(enemy); + enemy.aiTimer = 0.45 + Math.random() * 1.25; + } + + const dir = DIRS[enemy.dir]; + const moved = moveTank(enemy, dir.x * ENEMY_SPEED * dt, dir.y * ENEMY_SPEED * dt, livingEnemies); + if (!moved) { + enemy.dir = randomDirection(); + enemy.aiTimer = 0.25; + } + + if (canSeePlayer(enemy) || Math.random() < dt * 0.18) { + facePlayer(enemy); + fire(enemy); + } + } + } + + function chooseEnemyDirection(enemy) { + if (Math.random() < 0.65) { + const dx = player.x - enemy.x; + const dy = player.y - enemy.y; + if (Math.abs(dx) > Math.abs(dy)) return dx > 0 ? "right" : "left"; + return dy > 0 ? "down" : "up"; + } + return randomDirection(); + } + + function randomDirection() { + const names = Object.keys(DIRS); + return names[Math.floor(Math.random() * names.length)]; + } + + function facePlayer(enemy) { + const dx = player.x - enemy.x; + const dy = player.y - enemy.y; + enemy.dir = Math.abs(dx) > Math.abs(dy) ? (dx > 0 ? "right" : "left") : dy > 0 ? "down" : "up"; + } + + function canSeePlayer(enemy) { + const sameColumn = Math.abs(enemy.x - player.x) < TILE * 0.7; + const sameRow = Math.abs(enemy.y - player.y) < TILE * 0.7; + if (!sameColumn && !sameRow) return false; + const beam = sameColumn + ? rect(enemy.x + enemy.w / 2 - 3, Math.min(enemy.y, player.y), 6, Math.abs(enemy.y - player.y)) + : rect(Math.min(enemy.x, player.x), enemy.y + enemy.h / 2 - 3, Math.abs(enemy.x - player.x), 6); + return !walls.some((wall) => intersects(beam, wall)); + } + + function moveTank(entity, dx, dy, peerEnemies = enemies) { + const originalX = entity.x; + const originalY = entity.y; + entity.x += dx; + if (isBlocked(entity, peerEnemies)) entity.x = originalX; + entity.y += dy; + if (isBlocked(entity, peerEnemies)) entity.y = originalY; + return entity.x !== originalX || entity.y !== originalY; + } + + function isBlocked(entity, peerEnemies) { + if (entity.x < 0 || entity.y < 0 || entity.x + entity.w > WIDTH || entity.y + entity.h > HEIGHT) return true; + if (walls.some((wall) => intersects(entity, wall))) return true; + if (entity.team === "player") { + return enemies.some((enemy) => enemy.alive && intersects(entity, enemy)); + } + if (intersects(entity, player)) return true; + return peerEnemies.some((enemy) => enemy !== entity && enemy.alive && intersects(entity, enemy)); + } + + function updateBullets(dt) { + for (const bullet of bullets) { + bullet.x += bullet.vx * dt; + bullet.y += bullet.vy * dt; + } + + const remaining = []; + for (const bullet of bullets) { + const out = + bullet.x < -20 || bullet.y < -20 || bullet.x > WIDTH + 20 || bullet.y > HEIGHT + 20; + if (out) continue; + if (walls.some((wall) => intersects(bullet, wall))) { + spark(bullet.x, bullet.y, "#f0c451"); + continue; + } + if (bullet.owner === "player") { + const hit = enemies.find((enemy) => enemy.alive && intersects(bullet, enemy)); + if (hit) { + hit.alive = false; + score += 100; + spark(hit.x + hit.w / 2, hit.y + hit.h / 2, hit.color); + continue; + } + } else if (intersects(bullet, player)) { + lives -= 1; + spark(player.x + player.w / 2, player.y + player.h / 2, "#77b255"); + if (lives <= 0) { + player.alive = false; + setState("gameOver"); + } else { + player.x = 384; + player.y = 536; + player.dir = "up"; + } + continue; + } + remaining.push(bullet); + } + bullets = remaining; + } + + function updateParticles(dt) { + particles = particles + .map((particle) => ({ + ...particle, + life: particle.life - dt, + x: particle.x + particle.vx * dt, + y: particle.y + particle.vy * dt, + })) + .filter((particle) => particle.life > 0); + } + + function spark(x, y, color) { + for (let i = 0; i < 10; i += 1) { + const angle = Math.random() * Math.PI * 2; + const speed = 35 + Math.random() * 95; + particles.push({ + x, + y, + vx: Math.cos(angle) * speed, + vy: Math.sin(angle) * speed, + life: 0.22 + Math.random() * 0.24, + color, + }); + } + } + + function intersects(a, b) { + return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y; + } + + function draw() { + ctx.clearRect(0, 0, WIDTH, HEIGHT); + drawGrid(); + walls.forEach(drawWall); + enemies.filter((enemy) => enemy.alive).forEach(drawTank); + if (player?.alive !== false) drawTank(player); + bullets.forEach(drawBullet); + particles.forEach(drawParticle); + } + + function drawGrid() { + ctx.fillStyle = "#1a1f16"; + ctx.fillRect(0, 0, WIDTH, HEIGHT); + ctx.strokeStyle = "rgba(120, 129, 95, 0.15)"; + ctx.lineWidth = 1; + for (let x = 0; x <= WIDTH; x += TILE) { + ctx.beginPath(); + ctx.moveTo(x, 0); + ctx.lineTo(x, HEIGHT); + ctx.stroke(); + } + for (let y = 0; y <= HEIGHT; y += TILE) { + ctx.beginPath(); + ctx.moveTo(0, y); + ctx.lineTo(WIDTH, y); + ctx.stroke(); + } + } + + function drawWall(wall) { + ctx.fillStyle = "#5d5540"; + ctx.fillRect(wall.x, wall.y, wall.w, wall.h); + ctx.fillStyle = "rgba(255,255,255,0.08)"; + for (let y = wall.y + 4; y < wall.y + wall.h; y += 12) { + ctx.fillRect(wall.x + 4, y, wall.w - 8, 3); + } + } + + function drawTank(entity) { + const cx = entity.x + entity.w / 2; + const cy = entity.y + entity.h / 2; + ctx.save(); + ctx.translate(cx, cy); + ctx.rotate(directionAngle(entity.dir)); + ctx.fillStyle = "#10140f"; + ctx.fillRect(-17, -17, 34, 34); + ctx.fillStyle = entity.color; + ctx.fillRect(-13, -13, 26, 26); + ctx.fillStyle = "rgba(255,255,255,0.18)"; + ctx.fillRect(-7, -8, 14, 16); + ctx.fillStyle = "#15160f"; + ctx.fillRect(-4, -25, 8, 19); + ctx.restore(); + } + + function directionAngle(dir) { + if (dir === "right") return Math.PI / 2; + if (dir === "down") return Math.PI; + if (dir === "left") return -Math.PI / 2; + return 0; + } + + function drawBullet(bullet) { + ctx.fillStyle = bullet.owner === "player" ? "#f0c451" : "#f07a5a"; + ctx.fillRect(bullet.x, bullet.y, bullet.w, bullet.h); + } + + function drawParticle(particle) { + ctx.globalAlpha = Math.max(0, particle.life * 3); + ctx.fillStyle = particle.color; + ctx.fillRect(particle.x, particle.y, 4, 4); + ctx.globalAlpha = 1; + } + + function loop(time) { + const dt = Math.min(0.033, (time - lastTime) / 1000 || 0); + lastTime = time; + update(dt); + draw(); + requestAnimationFrame(loop); + } + + window.addEventListener("keydown", (event) => { + const key = event.key.toLowerCase(); + if (["arrowup", "arrowdown", "arrowleft", "arrowright", " ", "spacebar"].includes(key)) { + event.preventDefault(); + } + if (key === "enter") startOrResume(); + else if (key === "p") togglePause(); + else if (key === "escape") endGame(); + else if ((key === " " || key === "spacebar") && state === "playing") fire(player); + keys.add(key); + }); + + window.addEventListener("keyup", (event) => { + keys.delete(event.key.toLowerCase()); + }); + + primaryAction.addEventListener("click", startOrResume); + if (startAction) startAction.addEventListener("click", startOrResume); + if (pauseAction) pauseAction.addEventListener("click", togglePause); + if (stopAction) stopAction.addEventListener("click", endGame); + if (endAction) endAction.addEventListener("click", endGame); + + player = tank(384, 536, "#77b255", "up", "player"); + updateHud(); + setState("menu"); + requestAnimationFrame(loop); + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initGame, { once: true }); + } else { + initGame(); + } +})(); diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 0000000..2d1b920 --- /dev/null +++ b/src/styles.css @@ -0,0 +1,360 @@ +:root { + color-scheme: dark; + --bg: #11150f; + --panel: #1d2319; + --panel-strong: #262d20; + --line: #48533b; + --line-bright: #879365; + --text: #f1f3dc; + --muted: #b4ba9b; + --olive: #8da04f; + --amber: #f0b646; + --danger: #d45d45; + --shadow: rgba(0, 0, 0, 0.45); + --radius: 8px; +} + +* { + box-sizing: border-box; +} + +html { + min-height: 100%; + background: + radial-gradient(circle at 20% 10%, rgba(141, 160, 79, 0.18), transparent 28rem), + linear-gradient(135deg, #161b13, #0c0f0b 70%); +} + +body { + min-height: 100vh; + margin: 0; + color: var(--text); + font-family: + "Microsoft YaHei", "PingFang SC", "Segoe UI", Arial, sans-serif; + letter-spacing: 0; +} + +button, +canvas, +kbd { + font: inherit; +} + +button { + min-width: 0; + min-height: 44px; + border: 1px solid #c6d87b; + border-radius: 6px; + padding: 0.72rem 1rem; + background: linear-gradient(180deg, #a0b85a, #617434); + color: #10150d; + cursor: pointer; + font-weight: 900; + line-height: 1.1; + white-space: nowrap; + box-shadow: 0 8px 18px var(--shadow); +} + +.secondary-action { + border-color: var(--line-bright); + background: #27301f; + color: var(--text); +} + +.danger-action { + border-color: #f09d7e; + background: linear-gradient(180deg, #dc6d50, #883925); + color: var(--text); +} + +button:focus-visible { + outline: 3px solid rgba(240, 182, 70, 0.65); + outline-offset: 2px; +} + +.shell { + width: min(1180px, calc(100vw - 32px)); + min-height: 100vh; + margin: 0 auto; + padding: 18px 0; + display: grid; + place-items: center; +} + +.game-panel { + width: 100%; + display: grid; + gap: 14px; +} + +.topbar { + display: grid; + grid-template-columns: minmax(220px, 1fr) minmax(420px, 620px); + gap: 12px; + align-items: stretch; +} + +.brand, +.hud-item, +.controls div { + border: 1px solid var(--line); + border-radius: var(--radius); + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent), + var(--panel); + box-shadow: 0 16px 36px var(--shadow); +} + +.brand { + min-width: 0; + padding: 12px; + display: flex; + gap: 12px; + align-items: center; +} + +.rank-mark { + width: 38px; + aspect-ratio: 1; + flex: 0 0 auto; + background: + linear-gradient(90deg, transparent 42%, #11150f 42% 58%, transparent 58%), + linear-gradient(#a0b85a 0 0) center / 100% 32% no-repeat, + #53672f; + border: 2px solid #c6d87b; + clip-path: polygon(50% 0, 100% 28%, 82% 100%, 18% 100%, 0 28%); +} + +.eyebrow, +.status-label { + margin: 0; + color: var(--amber); + font-size: 0.78rem; + font-weight: 900; + letter-spacing: 0; + text-transform: uppercase; +} + +h1, +h2, +p { + margin: 0; +} + +h1 { + font-size: clamp(1.6rem, 4vw, 3rem); + line-height: 1; +} + +.hud { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 8px; +} + +.hud-item { + min-height: 70px; + padding: 10px 12px; +} + +.hud-item span { + display: block; + color: var(--muted); + font-size: 0.76rem; + font-weight: 800; + text-transform: uppercase; +} + +.hud-item strong { + display: block; + margin-top: 4px; + color: var(--amber); + font-family: Consolas, "Courier New", monospace; + font-size: clamp(1.3rem, 2.5vw, 2rem); + font-weight: 900; + line-height: 1; + overflow-wrap: anywhere; +} + +.status-chip { + min-height: 70px; + border: 1px solid #d6c06c; + border-radius: var(--radius); + padding: 10px 12px; + display: grid; + place-items: center; + background: linear-gradient(180deg, #f0b646, #b9842a); + color: #10150d; + font-weight: 900; + text-align: center; + box-shadow: 0 16px 36px var(--shadow); +} + +.stage-wrap { + position: relative; + width: 100%; + aspect-ratio: 4 / 3; + max-height: calc(100vh - 185px); + min-height: 280px; + overflow: hidden; + border: 2px solid var(--line-bright); + border-radius: var(--radius); + background: + linear-gradient(90deg, rgba(255, 255, 255, 0.04) 1px, transparent 1px) 0 0 / 28px 28px, + linear-gradient(rgba(255, 255, 255, 0.035) 1px, transparent 1px) 0 0 / 28px 28px, + #0b0e0a; + box-shadow: inset 0 0 0 4px #171d13, 0 18px 42px var(--shadow); +} + +canvas { + display: block; + width: 100%; + height: 100%; + image-rendering: pixelated; +} + +.overlay { + position: absolute; + inset: 0; + z-index: 4; + display: grid; + place-items: center; + padding: 18px; + background: rgba(7, 9, 7, 0.72); + backdrop-filter: blur(2px); +} + +.overlay.is-hidden { + display: none; +} + +.overlay-content { + width: min(420px, 100%); + border: 1px solid var(--line-bright); + border-radius: var(--radius); + padding: clamp(18px, 4vw, 30px); + text-align: center; + background: + linear-gradient(180deg, rgba(240, 182, 70, 0.1), transparent 42%), + #181e14; + box-shadow: 0 22px 50px rgba(0, 0, 0, 0.5); +} + +.overlay-content h2 { + margin-top: 6px; + font-size: clamp(1.75rem, 5vw, 3rem); + line-height: 1; +} + +#statusText { + margin-top: 12px; + color: var(--muted); + line-height: 1.55; +} + +.overlay-actions { + margin-top: 18px; + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 10px; +} + +.overlay-actions button { + width: min(180px, 100%); +} + +.controls { + display: flex; + flex-wrap: wrap; + gap: 8px; + color: var(--muted); +} + +.controls div { + min-height: 38px; + padding: 7px 10px; + display: flex; + align-items: center; + gap: 7px; +} + +.controls .action-group { + width: 100%; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; +} + +.controls .action-group button { + width: 100%; + padding-inline: 0.6rem; +} + +kbd { + min-width: 30px; + min-height: 28px; + display: inline-grid; + place-items: center; + border: 1px solid #727b5a; + border-bottom-width: 3px; + border-radius: 4px; + padding: 0 7px; + background: #10150d; + color: var(--text); + font-family: Consolas, "Courier New", monospace; + font-size: 0.78rem; + font-weight: 900; +} + +@media (max-width: 880px) { + .shell { + width: min(100vw - 20px, 760px); + padding: 10px 0; + place-items: start center; + } + + .topbar { + grid-template-columns: 1fr; + } + + .stage-wrap { + max-height: none; + } +} + +@media (max-width: 560px) { + .hud { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .brand { + align-items: flex-start; + } + + .rank-mark { + width: 32px; + } + + .hud-item { + min-height: 62px; + padding: 8px; + } + + .status-chip { + min-height: 52px; + } + + .controls div { + width: 100%; + } + + .controls .action-group { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + button { + max-width: 100%; + padding-inline: 0.5rem; + } +}