| <!doctype html> |
| <html lang="en"> |
| <head> |
| <meta charset="utf-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> |
| <title>QUANTDOOM</title> |
| <meta name="description" content="A quantum raycaster. Specters pulse between ghost and solid; your shot is a measurement and your hit chance is their amplitude squared — the Born rule as a trigger-timing skill. Fire when they collapse."> |
| <link rel="preconnect" href="https://fonts.googleapis.com"> |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
| <link href="https://fonts.googleapis.com/css2?family=Anton&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet"> |
| <style> |
| :root{--bg:#08040a;--ink:#f2e9ee;--dim:#9a7f92;--hot:#ff2f4e;--cool:#25e0ff;--gold:#ffcf3f;--mono:'JetBrains Mono',ui-monospace,monospace;--disp:'Anton',Impact,sans-serif;} |
| *{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent;user-select:none} |
| html,body{height:100%;overflow:hidden;background:#000;color:var(--ink);font-family:var(--mono)} |
| #stage{position:fixed;inset:0;background:#000} |
| canvas{position:absolute;inset:0;width:100%;height:100%;image-rendering:pixelated} |
| #hud{position:absolute;inset:0;pointer-events:none;z-index:5} |
| .top{position:absolute;top:0;left:0;right:0;display:flex;justify-content:space-between;padding:14px 18px} |
| .stat .l{font-size:9px;letter-spacing:.24em;color:var(--dim);text-transform:uppercase} |
| .stat .v{font-family:var(--disp);font-size:30px;line-height:.9;font-variant-numeric:tabular-nums} |
| #kills .v{color:var(--gold)} .stat.r{text-align:right} |
| #coh-wrap{position:absolute;left:18px;bottom:16px;width:min(46vw,240px)} |
| #coh-wrap .l{font-size:9px;letter-spacing:.22em;color:var(--dim);text-transform:uppercase;margin-bottom:4px;display:flex;justify-content:space-between} |
| #coh-bar{height:12px;background:rgba(255,47,78,.14);border:1px solid rgba(255,47,78,.4);border-radius:7px;overflow:hidden} |
| #coh-fill{height:100%;width:100%;background:linear-gradient(90deg,var(--hot),#ff7a3f);transition:width .12s} |
| #msg{position:absolute;top:22%;left:50%;transform:translateX(-50%);font-family:var(--disp);font-size:clamp(28px,7vw,60px);letter-spacing:.04em;opacity:0;text-align:center;text-shadow:0 0 30px currentColor} |
| /* touch controls */ |
| #pad{position:absolute;inset:0;z-index:6} |
| .btn{position:absolute;pointer-events:auto;font-family:var(--disp);color:#fff;background:rgba(37,224,255,.10);border:1.5px solid rgba(37,224,255,.32);border-radius:50%;display:grid;place-items:center;backdrop-filter:blur(2px)} |
| .btn:active{background:rgba(37,224,255,.28)} |
| #b-l{left:16px;bottom:96px;width:62px;height:62px;font-size:26px} |
| #b-r{left:104px;bottom:96px;width:62px;height:62px;font-size:26px} |
| #b-f{left:60px;bottom:172px;width:62px;height:62px;font-size:22px} |
| #b-fire{right:22px;bottom:110px;width:96px;height:96px;font-size:18px;background:rgba(255,47,78,.16);border-color:rgba(255,47,78,.5);color:var(--hot)} |
| #b-fire:active{background:rgba(255,47,78,.34)} |
| .hint{position:absolute;bottom:6px;left:0;right:0;text-align:center;font-size:9.5px;letter-spacing:.1em;color:var(--dim)} |
| /* overlays */ |
| .screen{position:absolute;inset:0;z-index:10;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:22px;background:radial-gradient(120% 90% at 50% 25%,rgba(255,47,78,.10),transparent 60%),rgba(6,3,8,.86);pointer-events:auto} |
| .screen.hide{display:none} |
| h1{font-family:var(--disp);font-size:clamp(52px,17vw,150px);line-height:.82;letter-spacing:.02em;background:linear-gradient(180deg,var(--cool),#7b6bff 45%,var(--hot));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;filter:drop-shadow(0 4px 40px rgba(255,47,78,.3))} |
| .tag{color:var(--dim);max-width:32rem;margin:14px 0 2px;font-size:13.5px;line-height:1.6}.tag b{color:var(--ink);font-weight:500} |
| .how{display:flex;gap:12px;flex-wrap:wrap;justify-content:center;margin:20px 0 4px;max-width:36rem} |
| .card{flex:1 1 150px;min-width:145px;background:rgba(30,14,26,.5);border:1px solid #2c1730;border-radius:12px;padding:13px 12px;font-size:11.5px;line-height:1.5;color:var(--dim)} |
| .card b{display:block;font-family:var(--disp);font-size:15px;letter-spacing:.03em;margin-bottom:5px;color:var(--ink)} |
| .card .g{color:var(--cool)}.card .s{color:var(--hot)} |
| button.go{font-family:var(--disp);font-size:22px;letter-spacing:.06em;cursor:pointer;color:#12040a;background:linear-gradient(180deg,var(--hot),#c81232);border:0;border-radius:12px;padding:14px 40px;margin-top:24px;box-shadow:0 6px 30px rgba(255,47,78,.4)} |
| button.go:active{transform:translateY(2px)} |
| .fine{margin-top:20px;font-size:10px;letter-spacing:.14em;color:#6a4a5c;text-transform:uppercase}.fine .f{color:var(--hot)} |
| #go-score{font-family:var(--disp);font-size:clamp(56px,16vw,120px);color:var(--gold);line-height:.9;margin:2px 0} |
| .kbd{font-size:10px;letter-spacing:.1em;color:#6a4a5c;margin-top:8px} |
| @media(hover:hover){#pad{display:none}} |
| </style> |
| </head> |
| <body> |
| <div id="stage"> |
| <canvas id="c"></canvas> |
| <div id="hud"> |
| <div class="top"> |
| <div class="stat" id="kills"><span class="l">specters collapsed</span><br><span class="v" id="kills-v">0</span></div> |
| <div class="stat r" id="best"><span class="l">best</span><br><span class="v" id="best-v">0</span></div> |
| </div> |
| <div id="coh-wrap"><div class="l"><span>coherence</span><span id="coh-pct">100%</span></div><div id="coh-bar"><div id="coh-fill"></div></div></div> |
| <div id="msg"></div> |
| </div> |
| <div id="pad"> |
| <div class="btn" id="b-l">◄</div><div class="btn" id="b-r">►</div><div class="btn" id="b-f">▲</div><div class="btn" id="b-fire">FIRE</div> |
| </div> |
|
|
| <div class="screen" id="start"> |
| <h1>QUANT<br>DOOM</h1> |
| <p class="tag">The specters are in <b>superposition</b> — pulsing between <span style="color:var(--cool)">ghost</span> and <span style="color:var(--hot)">collapsed</span>. Your shot is a <b>measurement</b>. Hit chance = their <b>amplitude²</b> (the Born rule). So fire when they turn solid — that's also exactly when they can hurt you.</p> |
| <div class="how"> |
| <div class="card"><b>SEE</b>A <span class="g">faint cyan</span> specter is a ghost — it can't touch you and can't be killed. A <span class="s">solid red</span> one has collapsed: lethal, and vulnerable.</div> |
| <div class="card"><b>SHOOT</b>Firing collapses its wavefunction. The more solid it is (brighter), the likelier your shot lands. Fire at a ghost and it just phases away.</div> |
| <div class="card"><b>SURVIVE</b>They <span class="g">tunnel</span> straight through walls toward you. Keep your coherence up. It never really ends — chase the count.</div> |
| </div> |
| <button class="go" id="start-btn">▶ ENTER THE FIELD</button> |
| <p class="kbd">desktop: <b>W/S</b> move · <b>A/D</b> or <b>←/→</b> turn · <b>SPACE/click</b> fire | touch: on-screen pads</p> |
| <p class="fine">🜂 born rule as a trigger · measurement is a weapon · <span class="f">ahogy a dolgok vannak</span></p> |
| </div> |
|
|
| <div class="screen hide" id="over"> |
| <p class="tag" style="margin-bottom:0">coherence lost · you decohered</p> |
| <div id="go-score">0</div> |
| <p class="tag" style="margin-top:0">specters collapsed · best <b id="go-best">0</b></p> |
| <button class="go" id="again-btn">▶ RE-ENTER</button> |
| <p class="fine">PeetPedro · entropy-om · <span class="f">quantum play</span></p> |
| </div> |
| </div> |
|
|
| <script> |
| (function(){ |
| "use strict"; |
| var cv=document.getElementById('c'), ctx=cv.getContext('2d',{alpha:false}); |
| var W=0,H=0,RAYS=0,colW=0, DPR=1; |
| var reduce=matchMedia('(prefers-reduced-motion: reduce)').matches; |
| |
| |
| var MAP=[ |
| "################", |
| "#..............#", |
| "#..##....##....#", |
| "#..#......#..#.#", |
| "#..#..##..#..#.#", |
| "#.....##.......#", |
| "#..............#", |
| "#.####....####.#", |
| "#.#..........#.#", |
| "#.#..######..#.#", |
| "#....#....#....#", |
| "#..#.#....#.#..#", |
| "#..#......#.#..#", |
| "#..#.####.....##", |
| "#..............#", |
| "################"]; |
| var MW=MAP[0].length, MH=MAP.length; |
| function cell(x,y){ if(x<0||y<0||x>=MW||y>=MH) return '#'; return MAP[y|0][x|0]; } |
| function isWall(x,y){ return cell(x,y)==='#'; } |
| |
| |
| var player={x:2.5,y:2.5,ang:0.4}, FOV=Math.PI/3; |
| var enemies=[], zbuf=[], running=false, kills=0, coherence=100, best=+(localStorage.getItem('qd_best')||0); |
| var spawnT=0, spawnEvery=2.2, wave=0, muzzle=0, shake=0, vign=0, dmgCooldown=0; |
| var keys={}, touch={l:false,r:false,f:false}; |
| document.getElementById('best-v').textContent=best; |
| |
| |
| var actx=null; |
| function A(){ if(!actx){ try{actx=new (window.AudioContext||window.webkitAudioContext)();}catch(e){} } if(actx&&actx.state==='suspended')actx.resume(); } |
| function zap(){ if(!actx)return; var n=actx.currentTime,o=actx.createOscillator(),g=actx.createGain(); |
| o.type='square'; o.frequency.setValueAtTime(880,n); o.frequency.exponentialRampToValueAtTime(140,n+.12); |
| g.gain.setValueAtTime(.14,n); g.gain.exponentialRampToValueAtTime(.0008,n+.14); o.connect(g);g.connect(actx.destination);o.start(n);o.stop(n+.15); } |
| function pop(hit){ if(!actx)return; var n=actx.currentTime,o=actx.createOscillator(),g=actx.createGain(); |
| o.type=hit?'triangle':'sine'; o.frequency.setValueAtTime(hit?520:180,n); o.frequency.exponentialRampToValueAtTime(hit?1400:90,n+.18); |
| g.gain.setValueAtTime(.18,n); g.gain.exponentialRampToValueAtTime(.0008,n+.25); o.connect(g);g.connect(actx.destination);o.start(n);o.stop(n+.26); } |
| function hurt(){ if(!actx)return; var n=actx.currentTime,o=actx.createOscillator(),g=actx.createGain(); |
| o.type='sawtooth'; o.frequency.setValueAtTime(120,n); o.frequency.exponentialRampToValueAtTime(40,n+.3); |
| g.gain.setValueAtTime(.2,n); g.gain.exponentialRampToValueAtTime(.001,n+.32); o.connect(g);g.connect(actx.destination);o.start(n);o.stop(n+.33); } |
| |
| |
| function resize(){ DPR=Math.min(2,window.devicePixelRatio||1); |
| W=Math.floor(innerWidth); H=Math.floor(innerHeight); |
| cv.width=Math.floor(W*0.6); cv.height=Math.floor(H*0.6); |
| RAYS=cv.width; colW=1; ctx.imageSmoothingEnabled=false; } |
| addEventListener('resize',resize); |
| |
| function spawnEnemy(){ |
| |
| for(var tries=0;tries<40;tries++){ |
| var x=1+Math.random()*(MW-2), y=1+Math.random()*(MH-2); |
| if(isWall(x,y)) continue; |
| var dx=x-player.x, dy=y-player.y; if(dx*dx+dy*dy<25) continue; |
| enemies.push({x:x,y:y, amp:0.5, rate:0.7+Math.random()*1.1, ph:Math.random()*6.28, dead:false, hitFlash:0, killT:0}); |
| return; |
| } |
| } |
| function start(){ |
| running=true; kills=0; coherence=100; enemies=[]; spawnT=0; spawnEvery=2.2; wave=0; muzzle=0; shake=0; vign=0; |
| player.x=2.5; player.y=2.5; player.ang=0.4; |
| document.getElementById('start').classList.add('hide'); document.getElementById('over').classList.add('hide'); |
| document.getElementById('kills-v').textContent='0'; setCoh(100); |
| for(var i=0;i<3;i++) spawnEnemy(); |
| } |
| function gameOver(){ running=false; |
| if(kills>best){ best=kills; localStorage.setItem('qd_best',best); document.getElementById('best-v').textContent=best; } |
| document.getElementById('go-score').textContent=kills; |
| document.getElementById('go-best').textContent=best; |
| document.getElementById('over').classList.remove('hide'); |
| } |
| function setCoh(v){ coherence=Math.max(0,Math.min(100,v)); |
| document.getElementById('coh-fill').style.width=coherence+'%'; |
| document.getElementById('coh-pct').textContent=Math.round(coherence)+'%'; } |
| function flashMsg(t,col){ var m=document.getElementById('msg'); m.textContent=t; m.style.color=col||'#fff'; |
| m.style.transition='none'; m.style.opacity='1'; |
| requestAnimationFrame(function(){ m.style.transition='opacity .7s'; m.style.opacity='0'; }); } |
| |
| |
| function fire(){ |
| if(!running) return; |
| muzzle=1; zap(); shake=Math.max(shake,3); |
| |
| var bestE=null, bestScore=1e9; |
| for(var i=0;i<enemies.length;i++){ var e=enemies[i]; if(e.dead) continue; |
| var dx=e.x-player.x, dy=e.y-player.y, dist=Math.hypot(dx,dy); |
| if(dist>11) continue; |
| var ang=Math.atan2(dy,dx)-player.ang; while(ang<-Math.PI)ang+=6.283; while(ang>Math.PI)ang-=6.283; |
| if(Math.abs(ang)>0.14) continue; |
| |
| var sc=Math.abs(ang)*4+dist*0.05; |
| if(sc<bestScore){ bestScore=sc; bestE=e; } |
| } |
| if(!bestE){ return; } |
| var amp=bestE.amp; |
| var p=amp*amp; |
| if(Math.random()<p){ |
| bestE.dead=true; bestE.killT=performance.now(); kills++; |
| document.getElementById('kills-v').textContent=kills; |
| pop(true); |
| if(kills%10===0) flashMsg(kills+' COLLAPSED', '#ffcf3f'); |
| } else { |
| bestE.amp=0.06; bestE.ph=Math.PI; |
| pop(false); |
| } |
| } |
| function wallBetween(x0,y0,x1,y1){ |
| var steps=Math.ceil(Math.hypot(x1-x0,y1-y0)*4); |
| for(var s=1;s<steps;s++){ var t=s/steps; if(isWall(x0+(x1-x0)*t, y0+(y1-y0)*t)) return true; } |
| return false; |
| } |
| |
| |
| function update(dt){ |
| |
| var turn=(keys.a||keys.ArrowLeft||touch.l?-1:0)+(keys.d||keys.ArrowRight||touch.r?1:0); |
| player.ang += turn*2.6*dt; |
| var mv=(keys.w||keys.ArrowUp||touch.f?1:0)+(keys.s||keys.ArrowDown?-1:0); |
| if(mv){ var nx=player.x+Math.cos(player.ang)*mv*2.8*dt, ny=player.y+Math.sin(player.ang)*mv*2.8*dt; |
| if(!isWall(nx,player.y)) player.x=nx; if(!isWall(player.x,ny)) player.y=ny; } |
| |
| spawnT+=dt; if(spawnT>spawnEvery && enemies.filter(function(e){return!e.dead;}).length<10){ spawnT=0; spawnEvery=Math.max(0.9,spawnEvery-0.04); spawnEnemy(); } |
| var melee=false; |
| for(var i=0;i<enemies.length;i++){ var e=enemies[i]; if(e.dead){ e.killT2=1; continue; } |
| e.ph+=e.rate*dt*2.2; |
| var tgt=0.5+0.5*Math.sin(e.ph); |
| e.amp += (tgt-e.amp)*Math.min(1,dt*6); |
| if(e.hitFlash>0) e.hitFlash-=dt*3; |
| var dx=player.x-e.x, dy=player.y-e.y, d=Math.hypot(dx,dy)||1; |
| var spd=0.9+Math.min(1.2,kills*0.03); |
| e.x+=dx/d*spd*dt; e.y+=dy/d*spd*dt; |
| if(d<0.7 && e.amp>0.6){ melee=true; } |
| } |
| enemies=enemies.filter(function(e){ return !e.dead || (performance.now()-e.killT)<260; }); |
| |
| dmgCooldown-=dt; |
| if(melee){ setCoh(coherence-38*dt); vign=Math.min(1,vign+dt*3); shake=Math.max(shake,4); if(dmgCooldown<=0){ hurt(); dmgCooldown=0.4; } if(coherence<=0) gameOver(); } |
| else vign=Math.max(0,vign-dt*1.6); |
| if(muzzle>0) muzzle=Math.max(0,muzzle-dt*6); |
| if(shake>0) shake=Math.max(0,shake-dt*22); |
| wave+=dt*0.03; |
| } |
| |
| |
| function draw(){ |
| var w=cv.width, h=cv.height; |
| |
| var g=ctx.createLinearGradient(0,0,0,h); |
| g.addColorStop(0,'#120a18'); g.addColorStop(0.5,'#05030a'); g.addColorStop(0.5,'#0a0710'); g.addColorStop(1,'#1a0e16'); |
| ctx.fillStyle=g; ctx.fillRect(0,0,w,h); |
| |
| var sx=0,sy=0; if(shake>0){ sx=(Math.random()*2-1)*shake; sy=(Math.random()*2-1)*shake; } |
| ctx.save(); ctx.translate(sx,sy); |
| |
| var dirx=Math.cos(player.ang), diry=Math.sin(player.ang); |
| var planex=-diry*Math.tan(FOV/2), planey=dirx*Math.tan(FOV/2); |
| zbuf=new Float32Array(w); |
| |
| |
| for(var x=0;x<w;x++){ |
| var camx=2*x/w-1, rdx=dirx+planex*camx, rdy=diry+planey*camx; |
| var mapx=player.x|0, mapy=player.y|0; |
| var ddx=Math.abs(1/rdx), ddy=Math.abs(1/rdy); |
| var stepx,stepy,sidex,sidey; |
| if(rdx<0){stepx=-1;sidex=(player.x-mapx)*ddx;}else{stepx=1;sidex=(mapx+1-player.x)*ddx;} |
| if(rdy<0){stepy=-1;sidey=(player.y-mapy)*ddy;}else{stepy=1;sidey=(mapy+1-player.y)*ddy;} |
| var hit=0,side=0,guard=0; |
| while(!hit && guard++<64){ if(sidex<sidey){sidex+=ddx;mapx+=stepx;side=0;}else{sidey+=ddy;mapy+=stepy;side=1;} if(cell(mapx,mapy)==='#')hit=1; } |
| var pd = side===0 ? (sidex-ddx) : (sidey-ddy); if(pd<0.01)pd=0.01; |
| zbuf[x]=pd; |
| var lh=h/pd, y0=(h-lh)/2, y1=y0+lh; |
| // wall shading: distance fog + side + subtle vertical banding |
| var shade=Math.max(0,1-pd/12); |
| var base= side? 62:96; |
| var r=Math.round(base*shade*0.55+8), gg=Math.round(base*shade*0.75+8), b=Math.round((base+40)*shade+12); |
| ctx.fillStyle='rgb('+r+','+gg+','+b+')'; |
| ctx.fillRect(x, y0<0?0:y0, 1, (y1>h?h:y1)-(y0<0?0:y0)); |
| // grid seam highlight |
| if((side===0 && (player.y+pd*rdy)%1<0.06) || (side===1 && (player.x+pd*rdx)%1<0.06)){ |
| ctx.fillStyle='rgba(37,224,255,'+(0.12*shade)+')'; ctx.fillRect(x,y0<0?0:y0,1,(y1>h?h:y1)-(y0<0?0:y0)); |
| } |
| } |
| |
| // sprites (enemies), far to near |
| var vis=enemies.slice().sort(function(a,b){ return dist2(b)-dist2(a); }); |
| for(var i=0;i<vis.length;i++){ |
| var e=vis[i]; |
| var ex=e.x-player.x, ey=e.y-player.y; |
| var inv=1/(planex*diry-dirx*planey); |
| var tx=inv*(diry*ex-dirx*ey); |
| var ty=inv*(-planey*ex+planex*ey); // depth (forward) |
| if(ty<=0.15) continue; |
| var scr=(w/2)*(1+tx/ty); |
| var size=Math.min(h*1.4, h/ty); |
| if(scr< -size || scr>w+size) continue; // off-screen |
| // specters glow THROUGH walls (they're tunnelling) — a wall only dims them a little |
| var occl = ty>zbuf[Math.max(0,Math.min(w-1,scr|0))] ? 0.45 : 1; |
| drawSpecter(scr, h/2, size, e, ty, occl); |
| } |
| ctx.restore(); |
| |
| // ---------- weapon + fx (screen space) ---------- |
| drawGun(w,h); |
| // crosshair |
| ctx.strokeStyle='rgba(37,224,255,.7)'; ctx.lineWidth=Math.max(1,w*0.0018); |
| var cxp=w/2, cyp=h/2, cr=w*0.012; |
| ctx.beginPath(); ctx.moveTo(cxp-cr,cyp);ctx.lineTo(cxp-cr*0.4,cyp); ctx.moveTo(cxp+cr*0.4,cyp);ctx.lineTo(cxp+cr,cyp); |
| ctx.moveTo(cxp,cyp-cr);ctx.lineTo(cxp,cyp-cr*0.4); ctx.moveTo(cxp,cyp+cr*0.4);ctx.lineTo(cxp,cyp+cr); ctx.stroke(); |
| // muzzle flash |
| if(muzzle>0){ ctx.fillStyle='rgba(255,'+(200)+',80,'+(0.5*muzzle)+')'; ctx.fillRect(0,0,w,h); } |
| // damage vignette |
| if(vign>0){ var vg=ctx.createRadialGradient(w/2,h/2,h*0.2,w/2,h/2,h*0.75); |
| vg.addColorStop(0,'rgba(255,47,78,0)'); vg.addColorStop(1,'rgba(255,47,78,'+(0.55*vign)+')'); |
| ctx.fillStyle=vg; ctx.fillRect(0,0,w,h); } |
| } |
| function dist2(e){ var dx=e.x-player.x,dy=e.y-player.y; return dx*dx+dy*dy; } |
| function drawSpecter(cx,cy,size,e,ty,occl){ |
| var solid=e.dead?1:e.amp; |
| var t=e.dead? Math.min(1,(performance.now()-e.killT)/260):0; |
| var alpha=(e.dead? (1-t) : (0.18+0.82*solid*solid)) * (occl||1); |
| var hue = e.dead? 50 : (200 - 160*solid); // cyan(ghost) -> red(solid) |
| var s=size*0.5, top=cy-s*0.9, bot=cy+s*0.95; |
| ctx.save(); ctx.globalAlpha=alpha; |
| // body glow |
| var gr=ctx.createRadialGradient(cx,cy,0,cx,cy,s); |
| gr.addColorStop(0,'hsla('+hue+',100%,'+(58+ (e.dead?20:0))+'%,0.95)'); |
| gr.addColorStop(0.6,'hsla('+hue+',100%,45%,0.5)'); |
| gr.addColorStop(1,'hsla('+hue+',100%,40%,0)'); |
| ctx.fillStyle=gr; |
| ctx.beginPath(); ctx.ellipse(cx,cy,s*0.7,s*0.95,0,0,7); ctx.fill(); |
| if(e.dead){ // collapse burst |
| ctx.strokeStyle='rgba(255,220,120,'+(1-t)+')'; ctx.lineWidth=size*0.03; |
| ctx.beginPath(); ctx.arc(cx,cy,s*(0.5+t*1.1),0,7); ctx.stroke(); |
| } else { |
| // eyes — brighter when solid (more "there") |
| var er=s*0.12, ey=cy-s*0.15, eb=0.4+0.6*solid; |
| ctx.fillStyle='rgba(255,255,255,'+eb+')'; |
| ctx.beginPath(); ctx.arc(cx-s*0.24,ey,er,0,7); ctx.arc(cx+s*0.24,ey,er,0,7); ctx.fill(); |
| ctx.fillStyle='rgba('+(solid>0.6?255:40)+',20,40,'+eb+')'; |
| ctx.beginPath(); ctx.arc(cx-s*0.24,ey,er*0.5,0,7); ctx.arc(cx+s*0.24,ey,er*0.5,0,7); ctx.fill(); |
| } |
| ctx.restore(); |
| } |
| function drawGun(w,h){ |
| // a simple "collapser" barrel bobbing at bottom centre |
| var bob=Math.sin(performance.now()/220)*(h*0.006); |
| var gw=w*0.16, gh=h*0.26, gx=w/2-gw/2, gy=h-gh+bob; |
| ctx.fillStyle='#14161f'; ctx.fillRect(gx,gy,gw,gh); |
| ctx.fillStyle='#20d0e0'; ctx.fillRect(gx+gw*0.4,gy-h*0.02,gw*0.2,gh*0.5); |
| ctx.fillStyle=muzzle>0?'rgba(255,230,120,'+muzzle+')':'#0a3a44'; |
| ctx.beginPath(); ctx.arc(w/2,gy-h*0.02, w*0.02*(1+muzzle*2),0,7); ctx.fill(); |
| ctx.fillStyle='rgba(37,224,255,.25)'; ctx.fillRect(gx,gy,gw,h*0.01); |
| } |
| |
| // ---------- loop ---------- |
| var last=performance.now(); |
| function frame(now){ var dt=Math.min(0.05,(now-last)/1000); last=now; if(running)update(dt); draw(); requestAnimationFrame(frame); } |
| |
| // ---------- input ---------- |
| addEventListener('keydown',function(e){ var k=e.key.length===1?e.key.toLowerCase():e.key; keys[k]=true; |
| if(k===' '||k==='Spacebar'){ e.preventDefault(); A(); fire(); } }); |
| addEventListener('keyup',function(e){ var k=e.key.length===1?e.key.toLowerCase():e.key; keys[k]=false; }); |
| cv.addEventListener('pointerdown',function(e){ A(); if(running) fire(); }); |
| function hold(id,on){ var el=document.getElementById(id); |
| el.addEventListener('pointerdown',function(e){e.preventDefault();A();on(true);}); |
| ['pointerup','pointerleave','pointercancel'].forEach(function(ev){el.addEventListener(ev,function(){on(false);});}); } |
| hold('b-l',function(v){touch.l=v;}); hold('b-r',function(v){touch.r=v;}); hold('b-f',function(v){touch.f=v;}); |
| document.getElementById('b-fire').addEventListener('pointerdown',function(e){e.preventDefault();A();fire();}); |
| document.getElementById('start-btn').addEventListener('click',function(){A();start();}); |
| document.getElementById('again-btn').addEventListener('click',function(){A();start();}); |
| |
| // smoke-test hook (harmless) |
| window.__qd={ start:start, fire:fire, turn:function(a){player.ang+=a;}, state:function(){return{running:running,kills:kills,coh:coherence,enemies:enemies.length};} }; |
| |
| resize(); requestAnimationFrame(frame); |
| })(); |
| </script> |
| </body> |
| </html> |
|
|