Compare commits

..

12 Commits

@ -23,8 +23,8 @@
<strong id="score">0</strong> <strong id="score">0</strong>
</div> </div>
<div class="hud-item"> <div class="hud-item">
<span>Lives</span> <span>Health</span>
<strong id="lives">3</strong> <strong id="lives">100%</strong>
</div> </div>
<div class="hud-item"> <div class="hud-item">
<span>Enemies</span> <span>Enemies</span>
@ -34,6 +34,12 @@
</div> </div>
</header> </header>
<div class="action-group" aria-label="游戏操作">
<button id="startAction" type="button">开始</button>
<button id="pauseAction" class="secondary-action" type="button">暂停</button>
<button id="stopAction" class="danger-action" type="button">结束</button>
</div>
<div class="stage-wrap"> <div class="stage-wrap">
<canvas id="game" width="800" height="600" aria-label="Tank Battle canvas"></canvas> <canvas id="game" width="800" height="600" aria-label="Tank Battle canvas"></canvas>
<div id="overlay" class="overlay"> <div id="overlay" class="overlay">
@ -50,10 +56,11 @@
</div> </div>
<footer class="controls"> <footer class="controls">
<div class="action-group" aria-label="游戏操作"> <div class="touch-controls" aria-label="触摸操作">
<button id="startAction" type="button">开始</button> <div id="moveStick" class="move-stick" aria-label="方向摇杆" role="application">
<button id="pauseAction" class="secondary-action" type="button">暂停</button> <div id="moveKnob" class="move-knob" aria-hidden="true"></div>
<button id="stopAction" class="danger-action" type="button">结束</button> </div>
<button id="fireAction" class="fire-action" type="button" aria-label="开火">开火</button>
</div> </div>
<div class="key-hints" aria-label="键盘快捷键"> <div class="key-hints" aria-label="键盘快捷键">
<div class="key-hint"><kbd>WASD</kbd><kbd>↑↓←→</kbd><span>Move</span></div> <div class="key-hint"><kbd>WASD</kbd><kbd>↑↓←→</kbd><span>Move</span></div>

@ -34,7 +34,7 @@
hud.style.cssText = 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;"; "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 = hud.innerHTML =
'Score: <span id="score">0</span> Lives: <span id="lives">3</span> Enemies: <span id="enemies">0</span>'; 'Score: <span id="score">0</span> Health: <span id="lives">100%</span> Enemies: <span id="enemies">0</span>';
canvas.parentNode.insertBefore(hud, canvas.nextSibling); canvas.parentNode.insertBefore(hud, canvas.nextSibling);
} }
@ -68,6 +68,9 @@
const pauseAction = document.querySelector("#pauseAction"); const pauseAction = document.querySelector("#pauseAction");
const stopAction = document.querySelector("#stopAction"); const stopAction = document.querySelector("#stopAction");
const endAction = document.querySelector("#endAction"); const endAction = document.querySelector("#endAction");
const fireAction = document.querySelector("#fireAction");
const moveStick = document.querySelector("#moveStick");
const moveKnob = document.querySelector("#moveKnob");
const WIDTH = canvas.width; const WIDTH = canvas.width;
const HEIGHT = canvas.height; const HEIGHT = canvas.height;
@ -80,6 +83,8 @@
const FIRE_COOLDOWN = 0.34; const FIRE_COOLDOWN = 0.34;
const ENEMY_FIRE_COOLDOWN = 1.35; const ENEMY_FIRE_COOLDOWN = 1.35;
const BRICK_SIZE = 20; const BRICK_SIZE = 20;
const PLAYER_MAX_HEALTH = 100;
const PLAYER_HIT_DAMAGE = 34;
const DIRS = { const DIRS = {
up: { x: 0, y: -1 }, up: { x: 0, y: -1 },
down: { x: 0, y: 1 }, down: { x: 0, y: 1 },
@ -90,27 +95,92 @@
const keys = new Set(); const keys = new Set();
let state = "menu"; let state = "menu";
let score = 0; let score = 0;
let lives = 3; let health = PLAYER_MAX_HEALTH;
let player; let player;
let enemies = []; let enemies = [];
let bullets = []; let bullets = [];
let particles = []; let particles = [];
let lastTime = 0; let lastTime = 0;
const walls = [ // Static brick layout (kept stable across games). Steel walls are randomized
...brickWall(120, 80, TILE, TILE * 3), // on every reset, so they are added separately in resetGame via regenerateWalls.
...brickWall(240, 120, TILE * 4, TILE), const BRICK_LAYOUT = [
...steelWall(520, 80, TILE, TILE * 3), [120, 80, TILE, TILE * 3],
...brickWall(640, 160, TILE * 2, TILE), [240, 120, TILE * 4, TILE],
...brickWall(80, 320, TILE * 3, TILE), [640, 160, TILE * 2, TILE],
...brickWall(280, 280, TILE, TILE * 3), [80, 320, TILE * 3, TILE],
...brickWall(400, 360, TILE * 4, TILE), [280, 280, TILE, TILE * 3],
...steelWall(640, 360, TILE, TILE * 3), [400, 360, TILE * 4, TILE],
...steelWall(560, 240, TILE * 2, TILE), [160, 480, TILE * 2, TILE],
...brickWall(160, 480, TILE * 2, TILE), [480, 500, TILE * 3, TILE],
...brickWall(480, 500, TILE * 3, TILE),
]; ];
// Rectangles that steel walls must avoid so spawns are not blocked or boxed in.
// Each entry is padded around a player/enemy starting position.
const SPAWN_KEEPOUTS = [
spawnKeepout(384, 536), // player
spawnKeepout(84, 64),
spawnKeepout(384, 64),
spawnKeepout(684, 64),
spawnKeepout(84, 220),
spawnKeepout(684, 260),
];
let walls = [];
function spawnKeepout(x, y) {
// Generous margin (1.5 tiles) around the spawn keeps lanes open so tanks
// are never immediately boxed in by randomized steel.
const margin = TILE * 1.5;
return {
x: x - margin,
y: y - margin,
w: TANK_SIZE + margin * 2,
h: TANK_SIZE + margin * 2,
};
}
function buildBrickWalls() {
const pieces = [];
for (const [x, y, w, h] of BRICK_LAYOUT) {
pieces.push(...brickWall(x, y, w, h));
}
return pieces;
}
function randomSteelWalls(occupied) {
// Generate a handful of vertical/horizontal steel segments at random tile
// positions, skipping any that overlap a spawn keep-out zone.
const pieces = [];
const segmentCount = 5 + Math.floor(Math.random() * 3); // 5-7 steel wall segments
const cols = Math.floor(WIDTH / TILE);
const rows = Math.floor(HEIGHT / TILE);
let placedSegments = 0;
let attempts = 0;
while (placedSegments < segmentCount && attempts < segmentCount * 30) {
attempts += 1;
const vertical = Math.random() < 0.5;
const length = (2 + Math.floor(Math.random() * 3)) * TILE; // 2-4 tiles long
const x = Math.floor(Math.random() * cols) * TILE;
const y = Math.floor(Math.random() * rows) * TILE;
const w = vertical ? TILE : length;
const h = vertical ? length : TILE;
const candidate = { x, y, w, h };
if (x + w > WIDTH || y + h > HEIGHT) continue;
if (SPAWN_KEEPOUTS.some((zone) => intersects(candidate, zone))) continue;
if (occupied.some((area) => intersects(candidate, area))) continue;
pieces.push(...steelWall(x, y, w, h));
occupied.push(candidate);
placedSegments += 1;
}
return pieces;
}
function regenerateWalls() {
const bricks = buildBrickWalls();
walls = [...bricks, ...randomSteelWalls([...bricks])];
}
function rect(x, y, w, h, type = "brick") { function rect(x, y, w, h, type = "brick") {
return { x, y, w, h, type, destructible: type === "brick" }; return { x, y, w, h, type, destructible: type === "brick" };
} }
@ -144,15 +214,17 @@
team, team,
fireTimer: 0, fireTimer: 0,
aiTimer: 0.4 + Math.random() * 1.2, aiTimer: 0.4 + Math.random() * 1.2,
avoidTimer: 0,
alive: true, alive: true,
}; };
} }
function resetGame() { function resetGame() {
score = 0; score = 0;
lives = 3; health = PLAYER_MAX_HEALTH;
bullets = []; bullets = [];
particles = []; particles = [];
regenerateWalls();
player = tank(384, 536, "#77b255", "up", "player"); player = tank(384, 536, "#77b255", "up", "player");
enemies = [ enemies = [
tank(84, 64, "#db5a42", "down"), tank(84, 64, "#db5a42", "down"),
@ -214,7 +286,7 @@
function updateHud() { function updateHud() {
scoreEl.textContent = score; scoreEl.textContent = score;
livesEl.textContent = lives; livesEl.textContent = `${health}%`;
enemiesEl.textContent = enemies.filter((enemy) => enemy.alive).length; enemiesEl.textContent = enemies.filter((enemy) => enemy.alive).length;
} }
@ -277,9 +349,10 @@
const livingEnemies = enemies.filter((enemy) => enemy.alive); const livingEnemies = enemies.filter((enemy) => enemy.alive);
for (const enemy of livingEnemies) { for (const enemy of livingEnemies) {
enemy.fireTimer = Math.max(0, enemy.fireTimer - dt); enemy.fireTimer = Math.max(0, enemy.fireTimer - dt);
enemy.avoidTimer = Math.max(0, enemy.avoidTimer - dt);
enemy.aiTimer -= dt; enemy.aiTimer -= dt;
if (enemy.aiTimer <= 0) { if (enemy.aiTimer <= 0 && enemy.avoidTimer <= 0) {
enemy.dir = chooseEnemyDirection(enemy); enemy.dir = chooseEnemyDirection(enemy);
enemy.aiTimer = 0.45 + Math.random() * 1.25; enemy.aiTimer = 0.45 + Math.random() * 1.25;
} }
@ -287,12 +360,16 @@
const dir = DIRS[enemy.dir]; const dir = DIRS[enemy.dir];
const moved = moveTank(enemy, dir.x * ENEMY_SPEED * dt, dir.y * ENEMY_SPEED * dt, livingEnemies); const moved = moveTank(enemy, dir.x * ENEMY_SPEED * dt, dir.y * ENEMY_SPEED * dt, livingEnemies);
if (!moved) { if (!moved) {
enemy.dir = randomDirection(); const escapeDir = chooseOpenEnemyDirection(enemy, livingEnemies);
enemy.aiTimer = 0.25; enemy.dir = escapeDir || randomDirection();
enemy.avoidTimer = 0.55;
enemy.aiTimer = 0.2;
const next = DIRS[enemy.dir];
moveTank(enemy, next.x * ENEMY_SPEED * dt, next.y * ENEMY_SPEED * dt, livingEnemies);
} }
if (canSeePlayer(enemy) || Math.random() < dt * 0.18) { if (canSeePlayer(enemy) || Math.random() < dt * 0.18) {
facePlayer(enemy); if (enemy.avoidTimer <= 0) facePlayer(enemy);
fire(enemy); fire(enemy);
} }
} }
@ -313,6 +390,35 @@
return names[Math.floor(Math.random() * names.length)]; return names[Math.floor(Math.random() * names.length)];
} }
function chooseOpenEnemyDirection(enemy, livingEnemies) {
const preferred = shuffledDirections().sort((a, b) => distanceFromPlayer(enemy, b) - distanceFromPlayer(enemy, a));
return preferred.find((name) => canMove(enemy, name, livingEnemies));
}
function shuffledDirections() {
const names = Object.keys(DIRS);
for (let i = names.length - 1; i > 0; i -= 1) {
const j = Math.floor(Math.random() * (i + 1));
[names[i], names[j]] = [names[j], names[i]];
}
return names;
}
function distanceFromPlayer(enemy, dirName) {
const dir = DIRS[dirName];
const nextX = enemy.x + dir.x * TILE;
const nextY = enemy.y + dir.y * TILE;
const dx = nextX - player.x;
const dy = nextY - player.y;
return dx * dx + dy * dy;
}
function canMove(entity, dirName, peerEnemies) {
const dir = DIRS[dirName];
const probe = { ...entity, x: entity.x + dir.x * 4, y: entity.y + dir.y * 4 };
return !isBlocked(probe, peerEnemies.filter((peer) => peer !== entity));
}
function facePlayer(enemy) { function facePlayer(enemy) {
const dx = player.x - enemy.x; const dx = player.x - enemy.x;
const dy = player.y - enemy.y; const dy = player.y - enemy.y;
@ -355,8 +461,24 @@
bullet.y += bullet.vy * dt; bullet.y += bullet.vy * dt;
} }
const canceledBullets = new Set();
for (let i = 0; i < bullets.length; i += 1) {
if (canceledBullets.has(i)) continue;
for (let j = i + 1; j < bullets.length; j += 1) {
if (canceledBullets.has(j)) continue;
if (bullets[i].owner === bullets[j].owner) continue;
if (!intersects(expandedBullet(bullets[i]), expandedBullet(bullets[j]))) continue;
canceledBullets.add(i);
canceledBullets.add(j);
spark((bullets[i].x + bullets[j].x) / 2, (bullets[i].y + bullets[j].y) / 2, "#f7df7b");
break;
}
}
const remaining = []; const remaining = [];
for (const bullet of bullets) { for (let index = 0; index < bullets.length; index += 1) {
if (canceledBullets.has(index)) continue;
const bullet = bullets[index];
const out = const out =
bullet.x < -20 || bullet.y < -20 || bullet.x > WIDTH + 20 || bullet.y > HEIGHT + 20; bullet.x < -20 || bullet.y < -20 || bullet.x > WIDTH + 20 || bullet.y > HEIGHT + 20;
if (out) continue; if (out) continue;
@ -381,9 +503,9 @@
continue; continue;
} }
} else if (intersects(bullet, player)) { } else if (intersects(bullet, player)) {
lives -= 1; health = Math.max(0, health - PLAYER_HIT_DAMAGE);
spark(player.x + player.w / 2, player.y + player.h / 2, "#77b255"); spark(player.x + player.w / 2, player.y + player.h / 2, "#77b255");
if (lives <= 0) { if (health <= 0) {
player.alive = false; player.alive = false;
setState("gameOver"); setState("gameOver");
} else { } else {
@ -398,6 +520,16 @@
bullets = remaining; bullets = remaining;
} }
function expandedBullet(bullet) {
const padding = 5;
return {
x: bullet.x - padding,
y: bullet.y - padding,
w: bullet.w + padding * 2,
h: bullet.h + padding * 2,
};
}
function updateParticles(dt) { function updateParticles(dt) {
particles = particles particles = particles
.map((particle) => ({ .map((particle) => ({
@ -487,22 +619,22 @@
let glowColor, glowBlur, outerColor, innerColor, coreColor; let glowColor, glowBlur, outerColor, innerColor, coreColor;
if (isPlayer) { if (isPlayer) {
if (lives >= 3) { if (health > 66) {
glowColor = "rgba(132, 238, 212, 0.75)"; glowColor = "rgba(132, 238, 212, 0.55)";
glowBlur = 12; glowBlur = 7;
outerColor = "#e8fff7"; outerColor = "#e8fff7";
innerColor = "#102822"; innerColor = "#102822";
coreColor = entity.color; coreColor = entity.color;
} else if (lives === 2) { } else if (health > 33) {
glowColor = "rgba(240, 182, 70, 0.45)"; glowColor = "rgba(240, 182, 70, 0.45)";
glowBlur = 8; glowBlur = 6;
outerColor = "#c8c0a0"; outerColor = "#c8c0a0";
innerColor = "#1e2818"; innerColor = "#1e2818";
coreColor = "#9a9850"; coreColor = "#9a9850";
} else { } else {
const pulse = 0.5 + 0.5 * Math.sin(Date.now() / 180); const pulse = 0.5 + 0.5 * Math.sin(Date.now() / 180);
glowColor = `rgba(212, 93, 69, ${0.5 + 0.4 * pulse})`; glowColor = `rgba(212, 93, 69, ${0.42 + 0.32 * pulse})`;
glowBlur = 10 + 6 * pulse; glowBlur = 7 + 4 * pulse;
outerColor = "#3a2020"; outerColor = "#3a2020";
innerColor = "#1a0c0c"; innerColor = "#1a0c0c";
coreColor = "#6b2828"; coreColor = "#6b2828";
@ -521,7 +653,7 @@
ctx.fillStyle = "rgba(255,255,255,0.18)"; ctx.fillStyle = "rgba(255,255,255,0.18)";
ctx.fillRect(-7, -8, 14, 16); ctx.fillRect(-7, -8, 14, 16);
if (isPlayer && lives === 2) { if (isPlayer && health <= 66 && health > 33) {
ctx.strokeStyle = "rgba(30, 20, 10, 0.55)"; ctx.strokeStyle = "rgba(30, 20, 10, 0.55)";
ctx.lineWidth = 2; ctx.lineWidth = 2;
ctx.beginPath(); ctx.beginPath();
@ -537,7 +669,7 @@
ctx.fillRect(5, -4, 4, 4); ctx.fillRect(5, -4, 4, 4);
} }
if (isPlayer && lives <= 1) { if (isPlayer && health <= 33) {
ctx.strokeStyle = "rgba(80, 10, 10, 0.6)"; ctx.strokeStyle = "rgba(80, 10, 10, 0.6)";
ctx.lineWidth = 2; ctx.lineWidth = 2;
ctx.beginPath(); ctx.beginPath();
@ -559,7 +691,7 @@
} }
ctx.fillStyle = isPlayer ? "#f4fff8" : "#1b0805"; ctx.fillStyle = isPlayer ? "#f4fff8" : "#1b0805";
if (isPlayer && lives <= 1 && Math.floor(Date.now() / 300) % 2 === 0) { if (isPlayer && health <= 33 && Math.floor(Date.now() / 300) % 2 === 0) {
ctx.globalAlpha = 0.3; ctx.globalAlpha = 0.3;
} }
ctx.fillRect(-4, -26, 8, 20); ctx.fillRect(-4, -26, 8, 20);
@ -569,7 +701,7 @@
ctx.fillRect(-10, -2, 20, 4); ctx.fillRect(-10, -2, 20, 4);
ctx.fillStyle = "#79e5c9"; ctx.fillStyle = "#79e5c9";
ctx.fillRect(-5, -5, 10, 10); ctx.fillRect(-5, -5, 10, 10);
if (lives <= 1 && Math.floor(Date.now() / 300) % 2 === 0) { if (health <= 33 && Math.floor(Date.now() / 300) % 2 === 0) {
ctx.fillStyle = "rgba(212, 93, 69, 0.7)"; ctx.fillStyle = "rgba(212, 93, 69, 0.7)";
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(0, -14); ctx.moveTo(0, -14);
@ -635,12 +767,83 @@
keys.delete(event.key.toLowerCase()); keys.delete(event.key.toLowerCase());
}); });
const stickKeys = ["arrowup", "arrowdown", "arrowleft", "arrowright"];
let activeStickKey = null;
function clearStickDirection() {
if (activeStickKey) keys.delete(activeStickKey);
activeStickKey = null;
if (moveKnob) moveKnob.style.transform = "translate(-50%, -50%)";
}
function setStickDirection(key) {
if (activeStickKey === key) return;
if (activeStickKey) keys.delete(activeStickKey);
activeStickKey = key;
if (key) keys.add(key);
}
function updateStick(event) {
if (!moveStick) return;
event.preventDefault();
const rect = moveStick.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const dx = event.clientX - centerX;
const dy = event.clientY - centerY;
const distance = Math.hypot(dx, dy);
const deadZone = rect.width * 0.12;
if (distance < deadZone) {
clearStickDirection();
return;
}
const key = Math.abs(dx) > Math.abs(dy) ? (dx > 0 ? "arrowright" : "arrowleft") : dy > 0 ? "arrowdown" : "arrowup";
setStickDirection(key);
if (moveKnob) {
const max = rect.width * 0.24;
const scale = Math.min(max, distance) / distance;
moveKnob.style.transform = `translate(calc(-50% + ${dx * scale}px), calc(-50% + ${dy * scale}px))`;
}
canvas.focus({ preventScroll: true });
}
if (moveStick) {
moveStick.addEventListener("pointerdown", (event) => {
try {
moveStick.setPointerCapture?.(event.pointerId);
} catch {
// Synthetic or interrupted pointer events may not be capturable.
}
updateStick(event);
});
moveStick.addEventListener("pointermove", updateStick);
for (const type of ["pointerup", "pointercancel", "pointerleave"]) {
moveStick.addEventListener(type, (event) => {
event.preventDefault();
clearStickDirection();
try {
moveStick.releasePointerCapture?.(event.pointerId);
} catch {
// Some browsers release capture automatically.
}
});
}
}
primaryAction.addEventListener("click", startOrResume); primaryAction.addEventListener("click", startOrResume);
if (startAction) startAction.addEventListener("click", startOrResume); if (startAction) startAction.addEventListener("click", startOrResume);
if (pauseAction) pauseAction.addEventListener("click", togglePause); if (pauseAction) pauseAction.addEventListener("click", togglePause);
if (stopAction) stopAction.addEventListener("click", endGame); if (stopAction) stopAction.addEventListener("click", endGame);
if (endAction) endAction.addEventListener("click", endGame); if (endAction) endAction.addEventListener("click", endGame);
if (fireAction) {
fireAction.addEventListener("pointerdown", (event) => {
event.preventDefault();
canvas.focus({ preventScroll: true });
if (state === "playing") fire(player);
});
}
regenerateWalls();
player = tank(384, 536, "#77b255", "up", "player"); player = tank(384, 536, "#77b255", "up", "player");
updateHud(); updateHud();
setState("menu"); setState("menu");

@ -32,6 +32,7 @@ body {
font-family: font-family:
"Microsoft YaHei", "PingFang SC", "Segoe UI", Arial, sans-serif; "Microsoft YaHei", "PingFang SC", "Segoe UI", Arial, sans-serif;
letter-spacing: 0; letter-spacing: 0;
overscroll-behavior: none;
} }
button, button,
@ -85,6 +86,7 @@ button:focus-visible {
width: 100%; width: 100%;
display: grid; display: grid;
gap: 14px; gap: 14px;
justify-items: stretch;
} }
.topbar { .topbar {
@ -216,6 +218,7 @@ canvas {
width: 100%; width: 100%;
height: 100%; height: 100%;
image-rendering: pixelated; image-rendering: pixelated;
touch-action: none;
} }
.overlay { .overlay {
@ -275,6 +278,93 @@ canvas {
gap: 10px; gap: 10px;
} }
.action-group {
width: 100%;
justify-self: stretch;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
}
.action-group button {
width: 100%;
padding-inline: 0.6rem;
}
.touch-controls {
display: grid;
grid-template-columns: minmax(150px, 190px) minmax(112px, 1fr);
column-gap: clamp(20px, 5vw, 44px);
row-gap: 12px;
align-items: center;
}
.move-stick {
position: relative;
width: min(190px, 48vw);
aspect-ratio: 1;
border: 1px solid var(--line-bright);
border-radius: 50%;
background:
radial-gradient(circle at 50% 50%, rgba(240, 182, 70, 0.1) 0 24%, transparent 25%),
radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.07), transparent 58%),
var(--panel);
box-shadow: inset 0 0 0 8px rgba(16, 21, 13, 0.72), 0 12px 28px var(--shadow);
touch-action: none;
user-select: none;
}
.move-stick::before,
.move-stick::after {
content: "";
position: absolute;
inset: 50% auto auto 50%;
background: rgba(198, 216, 123, 0.45);
transform: translate(-50%, -50%);
pointer-events: none;
}
.move-stick::before {
width: 70%;
height: 2px;
}
.move-stick::after {
width: 2px;
height: 70%;
}
.move-knob {
position: absolute;
left: 50%;
top: 50%;
width: 42%;
aspect-ratio: 1;
border: 1px solid #d8e58c;
border-radius: 50%;
background: linear-gradient(180deg, #adc764, #718536);
box-shadow: 0 8px 18px var(--shadow);
transform: translate(-50%, -50%);
pointer-events: none;
}
.fire-action {
min-height: 54px;
padding: 0;
display: grid;
place-items: center;
touch-action: none;
-webkit-user-select: none;
}
.fire-action {
min-height: 86px;
border-color: #ffd36a;
background: linear-gradient(180deg, #f1c55d, #b97127);
color: #151007;
font-size: 1.1rem;
}
.key-hints { .key-hints {
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: var(--radius); border-radius: var(--radius);
@ -307,17 +397,6 @@ canvas {
flex-shrink: 0; flex-shrink: 0;
} }
.controls .action-group {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
}
.controls .action-group button {
width: 100%;
padding-inline: 0.6rem;
}
kbd { kbd {
min-width: 30px; min-width: 30px;
min-height: 28px; min-height: 28px;
@ -373,10 +452,28 @@ kbd {
min-height: 52px; min-height: 52px;
} }
.controls .action-group { .action-group {
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
} }
.touch-controls {
grid-template-columns: minmax(144px, 40vw) minmax(104px, 1fr);
column-gap: 36px;
row-gap: 10px;
}
.move-stick {
width: 100%;
}
.fire-action {
min-height: 50px;
}
.fire-action {
min-height: 82px;
}
.key-hints { .key-hints {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));

Loading…
Cancel
Save