PeetPedro commited on
Commit
db2a693
·
verified ·
1 Parent(s): 8b96651

QUANTDOOM — quantum raycaster; shooting is a Born-rule measurement, specters tunnel through walls

Browse files
Files changed (2) hide show
  1. README.md +27 -4
  2. index.html +375 -17
README.md CHANGED
@@ -1,10 +1,33 @@
1
  ---
2
- title: Quantdoom
3
- emoji: 👀
4
- colorFrom: green
5
  colorTo: indigo
6
  sdk: static
 
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: QUANTDOOM
3
+ emoji: 👁️
4
+ colorFrom: red
5
  colorTo: indigo
6
  sdk: static
7
+ app_file: index.html
8
  pinned: false
9
+ license: mit
10
+ short_description: Quantum raycaster FPS — fire when the specter collapses
11
  ---
12
 
13
+ # 👁️ QUANTDOOM
14
+
15
+ A quantum raycaster. The specters are in **superposition** — pulsing between
16
+ <span>ghost</span> (faint cyan, harmless, can't be killed) and **collapsed** (solid
17
+ red, lethal, and vulnerable). Your shot is a **measurement**: your hit chance is the
18
+ specter's **amplitude²** — the Born rule, as a trigger-timing skill. Fire the instant
19
+ one turns solid… which is also exactly when it can hurt you.
20
+
21
+ The specters **tunnel** — they phase straight through the maze walls toward you (so
22
+ no pathfinding, and it's honest: they're delocalised). You see them glowing *through*
23
+ the walls; a wall only dims them. Keep your **coherence** up; it never really ends —
24
+ chase the count.
25
+
26
+ - **hue** cyan → red = how **collapsed** (solid) a specter is right now
27
+ - **shooting** = measurement; P(hit) = amplitude² (Born rule)
28
+ - only a **collapsed** specter can damage you — or be destroyed
29
+ - desktop: **W/S** move · **A/D** or **←/→** turn · **Space / click** fire · touch: on-screen pads
30
+
31
+ One `index.html`, no build, no trackers. Plays on desktop and touch.
32
+
33
+ 🜂 **AHOGY A DOLGOK VANNAK** — measurement is a weapon, as things are.
index.html CHANGED
@@ -1,19 +1,377 @@
1
  <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
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, maximum-scale=1, user-scalable=no">
6
+ <title>QUANTDOOM</title>
7
+ <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.">
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Anton&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
11
+ <style>
12
+ :root{--bg:#08040a;--ink:#f2e9ee;--dim:#9a7f92;--hot:#ff2f4e;--cool:#25e0ff;--gold:#ffcf3f;--mono:'JetBrains Mono',ui-monospace,monospace;--disp:'Anton',Impact,sans-serif;}
13
+ *{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent;user-select:none}
14
+ html,body{height:100%;overflow:hidden;background:#000;color:var(--ink);font-family:var(--mono)}
15
+ #stage{position:fixed;inset:0;background:#000}
16
+ canvas{position:absolute;inset:0;width:100%;height:100%;image-rendering:pixelated}
17
+ #hud{position:absolute;inset:0;pointer-events:none;z-index:5}
18
+ .top{position:absolute;top:0;left:0;right:0;display:flex;justify-content:space-between;padding:14px 18px}
19
+ .stat .l{font-size:9px;letter-spacing:.24em;color:var(--dim);text-transform:uppercase}
20
+ .stat .v{font-family:var(--disp);font-size:30px;line-height:.9;font-variant-numeric:tabular-nums}
21
+ #kills .v{color:var(--gold)} .stat.r{text-align:right}
22
+ #coh-wrap{position:absolute;left:18px;bottom:16px;width:min(46vw,240px)}
23
+ #coh-wrap .l{font-size:9px;letter-spacing:.22em;color:var(--dim);text-transform:uppercase;margin-bottom:4px;display:flex;justify-content:space-between}
24
+ #coh-bar{height:12px;background:rgba(255,47,78,.14);border:1px solid rgba(255,47,78,.4);border-radius:7px;overflow:hidden}
25
+ #coh-fill{height:100%;width:100%;background:linear-gradient(90deg,var(--hot),#ff7a3f);transition:width .12s}
26
+ #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}
27
+ /* touch controls */
28
+ #pad{position:absolute;inset:0;z-index:6}
29
+ .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)}
30
+ .btn:active{background:rgba(37,224,255,.28)}
31
+ #b-l{left:16px;bottom:96px;width:62px;height:62px;font-size:26px}
32
+ #b-r{left:104px;bottom:96px;width:62px;height:62px;font-size:26px}
33
+ #b-f{left:60px;bottom:172px;width:62px;height:62px;font-size:22px}
34
+ #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)}
35
+ #b-fire:active{background:rgba(255,47,78,.34)}
36
+ .hint{position:absolute;bottom:6px;left:0;right:0;text-align:center;font-size:9.5px;letter-spacing:.1em;color:var(--dim)}
37
+ /* overlays */
38
+ .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}
39
+ .screen.hide{display:none}
40
+ 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))}
41
+ .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}
42
+ .how{display:flex;gap:12px;flex-wrap:wrap;justify-content:center;margin:20px 0 4px;max-width:36rem}
43
+ .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)}
44
+ .card b{display:block;font-family:var(--disp);font-size:15px;letter-spacing:.03em;margin-bottom:5px;color:var(--ink)}
45
+ .card .g{color:var(--cool)}.card .s{color:var(--hot)}
46
+ 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)}
47
+ button.go:active{transform:translateY(2px)}
48
+ .fine{margin-top:20px;font-size:10px;letter-spacing:.14em;color:#6a4a5c;text-transform:uppercase}.fine .f{color:var(--hot)}
49
+ #go-score{font-family:var(--disp);font-size:clamp(56px,16vw,120px);color:var(--gold);line-height:.9;margin:2px 0}
50
+ .kbd{font-size:10px;letter-spacing:.1em;color:#6a4a5c;margin-top:8px}
51
+ @media(hover:hover){#pad{display:none}}
52
+ </style>
53
+ </head>
54
+ <body>
55
+ <div id="stage">
56
+ <canvas id="c"></canvas>
57
+ <div id="hud">
58
+ <div class="top">
59
+ <div class="stat" id="kills"><span class="l">specters collapsed</span><br><span class="v" id="kills-v">0</span></div>
60
+ <div class="stat r" id="best"><span class="l">best</span><br><span class="v" id="best-v">0</span></div>
61
+ </div>
62
+ <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>
63
+ <div id="msg"></div>
64
+ </div>
65
+ <div id="pad">
66
+ <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>
67
+ </div>
68
+
69
+ <div class="screen" id="start">
70
+ <h1>QUANT<br>DOOM</h1>
71
+ <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>
72
+ <div class="how">
73
+ <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>
74
+ <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>
75
+ <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>
76
+ </div>
77
+ <button class="go" id="start-btn">▶ ENTER THE FIELD</button>
78
+ <p class="kbd">desktop: <b>W/S</b> move &middot; <b>A/D</b> or <b>←/→</b> turn &middot; <b>SPACE/click</b> fire &nbsp;|&nbsp; touch: on-screen pads</p>
79
+ <p class="fine">🜂 born rule as a trigger &middot; measurement is a weapon &middot; <span class="f">ahogy a dolgok vannak</span></p>
80
+ </div>
81
+
82
+ <div class="screen hide" id="over">
83
+ <p class="tag" style="margin-bottom:0">coherence lost &middot; you decohered</p>
84
+ <div id="go-score">0</div>
85
+ <p class="tag" style="margin-top:0">specters collapsed &middot; best <b id="go-best">0</b></p>
86
+ <button class="go" id="again-btn">▶ RE-ENTER</button>
87
+ <p class="fine">PeetPedro &middot; entropy-om &middot; <span class="f">quantum play</span></p>
88
+ </div>
89
+ </div>
90
+
91
+ <script>
92
+ (function(){
93
+ "use strict";
94
+ var cv=document.getElementById('c'), ctx=cv.getContext('2d',{alpha:false});
95
+ var W=0,H=0,RAYS=0,colW=0, DPR=1;
96
+ var reduce=matchMedia('(prefers-reduced-motion: reduce)').matches;
97
+
98
+ // ---------- map ----------
99
+ var MAP=[
100
+ "################",
101
+ "#..............#",
102
+ "#..##....##....#",
103
+ "#..#......#..#.#",
104
+ "#..#..##..#..#.#",
105
+ "#.....##.......#",
106
+ "#..............#",
107
+ "#.####....####.#",
108
+ "#.#..........#.#",
109
+ "#.#..######..#.#",
110
+ "#....#....#....#",
111
+ "#..#.#....#.#..#",
112
+ "#..#......#.#..#",
113
+ "#..#.####.....##",
114
+ "#..............#",
115
+ "################"];
116
+ var MW=MAP[0].length, MH=MAP.length;
117
+ function cell(x,y){ if(x<0||y<0||x>=MW||y>=MH) return '#'; return MAP[y|0][x|0]; }
118
+ function isWall(x,y){ return cell(x,y)==='#'; }
119
+
120
+ // ---------- state ----------
121
+ var player={x:2.5,y:2.5,ang:0.4}, FOV=Math.PI/3;
122
+ var enemies=[], zbuf=[], running=false, kills=0, coherence=100, best=+(localStorage.getItem('qd_best')||0);
123
+ var spawnT=0, spawnEvery=2.2, wave=0, muzzle=0, shake=0, vign=0, dmgCooldown=0;
124
+ var keys={}, touch={l:false,r:false,f:false};
125
+ document.getElementById('best-v').textContent=best;
126
+
127
+ // ---------- audio ----------
128
+ var actx=null;
129
+ function A(){ if(!actx){ try{actx=new (window.AudioContext||window.webkitAudioContext)();}catch(e){} } if(actx&&actx.state==='suspended')actx.resume(); }
130
+ function zap(){ if(!actx)return; var n=actx.currentTime,o=actx.createOscillator(),g=actx.createGain();
131
+ o.type='square'; o.frequency.setValueAtTime(880,n); o.frequency.exponentialRampToValueAtTime(140,n+.12);
132
+ 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); }
133
+ function pop(hit){ if(!actx)return; var n=actx.currentTime,o=actx.createOscillator(),g=actx.createGain();
134
+ o.type=hit?'triangle':'sine'; o.frequency.setValueAtTime(hit?520:180,n); o.frequency.exponentialRampToValueAtTime(hit?1400:90,n+.18);
135
+ 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); }
136
+ function hurt(){ if(!actx)return; var n=actx.currentTime,o=actx.createOscillator(),g=actx.createGain();
137
+ o.type='sawtooth'; o.frequency.setValueAtTime(120,n); o.frequency.exponentialRampToValueAtTime(40,n+.3);
138
+ 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); }
139
+
140
+ // ---------- setup ----------
141
+ function resize(){ DPR=Math.min(2,window.devicePixelRatio||1);
142
+ W=Math.floor(innerWidth); H=Math.floor(innerHeight);
143
+ cv.width=Math.floor(W*0.6); cv.height=Math.floor(H*0.6); // render at 60% then CSS-upscale for speed + chunky look
144
+ RAYS=cv.width; colW=1; ctx.imageSmoothingEnabled=false; }
145
+ addEventListener('resize',resize);
146
+
147
+ function spawnEnemy(){
148
+ // spawn at a floor cell far from the player
149
+ for(var tries=0;tries<40;tries++){
150
+ var x=1+Math.random()*(MW-2), y=1+Math.random()*(MH-2);
151
+ if(isWall(x,y)) continue;
152
+ var dx=x-player.x, dy=y-player.y; if(dx*dx+dy*dy<25) continue;
153
+ 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});
154
+ return;
155
+ }
156
+ }
157
+ function start(){
158
+ running=true; kills=0; coherence=100; enemies=[]; spawnT=0; spawnEvery=2.2; wave=0; muzzle=0; shake=0; vign=0;
159
+ player.x=2.5; player.y=2.5; player.ang=0.4;
160
+ document.getElementById('start').classList.add('hide'); document.getElementById('over').classList.add('hide');
161
+ document.getElementById('kills-v').textContent='0'; setCoh(100);
162
+ for(var i=0;i<3;i++) spawnEnemy();
163
+ }
164
+ function gameOver(){ running=false;
165
+ if(kills>best){ best=kills; localStorage.setItem('qd_best',best); document.getElementById('best-v').textContent=best; }
166
+ document.getElementById('go-score').textContent=kills;
167
+ document.getElementById('go-best').textContent=best;
168
+ document.getElementById('over').classList.remove('hide');
169
+ }
170
+ function setCoh(v){ coherence=Math.max(0,Math.min(100,v));
171
+ document.getElementById('coh-fill').style.width=coherence+'%';
172
+ document.getElementById('coh-pct').textContent=Math.round(coherence)+'%'; }
173
+ function flashMsg(t,col){ var m=document.getElementById('msg'); m.textContent=t; m.style.color=col||'#fff';
174
+ m.style.transition='none'; m.style.opacity='1';
175
+ requestAnimationFrame(function(){ m.style.transition='opacity .7s'; m.style.opacity='0'; }); }
176
+
177
+ // ---------- shooting ----------
178
+ function fire(){
179
+ if(!running) return;
180
+ muzzle=1; zap(); shake=Math.max(shake,3);
181
+ // find the enemy closest to screen-centre (crosshair), in front, within range, roughly unoccluded
182
+ var bestE=null, bestScore=1e9;
183
+ for(var i=0;i<enemies.length;i++){ var e=enemies[i]; if(e.dead) continue;
184
+ var dx=e.x-player.x, dy=e.y-player.y, dist=Math.hypot(dx,dy);
185
+ if(dist>11) continue;
186
+ var ang=Math.atan2(dy,dx)-player.ang; while(ang<-Math.PI)ang+=6.283; while(ang>Math.PI)ang-=6.283;
187
+ if(Math.abs(ang)>0.14) continue; // must be near the crosshair
188
+ // no wall check: specters are delocalised — they tunnel, so they're shootable through walls
189
+ var sc=Math.abs(ang)*4+dist*0.05;
190
+ if(sc<bestScore){ bestScore=sc; bestE=e; }
191
+ }
192
+ if(!bestE){ return; }
193
+ var amp=bestE.amp;
194
+ var p=amp*amp; // Born rule: |amplitude|^2
195
+ if(Math.random()<p){ // collapsed to |hit>
196
+ bestE.dead=true; bestE.killT=performance.now(); kills++;
197
+ document.getElementById('kills-v').textContent=kills;
198
+ pop(true);
199
+ if(kills%10===0) flashMsg(kills+' COLLAPSED', '#ffcf3f');
200
+ } else { // collapsed to |ghost> — it phases out and survives
201
+ bestE.amp=0.06; bestE.ph=Math.PI; // force ghostly for a beat
202
+ pop(false);
203
+ }
204
+ }
205
+ function wallBetween(x0,y0,x1,y1){
206
+ var steps=Math.ceil(Math.hypot(x1-x0,y1-y0)*4);
207
+ for(var s=1;s<steps;s++){ var t=s/steps; if(isWall(x0+(x1-x0)*t, y0+(y1-y0)*t)) return true; }
208
+ return false;
209
+ }
210
+
211
+ // ---------- update ----------
212
+ function update(dt){
213
+ // turn + move
214
+ var turn=(keys.a||keys.ArrowLeft||touch.l?-1:0)+(keys.d||keys.ArrowRight||touch.r?1:0);
215
+ player.ang += turn*2.6*dt;
216
+ var mv=(keys.w||keys.ArrowUp||touch.f?1:0)+(keys.s||keys.ArrowDown?-1:0);
217
+ 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;
218
+ if(!isWall(nx,player.y)) player.x=nx; if(!isWall(player.x,ny)) player.y=ny; }
219
+ // enemies: pulse (superposition) + tunnel toward player
220
+ 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(); }
221
+ var melee=false;
222
+ for(var i=0;i<enemies.length;i++){ var e=enemies[i]; if(e.dead){ e.killT2=1; continue; }
223
+ e.ph+=e.rate*dt*2.2;
224
+ var tgt=0.5+0.5*Math.sin(e.ph);
225
+ e.amp += (tgt-e.amp)*Math.min(1,dt*6); // ease toward the pulsing target
226
+ if(e.hitFlash>0) e.hitFlash-=dt*3;
227
+ var dx=player.x-e.x, dy=player.y-e.y, d=Math.hypot(dx,dy)||1;
228
+ var spd=0.9+Math.min(1.2,kills*0.03); // ramps up as you rack collapses
229
+ e.x+=dx/d*spd*dt; e.y+=dy/d*spd*dt; // tunnels through walls
230
+ if(d<0.7 && e.amp>0.6){ melee=true; } // only a COLLAPSED specter can hurt you
231
+ }
232
+ enemies=enemies.filter(function(e){ return !e.dead || (performance.now()-e.killT)<260; });
233
+ // damage
234
+ dmgCooldown-=dt;
235
+ 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(); }
236
+ else vign=Math.max(0,vign-dt*1.6);
237
+ if(muzzle>0) muzzle=Math.max(0,muzzle-dt*6);
238
+ if(shake>0) shake=Math.max(0,shake-dt*22);
239
+ wave+=dt*0.03;
240
+ }
241
+
242
+ // ---------- render ----------
243
+ function draw(){
244
+ var w=cv.width, h=cv.height;
245
+ // ceiling / floor
246
+ var g=ctx.createLinearGradient(0,0,0,h);
247
+ g.addColorStop(0,'#120a18'); g.addColorStop(0.5,'#05030a'); g.addColorStop(0.5,'#0a0710'); g.addColorStop(1,'#1a0e16');
248
+ ctx.fillStyle=g; ctx.fillRect(0,0,w,h);
249
+
250
+ var sx=0,sy=0; if(shake>0){ sx=(Math.random()*2-1)*shake; sy=(Math.random()*2-1)*shake; }
251
+ ctx.save(); ctx.translate(sx,sy);
252
+
253
+ var dirx=Math.cos(player.ang), diry=Math.sin(player.ang);
254
+ var planex=-diry*Math.tan(FOV/2), planey=dirx*Math.tan(FOV/2);
255
+ zbuf=new Float32Array(w);
256
+
257
+ // raycast walls (DDA)
258
+ for(var x=0;x<w;x++){
259
+ var camx=2*x/w-1, rdx=dirx+planex*camx, rdy=diry+planey*camx;
260
+ var mapx=player.x|0, mapy=player.y|0;
261
+ var ddx=Math.abs(1/rdx), ddy=Math.abs(1/rdy);
262
+ var stepx,stepy,sidex,sidey;
263
+ if(rdx<0){stepx=-1;sidex=(player.x-mapx)*ddx;}else{stepx=1;sidex=(mapx+1-player.x)*ddx;}
264
+ if(rdy<0){stepy=-1;sidey=(player.y-mapy)*ddy;}else{stepy=1;sidey=(mapy+1-player.y)*ddy;}
265
+ var hit=0,side=0,guard=0;
266
+ 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; }
267
+ var pd = side===0 ? (sidex-ddx) : (sidey-ddy); if(pd<0.01)pd=0.01;
268
+ zbuf[x]=pd;
269
+ var lh=h/pd, y0=(h-lh)/2, y1=y0+lh;
270
+ // wall shading: distance fog + side + subtle vertical banding
271
+ var shade=Math.max(0,1-pd/12);
272
+ var base= side? 62:96;
273
+ var r=Math.round(base*shade*0.55+8), gg=Math.round(base*shade*0.75+8), b=Math.round((base+40)*shade+12);
274
+ ctx.fillStyle='rgb('+r+','+gg+','+b+')';
275
+ ctx.fillRect(x, y0<0?0:y0, 1, (y1>h?h:y1)-(y0<0?0:y0));
276
+ // grid seam highlight
277
+ if((side===0 && (player.y+pd*rdy)%1<0.06) || (side===1 && (player.x+pd*rdx)%1<0.06)){
278
+ 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));
279
+ }
280
+ }
281
+
282
+ // sprites (enemies), far to near
283
+ var vis=enemies.slice().sort(function(a,b){ return dist2(b)-dist2(a); });
284
+ for(var i=0;i<vis.length;i++){
285
+ var e=vis[i];
286
+ var ex=e.x-player.x, ey=e.y-player.y;
287
+ var inv=1/(planex*diry-dirx*planey);
288
+ var tx=inv*(diry*ex-dirx*ey);
289
+ var ty=inv*(-planey*ex+planex*ey); // depth (forward)
290
+ if(ty<=0.15) continue;
291
+ var scr=(w/2)*(1+tx/ty);
292
+ var size=Math.min(h*1.4, h/ty);
293
+ if(scr< -size || scr>w+size) continue; // off-screen
294
+ // specters glow THROUGH walls (they're tunnelling) — a wall only dims them a little
295
+ var occl = ty>zbuf[Math.max(0,Math.min(w-1,scr|0))] ? 0.45 : 1;
296
+ drawSpecter(scr, h/2, size, e, ty, occl);
297
+ }
298
+ ctx.restore();
299
+
300
+ // ---------- weapon + fx (screen space) ----------
301
+ drawGun(w,h);
302
+ // crosshair
303
+ ctx.strokeStyle='rgba(37,224,255,.7)'; ctx.lineWidth=Math.max(1,w*0.0018);
304
+ var cxp=w/2, cyp=h/2, cr=w*0.012;
305
+ 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);
306
+ 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();
307
+ // muzzle flash
308
+ if(muzzle>0){ ctx.fillStyle='rgba(255,'+(200)+',80,'+(0.5*muzzle)+')'; ctx.fillRect(0,0,w,h); }
309
+ // damage vignette
310
+ if(vign>0){ var vg=ctx.createRadialGradient(w/2,h/2,h*0.2,w/2,h/2,h*0.75);
311
+ vg.addColorStop(0,'rgba(255,47,78,0)'); vg.addColorStop(1,'rgba(255,47,78,'+(0.55*vign)+')');
312
+ ctx.fillStyle=vg; ctx.fillRect(0,0,w,h); }
313
+ }
314
+ function dist2(e){ var dx=e.x-player.x,dy=e.y-player.y; return dx*dx+dy*dy; }
315
+ function drawSpecter(cx,cy,size,e,ty,occl){
316
+ var solid=e.dead?1:e.amp;
317
+ var t=e.dead? Math.min(1,(performance.now()-e.killT)/260):0;
318
+ var alpha=(e.dead? (1-t) : (0.18+0.82*solid*solid)) * (occl||1);
319
+ var hue = e.dead? 50 : (200 - 160*solid); // cyan(ghost) -> red(solid)
320
+ var s=size*0.5, top=cy-s*0.9, bot=cy+s*0.95;
321
+ ctx.save(); ctx.globalAlpha=alpha;
322
+ // body glow
323
+ var gr=ctx.createRadialGradient(cx,cy,0,cx,cy,s);
324
+ gr.addColorStop(0,'hsla('+hue+',100%,'+(58+ (e.dead?20:0))+'%,0.95)');
325
+ gr.addColorStop(0.6,'hsla('+hue+',100%,45%,0.5)');
326
+ gr.addColorStop(1,'hsla('+hue+',100%,40%,0)');
327
+ ctx.fillStyle=gr;
328
+ ctx.beginPath(); ctx.ellipse(cx,cy,s*0.7,s*0.95,0,0,7); ctx.fill();
329
+ if(e.dead){ // collapse burst
330
+ ctx.strokeStyle='rgba(255,220,120,'+(1-t)+')'; ctx.lineWidth=size*0.03;
331
+ ctx.beginPath(); ctx.arc(cx,cy,s*(0.5+t*1.1),0,7); ctx.stroke();
332
+ } else {
333
+ // eyes — brighter when solid (more "there")
334
+ var er=s*0.12, ey=cy-s*0.15, eb=0.4+0.6*solid;
335
+ ctx.fillStyle='rgba(255,255,255,'+eb+')';
336
+ ctx.beginPath(); ctx.arc(cx-s*0.24,ey,er,0,7); ctx.arc(cx+s*0.24,ey,er,0,7); ctx.fill();
337
+ ctx.fillStyle='rgba('+(solid>0.6?255:40)+',20,40,'+eb+')';
338
+ 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();
339
+ }
340
+ ctx.restore();
341
+ }
342
+ function drawGun(w,h){
343
+ // a simple "collapser" barrel bobbing at bottom centre
344
+ var bob=Math.sin(performance.now()/220)*(h*0.006);
345
+ var gw=w*0.16, gh=h*0.26, gx=w/2-gw/2, gy=h-gh+bob;
346
+ ctx.fillStyle='#14161f'; ctx.fillRect(gx,gy,gw,gh);
347
+ ctx.fillStyle='#20d0e0'; ctx.fillRect(gx+gw*0.4,gy-h*0.02,gw*0.2,gh*0.5);
348
+ ctx.fillStyle=muzzle>0?'rgba(255,230,120,'+muzzle+')':'#0a3a44';
349
+ ctx.beginPath(); ctx.arc(w/2,gy-h*0.02, w*0.02*(1+muzzle*2),0,7); ctx.fill();
350
+ ctx.fillStyle='rgba(37,224,255,.25)'; ctx.fillRect(gx,gy,gw,h*0.01);
351
+ }
352
+
353
+ // ---------- loop ----------
354
+ var last=performance.now();
355
+ function frame(now){ var dt=Math.min(0.05,(now-last)/1000); last=now; if(running)update(dt); draw(); requestAnimationFrame(frame); }
356
+
357
+ // ---------- input ----------
358
+ addEventListener('keydown',function(e){ var k=e.key.length===1?e.key.toLowerCase():e.key; keys[k]=true;
359
+ if(k===' '||k==='Spacebar'){ e.preventDefault(); A(); fire(); } });
360
+ addEventListener('keyup',function(e){ var k=e.key.length===1?e.key.toLowerCase():e.key; keys[k]=false; });
361
+ cv.addEventListener('pointerdown',function(e){ A(); if(running) fire(); });
362
+ function hold(id,on){ var el=document.getElementById(id);
363
+ el.addEventListener('pointerdown',function(e){e.preventDefault();A();on(true);});
364
+ ['pointerup','pointerleave','pointercancel'].forEach(function(ev){el.addEventListener(ev,function(){on(false);});}); }
365
+ hold('b-l',function(v){touch.l=v;}); hold('b-r',function(v){touch.r=v;}); hold('b-f',function(v){touch.f=v;});
366
+ document.getElementById('b-fire').addEventListener('pointerdown',function(e){e.preventDefault();A();fire();});
367
+ document.getElementById('start-btn').addEventListener('click',function(){A();start();});
368
+ document.getElementById('again-btn').addEventListener('click',function(){A();start();});
369
+
370
+ // smoke-test hook (harmless)
371
+ window.__qd={ start:start, fire:fire, turn:function(a){player.ang+=a;}, state:function(){return{running:running,kills:kills,coh:coherence,enemies:enemies.length};} };
372
+
373
+ resize(); requestAnimationFrame(frame);
374
+ })();
375
+ </script>
376
+ </body>
377
  </html>