想起小时候玩的一款游戏,那时候还是游戏机玩的,魂斗罗,真的那时候玩的超级有意思,每天放学自己玩。
试着写了一版本地运行的纯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>













