-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathdoombreaker_openseason.html
More file actions
434 lines (413 loc) · 42.3 KB
/
Copy pathdoombreaker_openseason.html
File metadata and controls
434 lines (413 loc) · 42.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>DOOMBREAKER</title>
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Orbitron:wght@400;700;900&display=swap" rel="stylesheet">
<style>
:root {
--c-crimson: #ff2a2a;
--c-green: #00ffaa;
--font-mono: 'Share Tech Mono', monospace;
--font-title: 'Orbitron', monospace;
}
* { margin: 0; padding: 0; box-sizing: border-box; user-select: none; }
body { background: #000; overflow: hidden; font-family: var(--font-mono); color: var(--c-green); }
canvas { display: block; }
.screen {
position: fixed; inset: 0; display: flex; flex-direction: column; align-items: center;
justify-content: center; z-index: 200; background: radial-gradient(ellipse at center, #0a0100 0%, #000000 95%);
text-align: center; transition: opacity 0.25s ease;
}
.hidden { display: none !important; }
.boot-wrap { width: min(680px, 90vw); text-align: left; }
.boot-logo {
font-family: var(--font-title); font-size: clamp(22px, 5vw, 44px); white-space: pre; line-height: 1.1;
color: var(--c-crimson); text-shadow: 0 0 16px #ff0000; margin-bottom: 16px;
}
.boot-sub { font-family: var(--font-title); font-size: 12px; color: var(--c-green); letter-spacing: 6px; margin-bottom: 28px; }
.boot-bar-wrap { border: 1px solid #aa3333; padding: 2px; background: #110000; margin-bottom: 7px; }
.boot-bar-fill { height: 18px; width: 0%; background: repeating-linear-gradient(90deg, #ff2222 0px, #ff2222 4px, #aa3333 4px, #aa3333 6px); transition: width 0.2s ease; }
.boot-pct { font-size: 10px; color: #ff6666; text-align: right; margin-bottom: 26px; }
.skip-btn { position: absolute; bottom: 22px; right: 22px; padding: 8px 18px; background: rgba(20,5,5,0.9); border: 1px solid #aa3333; color: #ff6666; cursor: pointer; font-family: var(--font-mono); font-size: 10px; transition: 0.2s; z-index: 250; }
.skip-btn:hover { color: var(--c-green); border-color: var(--c-green); }
.menu-logo { font-family: var(--font-title); font-size: clamp(26px, 5vw, 44px); color: #ff4444; text-shadow: 0 0 20px #ff0000; margin-bottom: 10px; white-space: pre; }
.menu-sub { font-family: var(--font-title); font-size: 14px; color: var(--c-green); letter-spacing: 6px; text-shadow: 0 0 8px #00ffaa; }
.menu-divider { width: min(500px, 80vw); height: 1px; background: linear-gradient(90deg, transparent, var(--c-crimson), transparent); margin: 28px 0; opacity: 0.6; }
.menu-btns { display: flex; flex-direction: column; gap: 12px; width: min(380px, 86vw); }
.menu-btn { padding: 14px 32px; background: rgba(255,30,30,0.1); border: 1px solid rgba(255,50,50,0.5); color: #ffaaaa; font-family: var(--font-mono); font-size: 13px; cursor: pointer; letter-spacing: 4px; text-transform: uppercase; transition: 0.2s; text-align: left; position: relative; }
.menu-btn::before { content: '▸'; position: absolute; left: 14px; opacity: 0; transition: 0.14s; color: var(--c-green); }
.menu-btn span { margin-left: 4px; transition: margin 0.14s; }
.menu-btn:hover { background: rgba(255,30,30,0.3); border-color: var(--c-green); color: white; box-shadow: 0 0 18px rgba(0,255,136,0.3); }
.menu-btn:hover::before { opacity: 1; left: 14px; }
.menu-btn:hover span { margin-left: 22px; }
#gameHUD { position: fixed; top: 0; left: 0; right: 0; z-index: 100; pointer-events: none; padding: 12px 20px; display: flex; justify-content: space-between; }
.hud-panel { background: rgba(0,0,0,0.9); border: 1px solid var(--c-crimson); padding: 6px 16px; backdrop-filter: blur(3px); border-radius: 4px; }
.hud-label { font-size: 8px; color: #ff8888; letter-spacing: 2px; }
.hud-value { font-size: 22px; font-weight: bold; color: var(--c-green); text-shadow: 0 0 6px var(--c-green); line-height: 1; }
#levelPanel { position: fixed; top: 70px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.9); border: 1px solid #ff4444; border-radius: 12px; padding: 6px 20px; text-align: center; pointer-events: none; z-index: 110; }
#weaponPanel { position: fixed; bottom: 28px; left: 50%; transform: translateX(-50%); background: #000000cc; backdrop-filter: blur(10px); border: 1px solid #ffaa44; border-radius: 12px; padding: 8px 24px; text-align: center; pointer-events: none; z-index: 110; font-family: var(--font-title); color: #ffcc88; font-size: 18px; min-width: 280px; box-shadow: 0 0 18px #ff4444; white-space: nowrap; }
.crosshair { position: fixed; top: 50%; left: 50%; width: 22px; height: 22px; transform: translate(-50%, -50%); pointer-events: none; z-index: 200; }
.crosshair::before, .crosshair::after { content: ''; position: absolute; background: var(--c-green); box-shadow: 0 0 8px #00ffaa; }
.crosshair::before { width: 18px; height: 2px; top: 10px; left: 2px; }
.crosshair::after { width: 2px; height: 18px; top: 2px; left: 10px; }
#hitmarker { position: fixed; top: 50%; left: 50%; width: 32px; height: 32px; transform: translate(-50%, -50%); border: 2px solid white; border-radius: 50%; opacity: 0; pointer-events: none; z-index: 201; transition: opacity 0.05s linear; }
.flash { position: fixed; inset: 0; pointer-events: none; z-index: 105; display: none; }
#dmgFlash { background: rgba(255,20,0,0.4); }
#muzzleFlashOverlay { background: rgba(255,180,30,0.35); z-index: 106; }
.wave-alert { position: fixed; top: 35%; left: 50%; transform: translate(-50%, -50%); background: black; border: 2px solid #ff4444; color: #ff8888; padding: 12px 24px; font-size: 22px; font-weight: bold; letter-spacing: 4px; white-space: nowrap; z-index: 250; pointer-events: none; text-shadow: 0 0 12px red; font-family: var(--font-title); }
.shake { animation: screenShake 0.12s ease; }
@keyframes screenShake { 0% { transform: translate(0,0) } 25% { transform: translate(-3px,2px) } 75% { transform: translate(3px,-2px) } 100% { transform: translate(0,0) } }
.xp-bar { position: fixed; bottom: 0; left: 0; right: 0; height: 3px; background: rgba(255,68,68,0.2); z-index: 150; }
.xp-bar-fill { height: 100%; width: 0%; background: linear-gradient(90deg, #ff4444, #ffaa44); transition: width 0.5s ease; }
</style>
</head>
<body>
<div id="bootScreen" class="screen"><div class="boot-wrap"><div class="boot-logo">DOOMBREAKER<br><br> OPEN SEASON <br></div><div class="boot-sub">THREE JS · FPS· HORROR</div><div class="boot-bar-wrap"><div class="boot-bar-fill" id="bootBar"></div></div><div class="boot-pct" id="bootPct">0%</div></div><button class="skip-btn" id="skipBoot">SKIP ▸</button></div>
<div id="menuScreen" class="screen hidden"><div class="menu-logo"><br> DOOMBREAKER <br> OPEN SEASON <br></div><div class="menu-sub">FPS · THREE JS</div><div class="menu-divider"></div><div class="menu-btns"><button class="menu-btn" id="startGameBtn"><span>▸ ENTER THE PIT</span></button><button class="menu-btn" id="menuControlsBtn"><span>▸ CONTROLS</span></button></div><div class="menu-divider"></div><div class="menu-version" style="font-size:9px; color:#aa5555;">HORROR FPS · THREE JS · GORE · FLASHLIGHT · UZI</div></div>
<div id="gameHUD" class="hidden"><div class="hud-panel"><div class="hud-label">HEALTH</div><div class="hud-value" id="healthVal">100</div></div><div class="hud-panel"><div class="hud-label">SCORE</div><div class="hud-value" id="scoreVal">0</div></div><div class="hud-panel"><div class="hud-label">PLAYER LVL</div><div class="hud-value" id="playerLevelVal">1</div></div></div>
<div id="levelPanel" class="hud-panel hidden"><div class="hud-label">ZONE</div><div class="hud-value" id="zoneVal">1 / 10</div><div id="enemyCounter" style="font-size:12px; margin-top:5px;">⚔️ REMAINING: 0</div></div>
<div id="weaponPanel" class="hidden">UZI</div>
<div class="crosshair"></div><div id="hitmarker"></div><div id="dmgFlash" class="flash"></div><div id="muzzleFlashOverlay" class="flash"></div>
<div class="xp-bar"><div class="xp-bar-fill" id="xpBar"></div></div>
<div id="gameOverScreen" class="screen hidden"><div class="boot-wrap" style="text-align:center"><div class="boot-logo" style="font-size:38px;">GAME OVER</div><div class="boot-sub">THE ABYSS ENDURES</div><div id="statsPanel" style="margin:20px 0;"></div><button class="menu-btn" id="restartBtn"><span>▸ RETURN TO MENU</span></button></div></div>
<script type="importmap">{"imports":{"three":"https://unpkg.com/three@0.128.0/build/three.module.js","three/addons/":"https://unpkg.com/three@0.128.0/examples/jsm/"}}</script>
<script type="module">
import * as THREE from 'three';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
// --- Persistent Level ---
const DB_NAME = "DoombreakerDB";
const STORE = "playerProgress";
let playerLevel = 1;
let damageMultiplier = 1.0;
let baseEnemyHPMult = 1.0;
function initIndexedDB() { return new Promise((resolve) => { let req = indexedDB.open(DB_NAME, 1); req.onupgradeneeded = (e) => { let db = e.target.result; if(!db.objectStoreNames.contains(STORE)) db.createObjectStore(STORE, { keyPath: "id" }); }; req.onsuccess = (e) => { let db = e.target.result; let tx = db.transaction(STORE, "readonly"); let store = tx.objectStore(STORE); let getReq = store.get("playerData"); getReq.onsuccess = () => { if(getReq.result) playerLevel = getReq.result.level || 1; else playerLevel = 1; updateDifficultyScaling(); resolve(); }; getReq.onerror = () => { playerLevel=1; updateDifficultyScaling(); resolve();}; }; req.onerror = () => { playerLevel=1; updateDifficultyScaling(); resolve(); }; }); }
function savePlayerLevel() { let req = indexedDB.open(DB_NAME, 1); req.onsuccess = (e) => { let db = e.target.result; let tx = db.transaction(STORE, "readwrite"); let store = tx.objectStore(STORE); store.put({ id: "playerData", level: playerLevel }); }; }
function updateDifficultyScaling() { damageMultiplier = 1.0 + (playerLevel-1) * 0.07; baseEnemyHPMult = 1.0 + (playerLevel-1) * 0.1; }
function increasePlayerLevel() { playerLevel++; updateDifficultyScaling(); savePlayerLevel(); document.getElementById('playerLevelVal').innerText = playerLevel; showAlert(`⬆️ LEVEL ${playerLevel} ⬆️ | DMG +${Math.round((damageMultiplier-1)*100)}%`); updateXPBar(); }
function updateXPBar() { document.getElementById('xpBar').style.width = '0%'; setTimeout(() => { document.getElementById('xpBar').style.width = '100%'; }, 100); }
// --- Globals (UZI only) ---
let scene, camera, renderer, composer, clock;
let player, weaponGroup;
let enemies = [], projectiles = [], particles = [], pickups = [], coverObjects = [], tracers = [];
let gameActive = false, pointerLocked = false;
let health = 100, maxHealth = 100, score = 0;
let currentZone = 1;
let zoneCompleteFlag = false;
let invincibleTimer = 0;
let shooting = false, shootInterval = null, lastShotTime = 0;
let keys = { w: false, s: false, a: false, d: false, space: false };
let yaw = Math.PI, pitch = 0;
let bobTimer = 0, moveBob = { x: 0, y: 0 }, recoilOffset = 0;
let activeTimers = [];
let isBossWave = false;
let verticalVelocity = 0, isGrounded = true;
const GRAVITY = 25, JUMP_FORCE = 9;
let defaultFOV = 80, zoomFOV = 40;
let bloodTexture = null;
// --- SINGLE WEAPON: UZI (enhanced stats) ---
const weapon = {
id:0, name:"UZI (CUSTOM)", icon:"⚡", fireDelay:45, damage:78, spread:0.19, pellets:1, projectileType:"hitscan",
barrelZ:-0.95, range:420, tracerColor:0xffaa66
};
function getBloodTexture() {
if (bloodTexture) return bloodTexture;
const canvas = document.createElement('canvas'); canvas.width = 128; canvas.height = 128;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#1a0000'; ctx.fillRect(0,0,128,128);
for(let i=0;i<60;i++) { const x=Math.random()*128, y=Math.random()*128, r=Math.random()*12+2, g=Math.floor(Math.random()*60); ctx.fillStyle = `rgb(${120+g},${g},${g})`; ctx.beginPath(); ctx.arc(x,y,r,0,Math.PI*2); ctx.fill(); }
for(let i=0;i<15;i++) { const x=Math.random()*128, y=Math.random()*128, w=Math.random()*20+5, h=Math.random()*20+5; ctx.fillStyle = `rgba(${80+Math.random()*40},5,5,0.7)`; ctx.fillRect(x,y,w,h); }
bloodTexture = new THREE.CanvasTexture(canvas); return bloodTexture;
}
function clearTimers() { activeTimers.forEach(t => clearTimeout(t)); activeTimers = []; }
function showAlert(msg) { let div = document.createElement('div'); div.className = 'wave-alert'; div.innerText = msg; document.body.appendChild(div); let t = setTimeout(() => div.remove(), 1800); activeTimers.push(t); }
function spawnHitmarker() { let hm = document.getElementById('hitmarker'); hm.style.opacity = '0.9'; setTimeout(() => hm.style.opacity = '0', 80); }
// Lightweight audio
function playSound(type, vol=0.35) {
if(!window.AudioContext) return;
let ctx = new (AudioContext || webkitAudioContext)();
if(ctx.state === 'suspended') ctx.resume();
let gain = ctx.createGain(); gain.gain.value = Math.min(vol,0.6); gain.connect(ctx.destination);
let now = ctx.currentTime;
if(type === 'uzi'){
let osc = ctx.createOscillator(); osc.type = 'sawtooth'; osc.frequency.value = 2000;
let env = ctx.createGain(); env.gain.setValueAtTime(0.35, now); env.gain.exponentialRampToValueAtTime(0.001, now+0.045);
osc.connect(env); env.connect(gain); osc.start(); osc.stop(now+0.045);
let noise = ctx.createBufferSource(); let buf = ctx.createBuffer(1, 2048, ctx.sampleRate); let data = buf.getChannelData(0);
for(let i=0;i<2048;i++) data[i]=Math.random()*2-1; noise.buffer = buf;
let bp = ctx.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.value = 3800; bp.Q.value = 1.2;
noise.connect(bp); bp.connect(gain); noise.start(); noise.stop(now+0.04);
} else if(type === 'hit'){
let osc = ctx.createOscillator(); osc.type = 'sine'; osc.frequency.value = 1300;
let env = ctx.createGain(); env.gain.setValueAtTime(0.25, now); env.gain.exponentialRampToValueAtTime(0.001, now+0.05);
osc.connect(env); env.connect(gain); osc.start(); osc.stop(now+0.05);
} else if(type === 'death'){
let buf = ctx.createBuffer(1, 8192, ctx.sampleRate); let d = buf.getChannelData(0);
for(let i=0;i<8192;i++) d[i] = (Math.random()*2-1) * Math.exp(-i/1500);
let src = ctx.createBufferSource(); src.buffer = buf;
let lp = ctx.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.value = 400;
src.connect(lp); lp.connect(gain); src.start(); src.stop(now+0.5);
} else if(type === 'explosion'){
let buf = ctx.createBuffer(1, 8192, ctx.sampleRate); let d = buf.getChannelData(0);
for(let i=0;i<8192;i++) d[i] = (Math.random()*2-1) * Math.exp(-i/3000);
let src = ctx.createBufferSource(); src.buffer = buf;
let lp = ctx.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.value = 300;
src.connect(lp); lp.connect(gain); src.start(); src.stop(now+0.6);
}
}
// --- Enhanced UZI Model (detailed, high quality) ---
function buildWeaponModel() {
const group = new THREE.Group();
const metalDark = new THREE.MeshStandardMaterial({ color: 0x1a1a1a, metalness: 0.97, roughness: 0.28 });
const metalGrey = new THREE.MeshStandardMaterial({ color: 0x3a3a3a, metalness: 0.92, roughness: 0.32 });
const metalSteel = new THREE.MeshStandardMaterial({ color: 0x4a4a4a, metalness: 0.98, roughness: 0.2 });
const gripMat = new THREE.MeshStandardMaterial({ color: 0x222222, roughness: 0.9 });
const glowMat = new THREE.MeshStandardMaterial({ color: 0xff4400, emissive: 0xff2200, emissiveIntensity: 2.2 });
const brassMat = new THREE.MeshStandardMaterial({ color: 0xcc8844, metalness: 0.85 });
// Receiver body
const body = new THREE.Mesh(new THREE.BoxGeometry(0.22, 0.28, 0.72), metalDark);
body.position.set(0, 0.03, -0.18); group.add(body);
// Barrel with cooling fins
const barrel = new THREE.Mesh(new THREE.CylinderGeometry(0.048, 0.058, 0.85, 12), metalSteel);
barrel.rotation.x = Math.PI/2; barrel.position.set(0, -0.01, -0.70); group.add(barrel);
// Barrel shroud (perforated)
const shroud = new THREE.Mesh(new THREE.CylinderGeometry(0.068, 0.07, 0.75, 8), metalGrey);
shroud.rotation.x = Math.PI/2; shroud.position.set(0, -0.01, -0.69); group.add(shroud);
// Curved magazine
const mag = new THREE.Mesh(new THREE.BoxGeometry(0.13, 0.52, 0.19), metalDark);
mag.position.set(0, -0.34, -0.05); mag.rotation.x = -0.38; group.add(mag);
// Magazine base plate
const magPlate = new THREE.Mesh(new THREE.BoxGeometry(0.14, 0.06, 0.21), brassMat);
magPlate.position.set(0, -0.61, -0.04); magPlate.rotation.x = -0.38; group.add(magPlate);
// Pistol grip (checkered)
const grip = new THREE.Mesh(new THREE.BoxGeometry(0.16, 0.38, 0.19), gripMat);
grip.position.set(0, -0.28, 0.12); group.add(grip);
// Top rail (Picatinny)
const rail = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.06, 0.48), metalGrey);
rail.position.set(0, 0.22, -0.25); group.add(rail);
// Rear sight
const rearSight = new THREE.Mesh(new THREE.BoxGeometry(0.04, 0.09, 0.04), metalSteel);
rearSight.position.set(0, 0.28, -0.08); group.add(rearSight);
// Front sight hood
const frontSight = new THREE.Mesh(new THREE.BoxGeometry(0.05, 0.09, 0.05), metalSteel);
frontSight.position.set(0, 0.17, -0.98); group.add(frontSight);
// Muzzle device
const muzzle = new THREE.Mesh(new THREE.CylinderGeometry(0.058, 0.058, 0.12, 8), metalSteel);
muzzle.rotation.x = Math.PI/2; muzzle.position.set(0, -0.01, -1.08); group.add(muzzle);
// Muzzle glow (faint)
const glow = new THREE.Mesh(new THREE.SphereGeometry(0.05, 8), glowMat);
glow.position.set(0, -0.01, -1.15); group.add(glow);
// Ejection port detail
const ejection = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.04, 0.05), metalGrey);
ejection.position.set(0.1, 0.12, -0.22); group.add(ejection);
// Folding stock (folded, but visible as brace)
const stock = new THREE.Mesh(new THREE.BoxGeometry(0.18, 0.16, 0.14), metalDark);
stock.position.set(0, -0.02, 0.38); group.add(stock);
const stockPad = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.2, 0.06), gripMat);
stockPad.position.set(0, -0.02, 0.48); group.add(stockPad);
group.traverse(c=>{ if(c.isMesh) c.raycast=()=>{}; });
return group;
}
function updateWeaponGraphics() { if(weaponGroup) camera.remove(weaponGroup); weaponGroup = buildWeaponModel(); camera.add(weaponGroup); }
// --- Blood Decal System ---
function addBloodDecal(position, normal) {
const tex = getBloodTexture();
const decal = new THREE.Mesh(new THREE.PlaneGeometry(0.9+Math.random()*0.7, 0.9+Math.random()*0.7), new THREE.MeshStandardMaterial({map:tex, transparent:true, opacity:0.85, side:THREE.DoubleSide}));
decal.position.copy(position).add(normal.clone().multiplyScalar(0.07));
decal.quaternion.copy(new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0,0,1), normal));
decal.rotation.z = Math.random() * Math.PI * 2;
scene.add(decal);
setTimeout(() => { if(decal.parent) scene.remove(decal); }, 10000);
}
// --- Environment & Cover (optimized) ---
function createCover() {
const stoneMat = new THREE.MeshStandardMaterial({ color: 0x443322, roughness: 0.9 });
const positions = [[-45,0,-30],[20,0,40],[40,0,15],[-20,0,50],[10,0,-55],[55,0,-25],[-55,0,25],[-40,0,60]];
positions.forEach(pos => { const h=3+Math.random()*3; const block=new THREE.Mesh(new THREE.BoxGeometry(5,h,5), stoneMat); block.position.set(pos[0],h/2,pos[2]); block.castShadow=false; scene.add(block); coverObjects.push(block); });
for(let i=0;i<12;i++){ const ang=Math.random()*Math.PI*2, rad=40+Math.random()*70, x=Math.cos(ang)*rad, z=Math.sin(ang)*rad, h=2+Math.random()*2.5; const pillar=new THREE.Mesh(new THREE.CylinderGeometry(1.2,1.5,h,5), stoneMat); pillar.position.set(x,h/2,z); pillar.castShadow=false; scene.add(pillar); coverObjects.push(pillar); }
}
function isCollidingWithCover(pos, r=0.8) { for(let c of coverObjects) if(new THREE.Box3().setFromObject(c).intersectsBox(new THREE.Box3().setFromCenterAndSize(pos,new THREE.Vector3(r*2,1.6,r*2)))) return true; return Math.abs(pos.x)>138||Math.abs(pos.z)>138; }
// --- Enemy Models (horror style) ---
function createEnhancedEnemy(type, x, z) {
const group = new THREE.Group();
let baseHP, speed, damage, value;
const fleshMat = new THREE.MeshStandardMaterial({ color: 0x883322, emissive: 0x221100 });
const darkMat = new THREE.MeshStandardMaterial({ color: 0x4a2222 });
const boneMat = new THREE.MeshStandardMaterial({ color: 0xddbbaa });
const eyeMat = new THREE.MeshStandardMaterial({ color: 0xff1100, emissive: 0xff2200, emissiveIntensity: 2.0 });
switch(type){
case 'crawler': baseHP=95; speed=9.8; damage=16; value=50; let body=new THREE.Mesh(new THREE.SphereGeometry(0.6,8,6), fleshMat); body.scale.set(1.2,0.9,1); group.add(body); let eye=new THREE.Mesh(new THREE.SphereGeometry(0.2,6), eyeMat); eye.position.set(0,0.35,0.95); group.add(eye); break;
case 'shambler': baseHP=230; speed=7.2; damage=30; value=110; let torso=new THREE.Mesh(new THREE.CylinderGeometry(0.7,0.9,1.1,6), fleshMat); torso.position.y=0.1; group.add(torso); let lEye=new THREE.Mesh(new THREE.SphereGeometry(0.12,6), eyeMat); lEye.position.set(-0.22,1.1,0.6); group.add(lEye); let rEye=new THREE.Mesh(new THREE.SphereGeometry(0.12,6), eyeMat); rEye.position.set(0.22,1.1,0.6); group.add(rEye); break;
case 'wraith': baseHP=420; speed=7.6; damage=42; value=210; let core=new THREE.Mesh(new THREE.IcosahedronGeometry(0.4,1), new THREE.MeshStandardMaterial({color:0xff2200,emissive:0xff1100,emissiveIntensity:3})); group.add(core); let wEye=new THREE.Mesh(new THREE.SphereGeometry(0.2,6), eyeMat); wEye.position.set(0,0.4,0.9); group.add(wEye); break;
case 'doom_knight': baseHP=820; speed=5.9; damage=60; value=400; let chest=new THREE.Mesh(new THREE.BoxGeometry(0.9,1.1,0.8), new THREE.MeshStandardMaterial({color:0x664422, metalness:0.6})); chest.position.y=0.3; group.add(chest); let helm=new THREE.Mesh(new THREE.CylinderGeometry(0.5,0.6,0.8,6), boneMat); helm.position.y=1.0; group.add(helm); let visor=new THREE.Mesh(new THREE.BoxGeometry(0.4,0.08,0.1), eyeMat); visor.position.set(0,1.0,0.55); group.add(visor); break;
case 'malignant_boss': baseHP=4600; speed=4.4; damage=95; value=5000; let mbCore=new THREE.Mesh(new THREE.IcosahedronGeometry(1.2,2), new THREE.MeshStandardMaterial({color:0xbb2211,emissive:0x661100,emissiveIntensity:1.5})); group.add(mbCore); let mbEye=new THREE.Mesh(new THREE.SphereGeometry(0.55,10,10), eyeMat); mbEye.position.set(0,0.45,1.35); group.add(mbEye); group.userData.isBoss=true; break;
default: baseHP=90; speed=8; damage=16; value=40;
}
let finalHP = Math.floor(baseHP * baseEnemyHPMult * (type==='malignant_boss'?1.3:1));
group.userData = { type, hp: finalHP, speed, damage, value, lastAttack:0, velocity: new THREE.Vector3(), wobble: Math.random()*Math.PI*2 };
group.scale.set(2,2,2); group.position.set(x,0.55,z);
scene.add(group); enemies.push(group); return group;
}
// --- Zone Management ---
async function startZone(zoneNum, isBoss=false) {
zoneCompleteFlag=false; isBossWave=isBoss;
clearEnemiesAndPickups();
let enemyCount = isBoss ? 1 : Math.min(5, 2 + Math.floor((zoneNum-1)/3) + Math.floor((playerLevel-1)/6));
document.getElementById('zoneVal').innerText = `${zoneNum} / 10`;
if(isBoss){ showAlert(`⚠️ MALIGNANT BOSS ⚠️`); createEnhancedEnemy('malignant_boss', (Math.random()-0.5)*170, (Math.random()-0.5)*170); }
else { let types=['crawler','shambler','wraith','doom_knight']; for(let i=0;i<enemyCount;i++){ let rType=types[Math.min(3, Math.floor((zoneNum-1)/3)+Math.floor(Math.random()*2))]; if(zoneNum>=8 && Math.random()<0.45) rType='doom_knight'; createEnhancedEnemy(rType, (Math.random()-0.5)*190, (Math.random()-0.5)*190); } }
updateEnemyCounter();
}
function clearEnemiesAndPickups(){ enemies.forEach(e=>scene.remove(e)); projectiles.forEach(p=>scene.remove(p)); particles.forEach(p=>scene.remove(p)); pickups.forEach(p=>scene.remove(p)); tracers.forEach(t=>scene.remove(t)); enemies=[]; projectiles=[]; particles=[]; pickups=[]; tracers=[]; }
function updateEnemyCounter(){ document.getElementById('enemyCounter').innerHTML = `⚔️ REMAINING: ${enemies.length}`; }
async function checkZoneCompletion(){
if(!gameActive || zoneCompleteFlag) return;
if(enemies.length===0){
zoneCompleteFlag=true;
if(isBossWave){ showAlert(`💀 BOSS SLAIN! LEVEL UP! 💀`); increasePlayerLevel(); currentZone=1; await startZone(1, false); }
else { if(currentZone < 10){ currentZone++; await startZone(currentZone, false); } else { await startZone(10, true); } }
}
}
// --- Tracer & Combat (UZI only) ---
function spawnTracer(start, end, color, speed=80) { let dist=start.distanceTo(end), dur=dist/speed; if(dur<=0) return; let tracer=new THREE.Mesh(new THREE.SphereGeometry(0.08,6), new THREE.MeshStandardMaterial({color,emissive:color,emissiveIntensity:1.5})); tracer.userData={start:start.clone(),end:end.clone(),progress:0,duration:dur}; tracer.position.copy(start); scene.add(tracer); tracers.push(tracer); }
function spawnPickup(pos){ let pack=new THREE.Group(); let box=new THREE.Mesh(new THREE.BoxGeometry(0.6,0.6,0.6), new THREE.MeshStandardMaterial({color:0x33ffaa,emissive:0x22aa66})); pack.add(box); pack.position.set(pos.x,1.2,pos.z); pack.userData={type:'health',heal:15}; scene.add(pack); pickups.push(pack); }
function updatePickups(){ for(let i=pickups.length-1;i>=0;i--){ let p=pickups[i]; p.rotation.y+=0.05; if(p.position.distanceTo(player.position)<2.0){ health=Math.min(maxHealth,health+p.userData.heal); document.getElementById('healthVal').innerText=health; playSound('hit',0.2); scene.remove(p); pickups.splice(i,1); showAlert("🩸 +15 HEALTH"); } } }
function performShoot(){
if(!gameActive || !pointerLocked) return;
let now = Date.now();
if(now - lastShotTime < weapon.fireDelay) return;
lastShotTime = now;
playSound('uzi', 0.4);
let flashLight = new THREE.PointLight(0xff8833, 4.2, 18); flashLight.position.set(0,-0.02,weapon.barrelZ); weaponGroup.add(flashLight); setTimeout(()=>weaponGroup.remove(flashLight),65);
if(window._muzzleWorldLight){ window._muzzleWorldLight.intensity = 3.2; setTimeout(()=>{ if(window._muzzleWorldLight) window._muzzleWorldLight.intensity=0; }, 50); }
document.getElementById('muzzleFlashOverlay').style.display='block'; setTimeout(()=>document.getElementById('muzzleFlashOverlay').style.display='none',45);
document.body.classList.add('shake'); setTimeout(()=>document.body.classList.remove('shake'),100);
recoilOffset = 0.12; setTimeout(()=>{ recoilOffset*=0.5; },100);
const rayOrigin = camera.getWorldPosition(new THREE.Vector3());
let hitAnything = false;
for(let p=0;p<weapon.pellets;p++){
let spreadX=(Math.random()-0.5)*weapon.spread, spreadY=(Math.random()-0.5)*weapon.spread;
let dir=new THREE.Vector3(0,0,-1).applyEuler(new THREE.Euler(pitch+spreadY, yaw+spreadX, 0, 'YXZ'));
let raycaster=new THREE.Raycaster(rayOrigin, dir, 0, weapon.range);
let hits=raycaster.intersectObjects(enemies, true);
let hitPoint=rayOrigin.clone().add(dir.clone().multiplyScalar(weapon.range));
for(let hit of hits){
let obj=hit.object; while(obj.parent && !obj.userData?.hp) obj=obj.parent;
if(obj.userData && obj.userData.hp && enemies.includes(obj)){
let dmg = (weapon.damage) * damageMultiplier;
obj.userData.hp -= dmg;
hitAnything=true; hitPoint=hit.point;
playSound('hit',0.2);
let norm=hit.face?hit.face.normal:new THREE.Vector3(0,1,0);
if(hit.object.matrixWorld) norm=norm.clone().transformDirection(hit.object.matrixWorld).normalize();
addBloodDecal(hit.point, norm);
for(let k=0;k<2;k++){ let part=new THREE.Mesh(new THREE.SphereGeometry(0.05,4), new THREE.MeshStandardMaterial({color:0x880000})); part.position.copy(hit.point); part.userData={velocity:new THREE.Vector3((Math.random()-0.5)*4, Math.random()*4, (Math.random()-0.5)*4), life:0.5}; scene.add(part); particles.push(part); }
if(obj.userData.hp <= 0){
score += obj.userData.value; document.getElementById('scoreVal').innerText=score;
playSound('death',0.35);
for(let k=0;k<12;k++){ let gore=new THREE.Mesh(new THREE.SphereGeometry(0.08,4), new THREE.MeshStandardMaterial({color:0x880000})); gore.position.copy(obj.position); gore.userData={velocity:new THREE.Vector3((Math.random()-0.5)*12, Math.random()*10, (Math.random()-0.5)*12), life:0.9}; scene.add(gore); particles.push(gore); }
addBloodDecal(obj.position.clone().add(new THREE.Vector3(0,0.1,0)), new THREE.Vector3(0,1,0));
scene.remove(obj); let idx=enemies.indexOf(obj); if(idx!==-1) enemies.splice(idx,1);
updateEnemyCounter();
if(Math.random()<0.38) spawnPickup(obj.position);
}
break;
}
}
let muzzlePos=rayOrigin.clone().add(dir.clone().multiplyScalar(0.5));
spawnTracer(muzzlePos, hitPoint, weapon.tracerColor, 120);
}
if(hitAnything) spawnHitmarker();
}
function updateTracers(delta){ for(let i=tracers.length-1;i>=0;i--){ let t=tracers[i]; t.userData.progress+=delta/t.userData.duration; if(t.userData.progress>=1){ scene.remove(t); tracers.splice(i,1); continue; } t.position.copy(t.userData.start.clone().lerp(t.userData.end, t.userData.progress)); } }
function updateParticles(delta){ for(let i=particles.length-1;i>=0;i--){ let p=particles[i]; if(p.userData.velocity){ p.position.add(p.userData.velocity.clone().multiplyScalar(delta)); p.userData.life-=delta; if(p.userData.life<=0){ scene.remove(p); particles.splice(i,1); } } else { p.userData.life-=delta; if(p.userData.life<=0){ scene.remove(p); particles.splice(i,1); } } } }
// --- Movement & AI (standard) ---
function updateMovement(delta){
let speed=23; let move=new THREE.Vector3();
if(keys.w) move.z-=1; if(keys.s) move.z+=1; if(keys.a) move.x-=1; if(keys.d) move.x+=1;
let moving=move.length()>0; if(moving) move.normalize();
move.applyQuaternion(player.quaternion);
let nx=player.position.x+move.x*speed*delta, nz=player.position.z+move.z*speed*delta;
let test=new THREE.Vector3(nx, player.position.y, nz);
if(!isCollidingWithCover(test,0.7)){ player.position.x=nx; player.position.z=nz; }
else { test.set(nx, player.position.y, player.position.z); if(!isCollidingWithCover(test,0.7)) player.position.x=nx; test.set(player.position.x, player.position.y, nz); if(!isCollidingWithCover(test,0.7)) player.position.z=nz; }
player.position.x=Math.min(140,Math.max(-140,player.position.x)); player.position.z=Math.min(140,Math.max(-140,player.position.z));
verticalVelocity-=GRAVITY*delta; player.position.y+=verticalVelocity*delta;
if(player.position.y<=1.6){ player.position.y=1.6; verticalVelocity=0; if(keys.space) verticalVelocity=JUMP_FORCE; }
if(moving){ bobTimer+=delta*12; moveBob.x=Math.sin(bobTimer)*0.02; moveBob.y=Math.abs(Math.sin(bobTimer*1.8))*0.014; }
else{ moveBob.x*=0.9; moveBob.y*=0.9; }
recoilOffset*=0.85;
if(weaponGroup){ weaponGroup.position.x=0.34+moveBob.x; weaponGroup.position.y=-0.2+moveBob.y-recoilOffset; weaponGroup.position.z=-0.55; weaponGroup.rotation.z=Math.sin(Date.now()*0.006)*0.015; }
}
function updateEnemiesAI(delta){
let playerPos=player.position, t=Date.now()*0.001;
for(let i=0;i<enemies.length;i++){ let e=enemies[i], d=e.userData; if(d.hp<=0){ scene.remove(e); enemies.splice(i,1); updateEnemyCounter(); i--; continue; }
let dir=new THREE.Vector3().subVectors(playerPos,e.position).normalize(); d.velocity.lerp(dir.multiplyScalar(d.speed),0.14*delta*30);
e.position.add(d.velocity.clone().multiplyScalar(delta)); e.lookAt(playerPos);
e.position.y=0.55+Math.sin(t*2+(d.wobble||0))*0.08;
let dist=e.position.distanceTo(playerPos);
if(invincibleTimer<=0 && dist<1.6){ health-=d.damage; document.getElementById('healthVal').innerText=Math.max(0,health); document.getElementById('dmgFlash').style.display='block'; setTimeout(()=>document.getElementById('dmgFlash').style.display='none',130); invincibleTimer=0.7; if(health<=0) gameLose(); let push=new THREE.Vector3().subVectors(e.position,playerPos).normalize(); player.position.x+=push.x*2.5; player.position.z+=push.z*2.5; }
if(Date.now()>(d.lastAttack+950) && dist<34){ d.lastAttack=Date.now(); let proj=new THREE.Mesh(new THREE.SphereGeometry(0.32,8), new THREE.MeshStandardMaterial({color:0xff6633,emissive:0xaa2200})); proj.position.copy(e.position); let vel=new THREE.Vector3().subVectors(playerPos,e.position).normalize().multiplyScalar(14); proj.userData={velocity:vel,damage:d.isBoss?34:20,life:2.8}; scene.add(proj); projectiles.push(proj); }
} if(invincibleTimer>0) invincibleTimer-=delta;
}
function updateProjectileAttacks(delta){ for(let i=0;i<projectiles.length;i++){ let p=projectiles[i]; if(p.userData.velocity && !p.userData.isRocket){ p.position.add(p.userData.velocity.clone().multiplyScalar(delta)); if(p.position.distanceTo(player.position)<1.6 && invincibleTimer<=0){ health-=p.userData.damage; document.getElementById('healthVal').innerText=Math.max(0,health); invincibleTimer=0.6; if(health<=0) gameLose(); scene.remove(p); projectiles.splice(i,1); } p.userData.life-=delta; if(p.userData.life<=0||p.position.y<-2){ scene.remove(p); projectiles.splice(i,1); } } } }
function gameLose(){ gameActive=false; if(shootInterval) clearInterval(shootInterval); document.getElementById('gameHUD').classList.add('hidden'); document.getElementById('levelPanel').classList.add('hidden'); document.getElementById('weaponPanel').classList.add('hidden'); document.getElementById('gameOverScreen').classList.remove('hidden'); document.getElementById('statsPanel').innerHTML=`SCORE: ${score}<br>ZONE: ${currentZone}<br>PLAYER LVL: ${playerLevel}<br>PRESS RESTART`; if(pointerLocked) document.exitPointerLock(); }
function startGameSession(){ if(shootInterval) clearInterval(shootInterval); gameActive=true; health=100; score=0; currentZone=1; updateDifficultyScaling(); bobTimer=0; moveBob={x:0,y:0}; recoilOffset=0; verticalVelocity=0; document.getElementById('healthVal').innerText="100"; document.getElementById('scoreVal').innerText="0"; document.getElementById('playerLevelVal').innerText=playerLevel; clearEnemiesAndPickups(); updateWeaponGraphics(); startZone(1,false); document.getElementById('gameHUD').classList.remove('hidden'); document.getElementById('levelPanel').classList.remove('hidden'); document.getElementById('weaponPanel').classList.remove('hidden'); document.getElementById('gameOverScreen').classList.add('hidden'); document.getElementById('menuScreen').classList.add('hidden'); setTimeout(()=>{ if(gameActive && renderer.domElement) renderer.domElement.requestPointerLock(); },200); }
function autoFireStart(){ if(gameActive && pointerLocked && !shooting){ shooting=true; if(shootInterval) clearInterval(shootInterval); shootInterval=setInterval(performShoot, weapon.fireDelay); } }
function autoFireStop(){ shooting=false; if(shootInterval){ clearInterval(shootInterval); shootInterval=null; } }
// --- THREE.JS: Pitch Black + Wide Flashlight + Granular CRT ---
function initThree(){
scene=new THREE.Scene(); scene.background=new THREE.Color(0x000000); scene.fog=new THREE.FogExp2(0x000000,0.026);
camera=new THREE.PerspectiveCamera(defaultFOV,window.innerWidth/window.innerHeight,0.1,3000);
renderer=new THREE.WebGLRenderer({antialias:false, powerPreference:"high-performance"}); renderer.setPixelRatio(1); renderer.setSize(window.innerWidth,window.innerHeight); renderer.shadowMap.enabled=false; document.body.appendChild(renderer.domElement);
clock=new THREE.Clock();
let renderScene=new RenderPass(scene,camera);
composer=new EffectComposer(renderer); composer.addPass(renderScene);
let bloom=new UnrealBloomPass(new THREE.Vector2(window.innerWidth,window.innerHeight),0.55,0.32,0.45); composer.addPass(bloom);
// GRANULAR CRT SHADER (heavy scanlines, grain, flicker)
let crtShader={ uniforms:{tDiffuse:{value:null},time:{value:0}}, vertexShader:`varying vec2 vUv;void main(){vUv=uv;gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);}`, fragmentShader:`
uniform sampler2D tDiffuse; uniform float time; varying vec2 vUv;
float rand(vec2 co){ return fract(sin(dot(co,vec2(12.9898,78.233)))*43758.5453); }
void main(){ vec2 uv=vUv; float scan=sin(uv.y*1200.0+time*18.0)*0.12; float scan2=cos(uv.y*1800.0-time*22.0)*0.06; float ca=0.0025+sin(time*0.5)*0.0008; vec3 col; col.r=texture2D(tDiffuse,uv+vec2(ca,0.0)).r; col.g=texture2D(tDiffuse,uv).g; col.b=texture2D(tDiffuse,uv-vec2(ca,0.0)).b; col*=1.0-(scan+scan2)*0.35; float grain=rand(uv+floor(time*70.0))*0.12-0.06; col+=grain; float vig=1.0-0.45*length(uv-0.5); col.r*=1.1; col.b*=0.85; float flick=0.95+sin(time*97.0)*0.05; col*=flick; gl_FragColor=vec4(col*vig,1.0); }`
};
let crtPass=new ShaderPass(crtShader); crtPass.renderToScreen=true; composer.addPass(crtPass);
// LIGHTING: near pitch black + wide flashlight
let ambient=new THREE.AmbientLight(0x010000,0.02); scene.add(ambient);
let flashlight=new THREE.SpotLight(0xfff5e0,5.8,65,0.78,0.58,1.6); flashlight.position.set(0,0.08,0.3); flashlight.castShadow=false; camera.add(flashlight);
let flashTarget=new THREE.Object3D(); flashTarget.position.set(0,0,-9); camera.add(flashTarget); flashlight.target=flashTarget;
let fillGlow=new THREE.PointLight(0xffaa66,0.3,14); fillGlow.position.set(0,0.2,0.8); camera.add(fillGlow);
let muzzleWorld=new THREE.PointLight(0xff9933,0,20); muzzleWorld.position.set(0,0,-1.2); camera.add(muzzleWorld); window._muzzleWorldLight=muzzleWorld;
player=new THREE.Object3D(); player.position.set(0,1.6,-70); scene.add(player); player.add(camera); camera.position.set(0,0.25,0);
weaponGroup=buildWeaponModel(); camera.add(weaponGroup);
let ground=new THREE.Mesh(new THREE.PlaneGeometry(320,320), new THREE.MeshStandardMaterial({color:0x1a0800,roughness:0.95})); ground.rotation.x=-Math.PI/2; ground.position.y=-0.3; scene.add(ground);
createCover();
for(let i=0;i<12;i++){ let stake=new THREE.Mesh(new THREE.CylinderGeometry(0.15,0.25,3+Math.random()*4,4), new THREE.MeshStandardMaterial({color:0x3a2010})); let ang=Math.random()*Math.PI*2, rad=40+Math.random()*100; stake.position.set(Math.cos(ang)*rad,1.5,Math.sin(ang)*rad); scene.add(stake); }
}
function animate(){ requestAnimationFrame(animate); let delta=Math.min(clock.getDelta(),0.033); if(gameActive){ updateMovement(delta); updateEnemiesAI(delta); updateProjectileAttacks(delta); updatePickups(); updateParticles(delta); updateTracers(delta); checkZoneCompletion(); } if(composer && composer.passes.length>2) composer.passes[composer.passes.length-1].uniforms.time.value=Date.now()*0.001; if(composer) composer.render(); else renderer.render(scene,camera); }
function bindEvents(){
document.addEventListener('pointerlockchange',()=>{ pointerLocked=document.pointerLockElement===renderer.domElement; if(!pointerLocked) autoFireStop(); });
document.addEventListener('mousemove',(e)=>{ if(!pointerLocked||!gameActive) return; yaw-=e.movementX*0.0022; pitch-=e.movementY*0.0022; pitch=Math.min(Math.PI/2.1,Math.max(-Math.PI/2.1,pitch)); player.rotation.y=yaw; camera.rotation.x=pitch; });
window.addEventListener('keydown',(e)=>{ if(gameActive&&pointerLocked){ if(e.code==='KeyW') keys.w=true; if(e.code==='KeyS') keys.s=true; if(e.code==='KeyA') keys.a=true; if(e.code==='KeyD') keys.d=true; if(e.code==='Space'){ keys.space=true; e.preventDefault(); } if(e.code==='Escape'){ if(gameActive){ gameActive=false; autoFireStop(); if(pointerLocked) document.exitPointerLock(); document.getElementById('gameHUD').classList.add('hidden'); document.getElementById('levelPanel').classList.add('hidden'); document.getElementById('weaponPanel').classList.add('hidden'); document.getElementById('menuScreen').classList.remove('hidden'); } } } });
window.addEventListener('keyup',(e)=>{ if(e.code==='KeyW') keys.w=false; if(e.code==='KeyS') keys.s=false; if(e.code==='KeyA') keys.a=false; if(e.code==='KeyD') keys.d=false; if(e.code==='Space') keys.space=false; });
window.addEventListener('mousedown',(e)=>{ if(e.button===0&&gameActive&&pointerLocked) autoFireStart(); if(e.button===2&&gameActive&&pointerLocked){ camera.fov=zoomFOV; camera.updateProjectionMatrix(); e.preventDefault(); } });
window.addEventListener('mouseup',(e)=>{ if(e.button===0) autoFireStop(); if(e.button===2){ camera.fov=defaultFOV; camera.updateProjectionMatrix(); } });
window.addEventListener('contextmenu',(e)=>e.preventDefault());
window.addEventListener('resize',()=>{ camera.aspect=window.innerWidth/window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth,window.innerHeight); composer.setSize(window.innerWidth,window.innerHeight); });
}
async function startup(){
await initIndexedDB(); initThree(); bindEvents(); animate();
let bootBar=document.getElementById('bootBar'), pct=document.getElementById('bootPct'), prog=0;
let iv=setInterval(()=>{ prog=Math.min(100,prog+9); bootBar.style.width=prog+'%'; pct.innerText=prog+'%'; if(prog>=100){ clearInterval(iv); document.getElementById('bootScreen').classList.add('hidden'); document.getElementById('menuScreen').classList.remove('hidden'); } },100);
document.getElementById('skipBoot').onclick=()=>{ clearInterval(iv); document.getElementById('bootScreen').classList.add('hidden'); document.getElementById('menuScreen').classList.remove('hidden'); };
document.getElementById('startGameBtn').onclick=()=>{ startGameSession(); };
document.getElementById('restartBtn').onclick=()=>{ document.getElementById('gameOverScreen').classList.add('hidden'); startGameSession(); };
document.getElementById('menuControlsBtn').onclick=()=>{ alert("DOOMBREAKER · UZI ONLY\n\nWASD = Move\nMOUSE = Aim\nHOLD LMB = Fire UZI\nSPACE = Jump\nRMB = 2x Zoom\nESC = Menu\n\n🔦 WIDE FLASHLIGHT (pitch black atmosphere)\n📺 GRANULAR CRT EFFECT (scanlines, grain, flicker)\n🔫 ENHANCED UZI MODEL with detailed receiver, barrel, magazine\n💀 10 zones + boss, persistent leveling\nRIP AND TEAR."); };
}
startup();
</script>
</body>
</html>