想起小时候玩的一款游戏,那时候还是游戏机玩的,魂斗罗,真的那时候玩的超级有意思,每天放学自己玩。
试着写了一版本地运行的纯HTML页面,玩了一下挺好。
操作方式
按键 功能
← → / A D 移动
Space / ↑ / W 跳跃
Z 射击
X 炸弹
↓ / S 趴下 / 空中向下瞄准
Enter 开始 / 重新开始


<!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; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; display: flex; align-items: center; justify-content: center; }
canvas { display: block; image-rendering: pixelated; image-rendering: crisp-edges; background: #000; }
</style>
</head>
<body>
<canvas id="game"></canvas>
<script>
// ==================== CONTRA GAME ====================
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
// --- Responsive Canvas ---
const GAME_W = 800, GAME_H = 480;
canvas.width = GAME_W; canvas.height = GAME_H;
function resize() {
const s = Math.min(window.innerWidth / GAME_W, window.innerHeight / GAME_H);
canvas.style.width = (GAME_W * s) + 'px';
canvas.style.height = (GAME_H * s) + 'px';
}
window.addEventListener('resize', resize); resize();
// --- Audio Engine ---
const AudioCtx = window.AudioContext || window.webkitAudioContext;
let audioCtx;
function ensureAudio() { if (!audioCtx) audioCtx = new AudioCtx(); }
function playSound(type) {
ensureAudio();
const o = audioCtx.createOscillator();
const g = audioCtx.createGain();
o.connect(g); g.connect(audioCtx.destination);
const t = audioCtx.currentTime;
switch(type) {
case 'shoot':
o.type='square'; o.frequency.setValueAtTime(800,t); o.frequency.exponentialRampToValueAtTime(200,t+0.08);
g.gain.setValueAtTime(0.15,t); g.gain.exponentialRampToValueAtTime(0.001,t+0.08);
o.start(t); o.stop(t+0.08); break;
case 'spread':
o.type='sawtooth'; o.frequency.setValueAtTime(600,t); o.frequency.exponentialRampToValueAtTime(100,t+0.12);
g.gain.setValueAtTime(0.12,t); g.gain.exponentialRampToValueAtTime(0.001,t+0.12);
o.start(t); o.stop(t+0.12); break;
case 'explosion':
o.type='sawtooth'; o.frequency.setValueAtTime(200,t); o.frequency.exponentialRampToValueAtTime(30,t+0.3);
g.gain.setValueAtTime(0.2,t); g.gain.exponentialRampToValueAtTime(0.001,t+0.3);
o.start(t); o.stop(t+0.3); break;
case 'powerup':
o.type='sine'; o.frequency.setValueAtTime(400,t); o.frequency.exponentialRampToValueAtTime(1200,t+0.2);
g.gain.setValueAtTime(0.15,t); g.gain.exponentialRampToValueAtTime(0.001,t+0.25);
o.start(t); o.stop(t+0.25); break;
case 'jump':
o.type='sine'; o.frequency.setValueAtTime(300,t); o.frequency.exponentialRampToValueAtTime(600,t+0.1);
g.gain.setValueAtTime(0.1,t); g.gain.exponentialRampToValueAtTime(0.001,t+0.1);
o.start(t); o.stop(t+0.1); break;
case 'hit':
o.type='square'; o.frequency.setValueAtTime(150,t); o.frequency.exponentialRampToValueAtTime(50,t+0.2);
g.gain.setValueAtTime(0.2,t); g.gain.exponentialRampToValueAtTime(0.001,t+0.2);
o.start(t); o.stop(t+0.2); break;
case 'boss':
o.type='sawtooth'; o.frequency.setValueAtTime(100,t); o.frequency.exponentialRampToValueAtTime(40,t+0.5);
g.gain.setValueAtTime(0.25,t); g.gain.exponentialRampToValueAtTime(0.001,t+0.5);
o.start(t); o.stop(t+0.5); break;
}
}
// --- Input ---
const keys = {};
window.addEventListener('keydown', e => { keys[e.code] = true; e.preventDefault(); });
window.addEventListener('keyup', e => { keys[e.code] = false; e.preventDefault(); });
// --- Game State ---
let gameState = 'title'; // title, playing, gameover, win
let score = 0, lives = 3, hiScore = parseInt(localStorage.getItem('contraHi') || '0');
let cameraX = 0;
const LEVEL_W = 6400;
const GRAVITY = 0.5;
let frameCount = 0;
let screenShake = 0;
let bossDefeated = false;
// --- Platforms ---
const platforms = [];
function generateLevel() {
platforms.length = 0;
// Ground
for (let x = 0; x < LEVEL_W; x += 200) {
if (x > 400 && x < 600) continue; // gap
if (x > 1800 && x < 2000) continue; // gap
if (x > 3200 && x < 3400) continue; // gap
platforms.push({ x, y: GAME_H - 40, w: 200, h: 40, type: 'ground' });
}
// Floating platforms
const floats = [
{x:300,y:320,w:120},{x:500,y:280,w:100},{x:700,y:240,w:140},
{x:950,y:300,w:100},{x:1100,y:220,w:120},{x:1300,y:280,w:100},
{x:1500,y:200,w:140},{x:1700,y:320,w:100},{x:1900,y:260,w:120},
{x:2100,y:180,w:100},{x:2300,y:300,w:140},{x:2500,y:240,w:100},
{x:2700,y:320,w:120},{x:2900,y:200,w:100},{x:3100,y:280,w:140},
{x:3300,y:160,w:100},{x:3500,y:300,w:120},{x:3700,y:240,w:100},
{x:3900,y:320,w:140},{x:4100,y:200,w:100},{x:4300,y:280,w:120},
{x:4500,y:160,w:100},{x:4700,y:300,w:140},{x:4900,y:240,w:100},
{x:5100,y:320,w:120},{x:5300,y:200,w:100},{x:5500,y:280,w:140},
{x:5700,y:160,w:120},{x:5900,y:300,w:100}
];
floats.forEach(f => platforms.push({ x:f.x, y:f.y, w:f.w, h:16, type:'platform' }));
// Walls/barriers
[800,1600,2400,3600,4800].forEach(x => {
platforms.push({ x, y:GAME_H-120, w:30, h:80, type:'wall' });
});
}
// --- Player ---
const player = {
x: 100, y: 300, w: 28, h: 40, vx: 0, vy: 0,
dir: 1, onGround: false, jumping: false,
shooting: false, shootTimer: 0, shootCooldown: 8,
weapon: 'normal', // normal, spread, rapid, laser
weaponTimer: 0, invincible: 0,
prone: false, animFrame: 0, animTimer: 0,
alive: true, respawnTimer: 0
};
// --- Bullets ---
let playerBullets = [];
let enemyBullets = [];
// --- Enemies ---
let enemies = [];
let boss = null;
// --- Powerups ---
let powerups = [];
// --- Explosions ---
let explosions = [];
// --- Particles ---
let particles = [];
// --- Spawn enemies ---
function spawnEnemies() {
enemies.length = 0;
// Soldiers
for (let i = 0; i < 40; i++) {
const ex = 600 + i * 140 + Math.random() * 60;
enemies.push({
type: 'soldier', x: ex, y: GAME_H - 80, w: 24, h: 36,
hp: 1, dir: -1, vx: 0, vy: 0, onGround: false,
shootTimer: Math.random() * 120, alive: true,
animFrame: 0, patrol: ex - 60, patrolMax: ex + 60,
state: 'patrol' // patrol, chase, shoot
});
}
// Turrets
[1000, 1800, 2600, 3400, 4200, 5000, 5600].forEach(tx => {
enemies.push({
type: 'turret', x: tx, y: GAME_H - 72, w: 32, h: 32,
hp: 3, dir: -1, shootTimer: 60, alive: true,
angle: 0
});
});
// Runners (fast enemies)
for (let i = 0; i < 15; i++) {
const ex = 1200 + i * 350 + Math.random() * 100;
enemies.push({
type: 'runner', x: ex, y: GAME_H - 76, w: 22, h: 34,
hp: 1, dir: -1, vx: -3, vy: 0, onGround: false,
alive: true, animFrame: 0
});
}
// Powerup drops
[700, 1400, 2200, 3000, 3800, 4600, 5400].forEach((px, i) => {
const types = ['spread','rapid','laser','life','shield'];
powerups.push({
x: px, y: 180 + Math.random() * 80, w: 24, h: 24,
type: types[i % types.length], alive: true, bobTimer: Math.random() * 100
});
});
}
// --- Boss ---
function spawnBoss() {
boss = {
x: LEVEL_W - 300, y: GAME_H - 200, w: 120, h: 160,
hp: 50, maxHp: 50, phase: 0,
shootTimer: 0, moveTimer: 0, alive: true,
dir: -1, vy: 0
};
}
// --- Collision ---
function rectCollide(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 resolveCollision(entity) {
entity.onGround = false;
for (const p of platforms) {
if (rectCollide(entity, p)) {
const overlapX = Math.min(entity.x + entity.w - p.x, p.x + p.w - entity.x);
const overlapY = Math.min(entity.y + entity.h - p.y, p.y + p.h - entity.y);
if (overlapY < overlapX) {
if (entity.y + entity.h/2 < p.y + p.h/2) {
entity.y = p.y - entity.h;
entity.vy = 0;
entity.onGround = true;
} else {
entity.y = p.y + p.h;
entity.vy = 0;
}
} else {
if (entity.x + entity.w/2 < p.x + p.w/2) {
entity.x = p.x - entity.w;
} else {
entity.x = p.x + p.w;
}
entity.vx = 0;
}
}
}
}
// --- Draw Helpers ---
function drawPixelChar(x, y, dir, frame, prone, shooting) {
const sx = x - cameraX;
if (sx < -50 || sx > GAME_W + 50) return;
ctx.save();
ctx.translate(sx + 14, y);
ctx.scale(dir, 1);
if (prone) {
// Prone position
ctx.fillStyle = '#1e88e5';
ctx.fillRect(-14, 24, 28, 12); // body horizontal
ctx.fillStyle = '#ffcc80';
ctx.fillRect(10, 22, 10, 10); // head
ctx.fillStyle = '#1565c0';
ctx.fillRect(-14, 28, 28, 8); // legs
// Gun
ctx.fillStyle = '#757575';
ctx.fillRect(18, 26, 16, 4);
} else {
// Head
ctx.fillStyle = '#ffcc80';
ctx.fillRect(-6, 0, 12, 12);
// Hair/bandana
ctx.fillStyle = '#e53935';
ctx.fillRect(-7, 0, 14, 4);
ctx.fillRect(-9, 2, 4, 3); // bandana tail
// Eyes
ctx.fillStyle = '#000';
ctx.fillRect(2, 5, 3, 3);
// Body
ctx.fillStyle = '#1e88e5';
ctx.fillRect(-8, 12, 16, 14);
// Belt
ctx.fillStyle = '#795548';
ctx.fillRect(-8, 24, 16, 3);
// Legs
const legOff = Math.sin(frame * 0.3) * 4;
ctx.fillStyle = '#1565c0';
ctx.fillRect(-7, 27, 6, 13 + (frame % 2 === 0 ? legOff : -legOff));
ctx.fillRect(1, 27, 6, 13 + (frame % 2 === 0 ? -legOff : legOff));
// Boots
ctx.fillStyle = '#4e342e';
ctx.fillRect(-8, 37, 7, 3);
ctx.fillRect(1, 37, 7, 3);
// Gun arm
ctx.fillStyle = '#757575';
if (shooting) {
ctx.fillRect(8, 14, 18, 4);
// Muzzle flash
ctx.fillStyle = '#ffeb3b';
ctx.fillRect(26, 12, 6, 8);
} else {
ctx.fillRect(8, 16, 14, 3);
}
// Arm
ctx.fillStyle = '#ffcc80';
ctx.fillRect(6, 14, 4, 6);
}
ctx.restore();
}
function drawSoldier(e) {
const sx = e.x - cameraX;
if (sx < -50 || sx > GAME_W + 50) return;
ctx.save();
ctx.translate(sx + 12, e.y);
ctx.scale(e.dir, 1);
// Head
ctx.fillStyle = '#4caf50';
ctx.fillRect(-5, 0, 10, 10);
// Helmet
ctx.fillStyle = '#2e7d32';
ctx.fillRect(-6, -2, 12, 6);
// Eyes
ctx.fillStyle = '#f00';
ctx.fillRect(1, 4, 3, 2);
// Body
ctx.fillStyle = '#388e3c';
ctx.fillRect(-7, 10, 14, 14);
// Legs
const lo = Math.sin(e.animFrame * 0.2) * 3;
ctx.fillStyle = '#1b5e20';
ctx.fillRect(-6, 24, 5, 12 + lo);
ctx.fillRect(1, 24, 5, 12 - lo);
// Gun
ctx.fillStyle = '#616161';
ctx.fillRect(7, 14, 12, 3);
ctx.restore();
}
function drawTurret(e) {
const sx = e.x - cameraX;
if (sx < -50 || sx > GAME_W + 50) return;
// Base
ctx.fillStyle = '#616161';
ctx.fillRect(sx, e.y + 16, 32, 16);
// Dome
ctx.fillStyle = '#757575';
ctx.beginPath();
ctx.arc(sx + 16, e.y + 16, 14, Math.PI, 0);
ctx.fill();
// Barrel
ctx.save();
ctx.translate(sx + 16, e.y + 14);
ctx.rotate(e.angle);
ctx.fillStyle = '#424242';
ctx.fillRect(0, -3, 24, 6);
ctx.restore();
// Light
ctx.fillStyle = frameCount % 30 < 15 ? '#f44336' : '#ff8a80';
ctx.fillRect(sx + 13, e.y + 6, 6, 4);
}
function drawRunner(e) {
const sx = e.x - cameraX;
if (sx < -50 || sx > GAME_W + 50) return;
ctx.save();
ctx.translate(sx + 11, e.y);
ctx.scale(e.dir, 1);
// Head
ctx.fillStyle = '#ff6f00';
ctx.fillRect(-5, 0, 10, 10);
// Eyes
ctx.fillStyle = '#fff';
ctx.fillRect(1, 3, 4, 4);
ctx.fillStyle = '#f00';
ctx.fillRect(2, 4, 2, 2);
// Body
ctx.fillStyle = '#e65100';
ctx.fillRect(-6, 10, 12, 12);
// Legs (running fast)
const lo = Math.sin(e.animFrame * 0.5) * 6;
ctx.fillStyle = '#bf360c';
ctx.fillRect(-5, 22, 4, 12 + lo);
ctx.fillRect(1, 22, 4, 12 - lo);
// Claws
ctx.fillStyle = '#ff3d00';
ctx.fillRect(6, 12, 8, 3);
ctx.restore();
}
function drawBoss(b) {
if (!b || !b.alive) return;
const sx = b.x - cameraX;
ctx.save();
// Main body
ctx.fillStyle = '#455a64';
ctx.fillRect(sx, b.y, b.w, b.h);
// Armor plates
ctx.fillStyle = '#37474f';
ctx.fillRect(sx + 10, b.y + 10, b.w - 20, 30);
ctx.fillRect(sx + 10, b.y + 60, b.w - 20, 30);
ctx.fillRect(sx + 10, b.y + 110, b.w - 20, 30);
// Core (weak point)
const coreGlow = Math.sin(frameCount * 0.1) * 0.3 + 0.7;
ctx.fillStyle = `rgba(244, 67, 54, ${coreGlow})`;
ctx.beginPath();
ctx.arc(sx + b.w/2, b.y + b.h/2, 20, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#ff8a80';
ctx.beginPath();
ctx.arc(sx + b.w/2, b.y + b.h/2, 10, 0, Math.PI * 2);
ctx.fill();
// Cannons
ctx.fillStyle = '#263238';
ctx.fillRect(sx - 20, b.y + 20, 30, 12);
ctx.fillRect(sx - 20, b.y + b.h - 40, 30, 12);
ctx.fillRect(sx + b.w - 10, b.y + 40, 30, 12);
ctx.fillRect(sx + b.w - 10, b.y + b.h - 60, 30, 12);
// Eyes
ctx.fillStyle = '#f44336';
ctx.fillRect(sx + 25, b.y + 15, 12, 8);
ctx.fillRect(sx + b.w - 37, b.y + 15, 12, 8);
// HP bar
ctx.fillStyle = '#333';
ctx.fillRect(sx, b.y - 20, b.w, 10);
ctx.fillStyle = b.hp > b.maxHp * 0.3 ? '#f44336' : '#ff1744';
ctx.fillRect(sx + 1, b.y - 19, (b.w - 2) * (b.hp / b.maxHp), 8);
// Name
ctx.fillStyle = '#fff';
ctx.font = '10px monospace';
ctx.fillText('BOSS - ALIEN CORE', sx + 10, b.y - 24);
ctx.restore();
}
// --- Background ---
function drawBackground() {
// Sky gradient
const grad = ctx.createLinearGradient(0, 0, 0, GAME_H);
grad.addColorStop(0, '#0d1b2a');
grad.addColorStop(0.4, '#1b2838');
grad.addColorStop(0.7, '#2d4a3e');
grad.addColorStop(1, '#1a3a2a');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, GAME_W, GAME_H);
// Stars
ctx.fillStyle = '#fff';
for (let i = 0; i < 60; i++) {
const sx = ((i * 137 + 50) % GAME_W + GAME_W - cameraX * 0.05 % GAME_W) % GAME_W;
const sy = (i * 89 + 20) % (GAME_H * 0.5);
const size = (i % 3 === 0) ? 2 : 1;
ctx.globalAlpha = 0.3 + (Math.sin(frameCount * 0.02 + i) * 0.3);
ctx.fillRect(sx, sy, size, size);
}
ctx.globalAlpha = 1;
// Mountains (parallax)
ctx.fillStyle = '#1a3328';
for (let i = 0; i < 12; i++) {
const mx = i * 200 - (cameraX * 0.15) % 2400;
const mh = 80 + (i * 47) % 60;
ctx.beginPath();
ctx.moveTo(mx, GAME_H - 40);
ctx.lineTo(mx + 100, GAME_H - 40 - mh);
ctx.lineTo(mx + 200, GAME_H - 40);
ctx.fill();
}
// Trees (parallax)
ctx.fillStyle = '#0d2818';
for (let i = 0; i < 20; i++) {
const tx = i * 150 - (cameraX * 0.3) % 3000;
const th = 40 + (i * 31) % 30;
ctx.fillRect(tx + 15, GAME_H - 40 - th, 10, th);
ctx.beginPath();
ctx.moveTo(tx, GAME_H - 40 - th + 10);
ctx.lineTo(tx + 20, GAME_H - 40 - th - 25);
ctx.lineTo(tx + 40, GAME_H - 40 - th + 10);
ctx.fill();
}
}
function drawPlatforms() {
for (const p of platforms) {
const sx = p.x - cameraX;
if (sx > GAME_W + 10 || sx + p.w < -10) continue;
if (p.type === 'ground') {
ctx.fillStyle = '#3e2723';
ctx.fillRect(sx, p.y, p.w, p.h);
ctx.fillStyle = '#4caf50';
ctx.fillRect(sx, p.y, p.w, 6);
ctx.fillStyle = '#2e7d32';
ctx.fillRect(sx, p.y + 6, p.w, 4);
// Dirt texture
ctx.fillStyle = '#5d4037';
for (let dx = 0; dx < p.w; dx += 20) {
ctx.fillRect(sx + dx + 5, p.y + 15, 8, 4);
ctx.fillRect(sx + dx + 2, p.y + 25, 6, 3);
}
} else if (p.type === 'platform') {
ctx.fillStyle = '#78909c';
ctx.fillRect(sx, p.y, p.w, p.h);
ctx.fillStyle = '#90a4ae';
ctx.fillRect(sx, p.y, p.w, 4);
ctx.fillStyle = '#546e7a';
ctx.fillRect(sx + 2, p.y + 6, p.w - 4, 4);
// Rivets
ctx.fillStyle = '#b0bec5';
ctx.fillRect(sx + 4, p.y + 2, 3, 3);
ctx.fillRect(sx + p.w - 7, p.y + 2, 3, 3);
} else if (p.type === 'wall') {
ctx.fillStyle = '#5d4037';
ctx.fillRect(sx, p.y, p.w, p.h);
ctx.fillStyle = '#6d4c41';
for (let dy = 0; dy < p.h; dy += 12) {
ctx.fillRect(sx, p.y + dy, p.w, 1);
ctx.fillRect(sx + p.w/2, p.y + dy, 1, 12);
}
}
}
}
function drawPowerups() {
for (const p of powerups) {
if (!p.alive) continue;
const sx = p.x - cameraX;
if (sx < -30 || sx > GAME_W + 30) continue;
const bob = Math.sin((frameCount + p.bobTimer) * 0.05) * 4;
const py = p.y + bob;
// Wing container
ctx.fillStyle = '#e53935';
ctx.fillRect(sx - 2, py - 2, 28, 28);
ctx.fillStyle = '#c62828';
ctx.fillRect(sx, py, 24, 24);
// Letter
ctx.fillStyle = '#fff';
ctx.font = 'bold 14px monospace';
const letters = { spread: 'S', rapid: 'R', laser: 'L', life: '1UP', shield: '🛡' };
ctx.fillText(letters[p.type] || '?', sx + 4, py + 17);
// Glow
ctx.globalAlpha = 0.3 + Math.sin(frameCount * 0.1) * 0.2;
ctx.fillStyle = '#ffeb3b';
ctx.fillRect(sx - 4, py - 4, 32, 32);
ctx.globalAlpha = 1;
}
}
function drawBullets() {
// Player bullets
for (const b of playerBullets) {
const sx = b.x - cameraX;
if (sx < -10 || sx > GAME_W + 10) continue;
if (b.type === 'laser') {
ctx.fillStyle = '#00e5ff';
ctx.fillRect(sx, b.y - 1, b.w, 3);
ctx.fillStyle = '#b2ebf2';
ctx.fillRect(sx, b.y, b.w, 1);
} else if (b.type === 'spread') {
ctx.fillStyle = '#ff9800';
ctx.beginPath();
ctx.arc(sx + b.w/2, b.y + b.h/2, 3, 0, Math.PI * 2);
ctx.fill();
} else {
ctx.fillStyle = '#ffeb3b';
ctx.fillRect(sx, b.y, b.w, b.h);
ctx.fillStyle = '#fff';
ctx.fillRect(sx + 1, b.y + 1, b.w - 2, b.h - 2);
}
}
// Enemy bullets
for (const b of enemyBullets) {
const sx = b.x - cameraX;
if (sx < -10 || sx > GAME_W + 10) continue;
ctx.fillStyle = '#f44336';
ctx.beginPath();
ctx.arc(sx + 3, b.y + 3, 4, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#ff8a80';
ctx.beginPath();
ctx.arc(sx + 3, b.y + 3, 2, 0, Math.PI * 2);
ctx.fill();
}
}
function drawExplosions() {
for (let i = explosions.length - 1; i >= 0; i--) {
const e = explosions[i];
const sx = e.x - cameraX;
const progress = e.timer / e.maxTimer;
const r = e.radius * (1 - progress * 0.5);
ctx.globalAlpha = progress;
ctx.fillStyle = '#ff6f00';
ctx.beginPath(); ctx.arc(sx, e.y, r, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#ffeb3b';
ctx.beginPath(); ctx.arc(sx, e.y, r * 0.6, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#fff';
ctx.beginPath(); ctx.arc(sx, e.y, r * 0.2, 0, Math.PI * 2); ctx.fill();
ctx.globalAlpha = 1;
e.timer--;
if (e.timer <= 0) explosions.splice(i, 1);
}
}
function drawParticles() {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
const sx = p.x - cameraX;
ctx.globalAlpha = p.life / p.maxLife;
ctx.fillStyle = p.color;
ctx.fillRect(sx, p.y, p.size, p.size);
p.x += p.vx; p.y += p.vy; p.vy += 0.1; p.life--;
if (p.life <= 0) particles.splice(i, 1);
}
ctx.globalAlpha = 1;
}
function drawHUD() {
// Score
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(0, 0, GAME_W, 36);
ctx.fillStyle = '#fff';
ctx.font = 'bold 16px monospace';
ctx.fillText(`SCORE: ${score.toString().padStart(8, '0')}`, 10, 24);
ctx.fillText(`HI: ${hiScore.toString().padStart(8, '0')}`, 220, 24);
// Lives
ctx.fillStyle = '#e53935';
for (let i = 0; i < lives; i++) {
ctx.fillRect(460 + i * 22, 10, 14, 16);
ctx.fillStyle = '#ffcc80';
ctx.fillRect(463 + i * 22, 6, 8, 8);
ctx.fillStyle = '#e53935';
}
// Weapon
ctx.fillStyle = '#ffeb3b';
ctx.font = '12px monospace';
const wNames = { normal: 'NORMAL', spread: 'SPREAD', rapid: 'RAPID', laser: 'LASER' };
ctx.fillText(`WEAPON: ${wNames[player.weapon]}`, 580, 24);
// Weapon timer
if (player.weaponTimer > 0 && player.weapon !== 'normal') {
ctx.fillStyle = '#333';
ctx.fillRect(580, 28, 80, 4);
ctx.fillStyle = '#ffeb3b';
ctx.fillRect(580, 28, 80 * (player.weaponTimer / 600), 4);
}
// Progress bar
ctx.fillStyle = '#333';
ctx.fillRect(GAME_W - 160, 14, 150, 8);
ctx.fillStyle = '#4caf50';
ctx.fillRect(GAME_W - 160, 14, 150 * Math.min(1, cameraX / (LEVEL_W - GAME_W)), 8);
ctx.fillStyle = '#fff';
ctx.font = '8px monospace';
ctx.fillText('PROGRESS', GAME_W - 145, 12);
}
// --- Title Screen ---
function drawTitle() {
// Background
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, GAME_W, GAME_H);
// Animated background lines
ctx.strokeStyle = 'rgba(229, 57, 53, 0.1)';
ctx.lineWidth = 1;
for (let i = 0; i < 20; i++) {
const y = (i * 30 + frameCount * 0.5) % GAME_H;
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(GAME_W, y); ctx.stroke();
}
// Title
ctx.save();
const titleY = 100 + Math.sin(frameCount * 0.03) * 8;
ctx.fillStyle = '#e53935';
ctx.font = 'bold 72px monospace';
ctx.textAlign = 'center';
ctx.fillText('CONTRA', GAME_W / 2, titleY);
// Title shadow
ctx.fillStyle = '#b71c1c';
ctx.fillText('CONTRA', GAME_W / 2 + 3, titleY + 3);
// Subtitle
ctx.fillStyle = '#ffeb3b';
ctx.font = 'bold 20px monospace';
ctx.fillText('魂 斗 罗', GAME_W / 2, titleY + 40);
// Blinking text
if (frameCount % 60 < 40) {
ctx.fillStyle = '#fff';
ctx.font = '18px monospace';
ctx.fillText('PRESS ENTER TO START', GAME_W / 2, 280);
}
// Controls
ctx.fillStyle = '#90a4ae';
ctx.font = '13px monospace';
ctx.fillText('← → : MOVE ↑ : AIM UP ↓ : PRONE', GAME_W / 2, 340);
ctx.fillText('SPACE : JUMP Z : SHOOT X : BOMB', GAME_W / 2, 365);
// Hi-Score
ctx.fillStyle = '#ffeb3b';
ctx.font = '14px monospace';
ctx.fillText(`HI-SCORE: ${hiScore.toString().padStart(8, '0')}`, GAME_W / 2, 420);
ctx.restore();
}
function drawGameOver() {
ctx.fillStyle = 'rgba(0,0,0,0.7)';
ctx.fillRect(0, 0, GAME_W, GAME_H);
ctx.save();
ctx.textAlign = 'center';
ctx.fillStyle = '#e53935';
ctx.font = 'bold 56px monospace';
ctx.fillText('GAME OVER', GAME_W / 2, GAME_H / 2 - 30);
ctx.fillStyle = '#fff';
ctx.font = '20px monospace';
ctx.fillText(`FINAL SCORE: ${score.toString().padStart(8, '0')}`, GAME_W / 2, GAME_H / 2 + 20);
if (frameCount % 60 < 40) {
ctx.fillStyle = '#ffeb3b';
ctx.font = '16px monospace';
ctx.fillText('PRESS ENTER TO CONTINUE', GAME_W / 2, GAME_H / 2 + 70);
}
ctx.restore();
}
function drawWin() {
ctx.fillStyle = 'rgba(0,0,0,0.7)';
ctx.fillRect(0, 0, GAME_W, GAME_H);
ctx.save();
ctx.textAlign = 'center';
ctx.fillStyle = '#4caf50';
ctx.font = 'bold 48px monospace';
ctx.fillText('MISSION COMPLETE!', GAME_W / 2, GAME_H / 2 - 40);
ctx.fillStyle = '#ffeb3b';
ctx.font = 'bold 24px monospace';
ctx.fillText(`SCORE: ${score.toString().padStart(8, '0')}`, GAME_W / 2, GAME_H / 2 + 10);
ctx.fillStyle = '#fff';
ctx.font = '16px monospace';
ctx.fillText('THE ALIEN CORE HAS BEEN DESTROYED!', GAME_W / 2, GAME_H / 2 + 50);
if (frameCount % 60 < 40) {
ctx.fillStyle = '#90a4ae';
ctx.font = '14px monospace';
ctx.fillText('PRESS ENTER TO PLAY AGAIN', GAME_W / 2, GAME_H / 2 + 90);
}
ctx.restore();
}
// --- Update Functions ---
function updatePlayer() {
if (!player.alive) {
player.respawnTimer--;
if (player.respawnTimer <= 0) {
if (lives > 0) {
player.alive = true;
player.x = cameraX + 100;
player.y = 200;
player.vx = 0; player.vy = 0;
player.invincible = 120;
player.weapon = 'normal';
} else {
gameState = 'gameover';
if (score > hiScore) { hiScore = score; localStorage.setItem('contraHi', hiScore); }
}
}
return;
}
// Movement
const speed = 3;
if (keys['ArrowLeft'] || keys['KeyA']) { player.vx = -speed; player.dir = -1; }
else if (keys['ArrowRight'] || keys['KeyD']) { player.vx = speed; player.dir = 1; }
else { player.vx = 0; }
// Prone
player.prone = (keys['ArrowDown'] || keys['KeyS']) && player.onGround;
if (player.prone) { player.h = 20; player.vx *= 0.5; }
else { player.h = 40; }
// Jump
if ((keys['Space'] || keys['ArrowUp'] || keys['KeyW']) && player.onGround) {
player.vy = -10;
player.jumping = true;
player.onGround = false;
playSound('jump');
}
// Gravity
player.vy += GRAVITY;
player.x += player.vx;
player.y += player.vy;
// Resolve collisions
resolveCollision(player);
// Bounds
if (player.x < cameraX) player.x = cameraX;
if (player.y > GAME_H + 50) {
playerDie();
return;
}
// Shooting
if (player.shootTimer > 0) player.shootTimer--;
const cooldown = player.weapon === 'rapid' ? 4 : player.shootCooldown;
if (keys['KeyZ'] && player.shootTimer <= 0) {
shoot();
player.shootTimer = cooldown;
}
// Bomb (X key)
if (keys['KeyX'] && !player._bombUsed) {
player._bombUsed = true;
// Screen clear bomb
for (const e of enemies) {
if (e.alive && Math.abs(e.x - player.x) < 400) {
e.alive = false;
score += 100;
explosions.push({ x: e.x, y: e.y, radius: 30, timer: 20, maxTimer: 20 });
}
}
screenShake = 15;
playSound('explosion');
}
if (!keys['KeyX']) player._bombUsed = false;
// Invincibility
if (player.invincible > 0) player.invincible--;
// Weapon timer
if (player.weaponTimer > 0) {
player.weaponTimer--;
if (player.weaponTimer <= 0) player.weapon = 'normal';
}
// Animation
if (Math.abs(player.vx) > 0) {
player.animTimer++;
if (player.animTimer > 6) { player.animFrame++; player.animTimer = 0; }
}
// Camera
const targetCam = player.x - GAME_W * 0.35;
cameraX += (targetCam - cameraX) * 0.08;
cameraX = Math.max(0, Math.min(LEVEL_W - GAME_W, cameraX));
// Boss trigger
if (cameraX > LEVEL_W - GAME_W - 100 && !boss && !bossDefeated) {
spawnBoss();
playSound('boss');
}
}
function shoot() {
const bx = player.x + (player.dir > 0 ? player.w : -8);
const by = player.y + (player.prone ? 26 : 14);
let bvx = player.dir * 8;
let bvy = 0;
// Aim up
if (keys['ArrowUp'] && !player.onGround) {
bvy = -8;
bvx = player.dir * 4;
} else if (keys['ArrowUp'] && player.onGround && !player.prone) {
bvy = -8;
bvx = 0;
}
// Aim down (in air)
if (keys['ArrowDown'] && !player.onGround) {
bvy = 8;
bvx = player.dir * 4;
}
if (player.weapon === 'spread') {
playSound('spread');
for (let a = -0.3; a <= 0.3; a += 0.15) {
playerBullets.push({
x: bx, y: by, w: 6, h: 6,
vx: bvx * Math.cos(a) - bvy * Math.sin(a),
vy: bvx * Math.sin(a) + bvy * Math.cos(a),
type: 'spread', life: 60
});
}
} else if (player.weapon === 'laser') {
playSound('spread');
playerBullets.push({
x: bx, y: by, w: 30, h: 3,
vx: bvx * 1.5, vy: bvy * 1.5,
type: 'laser', life: 40
});
} else {
playSound('shoot');
playerBullets.push({
x: bx, y: by, w: 8, h: 3,
vx: bvx, vy: bvy,
type: 'normal', life: 80
});
}
player.shooting = true;
setTimeout(() => player.shooting = false, 80);
}
function playerDie() {
if (player.invincible > 0) return;
player.alive = false;
lives--;
player.respawnTimer = 90;
screenShake = 20;
playSound('hit');
explosions.push({ x: player.x, y: player.y + 20, radius: 25, timer: 25, maxTimer: 25 });
for (let i = 0; i < 15; i++) {
particles.push({
x: player.x + 14, y: player.y + 20,
vx: (Math.random() - 0.5) * 6, vy: (Math.random() - 0.5) * 6 - 3,
size: 2 + Math.random() * 3, color: ['#e53935','#ffeb3b','#ff9800'][Math.floor(Math.random()*3)],
life: 30 + Math.random() * 20, maxLife: 50
});
}
}
function updateBullets() {
// Player bullets
for (let i = playerBullets.length - 1; i >= 0; i--) {
const b = playerBullets[i];
b.x += b.vx; b.y += b.vy; b.life--;
if (b.life <= 0 || b.x < cameraX - 50 || b.x > cameraX + GAME_W + 50 || b.y < -20 || b.y > GAME_H + 20) {
playerBullets.splice(i, 1); continue;
}
// Hit enemies
for (const e of enemies) {
if (!e.alive) continue;
if (rectCollide(b, e)) {
e.hp--;
if (e.hp <= 0) {
e.alive = false;
score += e.type === 'turret' ? 300 : e.type === 'runner' ? 200 : 100;
explosions.push({ x: e.x + e.w/2, y: e.y + e.h/2, radius: 20, timer: 18, maxTimer: 18 });
playSound('explosion');
for (let j = 0; j < 8; j++) {
particles.push({
x: e.x + e.w/2, y: e.y + e.h/2,
vx: (Math.random()-0.5)*5, vy: (Math.random()-0.5)*5-2,
size: 2+Math.random()*2, color: ['#ff6f00','#ffeb3b','#fff'][Math.floor(Math.random()*3)],
life: 20+Math.random()*15, maxLife: 35
});
}
}
playerBullets.splice(i, 1); break;
}
}
// Hit boss
if (boss && boss.alive && rectCollide(b, boss)) {
boss.hp--;
if (boss.hp <= 0) {
boss.alive = false;
bossDefeated = true;
score += 5000;
screenShake = 30;
playSound('explosion');
for (let k = 0; k < 30; k++) {
explosions.push({
x: boss.x + Math.random() * boss.w,
y: boss.y + Math.random() * boss.h,
radius: 15 + Math.random() * 20,
timer: 20 + Math.random() * 20, maxTimer: 40
});
}
setTimeout(() => { gameState = 'win'; if (score > hiScore) { hiScore = score; localStorage.setItem('contraHi', hiScore); } }, 2000);
}
playerBullets.splice(i, 1);
playSound('hit');
}
// Hit platforms (walls only)
for (const p of platforms) {
if (p.type === 'wall' && rectCollide(b, p)) {
playerBullets.splice(i, 1); break;
}
}
}
// Enemy bullets
for (let i = enemyBullets.length - 1; i >= 0; i--) {
const b = enemyBullets[i];
b.x += b.vx; b.y += b.vy; b.life--;
if (b.life <= 0 || b.x < cameraX - 100 || b.x > cameraX + GAME_W + 100) {
enemyBullets.splice(i, 1); continue;
}
// Hit player
if (player.alive && rectCollide({ x: b.x, y: b.y, w: 6, h: 6 }, player)) {
playerDie();
enemyBullets.splice(i, 1);
}
}
}
function updateEnemies() {
for (const e of enemies) {
if (!e.alive) continue;
const dx = player.x - e.x;
const dist = Math.abs(dx);
if (e.type === 'soldier') {
// AI
if (dist < 350 && player.alive) {
e.dir = dx > 0 ? 1 : -1;
e.state = dist < 250 ? 'shoot' : 'chase';
} else {
e.state = 'patrol';
}
if (e.state === 'patrol') {
e.vx = e.dir * 0.8;
if (e.x <= e.patrol) e.dir = 1;
if (e.x >= e.patrolMax) e.dir = -1;
} else if (e.state === 'chase') {
e.vx = e.dir * 1.5;
} else {
e.vx = 0;
e.shootTimer--;
if (e.shootTimer <= 0 && dist < 400) {
e.shootTimer = 60 + Math.random() * 40;
const angle = Math.atan2(player.y - e.y, player.x - e.x);
enemyBullets.push({
x: e.x + 12, y: e.y + 14,
vx: Math.cos(angle) * 3, vy: Math.sin(angle) * 3,
life: 120
});
}
}
e.vy += GRAVITY;
e.x += e.vx;
e.y += e.vy;
resolveCollision(e);
e.animFrame++;
// Touch damage
if (player.alive && rectCollide(player, e)) playerDie();
} else if (e.type === 'turret') {
if (player.alive) {
e.angle = Math.atan2(player.y - e.y, player.x - e.x);
e.dir = dx > 0 ? 1 : -1;
}
e.shootTimer--;
if (e.shootTimer <= 0 && dist < 500) {
e.shootTimer = 80;
enemyBullets.push({
x: e.x + 16 + Math.cos(e.angle) * 24,
y: e.y + 14 + Math.sin(e.angle) * 24,
vx: Math.cos(e.angle) * 4, vy: Math.sin(e.angle) * 4,
life: 100
});
}
} else if (e.type === 'runner') {
if (dist < 500 && player.alive) {
e.dir = dx > 0 ? 1 : -1;
e.vx = e.dir * 3.5;
}
e.vy += GRAVITY;
e.x += e.vx;
e.y += e.vy;
resolveCollision(e);
e.animFrame++;
if (player.alive && rectCollide(player, e)) playerDie();
}
}
}
function updateBoss() {
if (!boss || !boss.alive) return;
boss.shootTimer--;
boss.moveTimer++;
// Movement
boss.y = GAME_H - 200 + Math.sin(boss.moveTimer * 0.02) * 40;
// Attack patterns
if (boss.shootTimer <= 0) {
boss.phase = (boss.phase + 1) % 3;
if (boss.phase === 0) {
// Spread shot
for (let a = -0.6; a <= 0.6; a += 0.2) {
enemyBullets.push({
x: boss.x + 20, y: boss.y + boss.h / 2,
vx: Math.cos(a + Math.PI) * 4, vy: Math.sin(a + Math.PI) * 4,
life: 150
});
}
boss.shootTimer = 40;
} else if (boss.phase === 1) {
// Aimed shot
const angle = Math.atan2(player.y - boss.y - boss.h/2, player.x - boss.x);
for (let i = 0; i < 3; i++) {
setTimeout(() => {
if (boss && boss.alive) {
enemyBullets.push({
x: boss.x + 20, y: boss.y + boss.h / 2,
vx: Math.cos(angle) * 5, vy: Math.sin(angle) * 5,
life: 120
});
}
}, i * 100);
}
boss.shootTimer = 60;
} else {
// Rain of bullets
for (let i = 0; i < 5; i++) {
enemyBullets.push({
x: boss.x + Math.random() * boss.w,
y: boss.y + boss.h,
vx: (Math.random() - 0.5) * 3,
vy: 2 + Math.random() * 3,
life: 120
});
}
boss.shootTimer = 50;
}
playSound('shoot');
}
// Touch damage
if (player.alive && rectCollide(player, boss)) playerDie();
}
function updatePowerups() {
for (const p of powerups) {
if (!p.alive) continue;
if (player.alive && rectCollide(player, p)) {
p.alive = false;
playSound('powerup');
score += 500;
switch (p.type) {
case 'spread': player.weapon = 'spread'; player.weaponTimer = 600; break;
case 'rapid': player.weapon = 'rapid'; player.weaponTimer = 600; break;
case 'laser': player.weapon = 'laser'; player.weaponTimer = 600; break;
case 'life': lives = Math.min(lives + 1, 9); break;
case 'shield': player.invincible = 300; break;
}
for (let i = 0; i < 10; i++) {
particles.push({
x: p.x + 12, y: p.y + 12,
vx: (Math.random()-0.5)*4, vy: (Math.random()-0.5)*4,
size: 2+Math.random()*2, color: '#ffeb3b',
life: 20+Math.random()*10, maxLife: 30
});
}
}
}
}
// --- Game Init ---
function initGame() {
score = 0; lives = 3; cameraX = 0;
bossDefeated = false; boss = null;
playerBullets = []; enemyBullets = [];
explosions = []; particles = [];
player.x = 100; player.y = 300;
player.vx = 0; player.vy = 0;
player.alive = true; player.invincible = 60;
player.weapon = 'normal'; player.weaponTimer = 0;
player.dir = 1; player.prone = false;
generateLevel();
spawnEnemies();
}
// --- Main Loop ---
function gameLoop() {
frameCount++;
if (gameState === 'title') {
drawTitle();
if (keys['Enter'] || keys['Space']) {
gameState = 'playing';
initGame();
ensureAudio();
}
} else if (gameState === 'playing') {
// Screen shake
ctx.save();
if (screenShake > 0) {
ctx.translate(
(Math.random() - 0.5) * screenShake,
(Math.random() - 0.5) * screenShake
);
screenShake--;
}
drawBackground();
drawPlatforms();
drawPowerups();
updatePlayer();
updateEnemies();
updateBoss();
updateBullets();
updatePowerups();
// Draw enemies
for (const e of enemies) {
if (!e.alive) continue;
const sx = e.x - cameraX;
if (sx < -50 || sx > GAME_W + 50) continue;
if (e.type === 'soldier') drawSoldier(e);
else if (e.type === 'turret') drawTurret(e);
else if (e.type === 'runner') drawRunner(e);
}
drawBoss(boss);
drawBullets();
drawExplosions();
drawParticles();
// Draw player
if (player.alive) {
if (player.invincible > 0 && frameCount % 4 < 2) {
// Flash when invincible
} else {
drawPixelChar(player.x, player.y, player.dir, player.animFrame, player.prone, player.shooting);
}
}
drawHUD();
ctx.restore();
} else if (gameState === 'gameover') {
drawBackground();
drawPlatforms();
drawGameOver();
if (keys['Enter']) { gameState = 'title'; keys['Enter'] = false; }
} else if (gameState === 'win') {
drawBackground();
drawPlatforms();
drawWin();
if (keys['Enter']) { gameState = 'title'; keys['Enter'] = false; }
}
requestAnimationFrame(gameLoop);
}
// --- Start ---
gameLoop();
</script>
</body>
</html>

