hfelix

Qwen3.8 27B - Space Shotter Game

Aug 14th, 2026
49
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
HTML 5 32.50 KB | Gaming | 0 0
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
  6. <title>Star Vanguard — Space Shooter</title>
  7. <style>
  8.   :root{
  9.     --cyan:#38e1ff; --magenta:#ff3b9a; --yellow:#ffd23b;
  10.     --green:#39ff9e; --red:#ff4d4d; --panel:rgba(10,16,32,0.72);
  11.   }
  12.   *{ box-sizing:border-box; margin:0; padding:0; }
  13.   html,body{
  14.     height:100%; width:100%; overflow:hidden;
  15.     background:radial-gradient(circle at 50% 20%, #10193b 0%, #05070f 55%, #02030a 100%);
  16.     font-family:"Segoe UI", system-ui, -apple-system, sans-serif;
  17.     color:#e8f0ff;
  18.   }
  19.   #stage{ position:fixed; inset:0; display:flex; align-items:center; justify-content:center; }
  20.   #wrap{
  21.     position:relative; aspect-ratio:500 / 720; height:min(100vh, 720px); max-width:100vw;
  22.     display:flex; align-items:center; justify-content:center;
  23.   }
  24.   canvas{
  25.     display:block; height:100%; width:100%; border-radius:14px;
  26.     box-shadow:0 0 0 1px rgba(56,225,255,.25), 0 20px 60px rgba(0,0,0,.6), 0 0 40px rgba(56,225,255,.12) inset;
  27.     background:#02030a;
  28.   }
  29.   #hud{
  30.     position:absolute; inset:0; pointer-events:none;
  31.     display:flex; flex-direction:column; justify-content:space-between; padding:14px 16px;
  32.   }
  33.   .hud-row{ display:flex; justify-content:space-between; align-items:flex-start; gap:10px; }
  34.   .hud-box{
  35.     background:var(--panel); border:1px solid rgba(56,225,255,.35);
  36.     border-radius:10px; padding:8px 12px; backdrop-filter:blur(4px);
  37.     box-shadow:0 4px 14px rgba(0,0,0,.5);
  38.   }
  39.   .label{ font-size:11px; letter-spacing:.14em; text-transform:uppercase; color:#8fb4ff; font-weight:700; }
  40.   .value{ font-size:22px; font-weight:800; line-height:1; margin-top:3px; color:#fff;
  41.           font-variant-numeric:tabular-nums; text-shadow:0 0 12px rgba(56,225,255,.6); }
  42.   #scoreVal{ color:var(--yellow); text-shadow:0 0 14px rgba(255,210,59,.7); }
  43.   #stageVal{ color:var(--cyan); }
  44.   #hpWrap{ width:150px; }
  45.   .bar{ width:100%; height:14px; margin-top:6px; border-radius:8px;
  46.         background:rgba(255,255,255,.08); border:1px solid rgba(255,255,255,.18); overflow:hidden; }
  47.   #hpFill{ height:100%; width:100%; background:linear-gradient(90deg,#39ff9e,#18d3ff);
  48.            transition:width .12s linear; box-shadow:0 0 12px rgba(57,255,158,.6); }
  49.   #hpFill.danger{ background:linear-gradient(90deg,#ff4d4d,#ff9a3b); box-shadow:0 0 12px rgba(255,77,77,.6); }
  50.   #lives{ font-size:20px; letter-spacing:3px; margin-top:3px; }
  51.   #bossBar{
  52.     position:absolute; top:64px; left:50%; transform:translateX(-50%);
  53.     width:70%; text-align:center; opacity:0; transition:opacity .3s;
  54.   }
  55.   #bossBar.show{ opacity:1; }
  56.   #bossBar .label{ color:var(--magenta); text-align:center; text-shadow:0 0 10px rgba(255,59,154,.7); }
  57.   #bossBar .bar{ height:12px; border-color:rgba(255,59,154,.5); }
  58.   #bossFill{ height:100%; width:100%; background:linear-gradient(90deg,#ff3b9a,#ff7a3b);
  59.              box-shadow:0 0 12px rgba(255,59,154,.7); transition:width .1s linear; }
  60.   .overlay{
  61.     position:absolute; inset:0; display:flex; flex-direction:column;
  62.     align-items:center; justify-content:center; text-align:center;
  63.     background:radial-gradient(circle at center, rgba(6,10,24,.86), rgba(2,3,10,.96));
  64.     backdrop-filter:blur(3px); z-index:20; padding:24px;
  65.   }
  66.   .overlay.hidden{ display:none; }
  67.   h1{
  68.     font-size:44px; font-weight:900; letter-spacing:.08em; margin-bottom:6px;
  69.     background:linear-gradient(90deg,#38e1ff,#ff3b9a,#ffd23b);
  70.     -webkit-background-clip:text; background-clip:text; color:transparent;
  71.     filter:drop-shadow(0 0 18px rgba(56,225,255,.4));
  72.   }
  73.   .subtitle{ color:#9fc0ff; font-size:15px; margin-bottom:22px; letter-spacing:.1em; }
  74.   .big-num{ font-size:56px; font-weight:900; color:var(--yellow);
  75.             text-shadow:0 0 24px rgba(255,210,59,.6); margin:6px 0 18px; }
  76.   .keys{ font-size:13px; color:#cfe0ff; line-height:1.9; margin-bottom:24px; }
  77.   .keys b{ color:var(--cyan); }
  78.   .btn{
  79.     pointer-events:auto; cursor:pointer; font:inherit; font-weight:800; letter-spacing:.12em;
  80.     text-transform:uppercase; font-size:16px; color:#04121a;
  81.     background:linear-gradient(90deg,#38e1ff,#18d3ff); border:none; border-radius:12px;
  82.     padding:14px 40px; box-shadow:0 8px 24px rgba(24,211,255,.5), 0 0 0 1px rgba(255,255,255,.3) inset;
  83.     transition:transform .1s, filter .1s;
  84.   }
  85.   .btn:hover{ transform:translateY(-2px); filter:brightness(1.1); }
  86.   .btn:active{ transform:translateY(0); }
  87.   .flash{
  88.     position:absolute; top:38%; left:50%; transform:translate(-50%,-50%) scale(.6);
  89.     font-size:30px; font-weight:900; letter-spacing:.15em; color:var(--magenta);
  90.     text-shadow:0 0 22px rgba(255,59,154,.9); opacity:0; pointer-events:none; z-index:15;
  91.     transition:opacity .2s;
  92.   }
  93.   .flash.show{ animation:flash 1.4s ease; }
  94.   @keyframes flash{
  95.     0%{opacity:0; transform:translate(-50%,-50%) scale(.5);}
  96.     15%{opacity:1; transform:translate(-50%,-50%) scale(1);}
  97.     70%{opacity:1; transform:translate(-50%,-50%) scale(1.02);}
  98.     100%{opacity:0; transform:translate(-50%,-50%) scale(1.1);}
  99.   }
  100.   @media (max-width:520px){ h1{font-size:32px;} .subtitle{font-size:13px;} .big-num{font-size:42px;} }
  101. </style>
  102. </head>
  103. <body>
  104. <div id="stage">
  105.   <div id="wrap">
  106.     <canvas id="game" width="500" height="720"></canvas>
  107.     <div id="hud">
  108.       <div class="hud-row">
  109.         <div class="hud-box">
  110.           <div class="label">Score</div>
  111.           <div class="value" id="scoreVal">0</div>
  112.         </div>
  113.         <div class="hud-box">
  114.           <div class="label">Stage</div>
  115.           <div class="value" id="stageVal">1</div>
  116.         </div>
  117.         <div class="hud-box">
  118.           <div class="label">Lives</div>
  119.           <div id="lives">▲▲▲</div>
  120.         </div>
  121.       </div>
  122.       <div class="hud-row" style="align-items:flex-end;">
  123.         <div class="hud-box" id="hpWrap">
  124.           <div class="label">Hull Integrity</div>
  125.           <div class="bar"><div id="hpFill"></div></div>
  126.         </div>
  127.       </div>
  128.     </div>
  129.     <div id="bossBar">
  130.       <div class="label">BOSS</div>
  131.       <div class="bar"><div id="bossFill"></div></div>
  132.     </div>
  133.     <div id="flash" class="flash"></div>
  134.  
  135.     <div id="startScreen" class="overlay">
  136.       <h1>STAR VANGUARD</h1>
  137.       <div class="subtitle">DEFEND THE LAST SECTOR</div>
  138.       <div class="keys">
  139.         <b>Move</b> — WASD / Arrow Keys<br>
  140.         <b>Fire</b> — Spacebar<br>
  141.         <b>Pause</b> — P &nbsp;·&nbsp; <b>Mute</b> — M<br>
  142.         Clear each wave, then destroy the boss to advance. 3 stages to victory.
  143.       </div>
  144.       <button id="startBtn" class="btn">Launch</button>
  145.     </div>
  146.  
  147.     <div id="overScreen" class="overlay hidden">
  148.       <h1>SHIP DESTROYED</h1>
  149.       <div class="subtitle">FINAL SCORE</div>
  150.       <div class="big-num" id="finalScore">0</div>
  151.       <button id="restartBtn" class="btn">Restart</button>
  152.     </div>
  153.  
  154.     <div id="winScreen" class="overlay hidden">
  155.       <h1>SECTOR SECURED</h1>
  156.       <div class="subtitle">ALL BOSSES DESTROYED — VICTORY</div>
  157.       <div class="big-num" id="winScore">0</div>
  158.       <button id="winRestartBtn" class="btn">Play Again</button>
  159.     </div>
  160.  
  161.     <div id="pauseScreen" class="overlay hidden">
  162.       <h1>PAUSED</h1>
  163.       <div class="subtitle">PRESS P TO RESUME</div>
  164.     </div>
  165.   </div>
  166. </div>
  167.  
  168. <script>
  169. "use strict";
  170. const canvas = document.getElementById("game");
  171. const ctx = canvas.getContext("2d");
  172. const W = canvas.width, H = canvas.height;
  173.  
  174. /* ---------------- Audio (Web Audio, procedural) ---------------- */
  175. const AudioSys = (() => {
  176.   let ac=null, master=null, noiseBuf=null, muted=false;
  177.   function ensure(){
  178.     if (ac) return;
  179.     ac = new (window.AudioContext||window.webkitAudioContext)();
  180.     master = ac.createGain(); master.gain.value = muted?0:0.5; master.connect(ac.destination);
  181.     const len = Math.floor(ac.sampleRate*1.2);
  182.     noiseBuf = ac.createBuffer(1,len,ac.sampleRate);
  183.     const d = noiseBuf.getChannelData(0);
  184.     for (let i=0;i<len;i++) d[i]=Math.random()*2-1;
  185.  }
  186.  function resume(){ ensure(); if (ac.state==="suspended") ac.resume(); }
  187.  function tone({type="square",f0=440,f1=f0,dur=0.12,vol=0.3,slide="exp"}){
  188.    if(!ac) return;
  189.    const o=ac.createOscillator(), g=ac.createGain();
  190.    o.type=type;
  191.    o.frequency.setValueAtTime(f0,ac.currentTime);
  192.    o.frequency[slide==="exp"?"exponentialRampToValueAtTime":"linearRampToValueAtTime"](Math.max(1,f1),ac.currentTime+dur);
  193.    g.gain.setValueAtTime(vol,ac.currentTime);
  194.    g.gain.exponentialRampToValueAtTime(0.0008,ac.currentTime+dur);
  195.    o.connect(g); g.connect(master); o.start(); o.stop(ac.currentTime+dur+0.02);
  196.  }
  197.  function noise({dur=0.4,vol=0.4,from=1800,to=180}){
  198.    if(!ac) return;
  199.    const s=ac.createBufferSource(); s.buffer=noiseBuf;
  200.    const f=ac.createBiquadFilter(); f.type="lowpass";
  201.    f.frequency.setValueAtTime(from,ac.currentTime);
  202.    f.frequency.exponentialRampToValueAtTime(to,ac.currentTime+dur);
  203.    const g=ac.createGain();
  204.    g.gain.setValueAtTime(vol,ac.currentTime);
  205.    g.gain.exponentialRampToValueAtTime(0.0008,ac.currentTime+dur);
  206.    s.connect(f); f.connect(g); g.connect(master); s.start(); s.stop(ac.currentTime+dur);
  207.  }
  208.  return {
  209.    resume,
  210.    fire(){ tone({type:"square",f0:880,f1:220,dur:0.09,vol:0.12}); },
  211.    enemyFire(){ tone({type:"sawtooth",f0:300,f1:120,dur:0.12,vol:0.07}); },
  212.    explode(big){ noise({dur:big?0.7:0.32,vol:big?0.5:0.3,from:big?2200:1600,to:80});
  213.                  if(big) tone({type:"sine",f0:120,f1:38,dur:0.5,vol:0.3}); },
  214.    pickup(){ [660,880,990,1320].forEach((f,i)=>setTimeout(()=>tone({type:"triangle",f0:f,f1:f,dur:0.1,vol:0.22}),i*55)); },
  215.     hit(){ tone({type:"square",f0:200,f1:60,dur:0.08,vol:0.12}); },
  216.     bossAlert(){ [160,160,240,160].forEach((f,i)=>setTimeout(()=>tone({type:"sawtooth",f0:f,f1:f,dur:0.2,vol:0.25}),i*180)); },
  217.     stage(){ [523,659,784,1046].forEach((f,i)=>setTimeout(()=>tone({type:"triangle",f0:f,f1:f,dur:0.14,vol:0.22}),i*110)); },
  218.     setMute(m){ muted=m; if(master) master.gain.value=m?0:0.5; },
  219.     getMute(){ return muted; }
  220.   };
  221. })();
  222.  
  223. /* ---------------- Utility ---------------- */
  224. const rand=(a,b)=>a+Math.random()*(b-a);
  225. const randi=(a,b)=>Math.floor(rand(a,b+1));
  226. const clamp=(v,a,b)=>v<a?a:v>b?b:v;
  227. function aabb(a,b){ return Math.abs(a.x-b.x)*2 < (a.r+b.r)*2 && Math.abs(a.y-b.y)*2 < (a.r+b.r)*2; }
  228.  
  229. /* ---------------- Global state ---------------- */
  230. const STATE={START:0,PLAYING:1,GAMEOVER:2,WIN:3,PAUSED:4};
  231. let state=STATE.START;
  232.  
  233. const BASE_SPAWN_INTERVAL = 1.1;   // base seconds between enemy spawns (Stage 1)
  234. const G = {
  235.  stage:1, score:0,
  236.  spawnRateMult:1.0,               // scales +10% per stage
  237.  spawnTimer:0,
  238.  killTarget:0, kills:0,
  239.  bossActive:false,
  240.  stars:[],
  241.  potionCooldown:0,
  242.  potionOnScreen:false,
  243.  shake:0,
  244. };
  245. const STAGE_KILLS=[14,20,26];
  246. const MAX_STAGE=3;
  247.  
  248. const player={ x:W/2, y:H-90, r:16, speed:340, hp:100, maxHp:100, lives:3,
  249.               fireCd:0, invuln:0, alive:true, respawnTimer:0 };
  250.  
  251. let playerShots=[], enemyShots=[], enemies=[], particles=[], pickups=[], boss=null;
  252.  
  253. /* ---------------- Stars ---------------- */
  254. function initStars(){
  255.  G.stars=[];
  256.  for (let i=0;i<110;i++) G.stars.push({x:Math.random()*W,y:Math.random()*H,s:rand(0.4,2.2),sp:rand(20,90),a:rand(.3,1)});
  257. }
  258. function updateStars(dt){
  259.  for (const s of G.stars){ s.y+=s.sp*dt; if (s.y>H){ s.y=0; s.x=Math.random()*W; } }
  260. }
  261. function drawStars(){
  262.   for (const s of G.stars){ ctx.globalAlpha=s.a; ctx.fillStyle="#cfe6ff"; ctx.fillRect(s.x,s.y,s.s,s.s*2.2); }
  263.   ctx.globalAlpha=1;
  264. }
  265.  
  266. /* ---------------- Particles ---------------- */
  267. function spawnExplosion(x,y,{color="#ffb13b",count=22,speed=180,life=0.6,size=3}={}){
  268.   for (let i=0;i<count;i++){
  269.    const a=Math.random()*Math.PI*2, sp=rand(30,speed);
  270.    particles.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,life,maxLife:life,size:rand(1,size),color});
  271.  }
  272. }
  273. function updateParticles(dt){
  274.  for (let i=particles.length-1;i>=0;i--){
  275.     const p=particles[i];
  276.     p.x+=p.vx*dt; p.y+=p.vy*dt; p.vx*=0.96; p.vy*=0.96; p.life-=dt;
  277.     if (p.life<=0) particles.splice(i,1);
  278.  }
  279. }
  280. function drawParticles(){
  281.  for (const p of particles){
  282.    const a=p.life/p.maxLife;
  283.    ctx.globalAlpha=a; ctx.fillStyle=p.color;
  284.    ctx.beginPath(); ctx.arc(p.x,p.y,p.size*(0.4+a),0,Math.PI*2); ctx.fill();
  285.  }
  286.  ctx.globalAlpha=1;
  287. }
  288.  
  289. /* ---------------- Player ---------------- */
  290. function drawPlayer(){
  291.  const {x,y}=player;
  292.  const flick=0.6+Math.random()*0.4;
  293.  ctx.globalCompositeOperation="lighter";
  294.  for (let i=0;i<3;i++){
  295.    const ty=y+18+i*7;
  296.    ctx.globalAlpha=(0.5-i*0.14)*flick;
  297.    ctx.fillStyle=i%2?"#38e1ff":"#ffd23b";
  298.    ctx.beginPath(); ctx.ellipse(x,ty,5-i,9*i+6,0,0,Math.PI*2); ctx.fill();
  299.  }
  300.  ctx.globalCompositeOperation="source-over"; ctx.globalAlpha=1;
  301.  if (player.invuln>0 && Math.floor(performance.now()/80)%2===0) ctx.globalAlpha=0.4;
  302.   const grd=ctx.createLinearGradient(x,y-24,x,y+24);
  303.   grd.addColorStop(0,"#eaffff"); grd.addColorStop(.5,"#38e1ff"); grd.addColorStop(1,"#123a63");
  304.   ctx.fillStyle=grd;
  305.   ctx.beginPath();
  306.   ctx.moveTo(x,y-24); ctx.lineTo(x+14,y+10); ctx.lineTo(x+8,y+18);
  307.   ctx.lineTo(x-8,y+18); ctx.lineTo(x-14,y+10); ctx.closePath(); ctx.fill();
  308.   ctx.fillStyle="#184a7a";
  309.   ctx.beginPath(); ctx.moveTo(x-14,y+10); ctx.lineTo(x-26,y+22); ctx.lineTo(x-10,y+16); ctx.closePath(); ctx.fill();
  310.   ctx.beginPath(); ctx.moveTo(x+14,y+10); ctx.lineTo(x+26,y+22); ctx.lineTo(x+10,y+16); ctx.closePath(); ctx.fill();
  311.   ctx.fillStyle="#eafcff"; ctx.beginPath(); ctx.arc(x,y-2,4.5,0,Math.PI*2); ctx.fill();
  312.   ctx.globalAlpha=1;
  313. }
  314. function firePlayer(){
  315.   playerShots.push({x:player.x,y:player.y-24,vy:-720,vx:0,r:5,dmg:1,life:1.4});
  316.   playerShots.push({x:player.x-9,y:player.y-16,vy:-700,vx:-14,r:4,dmg:0.5,life:1.4});
  317.   playerShots.push({x:player.x+9,y:player.y-16,vy:-700,vx:14,r:4,dmg:0.5,life:1.4});
  318.   AudioSys.fire();
  319. }
  320. function damagePlayer(amount){
  321.   if (!player.alive || player.invuln>0) return;
  322.   player.hp-=amount; G.shake=Math.max(G.shake,8); AudioSys.hit();
  323.   if (player.hp<=0){ player.hp=0; loseLife(); }
  324. }
  325. function loseLife(){
  326.  player.lives--;
  327.  spawnExplosion(player.x,player.y,{color:"#38e1ff",count:40,speed:260,life:0.9,size:4});
  328.  spawnExplosion(player.x,player.y,{color:"#ffffff",count:20,speed:160,life:0.5});
  329.  AudioSys.explode(true); G.shake=20;
  330.  if (player.lives<=0){ gameOver(); }
  331.  else { player.alive=false; player.respawnTimer=2.2; }
  332. }
  333. function respawnPlayer(){
  334.  player.alive=true; player.hp=player.maxHp;
  335.  player.x=W/2; player.y=H-90; player.invuln=2.5;
  336. }
  337.  
  338. /* ---------------- Enemy types ---------------- */
  339. const ENEMY_TYPES={
  340.  drone:{ hp:2,r:16,score:50,color:"#ff7a3b",fireRate:1.6,
  341.    move(e,dt,t){ e.y+=95*dt; },
  342.    fire(e,dt){ if (e.y<H-260 && Math.random()<dt/1.4) aimedShot(e,1); } },
  343.  striker:{ hp:3,r:17,score:90,color:"#c65cff",fireRate:1.2,
  344.    move(e,dt,t){ e.y+=120*dt; e.x+=Math.cos(t*3+e.phase)*160*dt; },
  345.    fire(e,dt){ if (Math.random()<dt/1.0) aimedShot(e,2); } },
  346.  gunner:{ hp:5,r:20,score:130,color:"#ffd23b",fireRate:0.9,
  347.    move(e,dt,t){ if (e.y<e.holdY) e.y+=160*dt; else e.x+=Math.cos(t*1.4+e.phase)*120*dt; },
  348.    fire(e,dt){ if (e.y>=e.holdY && Math.random()<dt/1.1) spreadShot(e,3); } },
  349.   rammer:{ hp:4,r:16,score:110,color:"#39ff9e",fireRate:0,
  350.     move(e,dt,t){ if (e.phase<0){ e.phase=0; e.vx=(player.x-e.x)*0.9; e.vy=(H-e.y)*1.4; } e.x+=e.vx*dt; e.y+=e.vy*dt; },
  351.    fire(){} }
  352. };
  353. function spawnEnemy(){
  354.  const type=["drone","drone","striker","striker","gunner","rammer"][randi(0,5)];
  355.  const def=ENEMY_TYPES[type];
  356.  enemies.push({ type, x:rand(40,W-40), y:-30, r:def.r, hp:def.hp, maxHp:def.hp,
  357.    color:def.color, score:def.score, phase:Math.random()*Math.PI*2,
  358.    vx:0, vy:0, holdY:rand(120,240), fireTimer:rand(0.5,1.5) });
  359. }
  360. /* Enemy spawn loop — the missing piece. Base interval / rateMult. */
  361. function updateSpawns(dt){
  362.  if (G.bossActive || boss) return;                 // no normal spawns during boss
  363.  G.spawnTimer -= dt;
  364.  if (G.spawnTimer<=0){
  365.    spawnEnemy();
  366.    G.spawnTimer = BASE_SPAWN_INTERVAL / G.spawnRateMult;
  367.  }
  368. }
  369. function aimedShot(e,n){
  370.  for (let i=0;i<n;i++){
  371.    const spread=(i-(n-1)/2)*0.18;
  372.    const ang=Math.atan2(player.y-e.y,player.x-e.x)+spread, sp=260;
  373.    enemyShots.push({x:e.x,y:e.y+e.r,vx:Math.cos(ang)*sp,vy:Math.sin(ang)*sp,r:6,dmg:10,life:4,color:e.color||"#ff4d4d",homing:false});
  374.  }
  375.  AudioSys.enemyFire();
  376. }
  377. function spreadShot(e,n){
  378.  const base=Math.atan2(player.y-e.y,player.x-e.x);
  379.  for (let i=0;i<n;i++){
  380.    const ang=base+(i-(n-1)/2)*0.32, sp=230;
  381.    enemyShots.push({x:e.x,y:e.y+e.r,vx:Math.cos(ang)*sp,vy:Math.sin(ang)*sp,r:6,dmg:9,life:4,color:e.color||"#ffd23b",homing:false});
  382.  }
  383.  AudioSys.enemyFire();
  384. }
  385. function updateEnemies(dt,t){
  386.  for (let i=enemies.length-1;i>=0;i--){
  387.     const e=enemies[i];
  388.     ENEMY_TYPES[e.type].move(e,dt,t);
  389.     if (e.type!=="rammer"){
  390.       e.fireTimer-=dt;
  391.       if (e.fireTimer<=0){ ENEMY_TYPES[e.type].fire(e,dt); e.fireTimer=ENEMY_TYPES[e.type].fireRate; }
  392.    }
  393.    if (e.y>H+40||e.x<-40||e.x>W+40) enemies.splice(i,1);
  394.   }
  395. }
  396. function drawEnemy(e){
  397.   const {x,y,r}=e;
  398.   ctx.save(); ctx.translate(x,y);
  399.   ctx.globalCompositeOperation="lighter"; ctx.globalAlpha=0.5+Math.random()*0.3;
  400.   ctx.fillStyle=e.color; ctx.beginPath(); ctx.ellipse(0,-r-6,4,8,0,0,Math.PI*2); ctx.fill();
  401.   ctx.globalCompositeOperation="source-over"; ctx.globalAlpha=1;
  402.   const grd=ctx.createLinearGradient(0,-r,0,r);
  403.   grd.addColorStop(0,e.color); grd.addColorStop(1,"rgba(0,0,0,0.25)");
  404.   ctx.fillStyle=grd;
  405.   if (e.type==="drone"){
  406.     ctx.beginPath(); ctx.moveTo(0,r+6); ctx.lineTo(r-3,-r*0.5); ctx.lineTo(0,-2); ctx.lineTo(-r+3,-r*0.5); ctx.closePath(); ctx.fill();
  407.   } else if (e.type==="striker"){
  408.     ctx.beginPath(); ctx.moveTo(0,r+4); ctx.lineTo(r+6,0); ctx.lineTo(r-4,-r*0.6); ctx.lineTo(0,-r*0.2);
  409.     ctx.lineTo(-r+4,-r*0.6); ctx.lineTo(-r-6,0); ctx.closePath(); ctx.fill();
  410.   } else if (e.type==="gunner"){
  411.     ctx.beginPath(); ctx.arc(0,0,r-1,0,Math.PI*2); ctx.fill();
  412.     ctx.fillStyle="rgba(0,0,0,.4)"; ctx.beginPath(); ctx.arc(0,0,r-7,0,Math.PI*2); ctx.fill();
  413.     ctx.fillStyle="#fff2b3"; ctx.beginPath(); ctx.arc(0,0,5,0,Math.PI*2); ctx.fill();
  414.   } else {
  415.     ctx.beginPath(); ctx.moveTo(0,r+8); ctx.lineTo(r*0.9,-r*0.4); ctx.lineTo(0,-r); ctx.lineTo(-r*0.9,-r*0.4); ctx.closePath(); ctx.fill();
  416.   }
  417.   ctx.restore();
  418.   if (e.hp<e.maxHp){
  419.    const w=r*1.8;
  420.    ctx.fillStyle="rgba(0,0,0,.5)"; ctx.fillRect(x-w/2,y-r-12,w,4);
  421.    ctx.fillStyle=e.color; ctx.fillRect(x-w/2,y-r-12,w*(e.hp/e.maxHp),4);
  422.  }
  423. }
  424.  
  425. /* ---------------- Bosses ---------------- */
  426. const BOSS_DEFS=[
  427.  {name:"CRUISER KRAIT",hp:120,r:56,score:1500,color:"#ff3b9a",pattern:"spread"},
  428.  {name:"DESTROYER VEX",hp:180,r:62,score:3000,color:"#c65cff",pattern:"homing"},
  429.  {name:"DREADNOUGHT OMEGA",hp:280,r:72,score:6000,color:"#ff6b3b",pattern:"laser"}
  430. ];
  431. function spawnBoss(idx){
  432.  const d=BOSS_DEFS[idx];
  433.  boss={ x:W/2, y:-d.r-20, r:d.r, hp:d.hp, maxHp:d.hp, targetY:130, color:d.color,
  434.         score:d.score, name:d.name, pattern:d.pattern, entering:true,
  435.         fireT:1, phase:Math.PI*1.4, waveT:0, laserT:2.5, homeT:1.2, angle:0 };
  436.  G.bossActive=true;
  437.  AudioSys.bossAlert();
  438.  showFlash("⚠ "+d.name+" APPROACHING ⚠");
  439. }
  440. function updateBoss(dt,t){
  441.  const b=boss; if(!b) return;
  442.  if (b.entering){ b.y+=(b.targetY-b.y)*Math.min(1,dt*1.6); if (Math.abs(b.y-b.targetY)<2) b.entering=false; return; }
  443.  b.phase+=dt*1.1;
  444.  b.x=W/2+Math.sin(b.phase)*(W*0.32);        // HORIZONTAL-ONLY
  445.  b.x=clamp(b.x,b.r+10,W-b.r-10);
  446.  if (b.pattern==="spread"){
  447.    b.fireT-=dt;
  448.    if (b.fireT<=0){
  449.      const base=Math.atan2(player.y-b.y,player.x-b.x);
  450.      for (let i=-3;i<=3;i++){
  451.        const a=base+i*0.22;
  452.        enemyShots.push({x:b.x,y:b.y+b.r*0.6,vx:Math.cos(a)*210,vy:Math.sin(a)*210,r:7,dmg:11,life:5,color:b.color,homing:false});
  453.      }
  454.      b.fireT=1.15; AudioSys.enemyFire();
  455.    }
  456.  } else if (b.pattern==="homing"){
  457.    b.homeT-=dt;
  458.    if (b.homeT<=0){
  459.      for (let i=0;i<2;i++){
  460.        const a=(player.x-b.x>0?0:Math.PI)+rand(-0.6,0.6);
  461.         enemyShots.push({x:b.x+rand(-30,30),y:b.y+b.r*0.5,vx:Math.cos(a)*160,vy:Math.sin(a)*160,r:9,dmg:14,life:5,color:b.color,homing:true,turn:1.4});
  462.       }
  463.       b.homeT=1.0; AudioSys.enemyFire();
  464.     }
  465.   } else {
  466.     b.laserT-=dt; b.angle+=dt*0.6;
  467.     if (b.laserT<=0){
  468.      const base=Math.atan2(player.y-b.y,player.x-b.x);
  469.      for (let k=0;k<2;k++){
  470.        const a=base+rand(-0.12,0.12);
  471.        enemyShots.push({x:b.x,y:b.y+b.r*0.6,vx:Math.cos(a)*340,vy:Math.sin(a)*340,r:8,dmg:12,life:5,color:"#ff2d6b",laser:true});
  472.      }
  473.      b.laserT=0.55; AudioSys.enemyFire();
  474.    }
  475.    b.waveT-=dt;
  476.    if (b.waveT<=0){
  477.      const n=16;
  478.      for (let i=0;i<n;i++){
  479.        const a=i/n*Math.PI*2;
  480.        enemyShots.push({x:b.x,y:b.y,vx:Math.cos(a)*150,vy:Math.sin(a)*150,r:6,dmg:10,life:6,color:b.color,homing:false});
  481.      }
  482.      b.waveT=3.2; AudioSys.enemyFire();
  483.    }
  484.  }
  485. }
  486. function drawBoss(){
  487.  const b=boss; if(!b) return;
  488.  const {x,y,r}=b;
  489.  ctx.save(); ctx.translate(x,y);
  490.  ctx.globalCompositeOperation="lighter"; ctx.globalAlpha=0.5;
  491.  ctx.fillStyle=b.color; ctx.beginPath(); ctx.arc(0,0,r*1.1,0,Math.PI*2); ctx.fill();
  492.  ctx.globalCompositeOperation="source-over"; ctx.globalAlpha=1;
  493.  const grd=ctx.createLinearGradient(0,-r,0,r);
  494.  grd.addColorStop(0,b.color); grd.addColorStop(1,"rgba(10,5,20,0.9)");
  495.  ctx.fillStyle=grd;
  496.  ctx.beginPath();
  497.  ctx.moveTo(0,r*0.7); ctx.lineTo(r*0.9,0); ctx.lineTo(r*0.55,-r*0.6); ctx.lineTo(0,-r*0.35);
  498.  ctx.lineTo(-r*0.55,-r*0.6); ctx.lineTo(-r*0.9,0); ctx.closePath(); ctx.fill();
  499.  ctx.fillStyle="rgba(0,0,0,.35)";
  500.  ctx.beginPath(); ctx.arc(-r*0.7,0,r*0.28,0,Math.PI*2); ctx.fill();
  501.  ctx.beginPath(); ctx.arc(r*0.7,0,r*0.28,0,Math.PI*2); ctx.fill();
  502.  const pulse=6+Math.sin(performance.now()/200)*3;
  503.  ctx.fillStyle="#fff"; ctx.beginPath(); ctx.arc(0,0,pulse,0,Math.PI*2); ctx.fill();
  504.  ctx.fillStyle=b.color; ctx.globalAlpha=.6; ctx.beginPath(); ctx.arc(0,0,pulse+5,0,Math.PI*2); ctx.fill();
  505.  ctx.globalAlpha=1;
  506.  ctx.restore();
  507. }
  508. function defeatBoss(){
  509.  const b=boss;
  510.  spawnExplosion(b.x,b.y,{color:b.color,count:70,speed:320,life:1.1,size:5});
  511.  spawnExplosion(b.x,b.y,{color:"#ffffff",count:40,speed:200,life:0.7});
  512.  spawnExplosion(b.x,b.y,{color:"#ffd23b",count:30,speed:260,life:0.9,size:4});
  513.  G.score+=b.score; G.shake=30; AudioSys.explode(true);
  514.  boss=null; G.bossActive=false;
  515.  document.getElementById("bossBar").classList.remove("show");
  516.  if (G.stage>=MAX_STAGE){ winGame(); } else { advanceStage(); }
  517. }
  518. function advanceStage(){
  519.   G.stage++;
  520.   G.spawnRateMult*=1.10;
  521.   G.kills=0;
  522.   G.killTarget=STAGE_KILLS[G.stage-1];
  523.   G.spawnTimer=0;
  524.   AudioSys.stage();
  525.   showFlash("STAGE "+G.stage);
  526. }
  527.  
  528. /* ---------------- Shots ---------------- */
  529. function updateShots(dt){
  530.   for (let i=playerShots.length-1;i>=0;i--){
  531.     const s=playerShots[i]; s.x+=s.vx*dt; s.y+=s.vy*dt; s.life-=dt;
  532.     if (s.life<=0||s.y<-20) playerShots.splice(i,1);
  533.  }
  534.  for (let i=enemyShots.length-1;i>=0;i--){
  535.     const s=enemyShots[i];
  536.     if (s.homing && player.alive){
  537.      const ta=Math.atan2(player.y-s.y,player.x-s.x), cur=Math.atan2(s.vy,s.vx);
  538.       let diff=ta-cur; diff=Math.atan2(Math.sin(diff),Math.cos(diff));
  539.       cur+=clamp(diff,-s.turn*dt,s.turn*dt);
  540.       const sp=Math.hypot(s.vx,s.vy); s.vx=Math.cos(cur)*sp; s.vy=Math.sin(cur)*sp;
  541.     }
  542.     s.x+=s.vx*dt; s.y+=s.vy*dt; s.life-=dt;
  543.     if (s.life<=0||s.y>H+30||s.y<-30||s.x<-30||s.x>W+30) enemyShots.splice(i,1);
  544.   }
  545. }
  546. function drawPlayerShots(){
  547.   ctx.globalCompositeOperation="lighter";
  548.   for (const s of playerShots){
  549.     ctx.fillStyle="#baffff"; ctx.globalAlpha=.9; ctx.beginPath(); ctx.ellipse(s.x,s.y,3,9,0,0,Math.PI*2); ctx.fill();
  550.     ctx.fillStyle="#38e1ff"; ctx.globalAlpha=.5; ctx.beginPath(); ctx.ellipse(s.x,s.y,6,13,0,0,Math.PI*2); ctx.fill();
  551.   }
  552.   ctx.globalCompositeOperation="source-over"; ctx.globalAlpha=1;
  553. }
  554. function drawEnemyShots(){
  555.   ctx.globalCompositeOperation="lighter";
  556.   for (const s of enemyShots){
  557.     ctx.fillStyle=s.color; ctx.globalAlpha=.9; ctx.beginPath(); ctx.arc(s.x,s.y,s.r,0,Math.PI*2); ctx.fill();
  558.     ctx.globalAlpha=.4; ctx.beginPath(); ctx.arc(s.x,s.y,s.r*1.8,0,Math.PI*2); ctx.fill();
  559.   }
  560.   ctx.globalCompositeOperation="source-over"; ctx.globalAlpha=1;
  561. }
  562.  
  563. /* ---------------- Health potions ---------------- */
  564. function trySpawnPotion(dt){
  565.   if (G.potionOnScreen) return;
  566.   if (G.potionCooldown>0){ G.potionCooldown-=dt; return; }
  567.   if (Math.random()<0.03*dt){
  568.    pickups.push({x:rand(50,W-50),y:-24,r:16,vy:110,t:0});
  569.    G.potionOnScreen=true;
  570.  }
  571. }
  572. function updatePickups(dt){
  573.  for (let i=pickups.length-1;i>=0;i--){
  574.     const p=pickups[i]; p.y+=p.vy*dt; p.t+=dt;
  575.     if (p.y>H+30){ pickups.splice(i,1); G.potionOnScreen=false; }
  576.   }
  577. }
  578. function drawPickup(p){
  579.   const bob=Math.sin(p.t*4)*3;
  580.   ctx.save(); ctx.translate(p.x,p.y+bob);
  581.   ctx.globalCompositeOperation="lighter"; ctx.globalAlpha=.4+Math.sin(p.t*5)*0.2;
  582.   ctx.fillStyle="#39ff9e"; ctx.beginPath(); ctx.arc(0,0,p.r*1.5,0,Math.PI*2); ctx.fill();
  583.   ctx.globalCompositeOperation="source-over"; ctx.globalAlpha=1;
  584.   ctx.fillStyle="#0c3b2a"; ctx.beginPath(); ctx.arc(0,3,p.r*0.8,0,Math.PI*2); ctx.fill();
  585.   ctx.fillStyle="#39ff9e"; ctx.beginPath(); ctx.arc(0,3,p.r*0.6,0,Math.PI*2); ctx.fill();
  586.   ctx.fillStyle="#c9ffe9"; ctx.fillRect(-5,-p.r*0.9,10,7);
  587.   ctx.fillStyle="#06331f"; ctx.fillRect(-2,-p.r*0.2,4,p.r*0.8); ctx.fillRect(-p.r*0.3,-p.r*0.2+4,p.r*0.6,4);
  588.   ctx.restore();
  589. }
  590. function collectPotion(){
  591.   player.hp=Math.min(player.maxHp,player.hp+40);
  592.   G.potionCooldown=30; G.potionOnScreen=false;
  593.   AudioSys.pickup();
  594.   spawnExplosion(player.x,player.y-30,{color:"#39ff9e",count:16,speed:120,life:0.5,size:3});
  595. }
  596.  
  597. /* ---------------- Collision ---------------- */
  598. function collide(){
  599.   for (let i=playerShots.length-1;i>=0;i--){
  600.     const s=playerShots[i]; let hit=false;
  601.     for (let j=enemies.length-1;j>=0;j--){
  602.       const e=enemies[j];
  603.       if (aabb(s,e)){
  604.         e.hp-=s.dmg;
  605.         spawnExplosion(s.x,s.y,{color:e.color,count:5,speed:90,life:0.3,size:2});
  606.         if (e.hp<=0){
  607.          G.score+=e.score;
  608.          spawnExplosion(e.x,e.y,{color:e.color,count:20,speed:200,life:0.6});
  609.          AudioSys.explode(false); enemies.splice(j,1); G.kills++;
  610.        }
  611.        hit=true; break;
  612.      }
  613.    }
  614.    if (!hit && boss && aabb(s,boss)){
  615.      boss.hp-=s.dmg;
  616.      spawnExplosion(s.x,s.y,{color:boss.color,count:5,speed:90,life:0.3,size:2});
  617.      hit=true;
  618.      if (boss.hp<=0) defeatBoss();
  619.    }
  620.    if (hit) playerShots.splice(i,1);
  621.  }
  622.  if (!player.alive) return;
  623.  for (let i=enemyShots.length-1;i>=0;i--){
  624.     const s=enemyShots[i];
  625.     if (aabb(s,player)){
  626.       damagePlayer(s.dmg);
  627.       spawnExplosion(s.x,s.y,{color:"#ff4d4d",count:10,speed:140,life:0.4,size:3});
  628.       enemyShots.splice(i,1);
  629.     }
  630.   }
  631.   for (let j=enemies.length-1;j>=0;j--){
  632.     const e=enemies[j];
  633.     if (aabb(e,player)){
  634.       damagePlayer(e.type==="rammer"?35:22);
  635.       spawnExplosion(e.x,e.y,{color:e.color,count:24,speed:220,life:0.7});
  636.       AudioSys.explode(false); enemies.splice(j,1);
  637.     }
  638.   }
  639.   if (boss && !boss.entering && aabb(boss,player)) damagePlayer(30);
  640.   for (let i=pickups.length-1;i>=0;i--){
  641.     if (aabb(pickups[i],player)){ collectPotion(); pickups.splice(i,1); }
  642.   }
  643. }
  644.  
  645. /* ---------------- HUD ---------------- */
  646. const elScore=document.getElementById("scoreVal"),
  647.       elStage=document.getElementById("stageVal"),
  648.       elLives=document.getElementById("lives"),
  649.       elHp=document.getElementById("hpFill"),
  650.       elBossBar=document.getElementById("bossBar"),
  651.       elBossFill=document.getElementById("bossFill");
  652. function updateHUD(){
  653.   elScore.textContent=G.score;
  654.   elStage.textContent=G.stage;
  655.   elLives.textContent="▲".repeat(Math.max(0,player.lives));
  656.   const pct=clamp(player.hp/player.maxHp,0,1)*100;
  657.   elHp.style.width=pct+"%";
  658.   elHp.classList.toggle("danger",pct<35);
  659.  if (G.bossActive && boss){ elBossBar.classList.add("show"); elBossFill.style.width=clamp(boss.hp/boss.maxHp,0,1)*100+"%"; }
  660. }
  661. function showFlash(txt){
  662.  const f=document.getElementById("flash");
  663.  f.textContent=txt; f.classList.remove("show"); void f.offsetWidth; f.classList.add("show");
  664. }
  665.  
  666. /* ---------------- Screens ---------------- */
  667. function startGame(){
  668.  AudioSys.resume();
  669.  state=STATE.PLAYING;
  670.  G.stage=1; G.score=0; G.spawnRateMult=1.0; G.kills=0;
  671.  G.killTarget=STAGE_KILLS[0]; G.bossActive=false; boss=null;
  672.  G.spawnTimer=0; G.potionCooldown=0; G.potionOnScreen=false; G.shake=0;
  673.  playerShots=[]; enemyShots=[]; enemies=[]; particles=[]; pickups=[];
  674.  player.hp=player.maxHp; player.lives=3; player.x=W/2; player.y=H-90;
  675.  player.alive=true; player.invuln=2; player.fireCd=0;
  676.  initStars();
  677.  document.getElementById("startScreen").classList.add("hidden");
  678.  document.getElementById("overScreen").classList.add("hidden");
  679.  document.getElementById("winScreen").classList.add("hidden");
  680.  document.getElementById("pauseScreen").classList.add("hidden");
  681.  document.getElementById("bossBar").classList.remove("show");
  682.  showFlash("STAGE 1");
  683. }
  684. function gameOver(){
  685.  player.alive=false; state=STATE.GAMEOVER;
  686.  document.getElementById("finalScore").textContent=G.score;
  687.  document.getElementById("overScreen").classList.remove("hidden");
  688. }
  689. function winGame(){
  690.  state=STATE.WIN;
  691.  document.getElementById("winScore").textContent=G.score;
  692.  document.getElementById("winScreen").classList.remove("hidden");
  693. }
  694.  
  695. /* ---------------- Input (keyboard only: WASD/Arrows + Space) ---------------- */
  696. const keys={};
  697. function togglePause(){
  698.  if (state===STATE.PLAYING){ state=STATE.PAUSED; document.getElementById("pauseScreen").classList.remove("hidden"); }
  699.  else if (state===STATE.PAUSED){ state=STATE.PLAYING; document.getElementById("pauseScreen").classList.add("hidden"); last=performance.now(); }
  700. }
  701. window.addEventListener("keydown",e=>{
  702.   const k=e.key.toLowerCase();
  703.   keys[k]=true;
  704.   if (k===" "||e.key==="ArrowUp"||e.key==="ArrowDown"||e.key==="ArrowLeft"||e.key==="ArrowRight") e.preventDefault();
  705.   if (k==="p"&&(state===STATE.PLAYING||state===STATE.PAUSED)) togglePause();
  706.   if (k==="m") AudioSys.setMute(!AudioSys.getMute());
  707.   if (k===" " && state===STATE.START) startGame();
  708. });
  709. window.addEventListener("keyup",e=>{ keys[e.key.toLowerCase()]=false; });
  710.  
  711. function handleInput(dt){
  712.   if (!player.alive) return;
  713.   player.invuln=Math.max(0,player.invuln-dt);
  714.   let dx=0, dy=0;
  715.   if (keys["arrowleft"]||keys["a"]) dx-=1;
  716.   if (keys["arrowright"]||keys["d"]) dx+=1;
  717.   if (keys["arrowup"]||keys["w"]) dy-=1;
  718.   if (keys["arrowdown"]||keys["s"]) dy+=1;
  719.   if (dx||dy){
  720.     const m=Math.hypot(dx,dy); dx/=m; dy/=m;
  721.     player.x+=dx*player.speed*dt; player.y+=dy*player.speed*dt;
  722.   }
  723.   player.x=clamp(player.x,player.r,W-player.r);
  724.   player.y=clamp(player.y,player.r,H-player.r);
  725.   // FIRE ONLY with Spacebar
  726.   player.fireCd-=dt;
  727.   if (keys[" "] && player.fireCd<=0){ firePlayer(); player.fireCd=0.16; }
  728. }
  729. document.getElementById("startBtn").onclick=startGame;
  730. document.getElementById("restartBtn").onclick=startGame;
  731. document.getElementById("winRestartBtn").onclick=startGame;
  732.  
  733. /* ---------------- Loop ---------------- */
  734. let last=performance.now();
  735. function loop(now){
  736.   requestAnimationFrame(loop);
  737.   let dt=(now-last)/1000; last=now;
  738.   if (dt>0.05) dt=0.05;
  739.   const t=now/1000;
  740.   if (state===STATE.PLAYING) update(dt,t); else updateStars(dt);
  741.   render();
  742.   updateHUD();
  743. }
  744. function update(dt,t){
  745.   updateStars(dt);
  746.   handleInput(dt);
  747.   if (player.invuln>0) player.invuln=Math.max(0,player.invuln-dt);
  748.   if (!player.alive){
  749.     player.respawnTimer-=dt;
  750.     if (player.respawnTimer<=0 && player.lives>0) respawnPlayer();
  751.   }
  752.   updateShots(dt);
  753.   updateSpawns(dt);
  754.   updateEnemies(dt,t);
  755.   if (boss) updateBoss(dt,t);
  756.   updatePickups(dt);
  757.   trySpawnPotion(dt);
  758.   updateParticles(dt);
  759.   collide();
  760.   if (!G.bossActive && !boss && G.kills>=G.killTarget && enemies.length===0) spawnBoss(G.stage-1);
  761.   G.shake*=0.86; if (G.shake<0.3) G.shake=0;
  762. }
  763. function render(){
  764.  ctx.setTransform(1,0,0,1,0,0);
  765.  ctx.clearRect(0,0,W,H);
  766.  if (G.shake>0) ctx.translate(rand(-G.shake,G.shake),rand(-G.shake,G.shake));
  767.   ctx.fillStyle="#02030a"; ctx.fillRect(-20,-20,W+40,H+40);
  768.   drawStars();
  769.   for (const p of pickups) drawPickup(p);
  770.   for (const e of enemies) drawEnemy(e);
  771.   if (boss) drawBoss();
  772.   drawPlayerShots();
  773.   drawEnemyShots();
  774.   drawParticles();
  775.   if (player.alive && (state===STATE.PLAYING||state===STATE.START)) drawPlayer();
  776. }
  777. requestAnimationFrame(loop);
  778. initStars();
  779. </script>
  780. </body>
  781. </html>
Tags: Qwen3.8
Advertisement
Add Comment
Please, Sign In to add comment