Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
- <title>Star Vanguard — Space Shooter</title>
- <style>
- :root{
- --cyan:#38e1ff; --magenta:#ff3b9a; --yellow:#ffd23b;
- --green:#39ff9e; --red:#ff4d4d; --panel:rgba(10,16,32,0.72);
- }
- *{ box-sizing:border-box; margin:0; padding:0; }
- html,body{
- height:100%; width:100%; overflow:hidden;
- background:radial-gradient(circle at 50% 20%, #10193b 0%, #05070f 55%, #02030a 100%);
- font-family:"Segoe UI", system-ui, -apple-system, sans-serif;
- color:#e8f0ff;
- }
- #stage{ position:fixed; inset:0; display:flex; align-items:center; justify-content:center; }
- #wrap{
- position:relative; aspect-ratio:500 / 720; height:min(100vh, 720px); max-width:100vw;
- display:flex; align-items:center; justify-content:center;
- }
- canvas{
- display:block; height:100%; width:100%; border-radius:14px;
- 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;
- background:#02030a;
- }
- #hud{
- position:absolute; inset:0; pointer-events:none;
- display:flex; flex-direction:column; justify-content:space-between; padding:14px 16px;
- }
- .hud-row{ display:flex; justify-content:space-between; align-items:flex-start; gap:10px; }
- .hud-box{
- background:var(--panel); border:1px solid rgba(56,225,255,.35);
- border-radius:10px; padding:8px 12px; backdrop-filter:blur(4px);
- box-shadow:0 4px 14px rgba(0,0,0,.5);
- }
- .label{ font-size:11px; letter-spacing:.14em; text-transform:uppercase; color:#8fb4ff; font-weight:700; }
- .value{ font-size:22px; font-weight:800; line-height:1; margin-top:3px; color:#fff;
- font-variant-numeric:tabular-nums; text-shadow:0 0 12px rgba(56,225,255,.6); }
- #scoreVal{ color:var(--yellow); text-shadow:0 0 14px rgba(255,210,59,.7); }
- #stageVal{ color:var(--cyan); }
- #hpWrap{ width:150px; }
- .bar{ width:100%; height:14px; margin-top:6px; border-radius:8px;
- background:rgba(255,255,255,.08); border:1px solid rgba(255,255,255,.18); overflow:hidden; }
- #hpFill{ height:100%; width:100%; background:linear-gradient(90deg,#39ff9e,#18d3ff);
- transition:width .12s linear; box-shadow:0 0 12px rgba(57,255,158,.6); }
- #hpFill.danger{ background:linear-gradient(90deg,#ff4d4d,#ff9a3b); box-shadow:0 0 12px rgba(255,77,77,.6); }
- #lives{ font-size:20px; letter-spacing:3px; margin-top:3px; }
- #bossBar{
- position:absolute; top:64px; left:50%; transform:translateX(-50%);
- width:70%; text-align:center; opacity:0; transition:opacity .3s;
- }
- #bossBar.show{ opacity:1; }
- #bossBar .label{ color:var(--magenta); text-align:center; text-shadow:0 0 10px rgba(255,59,154,.7); }
- #bossBar .bar{ height:12px; border-color:rgba(255,59,154,.5); }
- #bossFill{ height:100%; width:100%; background:linear-gradient(90deg,#ff3b9a,#ff7a3b);
- box-shadow:0 0 12px rgba(255,59,154,.7); transition:width .1s linear; }
- .overlay{
- position:absolute; inset:0; display:flex; flex-direction:column;
- align-items:center; justify-content:center; text-align:center;
- background:radial-gradient(circle at center, rgba(6,10,24,.86), rgba(2,3,10,.96));
- backdrop-filter:blur(3px); z-index:20; padding:24px;
- }
- .overlay.hidden{ display:none; }
- h1{
- font-size:44px; font-weight:900; letter-spacing:.08em; margin-bottom:6px;
- background:linear-gradient(90deg,#38e1ff,#ff3b9a,#ffd23b);
- -webkit-background-clip:text; background-clip:text; color:transparent;
- filter:drop-shadow(0 0 18px rgba(56,225,255,.4));
- }
- .subtitle{ color:#9fc0ff; font-size:15px; margin-bottom:22px; letter-spacing:.1em; }
- .big-num{ font-size:56px; font-weight:900; color:var(--yellow);
- text-shadow:0 0 24px rgba(255,210,59,.6); margin:6px 0 18px; }
- .keys{ font-size:13px; color:#cfe0ff; line-height:1.9; margin-bottom:24px; }
- .keys b{ color:var(--cyan); }
- .btn{
- pointer-events:auto; cursor:pointer; font:inherit; font-weight:800; letter-spacing:.12em;
- text-transform:uppercase; font-size:16px; color:#04121a;
- background:linear-gradient(90deg,#38e1ff,#18d3ff); border:none; border-radius:12px;
- padding:14px 40px; box-shadow:0 8px 24px rgba(24,211,255,.5), 0 0 0 1px rgba(255,255,255,.3) inset;
- transition:transform .1s, filter .1s;
- }
- .btn:hover{ transform:translateY(-2px); filter:brightness(1.1); }
- .btn:active{ transform:translateY(0); }
- .flash{
- position:absolute; top:38%; left:50%; transform:translate(-50%,-50%) scale(.6);
- font-size:30px; font-weight:900; letter-spacing:.15em; color:var(--magenta);
- text-shadow:0 0 22px rgba(255,59,154,.9); opacity:0; pointer-events:none; z-index:15;
- transition:opacity .2s;
- }
- .flash.show{ animation:flash 1.4s ease; }
- @keyframes flash{
- 0%{opacity:0; transform:translate(-50%,-50%) scale(.5);}
- 15%{opacity:1; transform:translate(-50%,-50%) scale(1);}
- 70%{opacity:1; transform:translate(-50%,-50%) scale(1.02);}
- 100%{opacity:0; transform:translate(-50%,-50%) scale(1.1);}
- }
- @media (max-width:520px){ h1{font-size:32px;} .subtitle{font-size:13px;} .big-num{font-size:42px;} }
- </style>
- </head>
- <body>
- <div id="stage">
- <div id="wrap">
- <canvas id="game" width="500" height="720"></canvas>
- <div id="hud">
- <div class="hud-row">
- <div class="hud-box">
- <div class="label">Score</div>
- <div class="value" id="scoreVal">0</div>
- </div>
- <div class="hud-box">
- <div class="label">Stage</div>
- <div class="value" id="stageVal">1</div>
- </div>
- <div class="hud-box">
- <div class="label">Lives</div>
- <div id="lives">▲▲▲</div>
- </div>
- </div>
- <div class="hud-row" style="align-items:flex-end;">
- <div class="hud-box" id="hpWrap">
- <div class="label">Hull Integrity</div>
- <div class="bar"><div id="hpFill"></div></div>
- </div>
- </div>
- </div>
- <div id="bossBar">
- <div class="label">BOSS</div>
- <div class="bar"><div id="bossFill"></div></div>
- </div>
- <div id="flash" class="flash"></div>
- <div id="startScreen" class="overlay">
- <h1>STAR VANGUARD</h1>
- <div class="subtitle">DEFEND THE LAST SECTOR</div>
- <div class="keys">
- <b>Move</b> — WASD / Arrow Keys<br>
- <b>Fire</b> — Spacebar<br>
- <b>Pause</b> — P · <b>Mute</b> — M<br>
- Clear each wave, then destroy the boss to advance. 3 stages to victory.
- </div>
- <button id="startBtn" class="btn">Launch</button>
- </div>
- <div id="overScreen" class="overlay hidden">
- <h1>SHIP DESTROYED</h1>
- <div class="subtitle">FINAL SCORE</div>
- <div class="big-num" id="finalScore">0</div>
- <button id="restartBtn" class="btn">Restart</button>
- </div>
- <div id="winScreen" class="overlay hidden">
- <h1>SECTOR SECURED</h1>
- <div class="subtitle">ALL BOSSES DESTROYED — VICTORY</div>
- <div class="big-num" id="winScore">0</div>
- <button id="winRestartBtn" class="btn">Play Again</button>
- </div>
- <div id="pauseScreen" class="overlay hidden">
- <h1>PAUSED</h1>
- <div class="subtitle">PRESS P TO RESUME</div>
- </div>
- </div>
- </div>
- <script>
- "use strict";
- const canvas = document.getElementById("game");
- const ctx = canvas.getContext("2d");
- const W = canvas.width, H = canvas.height;
- /* ---------------- Audio (Web Audio, procedural) ---------------- */
- const AudioSys = (() => {
- let ac=null, master=null, noiseBuf=null, muted=false;
- function ensure(){
- if (ac) return;
- ac = new (window.AudioContext||window.webkitAudioContext)();
- master = ac.createGain(); master.gain.value = muted?0:0.5; master.connect(ac.destination);
- const len = Math.floor(ac.sampleRate*1.2);
- noiseBuf = ac.createBuffer(1,len,ac.sampleRate);
- const d = noiseBuf.getChannelData(0);
- for (let i=0;i<len;i++) d[i]=Math.random()*2-1;
- }
- function resume(){ ensure(); if (ac.state==="suspended") ac.resume(); }
- function tone({type="square",f0=440,f1=f0,dur=0.12,vol=0.3,slide="exp"}){
- if(!ac) return;
- const o=ac.createOscillator(), g=ac.createGain();
- o.type=type;
- o.frequency.setValueAtTime(f0,ac.currentTime);
- o.frequency[slide==="exp"?"exponentialRampToValueAtTime":"linearRampToValueAtTime"](Math.max(1,f1),ac.currentTime+dur);
- g.gain.setValueAtTime(vol,ac.currentTime);
- g.gain.exponentialRampToValueAtTime(0.0008,ac.currentTime+dur);
- o.connect(g); g.connect(master); o.start(); o.stop(ac.currentTime+dur+0.02);
- }
- function noise({dur=0.4,vol=0.4,from=1800,to=180}){
- if(!ac) return;
- const s=ac.createBufferSource(); s.buffer=noiseBuf;
- const f=ac.createBiquadFilter(); f.type="lowpass";
- f.frequency.setValueAtTime(from,ac.currentTime);
- f.frequency.exponentialRampToValueAtTime(to,ac.currentTime+dur);
- const g=ac.createGain();
- g.gain.setValueAtTime(vol,ac.currentTime);
- g.gain.exponentialRampToValueAtTime(0.0008,ac.currentTime+dur);
- s.connect(f); f.connect(g); g.connect(master); s.start(); s.stop(ac.currentTime+dur);
- }
- return {
- resume,
- fire(){ tone({type:"square",f0:880,f1:220,dur:0.09,vol:0.12}); },
- enemyFire(){ tone({type:"sawtooth",f0:300,f1:120,dur:0.12,vol:0.07}); },
- explode(big){ noise({dur:big?0.7:0.32,vol:big?0.5:0.3,from:big?2200:1600,to:80});
- if(big) tone({type:"sine",f0:120,f1:38,dur:0.5,vol:0.3}); },
- pickup(){ [660,880,990,1320].forEach((f,i)=>setTimeout(()=>tone({type:"triangle",f0:f,f1:f,dur:0.1,vol:0.22}),i*55)); },
- hit(){ tone({type:"square",f0:200,f1:60,dur:0.08,vol:0.12}); },
- bossAlert(){ [160,160,240,160].forEach((f,i)=>setTimeout(()=>tone({type:"sawtooth",f0:f,f1:f,dur:0.2,vol:0.25}),i*180)); },
- stage(){ [523,659,784,1046].forEach((f,i)=>setTimeout(()=>tone({type:"triangle",f0:f,f1:f,dur:0.14,vol:0.22}),i*110)); },
- setMute(m){ muted=m; if(master) master.gain.value=m?0:0.5; },
- getMute(){ return muted; }
- };
- })();
- /* ---------------- Utility ---------------- */
- const rand=(a,b)=>a+Math.random()*(b-a);
- const randi=(a,b)=>Math.floor(rand(a,b+1));
- const clamp=(v,a,b)=>v<a?a:v>b?b:v;
- 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; }
- /* ---------------- Global state ---------------- */
- const STATE={START:0,PLAYING:1,GAMEOVER:2,WIN:3,PAUSED:4};
- let state=STATE.START;
- const BASE_SPAWN_INTERVAL = 1.1; // base seconds between enemy spawns (Stage 1)
- const G = {
- stage:1, score:0,
- spawnRateMult:1.0, // scales +10% per stage
- spawnTimer:0,
- killTarget:0, kills:0,
- bossActive:false,
- stars:[],
- potionCooldown:0,
- potionOnScreen:false,
- shake:0,
- };
- const STAGE_KILLS=[14,20,26];
- const MAX_STAGE=3;
- const player={ x:W/2, y:H-90, r:16, speed:340, hp:100, maxHp:100, lives:3,
- fireCd:0, invuln:0, alive:true, respawnTimer:0 };
- let playerShots=[], enemyShots=[], enemies=[], particles=[], pickups=[], boss=null;
- /* ---------------- Stars ---------------- */
- function initStars(){
- G.stars=[];
- 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)});
- }
- function updateStars(dt){
- for (const s of G.stars){ s.y+=s.sp*dt; if (s.y>H){ s.y=0; s.x=Math.random()*W; } }
- }
- function drawStars(){
- 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); }
- ctx.globalAlpha=1;
- }
- /* ---------------- Particles ---------------- */
- function spawnExplosion(x,y,{color="#ffb13b",count=22,speed=180,life=0.6,size=3}={}){
- for (let i=0;i<count;i++){
- const a=Math.random()*Math.PI*2, sp=rand(30,speed);
- particles.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,life,maxLife:life,size:rand(1,size),color});
- }
- }
- function updateParticles(dt){
- for (let i=particles.length-1;i>=0;i--){
- const p=particles[i];
- p.x+=p.vx*dt; p.y+=p.vy*dt; p.vx*=0.96; p.vy*=0.96; p.life-=dt;
- if (p.life<=0) particles.splice(i,1);
- }
- }
- function drawParticles(){
- for (const p of particles){
- const a=p.life/p.maxLife;
- ctx.globalAlpha=a; ctx.fillStyle=p.color;
- ctx.beginPath(); ctx.arc(p.x,p.y,p.size*(0.4+a),0,Math.PI*2); ctx.fill();
- }
- ctx.globalAlpha=1;
- }
- /* ---------------- Player ---------------- */
- function drawPlayer(){
- const {x,y}=player;
- const flick=0.6+Math.random()*0.4;
- ctx.globalCompositeOperation="lighter";
- for (let i=0;i<3;i++){
- const ty=y+18+i*7;
- ctx.globalAlpha=(0.5-i*0.14)*flick;
- ctx.fillStyle=i%2?"#38e1ff":"#ffd23b";
- ctx.beginPath(); ctx.ellipse(x,ty,5-i,9*i+6,0,0,Math.PI*2); ctx.fill();
- }
- ctx.globalCompositeOperation="source-over"; ctx.globalAlpha=1;
- if (player.invuln>0 && Math.floor(performance.now()/80)%2===0) ctx.globalAlpha=0.4;
- const grd=ctx.createLinearGradient(x,y-24,x,y+24);
- grd.addColorStop(0,"#eaffff"); grd.addColorStop(.5,"#38e1ff"); grd.addColorStop(1,"#123a63");
- ctx.fillStyle=grd;
- ctx.beginPath();
- ctx.moveTo(x,y-24); ctx.lineTo(x+14,y+10); ctx.lineTo(x+8,y+18);
- ctx.lineTo(x-8,y+18); ctx.lineTo(x-14,y+10); ctx.closePath(); ctx.fill();
- ctx.fillStyle="#184a7a";
- ctx.beginPath(); ctx.moveTo(x-14,y+10); ctx.lineTo(x-26,y+22); ctx.lineTo(x-10,y+16); ctx.closePath(); ctx.fill();
- ctx.beginPath(); ctx.moveTo(x+14,y+10); ctx.lineTo(x+26,y+22); ctx.lineTo(x+10,y+16); ctx.closePath(); ctx.fill();
- ctx.fillStyle="#eafcff"; ctx.beginPath(); ctx.arc(x,y-2,4.5,0,Math.PI*2); ctx.fill();
- ctx.globalAlpha=1;
- }
- function firePlayer(){
- playerShots.push({x:player.x,y:player.y-24,vy:-720,vx:0,r:5,dmg:1,life:1.4});
- playerShots.push({x:player.x-9,y:player.y-16,vy:-700,vx:-14,r:4,dmg:0.5,life:1.4});
- playerShots.push({x:player.x+9,y:player.y-16,vy:-700,vx:14,r:4,dmg:0.5,life:1.4});
- AudioSys.fire();
- }
- function damagePlayer(amount){
- if (!player.alive || player.invuln>0) return;
- player.hp-=amount; G.shake=Math.max(G.shake,8); AudioSys.hit();
- if (player.hp<=0){ player.hp=0; loseLife(); }
- }
- function loseLife(){
- player.lives--;
- spawnExplosion(player.x,player.y,{color:"#38e1ff",count:40,speed:260,life:0.9,size:4});
- spawnExplosion(player.x,player.y,{color:"#ffffff",count:20,speed:160,life:0.5});
- AudioSys.explode(true); G.shake=20;
- if (player.lives<=0){ gameOver(); }
- else { player.alive=false; player.respawnTimer=2.2; }
- }
- function respawnPlayer(){
- player.alive=true; player.hp=player.maxHp;
- player.x=W/2; player.y=H-90; player.invuln=2.5;
- }
- /* ---------------- Enemy types ---------------- */
- const ENEMY_TYPES={
- drone:{ hp:2,r:16,score:50,color:"#ff7a3b",fireRate:1.6,
- move(e,dt,t){ e.y+=95*dt; },
- fire(e,dt){ if (e.y<H-260 && Math.random()<dt/1.4) aimedShot(e,1); } },
- striker:{ hp:3,r:17,score:90,color:"#c65cff",fireRate:1.2,
- move(e,dt,t){ e.y+=120*dt; e.x+=Math.cos(t*3+e.phase)*160*dt; },
- fire(e,dt){ if (Math.random()<dt/1.0) aimedShot(e,2); } },
- gunner:{ hp:5,r:20,score:130,color:"#ffd23b",fireRate:0.9,
- 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; },
- fire(e,dt){ if (e.y>=e.holdY && Math.random()<dt/1.1) spreadShot(e,3); } },
- rammer:{ hp:4,r:16,score:110,color:"#39ff9e",fireRate:0,
- 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; },
- fire(){} }
- };
- function spawnEnemy(){
- const type=["drone","drone","striker","striker","gunner","rammer"][randi(0,5)];
- const def=ENEMY_TYPES[type];
- enemies.push({ type, x:rand(40,W-40), y:-30, r:def.r, hp:def.hp, maxHp:def.hp,
- color:def.color, score:def.score, phase:Math.random()*Math.PI*2,
- vx:0, vy:0, holdY:rand(120,240), fireTimer:rand(0.5,1.5) });
- }
- /* Enemy spawn loop — the missing piece. Base interval / rateMult. */
- function updateSpawns(dt){
- if (G.bossActive || boss) return; // no normal spawns during boss
- G.spawnTimer -= dt;
- if (G.spawnTimer<=0){
- spawnEnemy();
- G.spawnTimer = BASE_SPAWN_INTERVAL / G.spawnRateMult;
- }
- }
- function aimedShot(e,n){
- for (let i=0;i<n;i++){
- const spread=(i-(n-1)/2)*0.18;
- const ang=Math.atan2(player.y-e.y,player.x-e.x)+spread, sp=260;
- 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});
- }
- AudioSys.enemyFire();
- }
- function spreadShot(e,n){
- const base=Math.atan2(player.y-e.y,player.x-e.x);
- for (let i=0;i<n;i++){
- const ang=base+(i-(n-1)/2)*0.32, sp=230;
- 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});
- }
- AudioSys.enemyFire();
- }
- function updateEnemies(dt,t){
- for (let i=enemies.length-1;i>=0;i--){
- const e=enemies[i];
- ENEMY_TYPES[e.type].move(e,dt,t);
- if (e.type!=="rammer"){
- e.fireTimer-=dt;
- if (e.fireTimer<=0){ ENEMY_TYPES[e.type].fire(e,dt); e.fireTimer=ENEMY_TYPES[e.type].fireRate; }
- }
- if (e.y>H+40||e.x<-40||e.x>W+40) enemies.splice(i,1);
- }
- }
- function drawEnemy(e){
- const {x,y,r}=e;
- ctx.save(); ctx.translate(x,y);
- ctx.globalCompositeOperation="lighter"; ctx.globalAlpha=0.5+Math.random()*0.3;
- ctx.fillStyle=e.color; ctx.beginPath(); ctx.ellipse(0,-r-6,4,8,0,0,Math.PI*2); ctx.fill();
- ctx.globalCompositeOperation="source-over"; ctx.globalAlpha=1;
- const grd=ctx.createLinearGradient(0,-r,0,r);
- grd.addColorStop(0,e.color); grd.addColorStop(1,"rgba(0,0,0,0.25)");
- ctx.fillStyle=grd;
- if (e.type==="drone"){
- 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();
- } else if (e.type==="striker"){
- 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);
- ctx.lineTo(-r+4,-r*0.6); ctx.lineTo(-r-6,0); ctx.closePath(); ctx.fill();
- } else if (e.type==="gunner"){
- ctx.beginPath(); ctx.arc(0,0,r-1,0,Math.PI*2); ctx.fill();
- ctx.fillStyle="rgba(0,0,0,.4)"; ctx.beginPath(); ctx.arc(0,0,r-7,0,Math.PI*2); ctx.fill();
- ctx.fillStyle="#fff2b3"; ctx.beginPath(); ctx.arc(0,0,5,0,Math.PI*2); ctx.fill();
- } else {
- 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();
- }
- ctx.restore();
- if (e.hp<e.maxHp){
- const w=r*1.8;
- ctx.fillStyle="rgba(0,0,0,.5)"; ctx.fillRect(x-w/2,y-r-12,w,4);
- ctx.fillStyle=e.color; ctx.fillRect(x-w/2,y-r-12,w*(e.hp/e.maxHp),4);
- }
- }
- /* ---------------- Bosses ---------------- */
- const BOSS_DEFS=[
- {name:"CRUISER KRAIT",hp:120,r:56,score:1500,color:"#ff3b9a",pattern:"spread"},
- {name:"DESTROYER VEX",hp:180,r:62,score:3000,color:"#c65cff",pattern:"homing"},
- {name:"DREADNOUGHT OMEGA",hp:280,r:72,score:6000,color:"#ff6b3b",pattern:"laser"}
- ];
- function spawnBoss(idx){
- const d=BOSS_DEFS[idx];
- boss={ x:W/2, y:-d.r-20, r:d.r, hp:d.hp, maxHp:d.hp, targetY:130, color:d.color,
- score:d.score, name:d.name, pattern:d.pattern, entering:true,
- fireT:1, phase:Math.PI*1.4, waveT:0, laserT:2.5, homeT:1.2, angle:0 };
- G.bossActive=true;
- AudioSys.bossAlert();
- showFlash("⚠ "+d.name+" APPROACHING ⚠");
- }
- function updateBoss(dt,t){
- const b=boss; if(!b) return;
- 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; }
- b.phase+=dt*1.1;
- b.x=W/2+Math.sin(b.phase)*(W*0.32); // HORIZONTAL-ONLY
- b.x=clamp(b.x,b.r+10,W-b.r-10);
- if (b.pattern==="spread"){
- b.fireT-=dt;
- if (b.fireT<=0){
- const base=Math.atan2(player.y-b.y,player.x-b.x);
- for (let i=-3;i<=3;i++){
- const a=base+i*0.22;
- 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});
- }
- b.fireT=1.15; AudioSys.enemyFire();
- }
- } else if (b.pattern==="homing"){
- b.homeT-=dt;
- if (b.homeT<=0){
- for (let i=0;i<2;i++){
- const a=(player.x-b.x>0?0:Math.PI)+rand(-0.6,0.6);
- 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});
- }
- b.homeT=1.0; AudioSys.enemyFire();
- }
- } else {
- b.laserT-=dt; b.angle+=dt*0.6;
- if (b.laserT<=0){
- const base=Math.atan2(player.y-b.y,player.x-b.x);
- for (let k=0;k<2;k++){
- const a=base+rand(-0.12,0.12);
- 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});
- }
- b.laserT=0.55; AudioSys.enemyFire();
- }
- b.waveT-=dt;
- if (b.waveT<=0){
- const n=16;
- for (let i=0;i<n;i++){
- const a=i/n*Math.PI*2;
- 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});
- }
- b.waveT=3.2; AudioSys.enemyFire();
- }
- }
- }
- function drawBoss(){
- const b=boss; if(!b) return;
- const {x,y,r}=b;
- ctx.save(); ctx.translate(x,y);
- ctx.globalCompositeOperation="lighter"; ctx.globalAlpha=0.5;
- ctx.fillStyle=b.color; ctx.beginPath(); ctx.arc(0,0,r*1.1,0,Math.PI*2); ctx.fill();
- ctx.globalCompositeOperation="source-over"; ctx.globalAlpha=1;
- const grd=ctx.createLinearGradient(0,-r,0,r);
- grd.addColorStop(0,b.color); grd.addColorStop(1,"rgba(10,5,20,0.9)");
- ctx.fillStyle=grd;
- ctx.beginPath();
- 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);
- ctx.lineTo(-r*0.55,-r*0.6); ctx.lineTo(-r*0.9,0); ctx.closePath(); ctx.fill();
- ctx.fillStyle="rgba(0,0,0,.35)";
- ctx.beginPath(); ctx.arc(-r*0.7,0,r*0.28,0,Math.PI*2); ctx.fill();
- ctx.beginPath(); ctx.arc(r*0.7,0,r*0.28,0,Math.PI*2); ctx.fill();
- const pulse=6+Math.sin(performance.now()/200)*3;
- ctx.fillStyle="#fff"; ctx.beginPath(); ctx.arc(0,0,pulse,0,Math.PI*2); ctx.fill();
- ctx.fillStyle=b.color; ctx.globalAlpha=.6; ctx.beginPath(); ctx.arc(0,0,pulse+5,0,Math.PI*2); ctx.fill();
- ctx.globalAlpha=1;
- ctx.restore();
- }
- function defeatBoss(){
- const b=boss;
- spawnExplosion(b.x,b.y,{color:b.color,count:70,speed:320,life:1.1,size:5});
- spawnExplosion(b.x,b.y,{color:"#ffffff",count:40,speed:200,life:0.7});
- spawnExplosion(b.x,b.y,{color:"#ffd23b",count:30,speed:260,life:0.9,size:4});
- G.score+=b.score; G.shake=30; AudioSys.explode(true);
- boss=null; G.bossActive=false;
- document.getElementById("bossBar").classList.remove("show");
- if (G.stage>=MAX_STAGE){ winGame(); } else { advanceStage(); }
- }
- function advanceStage(){
- G.stage++;
- G.spawnRateMult*=1.10;
- G.kills=0;
- G.killTarget=STAGE_KILLS[G.stage-1];
- G.spawnTimer=0;
- AudioSys.stage();
- showFlash("STAGE "+G.stage);
- }
- /* ---------------- Shots ---------------- */
- function updateShots(dt){
- for (let i=playerShots.length-1;i>=0;i--){
- const s=playerShots[i]; s.x+=s.vx*dt; s.y+=s.vy*dt; s.life-=dt;
- if (s.life<=0||s.y<-20) playerShots.splice(i,1);
- }
- for (let i=enemyShots.length-1;i>=0;i--){
- const s=enemyShots[i];
- if (s.homing && player.alive){
- const ta=Math.atan2(player.y-s.y,player.x-s.x), cur=Math.atan2(s.vy,s.vx);
- let diff=ta-cur; diff=Math.atan2(Math.sin(diff),Math.cos(diff));
- cur+=clamp(diff,-s.turn*dt,s.turn*dt);
- const sp=Math.hypot(s.vx,s.vy); s.vx=Math.cos(cur)*sp; s.vy=Math.sin(cur)*sp;
- }
- s.x+=s.vx*dt; s.y+=s.vy*dt; s.life-=dt;
- if (s.life<=0||s.y>H+30||s.y<-30||s.x<-30||s.x>W+30) enemyShots.splice(i,1);
- }
- }
- function drawPlayerShots(){
- ctx.globalCompositeOperation="lighter";
- for (const s of playerShots){
- ctx.fillStyle="#baffff"; ctx.globalAlpha=.9; ctx.beginPath(); ctx.ellipse(s.x,s.y,3,9,0,0,Math.PI*2); ctx.fill();
- ctx.fillStyle="#38e1ff"; ctx.globalAlpha=.5; ctx.beginPath(); ctx.ellipse(s.x,s.y,6,13,0,0,Math.PI*2); ctx.fill();
- }
- ctx.globalCompositeOperation="source-over"; ctx.globalAlpha=1;
- }
- function drawEnemyShots(){
- ctx.globalCompositeOperation="lighter";
- for (const s of enemyShots){
- ctx.fillStyle=s.color; ctx.globalAlpha=.9; ctx.beginPath(); ctx.arc(s.x,s.y,s.r,0,Math.PI*2); ctx.fill();
- ctx.globalAlpha=.4; ctx.beginPath(); ctx.arc(s.x,s.y,s.r*1.8,0,Math.PI*2); ctx.fill();
- }
- ctx.globalCompositeOperation="source-over"; ctx.globalAlpha=1;
- }
- /* ---------------- Health potions ---------------- */
- function trySpawnPotion(dt){
- if (G.potionOnScreen) return;
- if (G.potionCooldown>0){ G.potionCooldown-=dt; return; }
- if (Math.random()<0.03*dt){
- pickups.push({x:rand(50,W-50),y:-24,r:16,vy:110,t:0});
- G.potionOnScreen=true;
- }
- }
- function updatePickups(dt){
- for (let i=pickups.length-1;i>=0;i--){
- const p=pickups[i]; p.y+=p.vy*dt; p.t+=dt;
- if (p.y>H+30){ pickups.splice(i,1); G.potionOnScreen=false; }
- }
- }
- function drawPickup(p){
- const bob=Math.sin(p.t*4)*3;
- ctx.save(); ctx.translate(p.x,p.y+bob);
- ctx.globalCompositeOperation="lighter"; ctx.globalAlpha=.4+Math.sin(p.t*5)*0.2;
- ctx.fillStyle="#39ff9e"; ctx.beginPath(); ctx.arc(0,0,p.r*1.5,0,Math.PI*2); ctx.fill();
- ctx.globalCompositeOperation="source-over"; ctx.globalAlpha=1;
- ctx.fillStyle="#0c3b2a"; ctx.beginPath(); ctx.arc(0,3,p.r*0.8,0,Math.PI*2); ctx.fill();
- ctx.fillStyle="#39ff9e"; ctx.beginPath(); ctx.arc(0,3,p.r*0.6,0,Math.PI*2); ctx.fill();
- ctx.fillStyle="#c9ffe9"; ctx.fillRect(-5,-p.r*0.9,10,7);
- 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);
- ctx.restore();
- }
- function collectPotion(){
- player.hp=Math.min(player.maxHp,player.hp+40);
- G.potionCooldown=30; G.potionOnScreen=false;
- AudioSys.pickup();
- spawnExplosion(player.x,player.y-30,{color:"#39ff9e",count:16,speed:120,life:0.5,size:3});
- }
- /* ---------------- Collision ---------------- */
- function collide(){
- for (let i=playerShots.length-1;i>=0;i--){
- const s=playerShots[i]; let hit=false;
- for (let j=enemies.length-1;j>=0;j--){
- const e=enemies[j];
- if (aabb(s,e)){
- e.hp-=s.dmg;
- spawnExplosion(s.x,s.y,{color:e.color,count:5,speed:90,life:0.3,size:2});
- if (e.hp<=0){
- G.score+=e.score;
- spawnExplosion(e.x,e.y,{color:e.color,count:20,speed:200,life:0.6});
- AudioSys.explode(false); enemies.splice(j,1); G.kills++;
- }
- hit=true; break;
- }
- }
- if (!hit && boss && aabb(s,boss)){
- boss.hp-=s.dmg;
- spawnExplosion(s.x,s.y,{color:boss.color,count:5,speed:90,life:0.3,size:2});
- hit=true;
- if (boss.hp<=0) defeatBoss();
- }
- if (hit) playerShots.splice(i,1);
- }
- if (!player.alive) return;
- for (let i=enemyShots.length-1;i>=0;i--){
- const s=enemyShots[i];
- if (aabb(s,player)){
- damagePlayer(s.dmg);
- spawnExplosion(s.x,s.y,{color:"#ff4d4d",count:10,speed:140,life:0.4,size:3});
- enemyShots.splice(i,1);
- }
- }
- for (let j=enemies.length-1;j>=0;j--){
- const e=enemies[j];
- if (aabb(e,player)){
- damagePlayer(e.type==="rammer"?35:22);
- spawnExplosion(e.x,e.y,{color:e.color,count:24,speed:220,life:0.7});
- AudioSys.explode(false); enemies.splice(j,1);
- }
- }
- if (boss && !boss.entering && aabb(boss,player)) damagePlayer(30);
- for (let i=pickups.length-1;i>=0;i--){
- if (aabb(pickups[i],player)){ collectPotion(); pickups.splice(i,1); }
- }
- }
- /* ---------------- HUD ---------------- */
- const elScore=document.getElementById("scoreVal"),
- elStage=document.getElementById("stageVal"),
- elLives=document.getElementById("lives"),
- elHp=document.getElementById("hpFill"),
- elBossBar=document.getElementById("bossBar"),
- elBossFill=document.getElementById("bossFill");
- function updateHUD(){
- elScore.textContent=G.score;
- elStage.textContent=G.stage;
- elLives.textContent="▲".repeat(Math.max(0,player.lives));
- const pct=clamp(player.hp/player.maxHp,0,1)*100;
- elHp.style.width=pct+"%";
- elHp.classList.toggle("danger",pct<35);
- if (G.bossActive && boss){ elBossBar.classList.add("show"); elBossFill.style.width=clamp(boss.hp/boss.maxHp,0,1)*100+"%"; }
- }
- function showFlash(txt){
- const f=document.getElementById("flash");
- f.textContent=txt; f.classList.remove("show"); void f.offsetWidth; f.classList.add("show");
- }
- /* ---------------- Screens ---------------- */
- function startGame(){
- AudioSys.resume();
- state=STATE.PLAYING;
- G.stage=1; G.score=0; G.spawnRateMult=1.0; G.kills=0;
- G.killTarget=STAGE_KILLS[0]; G.bossActive=false; boss=null;
- G.spawnTimer=0; G.potionCooldown=0; G.potionOnScreen=false; G.shake=0;
- playerShots=[]; enemyShots=[]; enemies=[]; particles=[]; pickups=[];
- player.hp=player.maxHp; player.lives=3; player.x=W/2; player.y=H-90;
- player.alive=true; player.invuln=2; player.fireCd=0;
- initStars();
- document.getElementById("startScreen").classList.add("hidden");
- document.getElementById("overScreen").classList.add("hidden");
- document.getElementById("winScreen").classList.add("hidden");
- document.getElementById("pauseScreen").classList.add("hidden");
- document.getElementById("bossBar").classList.remove("show");
- showFlash("STAGE 1");
- }
- function gameOver(){
- player.alive=false; state=STATE.GAMEOVER;
- document.getElementById("finalScore").textContent=G.score;
- document.getElementById("overScreen").classList.remove("hidden");
- }
- function winGame(){
- state=STATE.WIN;
- document.getElementById("winScore").textContent=G.score;
- document.getElementById("winScreen").classList.remove("hidden");
- }
- /* ---------------- Input (keyboard only: WASD/Arrows + Space) ---------------- */
- const keys={};
- function togglePause(){
- if (state===STATE.PLAYING){ state=STATE.PAUSED; document.getElementById("pauseScreen").classList.remove("hidden"); }
- else if (state===STATE.PAUSED){ state=STATE.PLAYING; document.getElementById("pauseScreen").classList.add("hidden"); last=performance.now(); }
- }
- window.addEventListener("keydown",e=>{
- const k=e.key.toLowerCase();
- keys[k]=true;
- if (k===" "||e.key==="ArrowUp"||e.key==="ArrowDown"||e.key==="ArrowLeft"||e.key==="ArrowRight") e.preventDefault();
- if (k==="p"&&(state===STATE.PLAYING||state===STATE.PAUSED)) togglePause();
- if (k==="m") AudioSys.setMute(!AudioSys.getMute());
- if (k===" " && state===STATE.START) startGame();
- });
- window.addEventListener("keyup",e=>{ keys[e.key.toLowerCase()]=false; });
- function handleInput(dt){
- if (!player.alive) return;
- player.invuln=Math.max(0,player.invuln-dt);
- let dx=0, dy=0;
- if (keys["arrowleft"]||keys["a"]) dx-=1;
- if (keys["arrowright"]||keys["d"]) dx+=1;
- if (keys["arrowup"]||keys["w"]) dy-=1;
- if (keys["arrowdown"]||keys["s"]) dy+=1;
- if (dx||dy){
- const m=Math.hypot(dx,dy); dx/=m; dy/=m;
- player.x+=dx*player.speed*dt; player.y+=dy*player.speed*dt;
- }
- player.x=clamp(player.x,player.r,W-player.r);
- player.y=clamp(player.y,player.r,H-player.r);
- // FIRE ONLY with Spacebar
- player.fireCd-=dt;
- if (keys[" "] && player.fireCd<=0){ firePlayer(); player.fireCd=0.16; }
- }
- document.getElementById("startBtn").onclick=startGame;
- document.getElementById("restartBtn").onclick=startGame;
- document.getElementById("winRestartBtn").onclick=startGame;
- /* ---------------- Loop ---------------- */
- let last=performance.now();
- function loop(now){
- requestAnimationFrame(loop);
- let dt=(now-last)/1000; last=now;
- if (dt>0.05) dt=0.05;
- const t=now/1000;
- if (state===STATE.PLAYING) update(dt,t); else updateStars(dt);
- render();
- updateHUD();
- }
- function update(dt,t){
- updateStars(dt);
- handleInput(dt);
- if (player.invuln>0) player.invuln=Math.max(0,player.invuln-dt);
- if (!player.alive){
- player.respawnTimer-=dt;
- if (player.respawnTimer<=0 && player.lives>0) respawnPlayer();
- }
- updateShots(dt);
- updateSpawns(dt);
- updateEnemies(dt,t);
- if (boss) updateBoss(dt,t);
- updatePickups(dt);
- trySpawnPotion(dt);
- updateParticles(dt);
- collide();
- if (!G.bossActive && !boss && G.kills>=G.killTarget && enemies.length===0) spawnBoss(G.stage-1);
- G.shake*=0.86; if (G.shake<0.3) G.shake=0;
- }
- function render(){
- ctx.setTransform(1,0,0,1,0,0);
- ctx.clearRect(0,0,W,H);
- if (G.shake>0) ctx.translate(rand(-G.shake,G.shake),rand(-G.shake,G.shake));
- ctx.fillStyle="#02030a"; ctx.fillRect(-20,-20,W+40,H+40);
- drawStars();
- for (const p of pickups) drawPickup(p);
- for (const e of enemies) drawEnemy(e);
- if (boss) drawBoss();
- drawPlayerShots();
- drawEnemyShots();
- drawParticles();
- if (player.alive && (state===STATE.PLAYING||state===STATE.START)) drawPlayer();
- }
- requestAnimationFrame(loop);
- initStars();
- </script>
- </body>
- </html>
Advertisement
Add Comment
Please, Sign In to add comment