[HTML] 纯文本查看 复制代码
<!DOCTYPE html><html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>坦克大战</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #000;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: 'Press Start 2P', 'Courier New', monospace;
overflow: hidden;
color: #fff;
}
#gameWrapper {
display: flex;
gap: 20px;
align-items: flex-start;
}
#gameCanvas {
border: 3px solid #555;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
#sidebar {
width: 160px;
display: flex;
flex-direction: column;
gap: 16px;
padding-top: 10px;
}
.info-block { text-align: center; }
.info-block .label {
font-size: 10px;
color: #888;
margin-bottom: 6px;
letter-spacing: 2px;
}
.info-block .value {
font-size: 22px;
color: #ff0;
text-shadow: 0 0 8px rgba(255,255,0,0.5);
}
.enemy-icons {
display: flex;
flex-wrap: wrap;
gap: 4px;
justify-content: center;
}
.enemy-icon {
width: 16px; height: 16px;
background: #e44;
border-radius: 2px;
}
.enemy-icon.dead { background: #333; }
#startScreen, #gameOverScreen, #winScreen {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.92);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 100;
gap: 30px;
}
#startScreen h1 {
font-size: 36px;
color: #ff0;
text-shadow: 0 0 20px rgba(255,255,0,0.6);
letter-spacing: 6px;
}
#startScreen .subtitle {
font-size: 12px;
color: #aaa;
letter-spacing: 3px;
}
.btn {
padding: 14px 40px;
font-size: 14px;
font-family: inherit;
background: transparent;
color: #ff0;
border: 2px solid #ff0;
cursor: pointer;
letter-spacing: 3px;
transition: all 0.2s;
}
.btn:hover {
background: #ff0;
color: #000;
}
#gameOverScreen h1 { font-size: 32px; color: #e44; letter-spacing: 4px; }
#winScreen h1 { font-size: 28px; color: #4f4; letter-spacing: 4px; }
.score-display { font-size: 16px; color: #ff0; }
.controls-hint {
font-size: 10px;
color: #666;
line-height: 2;
text-align: center;
}
.hidden { display: none !important; }
#pauseOverlay {
position: fixed;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
font-size: 28px;
color: #ff0;
letter-spacing: 6px;
z-index: 90;
text-shadow: 0 0 20px rgba(255,255,0,0.6);
}
/* Mobile Controls */
#mobileControls {
display: none;
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 20px;
z-index: 80;
pointer-events: none;
}
#mobileControls.active {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.dpad {
position: relative;
width: 140px;
height: 140px;
pointer-events: auto;
}
.dpad-btn {
position: absolute;
width: 46px;
height: 46px;
background: rgba(255, 255, 0, 0.3);
border: 2px solid rgba(255, 255, 0, 0.6);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: #ff0;
user-select: none;
-webkit-user-select: none;
touch-action: none;
}
.dpad-btn:active, .dpad-btn.pressed {
background: rgba(255, 255, 0, 0.6);
}
.dpad-up { top: 0; left: 50%; transform: translateX(-50%); }
.dpad-down { bottom: 0; left: 50%; transform: translateX(-50%); }
.dpad-left { left: 0; top: 50%; transform: translateY(-50%); }
.dpad-right { right: 0; top: 50%; transform: translateY(-50%); }
.fire-btn {
width: 80px;
height: 80px;
background: rgba(255, 68, 68, 0.4);
border: 3px solid rgba(255, 68, 68, 0.8);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: #fff;
font-weight: bold;
pointer-events: auto;
user-select: none;
-webkit-user-select: none;
touch-action: none;
}
.fire-btn:active, .fire-btn.pressed {
background: rgba(255, 68, 68, 0.7);
}
.pause-btn {
width: 44px;
height: 44px;
background: rgba(255, 255, 0, 0.3);
border: 2px solid rgba(255, 255, 0, 0.6);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #ff0;
font-weight: bold;
pointer-events: auto;
user-select: none;
-webkit-user-select: none;
touch-action: none;
}
.pause-btn:active, .pause-btn.pressed {
background: rgba(255, 255, 0, 0.6);
}
/* Responsive layout */
[url=home.php?mod=space&uid=945662]@media[/url] (max-width: 768px), (max-height: 600px) {
#gameWrapper {
flex-direction: column;
align-items: center;
gap: 10px;
}
#sidebar {
flex-direction: row;
width: auto;
gap: 20px;
padding: 10px;
flex-wrap: wrap;
justify-content: center;
}
#gameCanvas {
max-width: 90vw;
max-height: 50vh;
width: auto;
height: auto;
}
#mobileControls {
padding: 10px 20px 30px;
}
.dpad {
width: 120px;
height: 120px;
}
.dpad-btn {
width: 40px;
height: 40px;
font-size: 16px;
}
.fire-btn {
width: 70px;
height: 70px;
font-size: 12px;
}
.controls-hint {
display: none;
}
}
@media (max-width: 400px) {
.dpad {
width: 100px;
height: 100px;
}
.dpad-btn {
width: 34px;
height: 34px;
font-size: 14px;
}
.fire-btn {
width: 60px;
height: 60px;
}
}
</style>
</head>
<body tabindex="0">
<div id="startScreen">
<h1>坦克大战</h1>
<div class="subtitle">BATTLE CITY</div>
<button class="btn">开始游戏</button>
<div class="controls-hint" id="desktopHint">
方向键 / WASD 移动<br>
空格键 射击<br>
P 暂停
</div>
<div class="controls-hint" id="mobileHint" style="display:none;">
左侧方向键移动<br>
右侧 FIRE 射击<br>
II 按钮暂停
</div>
</div>
<div id="gameOverScreen" class="hidden">
<h1>游戏结束</h1>
<div class="score-display">得分: <span id="finalScore">0</span></div>
<button class="btn">再来一局</button>
</div>
<div id="winScreen" class="hidden">
<h1>胜利!</h1>
<div class="score-display">得分: <span id="winScore">0</span></div>
<button class="btn">下一关</button>
</div>
<div id="pauseOverlay" class="hidden">暂停</div>
<div id="gameWrapper">
<canvas id="gameCanvas" width="520" height="520"></canvas>
<div id="sidebar">
<div class="info-block">
<div class="label">关卡</div>
<div class="value" id="levelDisplay">1</div>
</div>
<div class="info-block">
<div class="label">得分</div>
<div class="value" id="scoreDisplay">0</div>
</div>
<div class="info-block">
<div class="label">生命</div>
<div class="value" id="livesDisplay">3</div>
</div>
<div class="info-block">
<div class="label">剩余敌人</div>
<div class="enemy-icons" id="enemyIcons"></div>
</div>
<div class="controls-hint">
方向键/WASD 移动<br>
空格 射击<br>
P 暂停
</div>
</div>
</div>
<!-- Mobile Controls -->
<div id="mobileControls">
<div class="dpad">
<div class="dpad-btn dpad-up" data-dir="up">▲</div>
<div class="dpad-btn dpad-down" data-dir="down">▼</div>
<div class="dpad-btn dpad-left" data-dir="left">◀</div>
<div class="dpad-btn dpad-right" data-dir="right">▶</div>
</div>
<div style="display:flex;flex-direction:column;gap:12px;align-items:center;pointer-events:auto;">
<div class="pause-btn" id="pauseBtn">II</div>
<div class="fire-btn" id="fireBtn">FIRE</div>
</div>
</div>
<script>
// ==================== CONSTANTS ====================
const TILE = 20;
const COLS = 26;
const ROWS = 26;
const W = COLS * TILE;
const H = ROWS * TILE;
const HALF = TILE / 2;
const DIR = { UP: 0, RIGHT: 1, DOWN: 2, LEFT: 3 };
const DX = [0, 1, 0, -1];
const DY = [-1, 0, 1, 0];
const TANK_SIZE = TILE * 2 - 2;
const BULLET_SIZE = 4;
const PLAYER_SPEED = 1.8;
const ENEMY_SPEED = 1.0;
const BULLET_SPEED = 3.5;
const ENEMY_BULLET_SPEED = 2.8;
const ENEMY_SHOOT_INTERVAL = 90;
const SPAWN_INTERVAL = 180;
const MAX_ENEMIES_ON_FIELD = 4;
const TILE_EMPTY = 0;
const TILE_BRICK = 1;
const TILE_STEEL = 2;
const TILE_WATER = 3;
const TILE_TREE = 4;
const TILE_BASE = 5;
const TILE_BASE_DEAD = 6;
const COLORS = {
bg: '#000',
brick: '#b35900',
brickLight: '#cc7a33',
steel: '#aaa',
steelLight: '#ccc',
water: '#2244aa',
waterLight: '#3366cc',
tree: '#226622',
treeLight: '#33aa33',
base: '#ff0',
baseDead: '#666',
playerBody: '#ff0',
playerTurret: '#cc0',
playerTrack: '#aa0',
enemyBody: '#e44',
enemyTurret: '#c22',
enemyTrack: '#a22',
enemyFastBody: '#e8e',
enemyFastTurret: '#c6c',
enemyArmorBody: '#4ae',
enemyArmorTurret: '#28c',
bullet: '#fff',
explosion: ['#ff0', '#fa0', '#f60', '#f00', '#800']
};
// ==================== LEVELS ====================
const LEVELS = [
// Level 1
[
"00000000000000000000000000",
"00000000000000000000000000",
"00110011001100110011001100",
"00110011001100110011001100",
"00110011001100110011001100",
"00110011001100110011001100",
"00110011002200110011001100",
"00110011000000110011001100",
"00110011000000110011001100",
"00110011001100110011001100",
"00000000001100000000000000",
"00000000001100000000000000",
"11001100000000001100110000",
"22001100000000001100002200",
"00000000001100000000000000",
"00000000001100000000000000",
"00110011001100110011001100",
"00110011001100110011001100",
"00110011001100110011001100",
"00110011000000110011001100",
"00110011000000110011001100",
"00000000000000000000000000",
"00000000000000000000000000",
"00000000001111000000000000",
"00000000001551000000000000",
"00000000001551000000000000",
],
// Level 2
[
"00000000000000000000000000",
"00000000000000000000000000",
"00110022001100110022001100",
"00110022001100110022001100",
"00110000001100110000001100",
"00110000001100110000001100",
"00000011220000221100000000",
"00000011220000221100000000",
"00220000001100000000220000",
"00220000001100000000220000",
"00001100110000110011000000",
"00001100110000110011000000",
"11000022000000002200001100",
"11000022000000002200001100",
"00001100110000110011000000",
"00001100110000110011000000",
"00220000001100000000220000",
"00220000001100000000220000",
"00110011001100110011001100",
"00110011001100110011001100",
"00110011000000110011001100",
"00110011000000110011001100",
"00000000000000000000000000",
"00000000001111000000000000",
"00000000001551000000000000",
"00000000001551000000000000",
],
// Level 3
[
"00000000000000000000000000",
"00000000000000000000000000",
"00110011003300110011001100",
"00110011003300110011001100",
"00110011000000110011001100",
"00110011000000110011001100",
"00000022001100220000001100",
"00000022001100220000001100",
"22110000002200000011002200",
"22110000002200000011002200",
"00001133000000331100000000",
"00001133000000331100000000",
"00110000221100220000110000",
"00110000221100220000110000",
"00001133000000331100000000",
"00001133000000331100000000",
"22110000002200000011002200",
"22110000002200000011002200",
"00000022001100220000001100",
"00000022001100220000001100",
"00110011000000110011001100",
"00110011000000110011001100",
"00000000000000000000000000",
"00000000001111000000000000",
"00000000001551000000000000",
"00000000001551000000000000",
],
];
// ==================== GAME STATE ====================
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
let gameState = 'start'; // start, playing, paused, gameover, win
let level = 0;
let score = 0;
let lives = 3;
let map = [];
let player = null;
let enemies = [];
let bullets = [];
let explosions = [];
let powerUps = [];
let enemiesRemaining = 0;
let enemiesSpawned = 0;
let totalEnemies = 20;
let spawnTimer = 0;
let frameCount = 0;
let baseAlive = true;
let spawnPoints = [];
let keys = {};
let playerInvincible = 0;
let freezeTimer = 0;
// ==================== INPUT ====================
function handleKeyDown(e) {
keys[e.code] = true;
// Also map to key for compatibility
if (e.key) keys[e.key] = true;
if (e.key === 'p' || e.key === 'P') {
if (gameState === 'playing') {
gameState = 'paused';
document.getElementById('pauseOverlay').classList.remove('hidden');
} else if (gameState === 'paused') {
gameState = 'playing';
document.getElementById('pauseOverlay').classList.add('hidden');
}
}
if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' '].includes(e.key)) {
e.preventDefault();
}
}
function handleKeyUp(e) {
keys[e.code] = false;
if (e.key) keys[e.key] = false;
}
document.addEventListener('keydown', handleKeyDown);
document.addEventListener('keyup', handleKeyUp);
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
// Ensure focus for keyboard input in iframe environments
window.addEventListener('load', () => { document.body.focus(); });
document.addEventListener('click', () => { document.body.focus(); });
document.addEventListener('touchstart', () => { document.body.focus(); });
// ==================== MOBILE CONTROLS ====================
const mobileControls = document.getElementById('mobileControls');
const dpadBtns = document.querySelectorAll('.dpad-btn');
const fireBtn = document.getElementById('fireBtn');
// Detect touch device and show controls
function checkTouchDevice() {
if ('ontouchstart' in window || navigator.maxTouchPoints > 0) {
mobileControls.classList.add('active');
document.getElementById('desktopHint').style.display = 'none';
document.getElementById('mobileHint').style.display = 'block';
}
}
checkTouchDevice();
// D-pad touch handling
const dirMap = { up: 'ArrowUp', down: 'ArrowDown', left: 'ArrowLeft', right: 'ArrowRight' };
const codeMap = { up: 'ArrowUp', down: 'ArrowDown', left: 'ArrowLeft', right: 'ArrowRight' };
dpadBtns.forEach(btn => {
const dir = btn.dataset.dir;
btn.addEventListener('touchstart', (e) => {
e.preventDefault();
btn.classList.add('pressed');
keys[dirMap[dir]] = true;
keys[codeMap[dir]] = true;
});
btn.addEventListener('touchend', (e) => {
e.preventDefault();
btn.classList.remove('pressed');
keys[dirMap[dir]] = false;
keys[codeMap[dir]] = false;
});
btn.addEventListener('touchcancel', (e) => {
e.preventDefault();
btn.classList.remove('pressed');
keys[dirMap[dir]] = false;
keys[codeMap[dir]] = false;
});
// Mouse fallback for testing
btn.addEventListener('mousedown', (e) => {
e.preventDefault();
btn.classList.add('pressed');
keys[dirMap[dir]] = true;
keys[codeMap[dir]] = true;
});
btn.addEventListener('mouseup', (e) => {
e.preventDefault();
btn.classList.remove('pressed');
keys[dirMap[dir]] = false;
keys[codeMap[dir]] = false;
});
btn.addEventListener('mouseleave', (e) => {
btn.classList.remove('pressed');
keys[dirMap[dir]] = false;
keys[codeMap[dir]] = false;
});
});
// Fire button touch handling
fireBtn.addEventListener('touchstart', (e) => {
e.preventDefault();
fireBtn.classList.add('pressed');
keys[' '] = true;
keys['Space'] = true;
});
fireBtn.addEventListener('touchend', (e) => {
e.preventDefault();
fireBtn.classList.remove('pressed');
keys[' '] = false;
keys['Space'] = false;
});
fireBtn.addEventListener('touchcancel', (e) => {
e.preventDefault();
fireBtn.classList.remove('pressed');
keys[' '] = false;
keys['Space'] = false;
});
// Mouse fallback for fire button
fireBtn.addEventListener('mousedown', (e) => {
e.preventDefault();
fireBtn.classList.add('pressed');
keys[' '] = true;
keys['Space'] = true;
});
fireBtn.addEventListener('mouseup', (e) => {
e.preventDefault();
fireBtn.classList.remove('pressed');
keys[' '] = false;
keys['Space'] = false;
});
fireBtn.addEventListener('mouseleave', (e) => {
fireBtn.classList.remove('pressed');
keys[' '] = false;
keys['Space'] = false;
});
// Pause button
const pauseBtn = document.getElementById('pauseBtn');
function togglePause() {
if (gameState === 'playing') {
gameState = 'paused';
document.getElementById('pauseOverlay').classList.remove('hidden');
} else if (gameState === 'paused') {
gameState = 'playing';
document.getElementById('pauseOverlay').classList.add('hidden');
}
}
pauseBtn.addEventListener('touchstart', (e) => {
e.preventDefault();
pauseBtn.classList.add('pressed');
togglePause();
});
pauseBtn.addEventListener('touchend', (e) => {
e.preventDefault();
pauseBtn.classList.remove('pressed');
});
pauseBtn.addEventListener('click', (e) => {
e.preventDefault();
togglePause();
});
// ==================== MAP ====================
function loadMap(lvl) {
const data = LEVELS[lvl % LEVELS.length];
map = [];
for (let r = 0; r < ROWS; r++) {
map[r] = [];
for (let c = 0; c < COLS; c++) {
map[r][c] = parseInt(data[r][c]);
}
}
}
function drawTile(r, c) {
const x = c * TILE;
const y = r * TILE;
const t = map[r][c];
if (t === TILE_BRICK) {
ctx.fillStyle = COLORS.brick;
ctx.fillRect(x, y, TILE, TILE);
ctx.fillStyle = COLORS.brickLight;
ctx.fillRect(x, y, TILE/2, TILE/2);
ctx.fillRect(x + TILE/2, y + TILE/2, TILE/2, TILE/2);
ctx.strokeStyle = '#000';
ctx.lineWidth = 0.5;
ctx.strokeRect(x, y, TILE, TILE);
} else if (t === TILE_STEEL) {
ctx.fillStyle = COLORS.steel;
ctx.fillRect(x, y, TILE, TILE);
ctx.fillStyle = COLORS.steelLight;
ctx.fillRect(x + 2, y + 2, TILE - 4, TILE - 4);
ctx.fillStyle = COLORS.steel;
ctx.fillRect(x + 5, y + 5, TILE - 10, TILE - 10);
} else if (t === TILE_WATER) {
const wave = Math.sin(frameCount * 0.05 + c + r) * 0.3 + 0.5;
ctx.fillStyle = wave > 0.5 ? COLORS.water : COLORS.waterLight;
ctx.fillRect(x, y, TILE, TILE);
ctx.fillStyle = wave > 0.5 ? COLORS.waterLight : COLORS.water;
for (let i = 0; i < 3; i++) {
const wy = y + 4 + i * 6 + Math.sin(frameCount * 0.08 + c * 0.5) * 2;
ctx.fillRect(x, wy, TILE, 2);
}
} else if (t === TILE_BASE) {
ctx.fillStyle = COLORS.base;
ctx.fillRect(x, y, TILE, TILE);
ctx.fillStyle = '#000';
ctx.fillRect(x + 4, y + 4, TILE - 8, TILE - 8);
ctx.fillStyle = COLORS.base;
ctx.fillRect(x + 7, y + 7, TILE - 14, TILE - 14);
} else if (t === TILE_BASE_DEAD) {
ctx.fillStyle = COLORS.baseDead;
ctx.fillRect(x, y, TILE, TILE);
ctx.fillStyle = '#444';
ctx.fillRect(x + 3, y + 3, TILE - 6, TILE - 6);
}
}
function drawTree(r, c) {
if (map[r][c] !== TILE_TREE) return;
const x = c * TILE;
const y = r * TILE;
ctx.fillStyle = COLORS.tree;
ctx.fillRect(x, y, TILE, TILE);
ctx.fillStyle = COLORS.treeLight;
ctx.fillRect(x + 2, y + 2, 6, 6);
ctx.fillRect(x + 10, y + 8, 6, 6);
ctx.fillRect(x + 4, y + 12, 6, 6);
}
// ==================== TANK ====================
class Tank {
constructor(x, y, dir, isPlayer, type) {
this.x = x;
this.y = y;
this.dir = dir;
this.isPlayer = isPlayer;
this.type = type || 'normal'; // normal, fast, armor
this.speed = isPlayer ? PLAYER_SPEED : (type === 'fast' ? ENEMY_SPEED * 1.6 : ENEMY_SPEED);
this.hp = isPlayer ? 1 : (type === 'armor' ? 3 : 1);
this.maxHp = this.hp;
this.shootCooldown = 0;
this.alive = true;
this.moveTimer = 0;
this.aiDir = dir;
this.aiDirTimer = 0;
this.flashTimer = 0;
this.spawnAnim = 30;
this.size = TANK_SIZE;
}
get cx() { return this.x + this.size / 2; }
get cy() { return this.y + this.size / 2; }
update() {
if (this.spawnAnim > 0) { this.spawnAnim--; return; }
if (this.shootCooldown > 0) this.shootCooldown--;
if (this.flashTimer > 0) this.flashTimer--;
if (this.isPlayer) {
this.updatePlayer();
} else {
if (freezeTimer <= 0) this.updateAI();
}
}
updatePlayer() {
let moving = false;
let newDir = this.dir;
if (keys['ArrowUp'] || keys['KeyW'] || keys['w'] || keys['W']) { newDir = DIR.UP; moving = true; }
else if (keys['ArrowDown'] || keys['KeyS'] || keys['s'] || keys['S']) { newDir = DIR.DOWN; moving = true; }
else if (keys['ArrowLeft'] || keys['KeyA'] || keys['a'] || keys['A']) { newDir = DIR.LEFT; moving = true; }
else if (keys['ArrowRight'] || keys['KeyD'] || keys['d'] || keys['D']) { newDir = DIR.RIGHT; moving = true; }
this.dir = newDir;
if (moving) this.move(this.dir);
if (keys[' '] || keys['Space']) this.shoot();
}
updateAI() {
this.aiDirTimer--;
if (this.aiDirTimer <= 0) {
// Change direction periodically or when blocked
const dirs = [DIR.UP, DIR.RIGHT, DIR.DOWN, DIR.LEFT];
// Bias towards player direction
if (player && player.alive && Math.random() < 0.3) {
const dx = player.cx - this.cx;
const dy = player.cy - this.cy;
if (Math.abs(dx) > Math.abs(dy)) {
this.aiDir = dx > 0 ? DIR.RIGHT : DIR.LEFT;
} else {
this.aiDir = dy > 0 ? DIR.DOWN : DIR.UP;
}
} else {
this.aiDir = dirs[Math.floor(Math.random() * 4)];
}
this.aiDirTimer = 60 + Math.floor(Math.random() * 120);
}
this.dir = this.aiDir;
const moved = this.move(this.dir);
if (!moved) {
this.aiDirTimer = 0; // change direction next frame
}
// Shooting
if (Math.random() < 0.02 || (this.shootCooldown <= 0 && Math.random() < 0.05)) {
this.shoot();
}
}
move(dir) {
const nx = this.x + DX[dir] * this.speed;
const ny = this.y + DY[dir] * this.speed;
// Boundary check
if (nx < 0 || ny < 0 || nx + this.size > W || ny + this.size > H) return false;
// Tile collision
if (this.checkTileCollision(nx, ny)) return false;
// Tank-tank collision
if (this.checkTankCollision(nx, ny)) return false;
this.x = nx;
this.y = ny;
return true;
}
checkTileCollision(nx, ny) {
const margin = 1;
const left = Math.floor((nx + margin) / TILE);
const top = Math.floor((ny + margin) / TILE);
const right = Math.floor((nx + this.size - 1 - margin) / TILE);
const bottom = Math.floor((ny + this.size - 1 - margin) / TILE);
for (let r = top; r <= bottom; r++) {
for (let c = left; c <= right; c++) {
if (r < 0 || r >= ROWS || c < 0 || c >= COLS) return true;
const t = map[r][c];
if (t === TILE_BRICK || t === TILE_STEEL || t === TILE_WATER || t === TILE_BASE || t === TILE_BASE_DEAD) {
return true;
}
}
}
return false;
}
checkTankCollision(nx, ny) {
const allTanks = [player, ...enemies].filter(t => t && t.alive && t !== this && t.spawnAnim <= 0);
for (const other of allTanks) {
if (nx < other.x + other.size && nx + this.size > other.x &&
ny < other.y + other.size && ny + this.size > other.y) {
return true;
}
}
return false;
}
shoot() {
if (this.shootCooldown > 0) return;
// Limit bullets per tank
const myBullets = bullets.filter(b => b.owner === this);
if (this.isPlayer && myBullets.length >= 2) return;
if (!this.isPlayer && myBullets.length >= 1) return;
const bx = this.cx - BULLET_SIZE / 2 + DX[this.dir] * (this.size / 2);
const by = this.cy - BULLET_SIZE / 2 + DY[this.dir] * (this.size / 2);
const speed = this.isPlayer ? BULLET_SPEED : ENEMY_BULLET_SPEED;
bullets.push(new Bullet(bx, by, this.dir, this.isPlayer, this, speed));
this.shootCooldown = this.isPlayer ? 15 : ENEMY_SHOOT_INTERVAL;
}
draw() {
if (!this.alive) return;
// Spawn animation
if (this.spawnAnim > 0) {
const flash = this.spawnAnim % 8 < 4;
ctx.strokeStyle = flash ? '#fff' : '#ff0';
ctx.lineWidth = 2;
const s = this.size * (1 - this.spawnAnim / 30) * 0.8;
ctx.strokeRect(this.cx - s/2, this.cy - s/2, s, s);
return;
}
ctx.save();
ctx.translate(this.cx, this.cy);
ctx.rotate(this.dir * Math.PI / 2);
const s = this.size;
const hs = s / 2;
// Flash when hit
if (this.flashTimer > 0 && this.flashTimer % 4 < 2) {
ctx.globalAlpha = 0.5;
}
// Body colors
let bodyColor, turretColor, trackColor;
if (this.isPlayer) {
bodyColor = COLORS.playerBody;
turretColor = COLORS.playerTurret;
trackColor = COLORS.playerTrack;
} else if (this.type === 'fast') {
bodyColor = COLORS.enemyFastBody;
turretColor = COLORS.enemyFastTurret;
trackColor = COLORS.enemyFastBody;
} else if (this.type === 'armor') {
bodyColor = this.hp > 1 ? COLORS.enemyArmorBody : COLORS.enemyBody;
turretColor = this.hp > 1 ? COLORS.enemyArmorTurret : COLORS.enemyTurret;
trackColor = COLORS.enemyArmorBody;
} else {
bodyColor = COLORS.enemyBody;
turretColor = COLORS.enemyTurret;
trackColor = COLORS.enemyTrack;
}
// Tracks
ctx.fillStyle = trackColor;
ctx.fillRect(-hs, -hs, s * 0.22, s);
ctx.fillRect(hs - s * 0.22, -hs, s * 0.22, s);
// Track details
ctx.fillStyle = '#000';
for (let i = 0; i < 5; i++) {
const ty = -hs + 3 + i * (s / 5);
ctx.fillRect(-hs + 1, ty, s * 0.2, 2);
ctx.fillRect(hs - s * 0.22 + 1, ty, s * 0.2, 2);
}
// Body
ctx.fillStyle = bodyColor;
ctx.fillRect(-hs + s * 0.22, -hs + 2, s * 0.56, s - 4);
// Turret base
ctx.fillStyle = turretColor;
ctx.fillRect(-s * 0.18, -s * 0.18, s * 0.36, s * 0.36);
// Barrel
ctx.fillStyle = turretColor;
ctx.fillRect(-2, -hs - 2, 4, hs);
// Invincibility shield
if (this.isPlayer && playerInvincible > 0) {
ctx.strokeStyle = playerInvincible % 6 < 3 ? '#0ff' : '#fff';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.arc(0, 0, hs + 2, 0, Math.PI * 2);
ctx.stroke();
}
ctx.restore();
}
hit() {
this.hp--;
this.flashTimer = 15;
if (this.hp <= 0) {
this.alive = false;
addExplosion(this.cx, this.cy, true);
if (!this.isPlayer) {
score += this.type === 'armor' ? 300 : this.type === 'fast' ? 200 : 100;
updateUI();
}
}
}
}
// ==================== BULLET ====================
class Bullet {
constructor(x, y, dir, isPlayerBullet, owner, speed) {
this.x = x;
this.y = y;
this.dir = dir;
this.isPlayerBullet = isPlayerBullet;
this.owner = owner;
this.speed = speed;
this.alive = true;
this.size = BULLET_SIZE;
}
update() {
this.x += DX[this.dir] * this.speed;
this.y += DY[this.dir] * this.speed;
// Boundary
if (this.x < 0 || this.y < 0 || this.x > W || this.y > H) {
this.alive = false;
addExplosion(this.x, this.y, false);
return;
}
// Tile collision
this.checkTileHit();
if (!this.alive) return;
// Tank collision
this.checkTankHit();
}
checkTileHit() {
const cx = this.x + this.size / 2;
const cy = this.y + this.size / 2;
const c = Math.floor(cx / TILE);
const r = Math.floor(cy / TILE);
// Check surrounding tiles
for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
const tr = r + dr;
const tc = c + dc;
if (tr < 0 || tr >= ROWS || tc < 0 || tc >= COLS) continue;
const tx = tc * TILE;
const ty = tr * TILE;
if (this.x + this.size <= tx || this.x >= tx + TILE ||
this.y + this.size <= ty || this.y >= ty + TILE) continue;
const t = map[tr][tc];
if (t === TILE_BRICK) {
map[tr][tc] = TILE_EMPTY;
this.alive = false;
addExplosion(cx, cy, false);
return;
} else if (t === TILE_STEEL) {
if (this.isPlayerBullet) {
// Player bullets can't destroy steel (unless power-up)
}
this.alive = false;
addExplosion(cx, cy, false);
return;
} else if (t === TILE_BASE) {
map[tr][tc] = TILE_BASE_DEAD;
baseAlive = false;
this.alive = false;
addExplosion(tx + TILE/2, ty + TILE/2, true);
return;
}
}
}
}
checkTankHit() {
if (this.isPlayerBullet) {
for (const enemy of enemies) {
if (!enemy.alive || enemy.spawnAnim > 0) continue;
if (this.x < enemy.x + enemy.size && this.x + this.size > enemy.x &&
this.y < enemy.y + enemy.size && this.y + this.size > enemy.y) {
this.alive = false;
enemy.hit();
return;
}
}
} else {
if (player && player.alive && player.spawnAnim <= 0) {
if (this.x < player.x + player.size && this.x + this.size > player.x &&
this.y < player.y + player.size && this.y + this.size > player.y) {
this.alive = false;
if (playerInvincible <= 0) {
player.hit();
if (!player.alive) {
lives--;
updateUI();
if (lives > 0) {
setTimeout(() => respawnPlayer(), 1500);
}
}
}
return;
}
}
}
// Bullet-bullet collision
for (const other of bullets) {
if (other === this || !other.alive) continue;
if (this.isPlayerBullet !== other.isPlayerBullet) {
if (this.x < other.x + other.size && this.x + this.size > other.x &&
this.y < other.y + other.size && this.y + this.size > other.y) {
this.alive = false;
other.alive = false;
return;
}
}
}
}
draw() {
if (!this.alive) return;
ctx.fillStyle = COLORS.bullet;
ctx.shadowColor = '#ff0';
ctx.shadowBlur = 4;
ctx.fillRect(this.x, this.y, this.size, this.size);
ctx.shadowBlur = 0;
}
}
// ==================== EXPLOSION ====================
class Explosion {
constructor(x, y, big) {
this.x = x;
this.y = y;
this.big = big;
this.frame = 0;
this.maxFrame = big ? 20 : 12;
this.alive = true;
}
update() {
this.frame++;
if (this.frame >= this.maxFrame) this.alive = false;
}
draw() {
if (!this.alive) return;
const progress = this.frame / this.maxFrame;
const radius = this.big ? 15 * (1 - progress) : 10 * (1 - progress);
const colorIdx = Math.floor(progress * COLORS.explosion.length);
ctx.fillStyle = COLORS.explosion[colorIdx] || COLORS.explosion[COLORS.explosion.length - 1];
ctx.beginPath();
ctx.arc(this.x, this.y, radius, 0, Math.PI * 2);
ctx.fill();
}
}
// ==================== GAME FUNCTIONS ====================
function addExplosion(x, y, big) {
explosions.push(new Explosion(x, y, big));
}
function respawnPlayer() {
if (lives <= 0) return;
player = new Tank(12 * TILE, 24 * TILE, DIR.UP, true);
playerInvincible = 120;
}
function updateUI() {
document.getElementById('levelDisplay').textContent = level + 1;
document.getElementById('scoreDisplay').textContent = score;
document.getElementById('livesDisplay').textContent = lives;
const enemyIcons = document.getElementById('enemyIcons');
enemyIcons.innerHTML = '';
for (let i = 0; i < totalEnemies; i++) {
const icon = document.createElement('div');
icon.className = 'enemy-icon';
if (i < enemiesSpawned) icon.classList.add('dead');
enemyIcons.appendChild(icon);
}
document.getElementById('finalScore').textContent = score;
document.getElementById('winScore').textContent = score;
}
function initLevel() {
loadMap(level);
enemies = [];
bullets = [];
explosions = [];
powerUps = [];
enemiesRemaining = totalEnemies;
enemiesSpawned = 0;
spawnTimer = 0;
baseAlive = true;
spawnPoints = [
{ x: 0, y: 0 },
{ x: 12 * TILE, y: 0 },
{ x: 24 * TILE, y: 0 }
];
// Reset base tiles
for (let r = ROWS - 2; r < ROWS; r++) {
for (let c = 11; c <= 14; c++) {
if (map[r][c] === TILE_BASE_DEAD) map[r][c] = TILE_BASE;
}
}
// Reset player
if (player && player.alive) {
player.x = 12 * TILE;
player.y = 24 * TILE;
player.dir = DIR.UP;
player.spawnAnim = 30;
} else {
player = new Tank(12 * TILE, 24 * TILE, DIR.UP, true);
}
playerInvincible = 60;
updateUI();
}
function spawnEnemy() {
if (enemies.length >= MAX_ENEMIES_ON_FIELD || enemiesSpawned >= totalEnemies) return;
const spawnPoint = spawnPoints[Math.floor(Math.random() * spawnPoints.length)];
const types = ['normal', 'fast', 'armor'];
const type = Math.random() < 0.3 ? 'armor' : (Math.random() < 0.4 ? 'fast' : 'normal');
const enemy = new Tank(spawnPoint.x, spawnPoint.y, DIR.DOWN, false, type);
enemies.push(enemy);
enemiesSpawned++;
updateUI();
}
function checkWinCondition() {
if (enemiesRemaining <= 0) {
gameState = 'win';
document.getElementById('winScreen').classList.remove('hidden');
document.getElementById('winScore').textContent = score;
}
}
function startGame() {
level = 0;
score = 0;
lives = 3;
initLevel();
gameState = 'playing';
document.getElementById('startScreen').classList.add('hidden');
document.getElementById('gameOverScreen').classList.add('hidden');
document.getElementById('winScreen').classList.add('hidden');
}
function restartGame() {
gameState = 'start';
document.getElementById('gameOverScreen').classList.add('hidden');
document.getElementById('winScreen').classList.add('hidden');
document.getElementById('startScreen').classList.remove('hidden');
}
function nextLevel() {
level++;
document.getElementById('winScreen').classList.add('hidden');
initLevel();
gameState = 'playing';
}
// ==================== GAME LOOP ====================
function gameLoop() {
frameCount++;
if (gameState === 'playing') {
// Update timers
if (playerInvincible > 0) playerInvincible--;
if (freezeTimer > 0) freezeTimer--;
// Spawn enemies
spawnTimer++;
if (spawnTimer >= SPAWN_INTERVAL && enemiesSpawned < totalEnemies) {
spawnTimer = 0;
spawnEnemy();
}
// Update player
if (player && player.alive) {
player.update();
}
// Update enemies
enemies = enemies.filter(enemy => enemy.alive);
enemies.forEach(enemy => enemy.update());
// Update bullets
bullets = bullets.filter(bullet => bullet.alive);
bullets.forEach(bullet => bullet.update());
// Update explosions
explosions = explosions.filter(explosion => explosion.alive);
explosions.forEach(explosion => explosion.update());
// Update enemy remaining count
enemiesRemaining = totalEnemies - enemiesSpawned + enemies.length;
// Check win/lose conditions
if (enemiesRemaining <= 0) {
checkWinCondition();
}
if (lives <= 0 || !baseAlive) {
gameState = 'gameover';
document.getElementById('gameOverScreen').classList.remove('hidden');
document.getElementById('finalScore').textContent = score;
}
}
// Draw everything
ctx.clearRect(0, 0, W, H);
// Draw map
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
drawTile(r, c);
}
}
// Draw trees (above everything)
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (map[r][c] === TILE_TREE) {
drawTree(r, c);
}
}
}
// Draw tanks
enemies.forEach(enemy => enemy.draw());
if (player && player.alive) player.draw();
// Draw bullets
bullets.forEach(bullet => bullet.draw());
// Draw explosions
explosions.forEach(explosion => explosion.draw());
// Update UI
updateUI();
requestAnimationFrame(gameLoop);
}
// ==================== INITIALIZATION ====================
function init() {
// Set canvas size
canvas.width = W;
canvas.height = H;
// Add button event listeners
const startBtn = document.querySelector('#startScreen .btn');
const restartBtn = document.querySelector('#gameOverScreen .btn');
const nextLevelBtn = document.querySelector('#winScreen .btn');
startBtn.addEventListener('click', startGame);
restartBtn.addEventListener('click', startGame);
nextLevelBtn.addEventListener('nextLevel', nextLevel);
// Start game loop
requestAnimationFrame(gameLoop);
}
// Start the game when page loads
window.addEventListener('load', init);
// ==================== POWER-UP ====================
class PowerUp {
constructor(x, y, type) {
this.x = x;
this.y = y;
this.type = type; // 'star', 'shield', 'bomb', 'life'
this.alive = true;
this.timer = 600; // 10 seconds
this.size = TILE * 2;
}
update() {
this.timer--;
if (this.timer <= 0) this.alive = false;
if (player && player.alive) {
if (this.x < player.x + player.size && this.x + this.size > player.x &&
this.y < player.y + player.size && this.y + this.size > player.y) {
this.alive = false;
this.apply();
}
}
}
apply() {
switch (this.type) {
case 'shield':
playerInvincible = 300;
break;
case 'bomb':
enemies.forEach(e => {
if (e.alive) {
e.alive = false;
addExplosion(e.cx, e.cy, true);
score += 50;
}
});
break;
case 'life':
lives++;
break;
case 'star':
player.speed = PLAYER_SPEED * 1.3;
break;
}
score += 500;
updateUI();
}
draw() {
if (!this.alive) return;
if (this.timer % 20 < 10 && this.timer < 120) return; // blink when about to expire
const x = this.x;
const y = this.y;
const s = this.size;
ctx.fillStyle = '#222';
ctx.fillRect(x, y, s, s);
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1;
ctx.strokeRect(x, y, s, s);
ctx.font = '16px monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
switch (this.type) {
case 'shield':
ctx.fillStyle = '#0ff';
ctx.fillText('S', x + s/2, y + s/2);
break;
case 'bomb':
ctx.fillStyle = '#f44';
ctx.fillText('B', x + s/2, y + s/2);
break;
case 'life':
ctx.fillStyle = '#4f4';
ctx.fillText('+', x + s/2, y + s/2);
break;
case 'star':
ctx.fillStyle = '#ff0';
ctx.fillText('*', x + s/2, y + s/2);
break;
}
}
}
// ==================== GAME LOGIC ====================
function spawnEnemy() {
if (enemiesSpawned >= totalEnemies) return;
if (enemies.filter(e => e.alive).length >= MAX_ENEMIES_ON_FIELD) return;
const sp = spawnPoints[enemiesSpawned % spawnPoints.length];
// Check if spawn point is clear
const allTanks = [player, ...enemies].filter(t => t && t.alive);
for (const t of allTanks) {
if (sp.x < t.x + t.size && sp.x + TANK_SIZE > t.x &&
sp.y < t.y + t.size && sp.y + TANK_SIZE > t.y) {
return; // spawn point blocked
}
}
const types = ['normal', 'normal', 'fast', 'armor'];
const typeIdx = Math.min(Math.floor(enemiesSpawned / 5), types.length - 1);
const type = types[Math.floor(Math.random() * (typeIdx + 1))];
const enemy = new Tank(sp.x, sp.y, DIR.DOWN, false, type);
enemies.push(enemy);
enemiesSpawned++;
updateUI();
}
function respawnPlayer() {
if (gameState !== 'playing') return;
player = new Tank(4 * TILE, 24 * TILE, DIR.UP, true);
playerInvincible = 120;
}
function startGame() {
document.getElementById('startScreen').classList.add('hidden');
document.getElementById('gameOverScreen').classList.add('hidden');
document.getElementById('winScreen').classList.add('hidden');
level = 0;
score = 0;
lives = 3;
initLevel();
gameState = 'playing';
document.body.focus();
}
function nextLevel() {
document.getElementById('winScreen').classList.add('hidden');
level++;
initLevel();
gameState = 'playing';
document.body.focus();
}
function initLevel() {
loadMap(level);
bullets = [];
explosions = [];
powerUps = [];
enemies = [];
enemiesSpawned = 0;
totalEnemies = 20;
enemiesRemaining = totalEnemies;
spawnTimer = 0;
baseAlive = true;
playerInvincible = 120;
freezeTimer = 0;
spawnPoints = [
{ x: 0, y: 0 },
{ x: 12 * TILE, y: 0 },
{ x: 24 * TILE, y: 0 }
];
player = new Tank(4 * TILE, 24 * TILE, DIR.UP, true);
updateUI();
}
function updateUI() {
document.getElementById('levelDisplay').textContent = level + 1;
document.getElementById('scoreDisplay').textContent = score;
document.getElementById('livesDisplay').textContent = lives;
const remaining = totalEnemies - enemiesSpawned + enemies.filter(e => e.alive).length;
const icons = document.getElementById('enemyIcons');
icons.innerHTML = '';
for (let i = 0; i < totalEnemies; i++) {
const div = document.createElement('div');
div.className = 'enemy-icon' + (i >= remaining ? ' dead' : '');
icons.appendChild(div);
}
}
function checkWinLose() {
if (!baseAlive) {
gameState = 'gameover';
document.getElementById('finalScore').textContent = score;
document.getElementById('gameOverScreen').classList.remove('hidden');
return;
}
if (lives <= 0 && (!player || !player.alive)) {
gameState = 'gameover';
document.getElementById('finalScore').textContent = score;
document.getElementById('gameOverScreen').classList.remove('hidden');
return;
}
const allDead = enemies.every(e => !e.alive);
if (enemiesSpawned >= totalEnemies && allDead) {
gameState = 'win';
document.getElementById('winScore').textContent = score;
document.getElementById('winScreen').classList.remove('hidden');
}
}
// ==================== MAIN LOOP ====================
function update() {
if (gameState !== 'playing') return;
frameCount++;
// Spawn enemies
spawnTimer++;
if (spawnTimer >= SPAWN_INTERVAL) {
spawnTimer = 0;
spawnEnemy();
}
// Update timers
if (playerInvincible > 0) playerInvincible--;
if (freezeTimer > 0) freezeTimer--;
// Update player
if (player && player.alive) player.update();
// Update enemies
enemies.forEach(e => { if (e.alive) e.update(); });
// Update bullets
bullets.forEach(b => { if (b.alive) b.update(); });
bullets = bullets.filter(b => b.alive);
// Update explosions
explosions.forEach(e => e.update());
explosions = explosions.filter(e => e.alive);
// Update power-ups
powerUps.forEach(p => { if (p.alive) p.update(); });
powerUps = powerUps.filter(p => p.alive);
// Random power-up drop
if (frameCount % 600 === 0 && Math.random() < 0.5) {
const types = ['shield', 'bomb', 'life', 'star'];
const type = types[Math.floor(Math.random() * types.length)];
const px = Math.floor(Math.random() * (COLS - 2)) * TILE;
const py = (4 + Math.floor(Math.random() * 16)) * TILE;
powerUps.push(new PowerUp(px, py, type));
}
checkWinLose();
}
function draw() {
// Background
ctx.fillStyle = COLORS.bg;
ctx.fillRect(0, 0, W, H);
// Don't draw map if not initialized
if (!map || map.length === 0) return;
// Draw map (bottom layer - no trees)
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (map[r][c] !== TILE_EMPTY && map[r][c] !== TILE_TREE) {
drawTile(r, c);
}
}
}
// Draw power-ups
powerUps.forEach(p => p.draw());
// Draw tanks
if (player && player.alive) player.draw();
enemies.forEach(e => { if (e.alive) e.draw(); });
// Draw bullets
bullets.forEach(b => { if (b.alive) b.draw(); });
// Draw trees (top layer - covers tanks)
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
drawTree(r, c);
}
}
// Draw explosions (topmost)
explosions.forEach(e => e.draw());
// Debug: show active keys
const activeKeys = Object.entries(keys).filter(([k, v]) => v).map(([k]) => k);
if (activeKeys.length > 0) {
ctx.fillStyle = 'rgba(0,0,0,0.7)';
ctx.fillRect(2, H - 22, 200, 20);
ctx.fillStyle = '#0f0';
ctx.font = '10px monospace';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.fillText('Keys: ' + activeKeys.join(', '), 6, H - 12);
}
}
function gameLoop() {
update();
draw();
requestAnimationFrame(gameLoop);
}
// Start the loop
gameLoop();
</script>
</body>
</html>