乐于分享
好东西不私藏

【源代码】经典:二战合金弹头飞机小游戏

【源代码】经典:二战合金弹头飞机小游戏
操作:WASD / 方向键控制移动,鼠标瞄准方向,鼠标按住射击双管机枪空格键投掷炸弹(范围伤害),初始 3 枚,最多 5 枚引擎尾焰动画、螺旋桨旋转、受伤闪烁、无敌帧
简单运行方法保存代码,格式html,直接运行
完整步骤:
  1. 1.新建文本文档
    在电脑桌面空白处右键 → 新建 → 文本文档(.txt)
  2. 2.复制全部代码
    把下面一整段从<!DOCTYPE html> 到</html> 的所有代码完整复制,粘贴进新建的记事本里。
  3. 3.保存并改后缀名
  • 记事本左上角:文件 → 保存
  • 点击左上角「文件 - 另存为」:
    • 文件名填:contra.html
    • 4.保存类型:选择所有文件
    • 编码选:UTF-8(关键,防止文字乱码)
  1. 5.运行游戏
    保存后桌面会出现contra.html 文件,双击文件,会自动用浏览器(Chrome/Edge/360 浏览器)打开,直接游玩

<!DOCTYPE html>

<html lang="zh-CN">

<head>

<meta charset="UTF-8">

<meta name="viewport" content="width=device-width, initial-scale=1.0">

<title>Sky Thunder - WWII Air Combat</title>

<style>

*{margin:0;padding:0;box-sizing:border-box}

html,body{width:100%;height:100%;overflow:hidden;background:#000}

canvas{display:block;margin:0 auto;cursor:crosshair;image-rendering:pixelated}

</style>

</head>

<body>

<canvas id="gameCanvas"></canvas>

<script>

const canvas=document.getElementById('gameCanvas');

const ctx=canvas.getContext('2d');

function resizeCanvas(){const r=16/10;let w=window.innerWidth,h=window.innerHeight;if(w/h>r)w=h*r;else h=w/r;canvas.width=Math.floor(w);canvas.height=Math.floor(h)}

resizeCanvas();window.addEventListener('resize',resizeCanvas);

const GS={MENU:0,PLAYING:1,GAME_OVER:2};

const S=()=>canvas.width/1280;

const keys={};const mouse={x:0,y:0,down:false};

window.addEventListener('keydown',e=>{keys[e.key.toLowerCase()]=true;if(e.code==='Space')keys['space']=true;if(e.key===' '||e.code==='Space')e.preventDefault()});

window.addEventListener('keyup',e=>{keys[e.key.toLowerCase()]=false;if(e.code==='Space')keys['space']=false});

canvas.addEventListener('mousemove',e=>{const r=canvas.getBoundingClientRect();mouse.x=e.clientX-r.left;mouse.y=e.clientY-r.top});

canvas.addEventListener('mousedown',()=>{mouse.down=true});

canvas.addEventListener('mouseup',()=>{mouse.down=false});

canvas.addEventListener('contextmenu',e=>e.preventDefault());

function rand(a,b){return Math.random()*(b-a)+a}

function randInt(a,b){return Math.floor(rand(a,b+1))}

function dst(a,b){return Math.hypot(a.x-b.x,a.y-b.y)}

function lerp(a,b,t){return a+(b-a)*t}

function clamp(v,mn,mx){return Math.max(mn,Math.min(mx,v))}

function angTo(f,t){return Math.atan2(t.y-f.y,t.x-f.x)}

let audioCtx=null;

function initAudio(){if(!audioCtx)audioCtx=new(window.AudioContext||window.webkitAudioContext)()}

function playSound(type){

  if(!audioCtx)return;try{

  const o=audioCtx.createOscillator(),g=audioCtx.createGain();

  o.connect(g);g.connect(audioCtx.destination);const t=audioCtx.currentTime;

  if(type==='shoot'){o.type='square';o.frequency.setValueAtTime(800,t);o.frequency.exponentialRampToValueAtTime(200,t+0.08);g.gain.setValueAtTime(0.06,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.08);o.start(t);o.stop(t+0.08)}

  else if(type==='explosion'){o.type='sawtooth';o.frequency.setValueAtTime(150,t);o.frequency.exponentialRampToValueAtTime(30,t+0.4);g.gain.setValueAtTime(0.12,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.4);o.start(t);o.stop(t+0.4)}

  else if(type==='bigExplosion'){o.type='sawtooth';o.frequency.setValueAtTime(80,t);o.frequency.exponentialRampToValueAtTime(15,t+0.8);g.gain.setValueAtTime(0.15,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.8);o.start(t);o.stop(t+0.8)}

  else if(type==='hit'){o.type='triangle';o.frequency.setValueAtTime(400,t);o.frequency.exponentialRampToValueAtTime(100,t+0.1);g.gain.setValueAtTime(0.08,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.1);o.start(t);o.stop(t+0.1)}

  else if(type==='bomb'){o.type='sawtooth';o.frequency.setValueAtTime(200,t);o.frequency.exponentialRampToValueAtTime(20,t+1);g.gain.setValueAtTime(0.15,t);g.gain.exponentialRampToValueAtTime(0.001,t+1);o.start(t);o.stop(t+1)}

  else if(type==='powerup'){o.type='sine';o.frequency.setValueAtTime(400,t);o.frequency.exponentialRampToValueAtTime(800,t+0.2);g.gain.setValueAtTime(0.08,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.3);o.start(t);o.stop(t+0.3)}

  }catch(e){}

}

// Particles

class Particle{

  constructor(x,y,vx,vy,life,size,color,type='circle'){this.x=x;this.y=y;this.vx=vx;this.vy=vy;this.life=life;this.maxLife=life;this.size=size;this.color=color;this.type=type;this.alpha=1;this.rot=rand(0,6.28);this.rs=rand(-0.1,0.1)}

  update(dt){this.x+=this.vx*dt;this.y+=this.vy*dt;this.life-=dt;this.alpha=Math.max(0,this.life/this.maxLife);this.rot+=this.rs;if(this.type==='spark')this.vy+=200*dt;if(this.type==='smoke'){this.size+=15*dt;this.vx*=0.98;this.vy*=0.98}}

  draw(c,s){c.save();c.globalAlpha=this.alpha;c.translate(this.x*s,this.y*s);c.rotate(this.rot);c.fillStyle=this.color;c.beginPath();c.arc(0,0,this.size*s,0,Math.PI*2);c.fill();c.restore()}

}

let particles=[];

function spawnExplosion(x,y,cnt,szR,spR,colors,life){

  for(let i=0;i<cnt;i++){const a=rand(0,6.28),sp=rand(spR[0],spR[1]),sz=rand(szR[0],szR[1]),cl=colors[randInt(0,colors.length-1)],tp=Math.random()<0.3?'spark':(Math.random()<0.3?'debris':'circle');particles.push(new Particle(x,y,Math.cos(a)*sp,Math.sin(a)*sp,rand(life*0.5,life),sz,cl,tp))}

  for(let i=0;i<cnt*0.3;i++){const a=rand(0,6.28),sp=rand(10,50);particles.push(new Particle(x,y,Math.cos(a)*sp,Math.sin(a)*sp,rand(0.5,1.2),rand(3,8),'rgba(80,80,80,0.6)','smoke'))}

}

function spawnSmallExplosion(x,y){spawnExplosion(x,y,12,[1,4],[30,150],['#ff6600','#ffaa00','#ff3300','#ffdd00'],0.5)}

function spawnBigExplosion(x,y){spawnExplosion(x,y,40,[2,8],[50,300],['#ff4400','#ff8800','#ffcc00','#ff2200','#fff'],1.0);spawnExplosion(x,y,20,[3,10],[20,100],['#333','#555','#777'],1.5)}

function spawnMuzzleFlash(x,y,a){for(let i=0;i<5;i++){const sp=rand(-0.3,0.3),spd=rand(100,300);particles.push(new Particle(x,y,Math.cos(a+sp)*spd,Math.sin(a+sp)*spd,rand(0.05,0.15),rand(1,3),'#ffff88','spark'))}}

// Background

class Background{

  constructor(){this.clouds=[];this.ground=[];for(let i=0;i<8;i++)this.clouds.push({x:rand(0,1280),y:rand(0,800),w:rand(80,200),h:rand(30,60),sp:rand(15,40),op:rand(0.1,0.3)});for(let i=0;i<30;i++)this.ground.push({x:rand(0,1280),y:rand(0,800),tp:randInt(0,3),sz:rand(2,8),sp:rand(60,120)})}

  update(dt){this.clouds.forEach(c=>{c.y+=c.sp*dt;if(c.y>850){c.y=-80;c.x=rand(0,1280)}});this.ground.forEach(d=>{d.y+=d.sp*dt;if(d.y>820){d.y=-20;d.x=rand(0,1280)}})}

  draw(c,s){

    const g=c.createLinearGradient(0,0,0,canvas.height);g.addColorStop(0,'#0a0a1a');g.addColorStop(0.3,'#1a2a3a');g.addColorStop(0.6,'#2a4a3a');g.addColorStop(1,'#3a5a3a');c.fillStyle=g;c.fillRect(0,0,canvas.width,canvas.height);

    c.fillStyle='rgba(34,60,34,0.3)';c.fillRect(0,canvas.height*0.4,canvas.width,canvas.height*0.6);

    this.ground.forEach(d=>{c.fillStyle='rgba(50,80,40,0.4)';if(d.tp===0)c.fillRect(d.x*s,d.y*s,d.sz*3*s,d.sz*s);else if(d.tp===1){c.fillStyle='rgba(60,50,30,0.3)';c.fillRect(d.x*s,d.y*s,d.sz*s,d.sz*4*s)}else if(d.tp===2){c.fillStyle='rgba(40,70,40,0.5)';c.beginPath();c.arc(d.x*s,d.y*s,d.sz*2*s,0,6.28);c.fill()}});

    this.clouds.forEach(cl=>{c.fillStyle=`rgba(200,210,220,${cl.op})`;c.beginPath();c.ellipse(cl.x*s,cl.y*s,cl.w*s*0.5,cl.h*s*0.5,0,0,6.28);c.fill();c.beginPath();c.ellipse((cl.x-cl.w*0.25)*s,(cl.y+5)*s,cl.w*s*0.35,cl.h*s*0.4,0,0,6.28);c.fill()})

  }

}

// Player

class Player{

  constructor(){this.x=640;this.y=650;this.w=48;this.h=56;this.speed=280;this.hp=60;this.maxHp=60;this.shootTimer=0;this.shootInterval=0.12;this.bombCount=3;this.bombTimer=0;this.inv=0;this.alive=true;this.ef=0;this.tilt=0}

  update(dt){

    if(!this.alive)return;let dx=0,dy=0;

    if(keys['w']||keys['arrowup'])dy=-1;if(keys['s']||keys['arrowdown'])dy=1;

    if(keys['a']||keys['arrowleft'])dx=-1;if(keys['d']||keys['arrowright'])dx=1;

    if(dx||dy){const l=Math.hypot(dx,dy);dx/=l;dy/=l}

    this.x+=dx*this.speed*dt;this.y+=dy*this.speed*dt;

    this.x=clamp(this.x,30,1250);this.y=clamp(this.y,30,770);

    this.tilt=lerp(this.tilt,dx*0.15,5*dt);

    this.shootTimer-=dt;if(mouse.down&&this.shootTimer<=0){this.shoot();this.shootTimer=this.shootInterval}

    if((keys[' ']||keys['space'])&&this.bombTimer<=0){this.dropBomb()}

    if(this.inv>0)this.inv-=dt;this.ef+=dt*20;if(this.bombTimer>0)this.bombTimer-=dt

  }

  shoot(){const a=angTo({x:this.x,y:this.y},{x:mouse.x/S(),y:mouse.y/S()});game.bullets.push(new Bullet(this.x-8,this.y-20,a,600,10,'player'));game.bullets.push(new Bullet(this.x+8,this.y-20,a,600,10,'player'));spawnMuzzleFlash(this.x-8,this.y-25,a);spawnMuzzleFlash(this.x+8,this.y-25,a);playSound('shoot')}

  dropBomb(){if(this.bombCount>0){game.bullets.push(new Bomb(this.x,this.y+20));this.bombCount--;this.bombTimer=0.5;playSound('bomb')}}

  takeDamage(d){if(this.inv>0)return;this.hp-=d;this.inv=0.3;playSound('hit');if(this.hp<=0){this.hp=0;this.alive=false;spawnBigExplosion(this.x,this.y);playSound('bigExplosion')}}

  draw(c,s){

    if(!this.alive)return;if(this.inv>0&&Math.floor(this.inv*10)%2===0)return;

    c.save();c.translate(this.x*s,this.y*s);c.rotate(this.tilt);

    const fl=12+Math.sin(this.ef)*5;const fg=c.createLinearGradient(0,20*s,0,(20+fl)*s);fg.addColorStop(0,'#fff');fg.addColorStop(0.3,'#ffaa00');fg.addColorStop(1,'rgba(255,50,0,0)');c.fillStyle=fg;c.beginPath();c.moveTo(-5*s,20*s);c.lineTo(5*s,20*s);c.lineTo(1*s,(20+fl)*s);c.lineTo(-1*s,(20+fl)*s);c.closePath();c.fill();

    c.fillStyle='rgba(0,0,0,0.2)';c.beginPath();c.ellipse(3*s,5*s,22*s,8*s,0,0,6.28);c.fill();

    c.fillStyle='#4a6741';c.beginPath();c.moveTo(-40*s,5*s);c.lineTo(-8*s,-5*s);c.lineTo(-6*s,10*s);c.lineTo(-35*s,12*s);c.closePath();c.fill();c.beginPath();c.moveTo(40*s,5*s);c.lineTo(8*s,-5*s);c.lineTo(6*s,10*s);c.lineTo(35*s,12*s);c.closePath();c.fill();

    c.fillStyle='#5a7a51';c.fillRect(-38*s,6*s,30*s,2*s);c.fillRect(8*s,6*s,30*s,2*s);

    c.fillStyle='#5a7a51';c.beginPath();c.moveTo(0,-28*s);c.lineTo(-8*s,-10*s);c.lineTo(-7*s,22*s);c.lineTo(7*s,22*s);c.lineTo(8*s,-10*s);c.closePath();c.fill();

    c.fillStyle='#88bbee';c.beginPath();c.ellipse(0,-8*s,4*s,7*s,0,0,6.28);c.fill();c.fillStyle='rgba(255,255,255,0.3)';c.beginPath();c.ellipse(-1*s,-10*s,2*s,4*s,-0.2,0,6.28);c.fill();

    c.fillStyle='#3a5731';c.beginPath();c.moveTo(0,-28*s);c.lineTo(-4*s,-20*s);c.lineTo(4*s,-20*s);c.closePath();c.fill();

    c.fillStyle='#4a6741';c.beginPath();c.moveTo(-12*s,20*s);c.lineTo(-3*s,15*s);c.lineTo(-2*s,22*s);c.lineTo(-10*s,24*s);c.closePath();c.fill();c.beginPath();c.moveTo(12*s,20*s);c.lineTo(3*s,15*s);c.lineTo(2*s,22*s);c.lineTo(10*s,24*s);c.closePath();c.fill();

    c.fillStyle='#333';c.save();c.translate(0,-28*s);c.rotate(Date.now()*0.03);c.fillRect(-14*s,-1.5*s,28*s,3*s);c.restore();

    c.restore()

  }

}

// Bullet

class Bullet{

  constructor(x,y,a,spd,dmg,owner,type='normal'){this.x=x;this.y=y;this.vx=Math.cos(a)*spd;this.vy=Math.sin(a)*spd;this.dmg=dmg;this.owner=owner;this.type=type;this.alive=true;this.trail=[];this.sz=owner==='player'?3:4}

  update(dt){this.trail.push({x:this.x,y:this.y,a:1});if(this.trail.length>6)this.trail.shift();this.trail.forEach(t=>t.a-=dt*5);this.x+=this.vx*dt;this.y+=this.vy*dt;if(this.x<-50||this.x>1330||this.y<-50||this.y>850)this.alive=false}

  draw(c,s){this.trail.forEach(t=>{if(t.a<=0)return;c.globalAlpha=t.a*0.5;c.fillStyle=this.owner==='player'?'#ffdd44':'#ff4444';c.beginPath();c.arc(t.x*s,t.y*s,this.sz*0.5*s,0,6.28);c.fill()});c.globalAlpha=1;c.fillStyle=this.owner==='player'?'#ffff88':'#ff6666';c.beginPath();c.arc(this.x*s,this.y*s,this.sz*s,0,6.28);c.fill();c.fillStyle='#fff';c.beginPath();c.arc(this.x*s,this.y*s,this.sz*0.4*s,0,6.28);c.fill()}

}

// Bomb

class Bomb{

  constructor(x,y){this.x=x;this.y=y;this.vy=150;this.vx=0;this.dmg=80;this.owner='player';this.type='bomb';this.alive=true;this.timer=1.5;this.sz=6;this.trail=[]}

  update(dt){this.trail.push({x:this.x,y:this.y,a:1});if(this.trail.length>15)this.trail.shift();this.trail.forEach(t=>t.a-=dt*2);this.vy+=300*dt;this.y+=this.vy*dt;this.timer-=dt;if(this.timer<=0||this.y>850)this.explode()}

  explode(){this.alive=false;spawnBigExplosion(this.x,this.y);playSound('bigExplosion');game.screenShake=0.4;game.enemies.forEach(e=>{if(e.alive&&dst(this,e)<150)e.takeDamage(this.dmg)});game.squadrons.forEach(sq=>sq.bombers.forEach(e=>{if(e.alive&&dst(this,e)<150)e.takeDamage(this.dmg)}))}

  draw(c,s){this.trail.forEach(t=>{if(t.a<=0)return;c.globalAlpha=t.a*0.5;c.fillStyle='#ff8844';c.beginPath();c.arc(t.x*s,t.y*s,4*s,0,6.28);c.fill()});c.globalAlpha=1;c.fillStyle='#222';c.beginPath();c.ellipse(this.x*s,this.y*s,5*s,8*s,0,0,6.28);c.fill();c.fillStyle='#444';c.beginPath();c.ellipse(this.x*s,(this.y-3)*s,3*s,3*s,0,0,6.28);c.fill();c.fillStyle='#555';c.fillRect((this.x-6)*s,(this.y-7)*s,12*s,3*s);if(this.timer<1&&Math.floor(this.timer*8)%2===0){c.fillStyle='#f00';c.beginPath();c.arc(this.x*s,this.y*s,3*s,0,6.28);c.fill()}}

}

// Enemy

class Enemy{

  constructor(x,y,type){

    this.x=x;this.y=y;this.type=type;this.alive=true;this.ft=0;this.st=rand(1,3);

    if(type==='tank'){this.w=40;this.h=30;this.hp=30;this.mhp=30;this.speed=rand(30,60);this.score=100;this.dir=Math.random()<0.5?-1:1;this.mp=randInt(0,2);this.mt=0}

    else if(type==='warship'){this.w=70;this.h=35;this.hp=80;this.mhp=80;this.speed=rand(20,40);this.score=300;this.dir=Math.random()<0.5?-1:1;this.si=2}

    else if(type==='fighter'){this.w=36;this.h=40;this.hp=20;this.mhp=20;this.speed=rand(100,180);this.score=150;this.pat=randInt(0,2);this.pt=0;this.si=1.5}

    else if(type==='bomber'){this.w=50;this.h=45;this.hp=50;this.mhp=50;this.speed=rand(60,90);this.score=250;this.si=2.5}

  }

  update(dt){

    this.ft-=dt;this.st-=dt;

    if(this.type==='tank'){

      this.mt+=dt;this.x+=this.dir*this.speed*dt;

      if(this.mp===1)this.y+=Math.sin(this.mt*2)*20*dt;

      this.y+=30*dt;if(this.x<50||this.x>1230)this.dir*=-1;

      if(this.st<=0){const a=angTo(this,game.player);game.bullets.push(new Bullet(this.x,this.y-10,a,280,15,'enemy','tank'));this.st=rand(1.2,2.5)}

    }else if(this.type==='warship'){

      this.x+=this.dir*this.speed*dt;this.y+=15*dt;if(this.x<80||this.x>1200)this.dir*=-1;

      if(this.st<=0){for(let i=-1;i<=1;i++){const a=angTo(this,game.player)+i*0.2;game.bullets.push(new Bullet(this.x,this.y,a,230,18,'enemy','ship'))}this.st=1.5}

    }else if(this.type==='fighter'){

      this.pt+=dt;

      if(this.pat===0){this.y+=this.speed*dt;this.x+=Math.sin(this.pt*3)*150*dt}

      else if(this.pat===1){this.y+=this.speed*0.7*dt;this.x+=Math.cos(this.pt*2)*200*dt}

      else{const a=angTo(this,game.player);this.x+=Math.cos(a)*this.speed*0.5*dt;this.y+=this.speed*0.6*dt}

      if(this.st<=0){const a=angTo(this,game.player);game.bullets.push(new Bullet(this.x,this.y+15,a,380,12,'enemy','normal'));this.st=1.0}

    }else if(this.type==='bomber'){

      this.y+=this.speed*dt;this.x+=Math.sin(this.y*0.01)*40*dt;

      if(this.st<=0){for(let i=0;i<3;i++)game.bullets.push(new Bullet(this.x+(i-1)*15,this.y+20,Math.PI/2,230,15,'enemy','bomb'));this.st=1.8}

    }

    if(this.y>850||this.x<-100||this.x>1380)this.alive=false

  }

  takeDamage(d){this.hp-=d;this.ft=0.1;if(this.hp<=0){this.alive=false;if(this.type==='warship'||this.type==='bomber'){spawnBigExplosion(this.x,this.y);playSound('bigExplosion')}else{spawnSmallExplosion(this.x,this.y);playSound('explosion')}game.score+=this.score;game.kills++;if(Math.random()<0.15)game.powerups.push(new PowerUp(this.x,this.y))}}

  draw(c,s){

    c.save();c.translate(this.x*s,this.y*s);if(this.ft>0)c.globalAlpha=0.7+Math.sin(this.ft*60)*0.3;

    if(this.type==='tank')this.drawTank(c,s);else if(this.type==='warship')this.drawWarship(c,s);else if(this.type==='fighter')this.drawFighter(c,s);else this.drawBomber(c,s);

    c.restore();

    if(this.hp<this.mhp){const bw=this.w*s,bh=3*s,bx=(this.x-this.w/2)*s,by=(this.y-this.h/2-8)*s;c.fillStyle='#333';c.fillRect(bx,by,bw,bh);c.fillStyle=this.hp>this.mhp*0.3?'#4f4':'#f44';c.fillRect(bx,by,bw*(this.hp/this.mhp),bh)}

  }

  drawTank(c,s){

    c.fillStyle='#5a5a3a';c.fillRect(-18*s,-10*s,36*s,20*s);

    c.fillStyle='#3a3a2a';c.fillRect(-20*s,-12*s,4*s,24*s);c.fillRect(16*s,-12*s,4*s,24*s);

    c.fillStyle='#4a4a3a';for(let i=0;i<5;i++){c.fillRect(-20*s,(-10+i*5)*s,4*s,2*s);c.fillRect(16*s,(-10+i*5)*s,4*s,2*s)}

    c.fillStyle='#6a6a4a';c.beginPath();c.arc(0,0,8*s,0,6.28);c.fill();

    const a=angTo(this,game.player);c.save();c.rotate(a);c.fillStyle='#4a4a3a';c.fillRect(0,-2*s,20*s,4*s);c.restore();

    c.fillStyle='#7a7a5a';c.beginPath();c.arc(0,-2*s,4*s,0,6.28);c.fill()

  }

  drawWarship(c,s){

    c.fillStyle='#4a4a5a';c.beginPath();c.moveTo(-35*s,0);c.lineTo(-25*s,-12*s);c.lineTo(25*s,-12*s);c.lineTo(35*s,0);c.lineTo(25*s,12*s);c.lineTo(-25*s,12*s);c.closePath();c.fill();

    c.fillStyle='#5a5a6a';c.fillRect(-20*s,-8*s,40*s,16*s);

    c.fillStyle='#6a6a7a';c.fillRect(-8*s,-6*s,16*s,12*s);

    c.fillStyle='#3a3a4a';c.beginPath();c.arc(-15*s,0,5*s,0,6.28);c.fill();c.beginPath();c.arc(15*s,0,5*s,0,6.28);c.fill();

    c.fillStyle='#2a2a3a';c.fillRect(-22*s,-1.5*s,10*s,3*s);c.fillRect(12*s,-1.5*s,10*s,3*s);

    c.fillStyle='rgba(200,220,255,0.3)';c.beginPath();c.moveTo(-25*s,14*s);c.lineTo(-15*s,20*s);c.lineTo(15*s,20*s);c.lineTo(25*s,14*s);c.closePath();c.fill()

  }

  drawFighter(c,s){

    c.fillStyle='#8a3030';c.beginPath();c.moveTo(-30*s,3*s);c.lineTo(-6*s,-3*s);c.lineTo(-5*s,8*s);c.lineTo(-25*s,10*s);c.closePath();c.fill();c.beginPath();c.moveTo(30*s,3*s);c.lineTo(6*s,-3*s);c.lineTo(5*s,8*s);c.lineTo(25*s,10*s);c.closePath();c.fill();

    c.fillStyle='#9a4040';c.beginPath();c.moveTo(0,-20*s);c.lineTo(-6*s,-5*s);c.lineTo(-5*s,18*s);c.lineTo(5*s,18*s);c.lineTo(6*s,-5*s);c.closePath();c.fill();

    c.fillStyle='#445566';c.beginPath();c.ellipse(0,-5*s,3*s,5*s,0,0,6.28);c.fill();

    c.fillStyle='#222';c.fillRect(-8*s,-1*s,16*s,2*s);c.fillRect(-1*s,-8*s,2*s,16*s);

    c.fillStyle='#ff6600';c.beginPath();c.arc(0,18*s,3*s,0,6.28);c.fill()

  }

  drawBomber(c,s){

    c.fillStyle='#5a4a3a';c.beginPath();c.moveTo(-40*s,5*s);c.lineTo(-10*s,-5*s);c.lineTo(-8*s,10*s);c.lineTo(-35*s,12*s);c.closePath();c.fill();c.beginPath();c.moveTo(40*s,5*s);c.lineTo(10*s,-5*s);c.lineTo(8*s,10*s);c.lineTo(35*s,12*s);c.closePath();c.fill();

    c.fillStyle='#6a5a4a';c.beginPath();c.moveTo(0,-22*s);c.lineTo(-9*s,-5*s);c.lineTo(-8*s,20*s);c.lineTo(8*s,20*s);c.lineTo(9*s,-5*s);c.closePath();c.fill();

    c.fillStyle='#4a3a2a';[-28,-16,16,28].forEach(ex=>{c.beginPath();c.arc(ex*s,5*s,4*s,0,6.28);c.fill();c.fillStyle='#ff8844';c.beginPath();c.arc(ex*s,8*s,2*s,0,6.28);c.fill();c.fillStyle='#4a3a2a'});

    c.fillStyle='#556677';c.beginPath();c.ellipse(0,-10*s,4*s,6*s,0,0,6.28);c.fill();

    c.fillStyle='#5a4a3a';c.beginPath();c.moveTo(-10*s,18*s);c.lineTo(-3*s,14*s);c.lineTo(-2*s,22*s);c.lineTo(-8*s,23*s);c.closePath();c.fill();c.beginPath();c.moveTo(10*s,18*s);c.lineTo(3*s,14*s);c.lineTo(2*s,22*s);c.lineTo(8*s,23*s);c.closePath();c.fill()

  }

}

// BomberSquadron

class BomberSquadron{

  constructor(){this.bombers=[];const sx=rand(200,1080);for(let r=0;r<2;r++)for(let cl=0;cl<3;cl++){const b=new Enemy(sx+(cl-1)*70,-50-r*55,'bomber');b.speed=70;this.bombers.push(b)}this.active=true}

  update(dt){this.bombers.forEach(b=>{if(b.alive)b.update(dt)});if(this.bombers.every(b=>!b.alive))this.active=false}

  draw(c,s){this.bombers.forEach(b=>{if(b.alive)b.draw(c,s)})}

}

// PowerUp

class PowerUp{

  constructor(x,y){this.x=x;this.y=y;this.vy=50;this.type=Math.random()<0.5?'health':'bomb';this.alive=true;this.timer=0;this.sz=12}

  update(dt){this.y+=this.vy*dt;this.timer+=dt;if(this.y>850)this.alive=false;if(dst(this,game.player)<25){this.alive=false;if(this.type==='health')game.player.hp=Math.min(game.player.maxHp,game.player.hp+25);else game.player.bombCount=Math.min(5,game.player.bombCount+1);playSound('powerup')}}

  draw(c,s){c.save();c.translate(this.x*s,this.y*s);c.rotate(Math.sin(this.timer*3)*0.2);c.fillStyle=this.type==='health'?'rgba(0,255,100,0.2)':'rgba(255,200,0,0.2)';c.beginPath();c.arc(0,0,16*s,0,6.28);c.fill();c.fillStyle=this.type==='health'?'#4c4':'#ca0';c.fillRect(-8*s,-8*s,16*s,16*s);c.strokeStyle='#fff';c.lineWidth=1.5*s;c.strokeRect(-8*s,-8*s,16*s,16*s);c.fillStyle='#fff';c.font=`bold ${12*s}px Arial`;c.textAlign='center';c.textBaseline='middle';c.fillText(this.type==='health'?'+':'B',0,0);c.restore()}

}

// WaveManager

class WaveManager{

  constructor(){this.wave=0;this.timer=3;this.spt=0;this.we=0;this.wme=0;this.sqt=15;this.diff=1}

  update(dt){

    this.timer-=dt;this.sqt-=dt;

    if(this.timer<=0){this.wave++;this.diff=1+this.wave*0.15;this.wme=5+this.wave*2;this.we=0;this.timer=999;this.spt=0;game.waveAnnounceTimer=2}

    if(this.wave>0){this.spt-=dt;if(this.spt<=0&&this.we<this.wme){this.spawn();this.we++;this.spt=Math.max(0.5,2-this.wave*0.1)}if(this.we>=this.wme&&game.enemies.length===0)this.timer=3}

    if(this.sqt<=0&&this.wave>=2){game.squadrons.push(new BomberSquadron());this.sqt=rand(20,35)}

  }

  spawn(){

    const r=Math.random();let tp;

    if(this.wave<=2)tp=r<0.5?'tank':(r<0.8?'fighter':'warship');

    else tp=r<0.3?'tank':(r<0.55?'fighter':(r<0.8?'warship':'bomber'));

    const x=rand(80,1200),y=tp==='tank'?-30:(tp==='warship'?-40:-50);

    const e=new Enemy(x,y,tp);e.hp*=this.diff;e.mhp=e.hp;game.enemies.push(e)

  }

}

// HUD

function drawHUD(c,s){

  c.fillStyle='#fff';c.font=`bold ${18*s}px 'Courier New',monospace`;c.textAlign='left';

  c.fillText(`SCORE: ${game.score}`,15*s,30*s);c.font=`${14*s}px 'Courier New',monospace`;

  c.fillText(`WAVE: ${game.waves.wave}`,15*s,50*s);c.fillText(`KILLS: ${game.kills}`,15*s,68*s);

  const hx=canvas.width-220*s,hy=15*s,hw=200*s,hh=18*s;

  c.fillStyle='rgba(0,0,0,0.5)';c.fillRect(hx-2*s,hy-2*s,hw+4*s,hh+4*s);

  c.fillStyle='#333';c.fillRect(hx,hy,hw,hh);

  const hr=game.player.hp/game.player.maxHp;const hc=hr>0.5?'#4d4':(hr>0.25?'#da0':'#d33');

  c.fillStyle=hc;c.fillRect(hx,hy,hw*hr,hh);c.strokeStyle='#888';c.lineWidth=s;c.strokeRect(hx,hy,hw,hh);

  c.fillStyle='#fff';c.font=`bold ${12*s}px 'Courier New',monospace`;c.textAlign='center';

  c.fillText(`HP ${game.player.hp}/${game.player.maxHp}`,hx+hw/2,hy+13*s);

  c.textAlign='right';c.font=`${14*s}px 'Courier New',monospace`;c.fillStyle='#fc0';

  c.fillText('BOMBS: '+'\u25CF'.repeat(game.player.bombCount)+'\u25CB'.repeat(5-game.player.bombCount),canvas.width-15*s,55*s);

  if(game.waveAnnounceTimer>0){c.globalAlpha=Math.min(1,game.waveAnnounceTimer);c.fillStyle='#fc0';c.font=`bold ${36*s}px 'Courier New',monospace`;c.textAlign='center';c.fillText(`WAVE ${game.waves.wave}`,canvas.width/2,canvas.height*0.3);c.font=`${16*s}px 'Courier New',monospace`;c.fillStyle='#fff';c.fillText('INCOMING!',canvas.width/2,canvas.height*0.3+35*s);c.globalAlpha=1}

  c.globalAlpha=0.5;c.fillStyle='#aaa';c.font=`${10*s}px 'Courier New',monospace`;c.textAlign='center';c.fillText('WASD: Move | Mouse: Aim & Shoot | SPACE: Bomb',canvas.width/2,canvas.height-10*s);c.globalAlpha=1

}

// Menu

function drawMenu(c,s){

  const g=c.createLinearGradient(0,0,0,canvas.height);g.addColorStop(0,'#0a0a1e');g.addColorStop(0.5,'#1a2a3e');g.addColorStop(1,'#0a1a0a');c.fillStyle=g;c.fillRect(0,0,canvas.width,canvas.height);

  const t=Date.now()*0.001;

  for(let i=0;i<5;i++){c.globalAlpha=0.1;c.fillStyle='#556';const px=(200+i*200+Math.sin(t+i)*50)*s,py=(150+i*80+Math.cos(t*0.7+i)*30)*s;c.beginPath();c.moveTo(px,py-15*s);c.lineTo(px-20*s,py+5*s);c.lineTo(px+20*s,py+5*s);c.closePath();c.fill()}

  c.globalAlpha=1;

  c.fillStyle='rgba(0,0,0,0.5)';c.font=`bold ${56*s}px 'Courier New',monospace`;c.textAlign='center';c.textBaseline='middle';c.fillText('SKY THUNDER',canvas.width/2+3*s,canvas.height*0.22+3*s);

  c.fillStyle='#ffcc00';c.fillText('SKY THUNDER',canvas.width/2,canvas.height*0.22);

  c.fillStyle='#aaa';c.font=`${18*s}px 'Courier New',monospace`;c.fillText('W W W   A I R   C O M B A T',canvas.width/2,canvas.height*0.30);

  c.strokeStyle='#ffcc00';c.lineWidth=2*s;c.beginPath();c.moveTo(canvas.width*0.25,canvas.height*0.35);c.lineTo(canvas.width*0.75,canvas.height*0.35);c.stroke();

  drawMenuPlane(c,canvas.width/2,canvas.height*0.48,s*2.5,t);

  const bw=280*s,bh=50*s,bx=canvas.width/2-bw/2,by=canvas.height*0.65;

  const p=Math.sin(t*3)*0.1+0.9;c.fillStyle=`rgba(255,200,0,${0.15*p})`;c.fillRect(bx-5*s,by-5*s,bw+10*s,bh+10*s);

  c.fillStyle='#fc0';c.fillRect(bx,by,bw,bh);c.fillStyle='#000';c.font=`bold ${22*s}px 'Courier New',monospace`;c.fillText('START MISSION',canvas.width/2,by+bh/2);

  c.fillStyle='#888';c.font=`${13*s}px 'Courier New',monospace`;

  c.fillText('WASD - Move Aircraft',canvas.width/2,canvas.height*0.78);c.fillText('Mouse - Aim & Fire',canvas.width/2,canvas.height*0.82);c.fillText('SPACE - Drop Bomb',canvas.width/2,canvas.height*0.86);c.fillText('Destroy tanks, warships & enemy aircraft!',canvas.width/2,canvas.height*0.92);

  game.menuBtn={x:bx,y:by,w:bw,h:bh}

}

function drawMenuPlane(c,cx,cy,s,t){

  c.save();c.translate(cx,cy);c.rotate(Math.sin(t*0.5)*0.05);

  const fl=15+Math.sin(t*15)*5;c.fillStyle='#ff8800';c.beginPath();c.moveTo(-6*s,22*s);c.lineTo(6*s,22*s);c.lineTo(2*s,(22+fl)*s);c.lineTo(-2*s,(22+fl)*s);c.closePath();c.fill();

  c.fillStyle='#4a6741';c.beginPath();c.moveTo(-45*s,5*s);c.lineTo(-10*s,-5*s);c.lineTo(-8*s,12*s);c.lineTo(-40*s,14*s);c.closePath();c.fill();c.beginPath();c.moveTo(45*s,5*s);c.lineTo(10*s,-5*s);c.lineTo(8*s,12*s);c.lineTo(40*s,14*s);c.closePath();c.fill();

  c.fillStyle='#5a7a51';c.beginPath();c.moveTo(0,-30*s);c.lineTo(-9*s,-10*s);c.lineTo(-8*s,24*s);c.lineTo(8*s,24*s);c.lineTo(9*s,-10*s);c.closePath();c.fill();

  c.fillStyle='#88bbee';c.beginPath();c.ellipse(0,-8*s,5*s,8*s,0,0,6.28);c.fill();

  c.fillStyle='#555';c.save();c.translate(0,-30*s);c.rotate(t*20);c.fillRect(-18*s,-2*s,36*s,4*s);c.restore();

  c.restore()

}

// Game Over

function drawGameOver(c,s){

  c.fillStyle='rgba(0,0,0,0.7)';c.fillRect(0,0,canvas.width,canvas.height);

  c.fillStyle='#f44';c.font=`bold ${48*s}px 'Courier New',monospace`;c.textAlign='center';c.textBaseline='middle';c.fillText('MISSION FAILED',canvas.width/2,canvas.height*0.2);

  c.strokeStyle='#f44';c.lineWidth=2*s;c.beginPath();c.moveTo(canvas.width*0.3,canvas.height*0.27);c.lineTo(canvas.width*0.7,canvas.height*0.27);c.stroke();

  const bw=350*s,bh=200*s,bx=canvas.width/2-bw/2,by=canvas.height*0.32;

  c.fillStyle='rgba(20,20,40,0.8)';c.fillRect(bx,by,bw,bh);c.strokeStyle='#fc0';c.lineWidth=2*s;c.strokeRect(bx,by,bw,bh);

  c.fillStyle='#fc0';c.font=`bold ${20*s}px 'Courier New',monospace`;c.fillText('BATTLE REPORT',canvas.width/2,by+30*s);

  c.fillStyle='#fff';c.font=`${16*s}px 'Courier New',monospace`;

  c.fillText(`FINAL SCORE: ${game.score}`,canvas.width/2,by+70*s);

  c.fillText(`WAVES SURVIVED: ${game.waves.wave}`,canvas.width/2,by+100*s);

  c.fillText(`ENEMIES DESTROYED: ${game.kills}`,canvas.width/2,by+130*s);

  let r='D';if(game.score>5000)r='S';else if(game.score>3000)r='A';else if(game.score>1500)r='B';else if(game.score>500)r='C';

  c.fillStyle=r==='S'?'#fc0':(r==='A'?'#4f4':'#fff');c.font=`bold ${36*s}px 'Courier New',monospace`;c.fillText(`RANK: ${r}`,canvas.width/2,by+175*s);

  const t=Date.now()*0.001;const rw=250*s,rh=45*s,rx=canvas.width/2-rw/2,ry=canvas.height*0.72;

  const p=Math.sin(t*3)*0.1+0.9;c.fillStyle=`rgba(255,200,0,${0.15*p})`;c.fillRect(rx-5*s,ry-5*s,rw+10*s,rh+10*s);

  c.fillStyle='#fc0';c.fillRect(rx,ry,rw,rh);c.fillStyle='#000';c.font=`bold ${20*s}px 'Courier New',monospace`;c.fillText('RETRY MISSION',canvas.width/2,ry+rh/2);

  game.retryBtn={x:rx,y:ry,w:rw,h:rh}

}

// Main Game Object

const game={

  state:GS.MENU,player:null,bullets:[],enemies:[],powerups:[],squadrons:[],bg:null,waves:null,

  score:0,kills:0,menuBtn:null,retryBtn:null,waveAnnounceTimer:0,prevWave:0,screenShake:0,gameOverDelay:0,

  init(){

    this.player=new Player();this.bullets=[];this.enemies=[];this.powerups=[];this.squadrons=[];

    this.bg=new Background();this.waves=new WaveManager();this.score=0;this.kills=0;this.waveAnnounceTimer=0;this.prevWave=0;this.screenShake=0;this.gameOverDelay=0;particles.length=0

  }

};

// Click handler

canvas.addEventListener('click',e=>{

  const r=canvas.getBoundingClientRect();const cx=e.clientX-r.left,cy=e.clientY-r.top;const s=S();

  if(game.state===GS.MENU&&game.menuBtn){const b=game.menuBtn;if(cx>=b.x&&cx<=b.x+b.w&&cy>=b.y&&cy<=b.y+b.h){initAudio();game.init();game.state=GS.PLAYING}}

  else if(game.state===GS.GAME_OVER&&game.retryBtn){const b=game.retryBtn;if(cx>=b.x&&cx<=b.x+b.w&&cy>=b.y&&cy<=b.y+b.h){game.init();game.state=GS.PLAYING}}

});

// Collision

function checkCollisions(){

  game.bullets.forEach(b=>{

    if(!b.alive)return;

    if(b.owner==='player'){

      game.enemies.forEach(e=>{if(e.alive&&Math.abs(b.x-e.x)<e.w/2+b.sz&&Math.abs(b.y-e.y)<e.h/2+b.sz){b.alive=false;e.takeDamage(b.dmg);spawnSmallExplosion(b.x,b.y)}});

      game.squadrons.forEach(sq=>sq.bombers.forEach(e=>{if(e.alive&&Math.abs(b.x-e.x)<e.w/2+b.sz&&Math.abs(b.y-e.y)<e.h/2+b.sz){b.alive=false;e.takeDamage(b.dmg);spawnSmallExplosion(b.x,b.y)}}))

    }else{

      if(game.player.alive&&Math.abs(b.x-game.player.x)<24&&Math.abs(b.y-game.player.y)<28){b.alive=false;game.player.takeDamage(b.dmg);spawnSmallExplosion(b.x,b.y)}

    }

  });

  // Enemy-player collision

  if(game.player.alive){

    game.enemies.forEach(e=>{if(e.alive&&Math.abs(e.x-game.player.x)<(e.w+40)/2&&Math.abs(e.y-game.player.y)<(e.h+50)/2){game.player.takeDamage(15);e.takeDamage(50)}});

    game.squadrons.forEach(sq=>sq.bombers.forEach(e=>{if(e.alive&&Math.abs(e.x-game.player.x)<(e.w+40)/2&&Math.abs(e.y-game.player.y)<(e.h+50)/2){game.player.takeDamage(15);e.takeDamage(50)}}))

  }

}

// Main Loop

let lastTime=0;

function gameLoop(timestamp){

  const dt=Math.min((timestamp-lastTime)/1000,0.05);lastTime=timestamp;

  const s=S();

  ctx.clearRect(0,0,canvas.width,canvas.height);

  ctx.save();

  if(game.screenShake>0){game.screenShake-=dt;const intensity=game.screenShake*15;ctx.translate(rand(-intensity,intensity)*s,rand(-intensity,intensity)*s)}

  if(game.state===GS.MENU){

    drawMenu(ctx,s)

  }else if(game.state===GS.PLAYING){

    game.bg.update(dt);game.player.update(dt);game.waves.update(dt);

    if(game.waves.wave!==game.prevWave){game.waveAnnounceTimer=2;game.prevWave=game.waves.wave}

    if(game.waveAnnounceTimer>0)game.waveAnnounceTimer-=dt;

    game.bullets.forEach(b=>b.update(dt));game.enemies.forEach(e=>e.update(dt));

    game.squadrons.forEach(sq=>sq.update(dt));game.powerups.forEach(p=>p.update(dt));

    particles.forEach(p=>p.update(dt));

    checkCollisions();

    game.bullets=game.bullets.filter(b=>b.alive);game.enemies=game.enemies.filter(e=>e.alive);

    game.squadrons=game.squadrons.filter(sq=>sq.active);game.powerups=game.powerups.filter(p=>p.alive);

    particles=particles.filter(p=>p.life>0);

    if(!game.player.alive&&game.gameOverDelay<=0){game.gameOverDelay=1.5}

    if(game.gameOverDelay>0){game.gameOverDelay-=dt;if(game.gameOverDelay<=0){game.state=GS.GAME_OVER}}

    game.bg.draw(ctx,s);

    game.powerups.forEach(p=>p.draw(ctx,s));

    game.enemies.forEach(e=>e.draw(ctx,s));

    game.squadrons.forEach(sq=>sq.draw(ctx,s));

    game.bullets.forEach(b=>b.draw(ctx,s));

    game.player.draw(ctx,s);

    particles.forEach(p=>p.draw(ctx,s));

    drawHUD(ctx,s)

  }else if(game.state===GS.GAME_OVER){

    game.bg.draw(ctx,s);

    game.enemies.forEach(e=>e.draw(ctx,s));

    particles.forEach(p=>{p.update(dt);p.draw(ctx,s)});

    particles=particles.filter(p=>p.life>0);

    drawGameOver(ctx,s)

  }

  ctx.restore();

  requestAnimationFrame(gameLoop)

}

requestAnimationFrame(gameLoop);

</script>

</body>

</html>


科技工作坊 · 让每个孩子都有机会走进科技

喜欢就点个吧↓