QUANTDOOM — quantum raycaster; shooting is a Born-rule measurement, specters tunnel through walls
Browse files- README.md +27 -4
- index.html +375 -17
README.md
CHANGED
|
@@ -1,10 +1,33 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
colorTo: indigo
|
| 6 |
sdk: static
|
|
|
|
| 7 |
pinned: false
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 · <b>A/D</b> or <b>←/→</b> turn · <b>SPACE/click</b> fire | touch: on-screen pads</p>
|
| 79 |
+
<p class="fine">🜂 born rule as a trigger · measurement is a weapon · <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 · you decohered</p>
|
| 84 |
+
<div id="go-score">0</div>
|
| 85 |
+
<p class="tag" style="margin-top:0">specters collapsed · best <b id="go-best">0</b></p>
|
| 86 |
+
<button class="go" id="again-btn">▶ RE-ENTER</button>
|
| 87 |
+
<p class="fine">PeetPedro · entropy-om · <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>
|