bbc123321 commited on
Commit
55b31c9
·
verified ·
1 Parent(s): a018dc0

Manual changes saved

Browse files
Files changed (1) hide show
  1. index.html +379 -403
index.html CHANGED
@@ -3,28 +3,32 @@
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>BattleZone Royale - Fixed</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <script src="https://unpkg.com/feather-icons"></script>
9
  <style>
10
  html,body { height:100%; margin:0; background:#0b1220; color:#fff; font-family:monospace; }
11
  #canvasContainer { position:relative; flex:1; display:flex; justify-content:center; align-items:center; height:100vh; overflow:hidden; }
12
  #gameCanvas { display:block; user-select:none; cursor:crosshair; box-shadow:0 0 20px rgba(0,0,0,.5); width:100%; height:100%; }
 
 
13
  #stormWarning { z-index:10; }
14
  #deathScreen { z-index:20; }
15
 
16
- /* HUD */
17
  #hudHealth { position:absolute; left:12px; bottom:12px; background:rgba(0,0,0,0.55); padding:6px 8px; border-radius:8px; font-weight:700; font-size:13px; display:flex; align-items:center; gap:8px; z-index:30; }
18
- #hudGearWrap { position:absolute; right:12px; bottom:12px; display:flex; gap:8px; align-items:center; z-index:30; }
19
- .pickaxe-slot { width:46px; height:46px; background: rgba(255,255,255,0.03); border-radius:6px; display:flex; align-items:center; justify-content:center; cursor:pointer; }
20
- .gear-slot { min-width:46px; height:36px; background: rgba(255,255,255,0.03); border-radius:6px; display:flex; align-items:center; justify-content:center; font-size:11px; color:#ddd; padding:4px; position:relative; cursor:pointer; }
21
- .selected { outline: 2px solid rgba(255,215,0,0.9); box-shadow: 0 0 6px rgba(255,215,0,0.12); }
22
- .equipped { box-shadow: inset 0 -6px 14px rgba(255,255,255,0.03); border:1px solid rgba(255,255,255,0.04); }
23
- .medkit-count { position:absolute; right:4px; bottom:2px; font-size:10px; color:#ffd; }
 
 
24
  </style>
25
  </head>
26
  <body>
27
- <!-- Landing -->
28
  <div id="landingScreen" class="flex flex-col items-center justify-center h-screen p-6">
29
  <h1 class="text-5xl font-bold text-yellow-400 mb-6 flex items-center">
30
  <i data-feather="target" class="mr-2"></i> BattleZone Royale
@@ -54,22 +58,20 @@
54
  </div>
55
  </div>
56
 
 
57
  <div class="bg-gray-800 p-4 rounded-lg max-w-xl w-full text-sm">
58
  <h3 class="font-bold text-yellow-400 mb-2">Controls</h3>
59
  <ul class="list-disc pl-5 space-y-1">
60
  <li><strong>WASD</strong> — Move</li>
61
- <li><strong>Mouse</strong> — Aim</li>
62
- <li><strong>Left Click</strong> — Shoot (if weapon equipped or in selected slot) or pickaxe melee</li>
63
- <li><strong>E</strong> — Use medkit in selected slot OR Interact / Loot / Harvest</li>
64
  <li><strong>Q</strong> — Build (costs 10 materials)</li>
65
- <li><strong>R</strong> — Reload selected weapon</li>
66
- <li><strong>1-5</strong> — Select inventory slot</li>
67
- <li><strong>F</strong> — Equip / Unequip selected slot (pickaxe is the default)</li>
68
  </ul>
69
  </div>
70
  </div>
71
 
72
- <!-- Game -->
73
  <div id="gameScreen" class="hidden h-screen flex flex-col">
74
  <header class="flex justify-between items-center border-b border-yellow-500 px-6 py-4" style="flex-shrink:0;">
75
  <h1 class="text-3xl font-bold text-yellow-400 flex items-center"><i data-feather="target" class="mr-2"></i>BattleZone Royale</h1>
@@ -98,78 +100,74 @@
98
  </button>
99
  </div>
100
 
 
101
  <div id="hudHealth" class="hidden">
102
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#ffd86b" stroke-width="1.6"><path d="M20.8 8.6a5.5 5.5 0 0 0-7.8 0L12 10.6l-1-1a5.5 5.5 0 0 0-7.8 7.8l1 1L12 22l7.8-3.6 1-1a5.5 5.5 0 0 0 0-7.8z"></path></svg>
103
  <span id="hudHealthText">100%</span>
104
  </div>
105
 
106
- <div id="hudGearWrap" class="hidden">
107
- <div id="pickaxeSlot" class="pickaxe-slot" title="Pickaxe (press F to equip)">⛏️</div>
108
- <div id="hudGear" aria-label="gear"></div>
109
- </div>
110
  </div>
 
 
111
  </div>
112
  </div>
113
 
114
  <script>
115
- // DOM
116
- const landingScreen = document.getElementById('landingScreen');
117
- const gameScreen = document.getElementById('gameScreen');
 
 
118
  const canvas = document.getElementById('gameCanvas');
119
  const ctx = canvas.getContext('2d');
120
- const hudHealth = document.getElementById('hudHealth');
121
  const hudHealthText = document.getElementById('hudHealthText');
122
- const hudGearWrap = document.getElementById('hudGearWrap');
123
- const hudGear = document.getElementById('hudGear');
124
- const pickaxeSlot = document.getElementById('pickaxeSlot');
125
- const stormWarning = document.getElementById('stormWarning');
126
- const deathScreen = document.getElementById('deathScreen');
127
-
128
- // World (bigger)
129
- const WORLD = { width: 6000, height: 4000 };
130
  let camera = { x:0, y:0 };
131
 
132
  function resizeCanvas(){
133
- const ctn = document.getElementById('canvasContainer');
134
- canvas.width = Math.max(900, Math.floor(ctn.clientWidth));
135
- canvas.height = Math.max(560, Math.floor(ctn.clientHeight));
136
  cameraUpdate();
137
  }
138
  window.addEventListener('resize', resizeCanvas);
139
 
140
- // Player (pickaxe separate from 5 inventory slots)
141
  const player = {
142
- id:'player', x: WORLD.width/2, y: WORLD.height/2, radius:16, angle:0, speed:220,
143
- health:100, armor:0, kills:0, materials:0,
144
- inventory: [null,null,null,null,null], // 5 slots
145
- selectedSlot:0,
146
- equippedIndex: -1, // -1 = pickaxe equipped, >=0 = inventory slot equipped
147
- lastShot:0, lastMelee:0
148
  };
149
 
150
- // Input
151
- const keys = { w:false,a:false,s:false,d:false,e:false,q:false,r:false,f:false };
152
  const mouse = { canvasX:0, canvasY:0, worldX:0, worldY:0, down:false };
153
 
154
- window.addEventListener('keydown',(e)=>{
155
  const k = e.key.toLowerCase();
 
156
  if (k in keys) keys[k] = true;
157
- if (['1','2','3','4','5'].includes(k)) { player.selectedSlot = parseInt(k)-1; updateHUD(); }
158
- if (k === 'f') {
159
- // Toggle equip/unequip
160
- player.equippedIndex = (player.equippedIndex === -1) ? player.selectedSlot : -1;
161
- updateHUD();
162
  }
163
- if (k === 'r') keys.r = true;
164
  });
165
- window.addEventListener('keyup',(e)=>{
166
  const k = e.key.toLowerCase();
 
167
  if (k in keys) keys[k] = false;
168
- if (k === 'e') keys.e = true; // single-use flag
169
  if (k === 'q') keys.q = true;
170
  });
171
 
172
- canvas.addEventListener('mousemove',(e)=>{
173
  const rect = canvas.getBoundingClientRect();
174
  mouse.canvasX = e.clientX - rect.left;
175
  mouse.canvasY = e.clientY - rect.top;
@@ -177,21 +175,20 @@
177
  mouse.worldY = mouse.canvasY + camera.y;
178
  player.angle = Math.atan2(mouse.worldY - player.y, mouse.worldX - player.x);
179
  });
180
- canvas.addEventListener('mousedown', ()=> mouse.down = true);
181
  window.addEventListener('mouseup', ()=> mouse.down = false);
182
 
183
- // Entities
184
  const bullets = [];
185
  const chests = [];
186
  const objects = []; // harvestables & walls
187
- const enemies = [];
188
- const pickups = []; // visible ground pickups
189
 
190
  function rand(min,max){ return Math.random()*(max-min)+min; }
191
  function randInt(min,max){ return Math.floor(Math.random()*(max-min))+min; }
192
 
193
  function biomeAt(x,y){
194
- const bx = Math.floor(x / 500), by = Math.floor(y / 500);
195
  const seed = (bx*73856093) ^ (by*19349663);
196
  const r = Math.abs(Math.sin(seed)) % 1;
197
  if (r < 0.25) return 'desert';
@@ -200,32 +197,33 @@
200
  return 'ruins';
201
  }
202
 
203
- // Weapon factory
204
- function makeWeaponProto(w){
205
- return { name:w.name, dmg:w.dmg, rate:w.rate, color:w.color, magSize:w.magSize || 12, startReserve:w.startReserve || (w.magSize*2 || 24) };
206
- }
207
-
208
  function generateLootForBiome(b){
209
  const roll = Math.random();
210
- if (roll < 0.35) return { type:'medkit', amount:1 };
211
- if (roll < 0.7) return { type:'materials', amount: randInt(5,20) };
212
  const weapons = [
213
- { name:'Pistol', dmg:12, rate:320, color:'#ffd86b', magSize:12, startReserve:24 },
214
- { name:'SMG', dmg:6, rate:120, color:'#8ef0ff', magSize:30, startReserve:90 },
215
- { name:'Shotgun', dmg:22, rate:800, color:'#ff9fb8', magSize:6, startReserve:18 },
216
- { name:'Rifle', dmg:18, rate:400, color:'#c7ff9a', magSize:20, startReserve:60 }
217
  ];
218
  return { type:'weapon', weapon: weapons[randInt(0, weapons.length)] };
219
  }
220
 
221
- // Populate world
222
  function populateWorld(){
223
- chests.length = 0; objects.length = 0; enemies.length = 0; pickups.length = 0;
224
- for (let i=0;i<260;i++){
 
 
 
 
225
  const x = rand(150, WORLD.width-150);
226
  const y = rand(150, WORLD.height-150);
227
  chests.push({ x,y, opened:false, loot: generateLootForBiome(biomeAt(x,y)) });
228
  }
 
 
229
  for (let i=0;i<700;i++){
230
  const t = Math.random();
231
  let type='wood';
@@ -235,276 +233,194 @@
235
  const hp = type==='wood'?40 : (type==='stone'?80:160);
236
  objects.push({ x,y, type, hp, maxHp:hp, dead:false });
237
  }
 
 
238
  for (let i=0;i<19;i++){
239
  const ex = rand(300, WORLD.width-300);
240
  const ey = rand(300, WORLD.height-300);
 
 
 
 
 
241
  enemies.push({
242
- id:'e'+i, x:ex, y:ey, radius:14, angle:0, speed:110+rand(-20,20),
243
- health: 80 + randInt(0,40), lastMelee:0, meleeRate:800 + randInt(-200,200),
244
- roamTimer: rand(0,3), weaponPickup:null
245
  });
246
  }
247
  updatePlayerCount();
248
  }
249
 
250
- // HUD init (pickaxe left of inventory)
251
  function initHUD(){
252
- hudHealth.classList.remove('hidden');
253
- hudGearWrap.classList.remove('hidden');
254
- hudGear.innerHTML = '';
255
- for (let i=0;i<5;i++){
256
  const slot = document.createElement('div');
257
  slot.className = 'gear-slot';
258
  slot.dataset.index = i;
259
- slot.addEventListener('click', ()=> { player.selectedSlot = i; updateHUD(); });
260
- hudGear.appendChild(slot);
261
  }
262
- // pickaxe click toggles equip
263
- pickaxeSlot.onclick = () => { player.equippedIndex = (player.equippedIndex === -1) ? player.selectedSlot : -1; updateHUD(); };
264
  updateHUD();
265
  }
266
-
267
  function updateHUD(){
268
  hudHealthText.textContent = `${Math.max(0,Math.floor(player.health))}%`;
269
- const slots = hudGear.querySelectorAll('.gear-slot');
270
  slots.forEach(s => {
271
  const idx = parseInt(s.dataset.index);
272
- const it = player.inventory[idx];
273
- s.classList.toggle('selected', idx === player.selectedSlot);
274
- s.classList.toggle('equipped', player.equippedIndex === idx);
275
- if (!it) s.innerHTML = 'Empty';
276
- else if (it.type === 'weapon') s.innerHTML = `<div style="text-align:center;"><div style="font-size:11px">${it.weapon.name}</div><div style="color:${it.weapon.color}">${it.ammoInMag}/${it.ammoReserve}</div></div>`;
277
- else if (it.type === 'medkit') s.innerHTML = `<div style="font-size:12px">Med</div><div class="medkit-count">x${it.amount}</div>`;
278
- else if (it.type === 'materials') s.innerHTML = `<div style="font-size:11px">Mat</div><div class="medkit-count">x${it.amount}</div>`;
279
- else s.innerHTML = 'Item';
280
  });
281
- // pickaxe highlight when equipped
282
- pickaxeSlot.classList.toggle('selected', player.equippedIndex === -1);
283
- // also indicate pickaxe equipped visually
284
- pickaxeSlot.title = (player.equippedIndex === -1) ? 'Pickaxe (equipped)' : 'Pickaxe (click or press F to equip)';
285
  feather.replace();
286
  }
287
 
288
- // Camera
289
  function cameraUpdate(){
290
  if (!canvas.width || !canvas.height) return;
291
- camera.x = player.x - canvas.width/2;
292
- camera.y = player.y - canvas.height/2;
293
- camera.x = Math.max(0, Math.min(camera.x, WORLD.width - canvas.width));
294
- camera.y = Math.max(0, Math.min(camera.y, WORLD.height - canvas.height));
 
 
 
 
 
 
295
  }
296
  function worldToScreen(wx,wy){ return { x: Math.round(wx - camera.x), y: Math.round(wy - camera.y) }; }
297
 
298
- // Shooting, reload, melee
299
- function shootBullet(originX, originY, targetX, targetY, weaponObj, shooterId){
300
- if (!weaponObj || weaponObj.ammoInMag <= 0) return false;
301
  const speed = 1100;
302
  const angle = Math.atan2(targetY-originY, targetX-originX);
303
- weaponObj.ammoInMag -= 1;
304
  bullets.push({
305
  x: originX, y: originY, vx: Math.cos(angle)*speed, vy: Math.sin(angle)*speed,
306
- dmg: weaponObj.weapon.dmg, color: weaponObj.weapon.color, life:1.6, traveled:0,
307
  shooter: shooterId, born: performance.now(), tracer:false
308
  });
309
- return true;
310
- }
311
-
312
- function reloadEquipped(){
313
- let item = null;
314
- if (player.equippedIndex >= 0) item = player.inventory[player.equippedIndex];
315
- else item = player.inventory[player.selectedSlot];
316
- if (!item || item.type !== 'weapon') return;
317
- const need = item.weapon.magSize - item.ammoInMag;
318
- if (need <= 0 || item.ammoReserve <= 0) return;
319
- const take = Math.min(need, item.ammoReserve);
320
- item.ammoInMag += take;
321
- item.ammoReserve -= take;
322
- updateHUD();
323
- }
324
-
325
- function playerMeleeHit(){
326
- const now = performance.now();
327
- if (now - player.lastMelee < 350) return;
328
- player.lastMelee = now;
329
- // damage enemies and objects (pickaxe)
330
- for (const e of enemies){
331
- if (e.health <= 0) continue;
332
- const d = Math.hypot(e.x - player.x, e.y - player.y);
333
- if (d < 36){ e.health -= 18; if (e.health <= 0) { e.health = 0; player.kills++; player.materials += 2; updatePlayerCount(); } }
334
- }
335
- for (const obj of objects){
336
- if (obj.dead) continue;
337
- const d = Math.hypot(obj.x - player.x, obj.y - player.y);
338
- if (d < 36){
339
- obj.hp -= 20;
340
- if (obj.hp <= 0){ obj.dead = true; player.materials += (obj.type === 'wood' ? 3 : 6); }
341
- }
342
- }
343
  }
344
 
345
- // Interact: use medkit in selected slot OR loot chests / harvest (pickups)
346
  function interactNearby(){
347
- // use medkit in selected slot
348
- const sel = player.selectedSlot;
349
- const selItem = player.inventory[sel];
350
- if (selItem && selItem.type === 'medkit'){
351
- selItem.amount -= 1;
352
- player.health = Math.min(100, player.health + 50);
353
- if (selItem.amount <= 0) player.inventory[sel] = null;
354
- updateHUD();
355
- return;
356
- }
357
-
358
  const range = 56;
359
- // chests
360
  for (const chest of chests){
361
  if (chest.opened) continue;
362
  const d = Math.hypot(chest.x - player.x, chest.y - player.y);
363
  if (d < range){
364
  chest.opened = true;
365
- // spawn pickup on ground (weapon/medkit/materials/ammo)
366
  const loot = chest.loot;
367
- const px = chest.x + rand(-20,20), py = chest.y + rand(-20,20);
368
- if (loot.type === 'weapon'){
369
- pickups.push({ x:px, y:py, type:'weapon', weapon: makeWeaponProto(loot.weapon), ammoInMag: loot.weapon.magSize || 12, ammoReserve: loot.weapon.startReserve || (loot.weapon.magSize*2 || 24) });
370
- } else if (loot.type === 'medkit') {
371
- pickups.push({ x:px, y:py, type:'medkit', amount: loot.amount || 1 });
372
- } else if (loot.type === 'materials'){
373
- pickups.push({ x:px, y:py, type:'materials', amount: loot.amount || 5 });
374
- }
375
- // some chests drop ammo too sometimes
376
- if (Math.random() < 0.25){
377
- pickups.push({ x:px+8, y:py+8, type:'ammo', forWeapon: null, amount: randInt(6,30) });
378
  }
 
 
379
  updateHUD();
380
  return;
381
  }
382
  }
383
-
384
- // harvest objects (only via pickaxe)
385
  for (const obj of objects){
386
  if (obj.dead) continue;
387
  const d = Math.hypot(obj.x - player.x, obj.y - player.y);
388
  if (d < range){
389
- // only harvest (break) if using pickaxe (equippedIndex === -1) AND clicking E triggers same as swing
390
- if (player.equippedIndex === -1){
391
- const gain = obj.type === 'wood' ? 2 : 5;
392
- player.materials += gain;
393
- obj.dead = true;
394
- updateHUD();
395
- return;
396
- }
397
- }
398
- }
399
-
400
- // pick up ground pickups if near
401
- for (let i=pickups.length-1;i>=0;i--){
402
- const p = pickups[i];
403
- const d = Math.hypot(p.x - player.x, p.y - player.y);
404
- if (d < range){
405
- pickupCollect(p);
406
- pickups.splice(i,1);
407
  updateHUD();
408
  return;
409
  }
410
  }
411
  }
412
 
413
- function pickupCollect(p){
414
- if (p.type === 'weapon'){
415
- // place in first empty or merge ammo
416
- let merged=false;
417
- for (let s=0;s<5;s++){
418
- const it = player.inventory[s];
419
- if (it && it.type==='weapon' && it.weapon.name === p.weapon.name){
420
- it.ammoReserve += p.ammoReserve;
421
- it.ammoInMag = Math.min(it.weapon.magSize, it.ammoInMag + p.ammoInMag);
422
- merged=true; break;
423
- }
424
- }
425
- if (!merged){
426
- let placed=false;
427
- for (let s=0;s<5;s++){ if (!player.inventory[s]) { player.inventory[s] = { type:'weapon', weapon:p.weapon, ammoInMag:p.ammoInMag, ammoReserve:p.ammoReserve }; placed=true; break; } }
428
- if (!placed) player.inventory[player.selectedSlot] = { type:'weapon', weapon:p.weapon, ammoInMag:p.ammoInMag, ammoReserve:p.ammoReserve };
429
- }
430
- } else if (p.type === 'medkit'){
431
- let stacked=false;
432
- for (let s=0;s<5;s++){ const it=player.inventory[s]; if (it && it.type==='medkit'){ it.amount += p.amount; stacked=true; break; } }
433
- if (!stacked){ let placed=false; for (let s=0;s<5;s++){ if (!player.inventory[s]) { player.inventory[s] = { type:'medkit', amount:p.amount }; placed=true; break; } } if (!placed) player.materials += 5; }
434
- } else if (p.type === 'materials'){
435
- player.materials += p.amount;
436
- } else if (p.type === 'ammo'){
437
- // add ammo to any weapon or to materials as fallback
438
- let added=false;
439
- for (let s=0;s<5;s++){ const it=player.inventory[s]; if (it && it.type==='weapon'){ it.ammoReserve += p.amount; added=true; break; } }
440
- if (!added) player.materials += p.amount;
441
- }
442
- }
443
-
444
- // Build Q
445
  function tryBuild(){
446
  if (player.materials < 10) return;
447
  player.materials -= 10;
448
- const bx = player.x + Math.cos(player.angle) * 48;
449
- const by = player.y + Math.sin(player.angle) * 48;
450
  objects.push({ x:bx, y:by, type:'wall', hp:160, maxHp:160, dead:false });
451
  updateHUD();
452
  }
453
 
454
- // LOS helper
455
  function hasLineOfSight(x1,y1,x2,y2){
 
456
  const vx = x2 - x1, vy = y2 - y1;
457
  const vlen2 = vx*vx + vy*vy;
458
  for (const obj of objects){
459
  if (obj.dead) continue;
460
- let br=0;
461
- if (obj.type==='wall') br=28; else if (obj.type==='stone') br=22; else if (obj.type==='wood') br=16; else continue;
 
 
 
 
462
  const wx = obj.x - x1, wy = obj.y - y1;
463
  const c1 = vx*wx + vy*wy;
464
- const t = vlen2>0 ? c1/vlen2 : 0;
465
  if (t < 0 || t > 1) continue;
466
  const projx = x1 + vx * t, projy = y1 + vy * t;
467
  const dist = Math.hypot(projx - obj.x, projy - obj.y);
468
- if (dist < br) return false;
469
  }
 
470
  return true;
471
  }
472
 
473
- // Bullets update: bullets do NOT break wood/stone (only walls)
474
  function bulletsUpdate(dt){
475
  for (let i=bullets.length-1;i>=0;i--){
476
  const b = bullets[i];
477
  b.x += b.vx * dt; b.y += b.vy * dt;
478
  b.traveled += Math.hypot(b.vx*dt, b.vy*dt);
479
  b.life -= dt;
480
- // hit player
 
 
481
  if (b.dmg > 0 && b.shooter !== 'player'){
482
- if (Math.hypot(player.x - b.x, player.y - b.y) < 16){
483
  player.health -= b.dmg;
484
  if (player.health <= 0){ player.health = 0; playerDeath(); }
485
  bullets.splice(i,1); continue;
486
  }
487
  }
488
- // hit enemies
 
489
  if (b.dmg > 0){
490
  for (const e of enemies){
491
  if (e.health <= 0) continue;
492
- if (b.shooter === e.id) continue;
493
- if (Math.hypot(e.x - b.x, e.y - b.y) < 14){
494
  e.health -= b.dmg;
495
- if (e.health <= 0){ e.health = 0; if (b.shooter === 'player'){ player.kills++; player.materials += 2; updatePlayerCount(); } }
 
 
 
 
 
496
  bullets.splice(i,1); break;
497
  }
498
  }
499
  if (!bullets[i]) continue;
500
  }
 
501
  // collision with objects & chests
502
  if (!b.tracer && b.dmg > 0){
503
  for (const obj of objects){
504
  if (obj.dead) continue;
505
- if (obj.type==='wall' && Math.hypot(obj.x - b.x, obj.y - b.y) < 18){
506
  obj.hp -= b.dmg;
507
- if (obj.hp <= 0 && !obj.dead){ obj.dead = true; if (b.shooter === 'player') player.materials += 6; }
 
 
 
508
  bullets.splice(i,1); break;
509
  }
510
  }
@@ -512,35 +428,34 @@
512
  for (const chest of chests){
513
  if (!chest.opened && Math.hypot(chest.x - b.x, chest.y - b.y) < 18){
514
  chest.opened = true;
515
- // spawn pickup from chest
516
- const loot = chest.loot;
517
- const px = chest.x + rand(-20,20), py = chest.y + rand(-20,20);
518
- if (loot.type === 'weapon') pickups.push({ x:px, y:py, type:'weapon', weapon: makeWeaponProto(loot.weapon), ammoInMag: loot.weapon.magSize || 12, ammoReserve: loot.weapon.startReserve || (loot.weapon.magSize*2 || 24) });
519
- else if (loot.type === 'medkit') pickups.push({ x:px, y:py, type:'medkit', amount: loot.amount || 1 });
520
- else if (loot.type === 'materials') pickups.push({ x:px, y:py, type:'materials', amount: loot.amount || 5 });
521
  bullets.splice(i,1); break;
522
  }
523
  }
524
  if (!bullets[i]) continue;
525
  }
526
- if (b.life <= 0 || b.traveled > 2600) bullets.splice(i,1);
 
527
  }
528
  }
529
 
530
- // Enemies: can loot chests; shooting only when within 30px and LOS; they can pick up weapons and use them.
531
  function updateEnemies(dt, now){
532
  for (const e of enemies){
533
  if (e.health <= 0) continue;
534
  e.roamTimer -= dt;
535
- // choose target (player or nearby enemy)
 
536
  let target = player;
537
  let bestDist = Math.hypot(player.x - e.x, player.y - e.y);
 
538
  for (const other of enemies){
539
  if (other === e || other.health <= 0) continue;
540
  const d = Math.hypot(other.x - e.x, other.y - e.y);
541
- if (d < bestDist && Math.random() < 0.6){ bestDist = d; target = other; }
 
542
  }
543
- // move
 
544
  if (bestDist < 900 || e.roamTimer <= 0){
545
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
546
  const avoid = e.health < 20 && Math.random() < 0.6;
@@ -548,133 +463,204 @@
548
  e.x += Math.cos(e.angle) * e.speed * dt * moveDir;
549
  e.y += Math.sin(e.angle) * e.speed * dt * moveDir;
550
  } else {
 
551
  e.x += Math.cos(e.angle) * e.speed * dt * 0.25;
552
  e.y += Math.sin(e.angle) * e.speed * dt * 0.25;
553
  if (Math.random() < 0.01) e.angle += (Math.random()-0.5)*2;
554
  }
 
 
555
  e.x = Math.max(12, Math.min(WORLD.width-12, e.x));
556
  e.y = Math.max(12, Math.min(WORLD.height-12, e.y));
557
 
558
- // enemy loot chests; no change
559
- for (const chest of chests){
560
- if (chest.opened) continue;
561
- const d = Math.hypot(chest.x - e.x, chest.y - e.y);
562
- if (d < 24){
563
- chest.opened = true;
564
- const loot = chest.loot;
565
- const px = chest.x + rand(-12,12), py = chest.y + rand(-12,12);
566
- if (loot.type === 'weapon') pickups.push({ x:px, y:py, type:'weapon', weapon: makeWeaponProto(loot.weapon), ammoInMag: loot.weapon.magSize || 12, ammoReserve: loot.weapon.startReserve || (loot.weapon.magSize*2 || 24) });
567
- else if (loot.type === 'medkit') pickups.push({ x:px, y:py, type:'medkit', amount: loot.amount || 1 });
568
- else if (loot.type === 'materials') pickups.push({ x:px, y:py, type:'materials', amount: loot.amount || 5 });
569
  }
570
  }
 
 
571
 
572
- // enemies pick up nearby pickups automatically
573
- for (let i=pickups.length-1;i>=0;i--){
574
- const p = pickups[i];
575
- if (Math.hypot(p.x - e.x, p.y - e.y) < 18){
576
- if (p.type === 'weapon' && !e.weaponPickup){
577
- e.weaponPickup = { weapon:p.weapon, ammoInMag:p.ammoInMag, ammoReserve:p.ammoReserve };
578
- pickups.splice(i,1);
579
- } else if (p.type === 'medkit'){
580
- if (e.health < 60) e.health = Math.min(120, e.health + 50);
581
- pickups.splice(i,1);
582
- } else pickups.splice(i,1);
583
- }
 
 
 
 
 
 
 
 
 
 
 
584
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
585
 
586
- // attack: melee if close; ranged if within 30px AND LOS or within 50px (ignore LOS)
587
- const distToTarget = Math.hypot(target.x - e.x, target.y - e.y);
588
- // Determine if enemy can see player within 50 pixels or LOS
589
- const canSeePlayer = distToTarget < 50 || hasLineOfSight(e.x, e.y, target.x, target.y);
590
- if (distToTarget < 34 && now - e.lastMelee > e.meleeRate){
591
- e.lastMelee = now;
592
- const dmg = 10 + randInt(0,8);
593
- if (target === player){
594
- player.health -= dmg;
595
- if (player.health <= 0) { player.health = 0; playerDeath(); }
596
- } else {
597
- target.health -= dmg;
598
- if (target.health <= 0) target.health = 0;
599
- }
600
- } else if (e.weaponPickup && distToTarget <= 30 && now - (e.lastShot||0) > (e.weaponPickup.weapon.rate || 300)){
601
- // Shoot if within 30 but only if has ammo
602
- if (hasLineOfSight(e.x, e.y, target.x, target.y) && e.weaponPickup.ammoInMag > 0){
603
- e.lastShot = now;
604
- shootBullet(e.x + Math.cos(e.angle)*12, e.y + Math.sin(e.angle)*12, target.x, target.y, e.weaponPickup, e.id);
605
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
606
  }
 
607
  }
608
- // After updating enemies, check if all are dead
609
- checkWinCondition();
610
  }
611
 
612
- function checkWinCondition() {
613
- const aliveEnemies = enemies.filter(e => e.health > 0);
614
- if (aliveEnemies.length === 0){
615
- alert('You Win! Returning to home screen.');
616
- endGame();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
617
  }
618
  }
619
 
620
- // Storm: slower, damages enemies too
621
- const storm = { maxRadius: 2400, radius:2400, centerX: WORLD.width/2, centerY: WORLD.height/2, damagePerSecond:1, closingSpeed: 0.6, active:false };
 
 
 
 
 
 
 
622
  let stormDamageAccumulator = 0;
623
- function playerInStorm(){ return Math.hypot(player.x - storm.centerX, player.y - storm.centerY) > storm.radius; }
 
 
624
  function updateStorm(dt){
625
  if (!storm.active) return;
626
- storm.radius -= storm.closingSpeed * dt * 60;
627
  if (storm.radius < 120) storm.radius = 120;
628
- // damage player
629
  if (playerInStorm()){
630
- const prog = 1 - storm.radius / storm.maxRadius;
631
- const rate = storm.damagePerSecond * (1 + prog*4);
632
  stormDamageAccumulator += rate * dt;
633
  while (stormDamageAccumulator >= 1){
634
- stormDamageAccumulator -=1;
635
  player.health -= 1;
636
  if (player.health <= 0){ player.health = 0; playerDeath(); }
637
  }
638
  } else stormDamageAccumulator = 0;
639
- // damage enemies
640
- for (const e of enemies){
641
- if (e.health <= 0) continue;
642
- const d = Math.hypot(e.x - storm.centerX, e.y - storm.centerY);
643
- if (d > storm.radius){
644
- e.health -= 8 * dt;
645
- if (e.health <= 0){ e.health = 0; updatePlayerCount(); }
646
- }
647
- }
648
  }
649
 
650
- // Utility
651
- function updatePlayerCount(){
652
- const alive = enemies.filter(e => e.health > 0).length;
653
- document.getElementById('playerCount').textContent = `${alive}/20`;
654
- }
655
-
656
- // Drawing functions (unchanged, omitted here for brevity, assume they are same as before, unchanged)
657
-
658
- function drawWorld(){ /*...*/ }
659
- function drawObjects(){ /*...*/ }
660
- function drawChests(){ /*...*/ }
661
- function drawPickups(){ /*...*/ }
662
- function drawEnemies(){ /*...*/ }
663
- function drawBullets(){ /*...*/ }
664
- function drawPlayer(){ /*...*/ }
665
- function drawCrosshair(){ /*...*/ }
666
-
667
- // Main loop
668
  let lastTime = 0;
669
  function gameLoop(ts){
670
  if (!gameActive) return;
671
  if (!lastTime) lastTime = ts;
672
- const dt = Math.min(0.05, (ts - lastTime)/1000);
673
  lastTime = ts;
674
 
675
- // movement
676
  let dx=0, dy=0;
677
- if (keys.w) dy -= 1; if (keys.s) dy += 1; if (keys.a) dx -= 1; if (keys.d) dx += 1;
 
 
 
678
  if (dx !== 0 || dy !== 0){
679
  const len = Math.hypot(dx,dy) || 1;
680
  player.x += (dx/len) * player.speed * dt;
@@ -683,92 +669,81 @@
683
  player.x = Math.max(16, Math.min(WORLD.width-16, player.x));
684
  player.y = Math.max(16, Math.min(WORLD.height-16, player.y));
685
 
 
686
  cameraUpdate();
687
  mouse.worldX = mouse.canvasX + camera.x;
688
  mouse.worldY = mouse.canvasY + camera.y;
689
 
690
- // Determine active weapon
691
- let activeWeaponItem = null;
692
- if (player.equippedIndex >= 0) activeWeaponItem = player.inventory[player.equippedIndex];
693
- else {
694
- const selected = player.inventory[player.selectedSlot];
695
- if (selected && selected.type === 'weapon') activeWeaponItem = selected;
696
- }
697
-
698
- // Shooting or melee
699
  if (mouse.down){
700
- if (player.equippedIndex === -1){
701
- playerMeleeHit();
702
- } else if (activeWeaponItem && activeWeaponItem.type === 'weapon'){
703
- const now = performance.now();
704
- if (now - player.lastShot > (activeWeaponItem.weapon.rate || 300)){
705
- if (activeWeaponItem.ammoInMag > 0){
706
- player.lastShot = now;
707
- shootBullet(player.x + Math.cos(player.angle)*18, player.y + Math.sin(player.angle)*18, mouse.worldX, mouse.worldY, activeWeaponItem, 'player');
708
- updateHUD();
709
- }
710
- }
711
  }
712
  }
713
 
714
- // reload
715
- if (keys.r) { reloadEquipped(); keys.r = false; }
716
-
717
- // interact
718
  if (keys.e){ interactNearby(); keys.e = false; }
719
-
720
- // build
721
  if (keys.q){ tryBuild(); keys.q = false; }
722
 
723
- // update enemies
724
  updateEnemies(dt, performance.now());
725
- bulletsUpdate(dt);
726
 
727
- // auto-pickup
728
- for (let i=pickups.length-1;i>=0;i--){
729
- const p = pickups[i];
730
- if (Math.hypot(p.x - player.x, p.y - player.y) < 18){ pickupCollect(p); pickups.splice(i,1); updateHUD(); }
731
- }
732
 
733
- // clean dead objects
734
  for (let i=objects.length-1;i>=0;i--) if (objects[i].dead) objects.splice(i,1);
735
 
736
  updatePlayerCount();
 
 
737
  updateStorm(dt);
738
 
739
  // render
740
  ctx.clearRect(0,0,canvas.width,canvas.height);
741
- drawWorld(); drawObjects(); drawChests(); drawPickups(); drawEnemies(); drawBullets(); drawPlayer(); drawCrosshair();
 
 
 
 
 
 
 
 
 
742
 
743
  // storm warning
744
- if (storm.active && playerInStorm()) stormWarning.classList.remove('hidden'); else stormWarning.classList.add('hidden');
745
 
746
  requestAnimationFrame(gameLoop);
747
  }
748
 
749
- // Start/end
750
  let timerInterval = null;
751
  let gameTime = 300;
752
- let gameActive = false;
753
-
754
  function startGame(){
755
  gameActive = true;
756
- landingScreen.classList.add('hidden');
757
- gameScreen.classList.remove('hidden');
758
  resizeCanvas();
759
 
 
760
  player.x = WORLD.width/2 + (Math.random()-0.5)*400;
761
  player.y = WORLD.height/2 + (Math.random()-0.5)*400;
762
  player.health = 100; player.armor = 0; player.kills = 0; player.materials = 0;
763
- player.inventory = [null,null,null,null,null];
764
- player.selectedSlot = 0; player.equippedIndex = -1; player.lastShot = 0; player.lastMelee = 0;
765
 
766
  populateWorld();
767
  initHUD();
768
  cameraUpdate();
769
 
770
- // timer & storm
771
- gameTime = 300; storm.active = false; storm.radius = storm.maxRadius;
 
 
772
  document.getElementById('gameTimer').textContent = '5:00';
773
  timerInterval && clearInterval(timerInterval);
774
  timerInterval = setInterval(()=>{
@@ -776,13 +751,14 @@
776
  gameTime--;
777
  const m = Math.floor(gameTime/60), s = gameTime%60;
778
  document.getElementById('gameTimer').textContent = `${m}:${s<10?'0'+s:s}`;
 
779
  if (gameTime === 240 && !storm.active){
780
  storm.active = true;
781
  storm.centerX = rand(400, WORLD.width-400);
782
  storm.centerY = rand(400, WORLD.height-400);
783
  storm.radius = storm.maxRadius;
784
- stormWarning.classList.remove('hidden');
785
- setTimeout(()=>stormWarning.classList.add('hidden'),4000);
786
  }
787
  if (gameTime <= 0){ clearInterval(timerInterval); endGame(); }
788
  }, 1000);
@@ -791,40 +767,40 @@
791
  requestAnimationFrame(gameLoop);
792
  }
793
 
794
- function endGame(){
795
- gameActive = false;
796
- alert('Match over!');
797
  }
798
 
799
- function playerDeath(){
800
- gameActive = false;
801
- deathScreen.classList.remove('hidden');
802
  }
803
 
804
  document.getElementById('respawnBtn').addEventListener('click', ()=>{
805
- deathScreen.classList.add('hidden');
806
- landingScreen.classList.remove('hidden');
807
  });
808
 
809
- document.querySelectorAll('.biome-selector').forEach(el => el.addEventListener('click', ()=> startGame()));
 
 
 
 
 
 
 
 
810
  window.addEventListener('keydown', (e)=>{
811
- const k = e.key.toLowerCase();
812
- if (!gameActive) return;
813
- if (k==='e') keys.e = true;
814
- if (k==='q') keys.q = true;
815
- if (k==='f') {
816
- // Toggle equip/unequip
817
- player.equippedIndex = (player.equippedIndex === -1) ? player.selectedSlot : -1;
818
- updateHUD();
819
- }
820
- if (k==='r') keys.r = true;
821
  });
822
 
823
- // init
824
  resizeCanvas();
825
  populateWorld();
826
  feather.replace();
827
-
828
  </script>
829
  </body>
830
  </html>
 
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>BattleZone Royale - Updated</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <script src="https://unpkg.com/feather-icons"></script>
9
  <style>
10
  html,body { height:100%; margin:0; background:#0b1220; color:#fff; font-family:monospace; }
11
  #canvasContainer { position:relative; flex:1; display:flex; justify-content:center; align-items:center; height:100vh; overflow:hidden; }
12
  #gameCanvas { display:block; user-select:none; cursor:crosshair; box-shadow:0 0 20px rgba(0,0,0,.5); width:100%; height:100%; }
13
+ .biome-selector { transition:all .25s ease; }
14
+ .biome-selector:hover { transform:scale(1.03); box-shadow:0 0 10px rgba(255,215,0,.15); }
15
  #stormWarning { z-index:10; }
16
  #deathScreen { z-index:20; }
17
 
18
+ /* HUD positions - small */
19
  #hudHealth { position:absolute; left:12px; bottom:12px; background:rgba(0,0,0,0.55); padding:6px 8px; border-radius:8px; font-weight:700; font-size:13px; display:flex; align-items:center; gap:8px; z-index:30; }
20
+ #hudGear { position:absolute; right:12px; bottom:12px; background:rgba(0,0,0,0.0); padding:6px; border-radius:8px; z-index:30; display:flex; gap:6px; }
21
+ .gear-slot { width:44px; height:36px; background: rgba(255,255,255,0.03); border-radius:6px; display:flex; align-items:center; justify-content:center; font-size:11px; color:#ddd; }
22
+ /* Chest glow pulse */
23
+ @keyframes chestGlow {
24
+ 0% { box-shadow: 0 0 0px rgba(255,215,0,0.0); }
25
+ 50% { box-shadow: 0 0 18px rgba(255,215,0,0.35); }
26
+ 100% { box-shadow: 0 0 0px rgba(255,215,0,0.0); }
27
+ }
28
  </style>
29
  </head>
30
  <body>
31
+ <!-- Landing / Instructions screen -->
32
  <div id="landingScreen" class="flex flex-col items-center justify-center h-screen p-6">
33
  <h1 class="text-5xl font-bold text-yellow-400 mb-6 flex items-center">
34
  <i data-feather="target" class="mr-2"></i> BattleZone Royale
 
58
  </div>
59
  </div>
60
 
61
+ <!-- Instructions -->
62
  <div class="bg-gray-800 p-4 rounded-lg max-w-xl w-full text-sm">
63
  <h3 class="font-bold text-yellow-400 mb-2">Controls</h3>
64
  <ul class="list-disc pl-5 space-y-1">
65
  <li><strong>WASD</strong> — Move</li>
66
+ <li><strong>Mouse</strong> — Aim & Shoot</li>
67
+ <li><strong>E</strong> — Interact / Loot / Harvest</li>
 
68
  <li><strong>Q</strong> — Build (costs 10 materials)</li>
69
+ <li><strong>1-5</strong> — Switch weapons</li>
 
 
70
  </ul>
71
  </div>
72
  </div>
73
 
74
+ <!-- Game screen -->
75
  <div id="gameScreen" class="hidden h-screen flex flex-col">
76
  <header class="flex justify-between items-center border-b border-yellow-500 px-6 py-4" style="flex-shrink:0;">
77
  <h1 class="text-3xl font-bold text-yellow-400 flex items-center"><i data-feather="target" class="mr-2"></i>BattleZone Royale</h1>
 
100
  </button>
101
  </div>
102
 
103
+ <!-- Small health at bottom-left -->
104
  <div id="hudHealth" class="hidden">
105
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#ffd86b" stroke-width="1.6"><path d="M20.8 8.6a5.5 5.5 0 0 0-7.8 0L12 10.6l-1-1a5.5 5.5 0 0 0-7.8 7.8l1 1L12 22l7.8-3.6 1-1a5.5 5.5 0 0 0 0-7.8z"></path></svg>
106
  <span id="hudHealthText">100%</span>
107
  </div>
108
 
109
+ <!-- Gear / small inventory at bottom-right -->
110
+ <div id="hudGear" class="hidden" aria-label="gear"></div>
 
 
111
  </div>
112
+
113
+ <!-- NOTE: Sidebar removed per request -->
114
  </div>
115
  </div>
116
 
117
  <script>
118
+ // --- Globals & DOM ---
119
+ let gameActive = false;
120
+ let selectedBiome = 'forest';
121
+ const landingScreenEl = document.getElementById('landingScreen');
122
+ const gameScreenEl = document.getElementById('gameScreen');
123
  const canvas = document.getElementById('gameCanvas');
124
  const ctx = canvas.getContext('2d');
125
+ const hudHealthEl = document.getElementById('hudHealth');
126
  const hudHealthText = document.getElementById('hudHealthText');
127
+ const hudGearEl = document.getElementById('hudGear');
128
+ const stormWarningEl = document.getElementById('stormWarning');
129
+ const deathScreenEl = document.getElementById('deathScreen');
130
+
131
+ // --- Make world bigger per request ---
132
+ const WORLD = { width: 6000, height: 4000 }; // enlarged world
 
 
133
  let camera = { x:0, y:0 };
134
 
135
  function resizeCanvas(){
136
+ const container = document.getElementById('canvasContainer');
137
+ canvas.width = Math.max(600, Math.floor(container.clientWidth));
138
+ canvas.height = Math.max(400, Math.floor(container.clientHeight));
139
  cameraUpdate();
140
  }
141
  window.addEventListener('resize', resizeCanvas);
142
 
143
+ // --- Player ---
144
  const player = {
145
+ id: 'player', x: WORLD.width/2, y: WORLD.height/2, radius: 16, angle:0, speed:220,
146
+ health:100, armor:0, kills:0, materials:0, weapons:[], currentWeaponIndex:0, lastShot:0
 
 
 
 
147
  };
148
 
149
+ // --- Input ---
150
+ const keys = { w:false,a:false,s:false,d:false,e:false,q:false };
151
  const mouse = { canvasX:0, canvasY:0, worldX:0, worldY:0, down:false };
152
 
153
+ window.addEventListener('keydown', (e)=>{
154
  const k = e.key.toLowerCase();
155
+ if (!gameActive) return;
156
  if (k in keys) keys[k] = true;
157
+ if (['1','2','3','4','5'].includes(k)){
158
+ const idx = parseInt(k)-1;
159
+ if (player.weapons[idx]) player.currentWeaponIndex = idx;
 
 
160
  }
 
161
  });
162
+ window.addEventListener('keyup', (e)=>{
163
  const k = e.key.toLowerCase();
164
+ if (!gameActive) return;
165
  if (k in keys) keys[k] = false;
166
+ if (k === 'e') keys.e = true;
167
  if (k === 'q') keys.q = true;
168
  });
169
 
170
+ canvas.addEventListener('mousemove', (e)=>{
171
  const rect = canvas.getBoundingClientRect();
172
  mouse.canvasX = e.clientX - rect.left;
173
  mouse.canvasY = e.clientY - rect.top;
 
175
  mouse.worldY = mouse.canvasY + camera.y;
176
  player.angle = Math.atan2(mouse.worldY - player.y, mouse.worldX - player.x);
177
  });
178
+ canvas.addEventListener('mousedown', ()=> { if (gameActive) mouse.down = true; });
179
  window.addEventListener('mouseup', ()=> mouse.down = false);
180
 
181
+ // --- Entities ---
182
  const bullets = [];
183
  const chests = [];
184
  const objects = []; // harvestables & walls
185
+ const enemies = []; // 19 NPCs
 
186
 
187
  function rand(min,max){ return Math.random()*(max-min)+min; }
188
  function randInt(min,max){ return Math.floor(Math.random()*(max-min))+min; }
189
 
190
  function biomeAt(x,y){
191
+ const bx = Math.floor(x / 400), by = Math.floor(y / 400);
192
  const seed = (bx*73856093) ^ (by*19349663);
193
  const r = Math.abs(Math.sin(seed)) % 1;
194
  if (r < 0.25) return 'desert';
 
197
  return 'ruins';
198
  }
199
 
 
 
 
 
 
200
  function generateLootForBiome(b){
201
  const roll = Math.random();
202
+ if (roll < 0.4) return { type:'medkit', heal:25 };
203
+ if (roll < 0.75) return { type:'materials', amount: randInt(5,20) };
204
  const weapons = [
205
+ { name:'Pistol', dmg:12, rate:320, color:'#ffd86b' },
206
+ { name:'SMG', dmg:6, rate:120, color:'#8ef0ff' },
207
+ { name:'Shotgun', dmg:22, rate:800, color:'#ff9fb8' },
208
+ { name:'Rifle', dmg:18, rate:400, color:'#c7ff9a' }
209
  ];
210
  return { type:'weapon', weapon: weapons[randInt(0, weapons.length)] };
211
  }
212
 
213
+ // Populate world with more content (scaled to bigger map)
214
  function populateWorld(){
215
+ chests.length = 0;
216
+ objects.length = 0;
217
+ enemies.length = 0;
218
+
219
+ // chests
220
+ for (let i=0;i<250;i++){
221
  const x = rand(150, WORLD.width-150);
222
  const y = rand(150, WORLD.height-150);
223
  chests.push({ x,y, opened:false, loot: generateLootForBiome(biomeAt(x,y)) });
224
  }
225
+
226
+ // breakable objects and some walls
227
  for (let i=0;i<700;i++){
228
  const t = Math.random();
229
  let type='wood';
 
233
  const hp = type==='wood'?40 : (type==='stone'?80:160);
234
  objects.push({ x,y, type, hp, maxHp:hp, dead:false });
235
  }
236
+
237
+ // enemies (19)
238
  for (let i=0;i<19;i++){
239
  const ex = rand(300, WORLD.width-300);
240
  const ey = rand(300, WORLD.height-300);
241
+ const weaponPool = [
242
+ { name:'Pistol', dmg:12, rate:320, color:'#ffd86b' },
243
+ { name:'SMG', dmg:6, rate:140, color:'#8ef0ff' },
244
+ { name:'Rifle', dmg:16, rate:380, color:'#c7ff9a' }
245
+ ];
246
  enemies.push({
247
+ id: 'e'+i, x:ex, y:ey, radius:14, angle:0, speed:120 + rand(-20,20),
248
+ health: 80 + randInt(0,40), lastShot:0, weapon: weaponPool[randInt(0,weaponPool.length)],
249
+ target: null, roamTimer: rand(0,3)
250
  });
251
  }
252
  updatePlayerCount();
253
  }
254
 
255
+ // HUD: small health + gear slots bottom corners
256
  function initHUD(){
257
+ hudHealthEl.classList.remove('hidden');
258
+ hudGearEl.classList.remove('hidden');
259
+ hudGearEl.innerHTML = '';
260
+ for (let i=0;i<6;i++){
261
  const slot = document.createElement('div');
262
  slot.className = 'gear-slot';
263
  slot.dataset.index = i;
264
+ slot.innerHTML = 'Empty';
265
+ hudGearEl.appendChild(slot);
266
  }
 
 
267
  updateHUD();
268
  }
 
269
  function updateHUD(){
270
  hudHealthText.textContent = `${Math.max(0,Math.floor(player.health))}%`;
271
+ const slots = hudGearEl.querySelectorAll('.gear-slot');
272
  slots.forEach(s => {
273
  const idx = parseInt(s.dataset.index);
274
+ const w = player.weapons[idx];
275
+ s.innerHTML = w ? `<div style="text-align:center;"><div style="font-size:11px">${w.name}</div><div style="color:${w.color}">●</div></div>` : 'Empty';
 
 
 
 
 
 
276
  });
 
 
 
 
277
  feather.replace();
278
  }
279
 
280
+ // Camera centering
281
  function cameraUpdate(){
282
  if (!canvas.width || !canvas.height) return;
283
+ if (WORLD.width <= canvas.width) camera.x = (WORLD.width - canvas.width)/2;
284
+ else {
285
+ camera.x = player.x - canvas.width/2;
286
+ camera.x = Math.max(0, Math.min(camera.x, WORLD.width - canvas.width));
287
+ }
288
+ if (WORLD.height <= canvas.height) camera.y = (WORLD.height - canvas.height)/2;
289
+ else {
290
+ camera.y = player.y - canvas.height/2;
291
+ camera.y = Math.max(0, Math.min(camera.y, WORLD.height - canvas.height));
292
+ }
293
  }
294
  function worldToScreen(wx,wy){ return { x: Math.round(wx - camera.x), y: Math.round(wy - camera.y) }; }
295
 
296
+ // Shooting bullets
297
+ function shootBullet(originX, originY, targetX, targetY, weapon, shooterId){
 
298
  const speed = 1100;
299
  const angle = Math.atan2(targetY-originY, targetX-originX);
 
300
  bullets.push({
301
  x: originX, y: originY, vx: Math.cos(angle)*speed, vy: Math.sin(angle)*speed,
302
+ dmg: weapon ? weapon.dmg : 8, color: weapon ? weapon.color : '#fff', life: 1.6, traveled:0,
303
  shooter: shooterId, born: performance.now(), tracer:false
304
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
  }
306
 
307
+ // Interaction: loot & harvest
308
  function interactNearby(){
 
 
 
 
 
 
 
 
 
 
 
309
  const range = 56;
 
310
  for (const chest of chests){
311
  if (chest.opened) continue;
312
  const d = Math.hypot(chest.x - player.x, chest.y - player.y);
313
  if (d < range){
314
  chest.opened = true;
 
315
  const loot = chest.loot;
316
+ if (loot.type === 'medkit') player.health = Math.min(100, player.health + loot.heal);
317
+ else if (loot.type === 'materials') player.materials += loot.amount;
318
+ else if (loot.type === 'weapon') {
319
+ let placed=false;
320
+ for (let i=0;i<6;i++){ if (!player.weapons[i]) { player.weapons[i] = loot.weapon; player.currentWeaponIndex = i; placed=true; break; } }
321
+ if (!placed) player.weapons[player.currentWeaponIndex] = loot.weapon;
 
 
 
 
 
322
  }
323
+ // glow pop tracers
324
+ for (let i=0;i<6;i++) bullets.push({ x:chest.x, y:chest.y, vx:(Math.random()-0.5)*200, vy:(Math.random()-0.5)*200, dmg:0, color:'#ffd86b', life:0.6, traveled:0, shooter:null, tracer:true, born:performance.now() });
325
  updateHUD();
326
  return;
327
  }
328
  }
 
 
329
  for (const obj of objects){
330
  if (obj.dead) continue;
331
  const d = Math.hypot(obj.x - player.x, obj.y - player.y);
332
  if (d < range){
333
+ const gain = obj.type === 'wood' ? 2 : 5;
334
+ player.materials += gain;
335
+ obj.dead = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
  updateHUD();
337
  return;
338
  }
339
  }
340
  }
341
 
342
+ // Build with Q: place wall in front if have >=10 materials
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  function tryBuild(){
344
  if (player.materials < 10) return;
345
  player.materials -= 10;
346
+ const bx = player.x + Math.cos(player.angle)*48;
347
+ const by = player.y + Math.sin(player.angle)*48;
348
  objects.push({ x:bx, y:by, type:'wall', hp:160, maxHp:160, dead:false });
349
  updateHUD();
350
  }
351
 
352
+ // Visibility / Line of Sight: returns true if unobstructed
353
  function hasLineOfSight(x1,y1,x2,y2){
354
+ // For each blocking object, treat as circle/box and check distance to segment
355
  const vx = x2 - x1, vy = y2 - y1;
356
  const vlen2 = vx*vx + vy*vy;
357
  for (const obj of objects){
358
  if (obj.dead) continue;
359
+ // consider blocking types: wall and stone primarily; wood also blocks a little
360
+ let blockRadius = 0;
361
+ if (obj.type === 'wall') blockRadius = 28;
362
+ else if (obj.type === 'stone') blockRadius = 22;
363
+ else if (obj.type === 'wood') blockRadius = 16;
364
+ else continue;
365
  const wx = obj.x - x1, wy = obj.y - y1;
366
  const c1 = vx*wx + vy*wy;
367
+ const t = vlen2 > 0 ? c1 / vlen2 : 0;
368
  if (t < 0 || t > 1) continue;
369
  const projx = x1 + vx * t, projy = y1 + vy * t;
370
  const dist = Math.hypot(projx - obj.x, projy - obj.y);
371
+ if (dist < blockRadius) return false;
372
  }
373
+ // world bounds not considered blocking
374
  return true;
375
  }
376
 
377
+ // Bullets update & collisions
378
  function bulletsUpdate(dt){
379
  for (let i=bullets.length-1;i>=0;i--){
380
  const b = bullets[i];
381
  b.x += b.vx * dt; b.y += b.vy * dt;
382
  b.traveled += Math.hypot(b.vx*dt, b.vy*dt);
383
  b.life -= dt;
384
+ const bornAgo = performance.now() - (b.born || performance.now());
385
+
386
+ // Bullet hit player
387
  if (b.dmg > 0 && b.shooter !== 'player'){
388
+ if (Math.hypot(player.x - b.x, player.y - b.y) < 16 && bornAgo > 30){
389
  player.health -= b.dmg;
390
  if (player.health <= 0){ player.health = 0; playerDeath(); }
391
  bullets.splice(i,1); continue;
392
  }
393
  }
394
+
395
+ // Bullet hits enemies
396
  if (b.dmg > 0){
397
  for (const e of enemies){
398
  if (e.health <= 0) continue;
399
+ if (b.shooter === e.id) continue; // don't hit shooter
400
+ if (Math.hypot(e.x - b.x, e.y - b.y) < 14 && bornAgo > 30){
401
  e.health -= b.dmg;
402
+ if (e.health <= 0){
403
+ e.health = 0;
404
+ if (b.shooter === 'player') player.kills++;
405
+ // small material reward for kills if player shot
406
+ if (b.shooter === 'player') player.materials += 2;
407
+ }
408
  bullets.splice(i,1); break;
409
  }
410
  }
411
  if (!bullets[i]) continue;
412
  }
413
+
414
  // collision with objects & chests
415
  if (!b.tracer && b.dmg > 0){
416
  for (const obj of objects){
417
  if (obj.dead) continue;
418
+ if (Math.hypot(obj.x - b.x, obj.y - b.y) < 18){
419
  obj.hp -= b.dmg;
420
+ if (obj.hp <= 0 && !obj.dead){
421
+ obj.dead = true;
422
+ if (b.shooter === 'player') player.materials += (obj.type==='wood'?3:6);
423
+ }
424
  bullets.splice(i,1); break;
425
  }
426
  }
 
428
  for (const chest of chests){
429
  if (!chest.opened && Math.hypot(chest.x - b.x, chest.y - b.y) < 18){
430
  chest.opened = true;
 
 
 
 
 
 
431
  bullets.splice(i,1); break;
432
  }
433
  }
434
  if (!bullets[i]) continue;
435
  }
436
+
437
+ if (b.life <= 0 || b.traveled > 2400) bullets.splice(i,1);
438
  }
439
  }
440
 
441
+ // Enemy AI: choose nearby visible targets; only shoot if target in visible range
442
  function updateEnemies(dt, now){
443
  for (const e of enemies){
444
  if (e.health <= 0) continue;
445
  e.roamTimer -= dt;
446
+
447
+ // choose limited set of potential targets: player and a few nearby enemies
448
  let target = player;
449
  let bestDist = Math.hypot(player.x - e.x, player.y - e.y);
450
+
451
  for (const other of enemies){
452
  if (other === e || other.health <= 0) continue;
453
  const d = Math.hypot(other.x - e.x, other.y - e.y);
454
+ // prefer closer opponents sometimes
455
+ if (d < bestDist && Math.random() < 0.65) { bestDist = d; target = other; }
456
  }
457
+
458
+ // movement: move toward target if reasonably close, else roam
459
  if (bestDist < 900 || e.roamTimer <= 0){
460
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
461
  const avoid = e.health < 20 && Math.random() < 0.6;
 
463
  e.x += Math.cos(e.angle) * e.speed * dt * moveDir;
464
  e.y += Math.sin(e.angle) * e.speed * dt * moveDir;
465
  } else {
466
+ // slow roam
467
  e.x += Math.cos(e.angle) * e.speed * dt * 0.25;
468
  e.y += Math.sin(e.angle) * e.speed * dt * 0.25;
469
  if (Math.random() < 0.01) e.angle += (Math.random()-0.5)*2;
470
  }
471
+
472
+ // clamp
473
  e.x = Math.max(12, Math.min(WORLD.width-12, e.x));
474
  e.y = Math.max(12, Math.min(WORLD.height-12, e.y));
475
 
476
+ // Shooting logic: only if within distance threshold and hasLineOfSight to the chosen target
477
+ const distToTarget = Math.hypot(target.x - e.x, target.y - e.y);
478
+ const shootRange = 650; // visible range threshold
479
+ if (distToTarget < shootRange && now - e.lastShot > e.weapon.rate){
480
+ // check line of sight (blocks by objects)
481
+ if (hasLineOfSight(e.x, e.y, target.x, target.y)){
482
+ e.lastShot = now;
483
+ // fire slightly lead/randomness
484
+ const aimX = target.x + (Math.random()-0.5)*8;
485
+ const aimY = target.y + (Math.random()-0.5)*8;
486
+ shootBullet(e.x + Math.cos(e.angle)*12, e.y + Math.sin(e.angle)*12, aimX, aimY, e.weapon, e.id);
487
  }
488
  }
489
+ }
490
+ }
491
 
492
+ // Utility: update player count
493
+ function updatePlayerCount(){
494
+ const aliveEnemies = enemies.filter(e => e.health > 0).length;
495
+ document.getElementById('playerCount').textContent = `${1 + aliveEnemies}/20`;
496
+ }
497
+
498
+ // Drawing functions (chests glow gold)
499
+ function drawWorld(){
500
+ const TILE = 600; // slightly larger tiles to reduce draw calls in big world
501
+ const cols = Math.ceil(WORLD.width / TILE);
502
+ const rows = Math.ceil(WORLD.height / TILE);
503
+ for (let by=0; by<rows; by++){
504
+ for (let bx=0; bx<cols; bx++){
505
+ const x = bx*TILE, y = by*TILE;
506
+ const b = biomeAt(x+1,y+1);
507
+ let color = '#203a2b';
508
+ if (b === 'desert') color = '#cbb78b';
509
+ else if (b === 'forest') color = '#16411f';
510
+ else if (b === 'oasis') color = '#274b52';
511
+ else if (b === 'ruins') color = '#4a3b3b';
512
+ const s = worldToScreen(x,y);
513
+ ctx.fillStyle = color; ctx.fillRect(s.x, s.y, TILE, TILE);
514
+ ctx.strokeStyle = 'rgba(0,0,0,0.08)'; ctx.strokeRect(s.x, s.y, TILE, TILE);
515
  }
516
+ }
517
+ // storm overlay (big shrinking circle)
518
+ if (storm.active){
519
+ const screenCenter = worldToScreen(storm.centerX, storm.centerY);
520
+ ctx.save();
521
+ // outer shading
522
+ const grad = ctx.createRadialGradient(screenCenter.x, screenCenter.y, storm.radius*0.15, screenCenter.x, screenCenter.y, storm.radius);
523
+ grad.addColorStop(0, 'rgba(100,149,237,0.02)'); grad.addColorStop(1, 'rgba(100,149,237,0.45)');
524
+ ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(screenCenter.x, screenCenter.y, storm.radius, 0, Math.PI*2); ctx.fill();
525
+ // visible circle outline
526
+ ctx.strokeStyle = 'rgba(255,200,80,0.9)';
527
+ ctx.lineWidth = 4;
528
+ ctx.beginPath();
529
+ ctx.arc(screenCenter.x, screenCenter.y, storm.radius, 0, Math.PI*2);
530
+ ctx.stroke();
531
+ ctx.restore();
532
+ }
533
+ }
534
 
535
+ function drawObjects(){
536
+ for (const obj of objects){
537
+ if (obj.dead) continue;
538
+ const s = worldToScreen(obj.x, obj.y);
539
+ ctx.save();
540
+ const h = obj.type === 'wood' ? 18 : (obj.type==='stone'?12:28);
541
+ // shadow
542
+ ctx.fillStyle = 'rgba(0,0,0,0.18)';
543
+ ctx.beginPath(); ctx.ellipse(s.x, s.y+8, 18, 8, 0,0,Math.PI*2); ctx.fill();
544
+ // body
545
+ ctx.fillStyle = obj.type === 'wood' ? '#6b3b1a' : (obj.type==='stone' ? '#6b6b6b' : '#8b5a32');
546
+ ctx.fillRect(s.x-12, s.y-h, 24, h);
547
+ ctx.restore();
548
+ }
549
+ }
550
+
551
+ function drawChests(){
552
+ const now = performance.now();
553
+ for (const chest of chests){
554
+ if (chest.opened) continue;
555
+ const s = worldToScreen(chest.x, chest.y);
556
+ ctx.save();
557
+ // glowing halo
558
+ const glowRadius = 30 + Math.sin(now/300 + chest.x*0.001)*6;
559
+ const g = ctx.createRadialGradient(s.x, s.y-6, 6, s.x, s.y-6, glowRadius);
560
+ g.addColorStop(0, 'rgba(255,215,0,0.95)');
561
+ g.addColorStop(0.6, 'rgba(255,215,0,0.25)');
562
+ g.addColorStop(1, 'rgba(255,215,0,0.00)');
563
+ ctx.fillStyle = g;
564
+ ctx.beginPath(); ctx.arc(s.x, s.y-6, glowRadius, 0, Math.PI*2); ctx.fill();
565
+
566
+ // chest base
567
+ ctx.fillStyle = '#a56b2a'; ctx.fillRect(s.x-18, s.y-12, 36, 20);
568
+ // lid
569
+ ctx.fillStyle = '#caa15e';
570
+ ctx.save();
571
+ ctx.translate(s.x, s.y-12);
572
+ ctx.fillRect(-18, -16, 36, 8);
573
+ ctx.restore();
574
+ ctx.restore();
575
+ }
576
+ }
577
+
578
+ function drawBullets(){
579
+ for (const b of bullets){
580
+ const s = worldToScreen(b.x, b.y);
581
+ ctx.save();
582
+ if (b.tracer){
583
+ ctx.fillStyle = b.color; ctx.globalAlpha = 0.9; ctx.beginPath(); ctx.arc(s.x, s.y, 3, 0, Math.PI*2); ctx.fill();
584
+ } else {
585
+ const backX = s.x - (b.vx * 0.02);
586
+ const backY = s.y - (b.vy * 0.02);
587
+ const grad = ctx.createLinearGradient(backX, backY, s.x, s.y);
588
+ grad.addColorStop(0, 'rgba(255,255,255,0)'); grad.addColorStop(0.7, b.color); grad.addColorStop(1, '#fff');
589
+ ctx.strokeStyle = grad; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(backX, backY); ctx.lineTo(s.x, s.y); ctx.stroke();
590
+ ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(s.x, s.y, 2.5, 0, Math.PI*2); ctx.fill();
591
  }
592
+ ctx.restore();
593
  }
 
 
594
  }
595
 
596
+ function drawPlayer(){
597
+ const s = worldToScreen(player.x, player.y);
598
+ ctx.save();
599
+ ctx.translate(s.x, s.y); ctx.rotate(player.angle);
600
+ ctx.fillStyle = 'rgba(0,0,0,0.25)'; ctx.beginPath(); ctx.ellipse(0, 14, 18, 8, 0,0,Math.PI*2); ctx.fill();
601
+ ctx.fillStyle = 'yellow'; ctx.beginPath(); ctx.moveTo(18,0); ctx.lineTo(-12,-10); ctx.lineTo(-12,10); ctx.closePath(); ctx.fill();
602
+ ctx.restore();
603
+ }
604
+
605
+ function drawEnemies(){
606
+ for (const e of enemies){
607
+ if (e.health <= 0) continue;
608
+ const s = worldToScreen(e.x, e.y);
609
+ ctx.save();
610
+ ctx.translate(s.x, s.y); ctx.rotate(e.angle);
611
+ ctx.fillStyle = 'rgba(0,0,0,0.18)'; ctx.beginPath(); ctx.ellipse(0, 12, 14, 6, 0,0,Math.PI*2); ctx.fill();
612
+ ctx.fillStyle = '#ff6b6b'; ctx.beginPath(); ctx.moveTo(12,0); ctx.lineTo(-10,-8); ctx.lineTo(-10,8); ctx.closePath(); ctx.fill();
613
+ // small health bar above
614
+ ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(-18, -22, 36, 6);
615
+ const hpPct = Math.max(0, e.health / 120);
616
+ ctx.fillStyle = '#ff6b6b'; ctx.fillRect(-18, -22, 36*hpPct, 6);
617
+ ctx.restore();
618
  }
619
  }
620
 
621
+ function drawCrosshair(){
622
+ const screenX = mouse.canvasX, screenY = mouse.canvasY;
623
+ ctx.save(); ctx.strokeStyle = 'yellow'; ctx.lineWidth = 2; ctx.beginPath();
624
+ ctx.moveTo(screenX-8, screenY); ctx.lineTo(screenX+8, screenY);
625
+ ctx.moveTo(screenX, screenY-8); ctx.lineTo(screenX, screenY+8); ctx.stroke(); ctx.restore();
626
+ }
627
+
628
+ // --- Storm mechanics: random center when activated ---
629
+ const storm = { maxRadius: 2200, radius: 2200, centerX: WORLD.width/2, centerY: WORLD.height/2, damagePerSecond:1, closingSpeed: 6, active:false };
630
  let stormDamageAccumulator = 0;
631
+ function playerInStorm(){
632
+ return Math.hypot(player.x - storm.centerX, player.y - storm.centerY) > storm.radius;
633
+ }
634
  function updateStorm(dt){
635
  if (!storm.active) return;
636
+ storm.radius -= storm.closingSpeed * dt * 60; // shrink noticeably
637
  if (storm.radius < 120) storm.radius = 120;
 
638
  if (playerInStorm()){
639
+ const progress = 1 - storm.radius / storm.maxRadius;
640
+ const rate = storm.damagePerSecond * (1 + progress*4);
641
  stormDamageAccumulator += rate * dt;
642
  while (stormDamageAccumulator >= 1){
643
+ stormDamageAccumulator -= 1;
644
  player.health -= 1;
645
  if (player.health <= 0){ player.health = 0; playerDeath(); }
646
  }
647
  } else stormDamageAccumulator = 0;
 
 
 
 
 
 
 
 
 
648
  }
649
 
650
+ // --- Main loop & game logic ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
651
  let lastTime = 0;
652
  function gameLoop(ts){
653
  if (!gameActive) return;
654
  if (!lastTime) lastTime = ts;
655
+ const dt = Math.min(0.05, (ts - lastTime) / 1000);
656
  lastTime = ts;
657
 
658
+ // Player movement
659
  let dx=0, dy=0;
660
+ if (keys.w) dy -= 1;
661
+ if (keys.s) dy += 1;
662
+ if (keys.a) dx -= 1;
663
+ if (keys.d) dx += 1;
664
  if (dx !== 0 || dy !== 0){
665
  const len = Math.hypot(dx,dy) || 1;
666
  player.x += (dx/len) * player.speed * dt;
 
669
  player.x = Math.max(16, Math.min(WORLD.width-16, player.x));
670
  player.y = Math.max(16, Math.min(WORLD.height-16, player.y));
671
 
672
+ // camera & mouse world mapping
673
  cameraUpdate();
674
  mouse.worldX = mouse.canvasX + camera.x;
675
  mouse.worldY = mouse.canvasY + camera.y;
676
 
677
+ // player shooting
 
 
 
 
 
 
 
 
678
  if (mouse.down){
679
+ const weapon = player.weapons[player.currentWeaponIndex] || { dmg:10, rate:300, color:'#fff' };
680
+ if (performance.now() - player.lastShot > (weapon.rate || 300)){
681
+ player.lastShot = performance.now();
682
+ shootBullet(player.x + Math.cos(player.angle)*18, player.y + Math.sin(player.angle)*18, mouse.worldX, mouse.worldY, weapon, 'player');
 
 
 
 
 
 
 
683
  }
684
  }
685
 
686
+ // interactions
 
 
 
687
  if (keys.e){ interactNearby(); keys.e = false; }
 
 
688
  if (keys.q){ tryBuild(); keys.q = false; }
689
 
690
+ // enemy AI
691
  updateEnemies(dt, performance.now());
 
692
 
693
+ // bullets & collisions
694
+ bulletsUpdate(dt);
 
 
 
695
 
696
+ // cleanup dead objects
697
  for (let i=objects.length-1;i>=0;i--) if (objects[i].dead) objects.splice(i,1);
698
 
699
  updatePlayerCount();
700
+
701
+ // storm
702
  updateStorm(dt);
703
 
704
  // render
705
  ctx.clearRect(0,0,canvas.width,canvas.height);
706
+ drawWorld();
707
+ drawObjects();
708
+ drawChests();
709
+ drawEnemies();
710
+ drawBullets();
711
+ drawPlayer();
712
+ drawCrosshair();
713
+
714
+ // HUD
715
+ updateHUD();
716
 
717
  // storm warning
718
+ if (storm.active && playerInStorm()) stormWarningEl.classList.remove('hidden'); else stormWarningEl.classList.add('hidden');
719
 
720
  requestAnimationFrame(gameLoop);
721
  }
722
 
723
+ // --- Game start / end ---
724
  let timerInterval = null;
725
  let gameTime = 300;
 
 
726
  function startGame(){
727
  gameActive = true;
728
+ landingScreenEl.classList.add('hidden');
729
+ gameScreenEl.classList.remove('hidden');
730
  resizeCanvas();
731
 
732
+ // reset player & world
733
  player.x = WORLD.width/2 + (Math.random()-0.5)*400;
734
  player.y = WORLD.height/2 + (Math.random()-0.5)*400;
735
  player.health = 100; player.armor = 0; player.kills = 0; player.materials = 0;
736
+ player.weapons = []; player.weapons[0] = { name:'Pistol', dmg:12, rate:320, color:'#ffd86b' };
737
+ player.currentWeaponIndex = 0;
738
 
739
  populateWorld();
740
  initHUD();
741
  cameraUpdate();
742
 
743
+ // timer + storm schedule: when storm activates, pick random center on map
744
+ gameTime = 300;
745
+ storm.active = false;
746
+ storm.radius = storm.maxRadius;
747
  document.getElementById('gameTimer').textContent = '5:00';
748
  timerInterval && clearInterval(timerInterval);
749
  timerInterval = setInterval(()=>{
 
751
  gameTime--;
752
  const m = Math.floor(gameTime/60), s = gameTime%60;
753
  document.getElementById('gameTimer').textContent = `${m}:${s<10?'0'+s:s}`;
754
+ // start storm at 4 minutes left: pick a random center inside world margin
755
  if (gameTime === 240 && !storm.active){
756
  storm.active = true;
757
  storm.centerX = rand(400, WORLD.width-400);
758
  storm.centerY = rand(400, WORLD.height-400);
759
  storm.radius = storm.maxRadius;
760
+ stormWarningEl.classList.remove('hidden');
761
+ setTimeout(()=>stormWarningEl.classList.add('hidden'),4000);
762
  }
763
  if (gameTime <= 0){ clearInterval(timerInterval); endGame(); }
764
  }, 1000);
 
767
  requestAnimationFrame(gameLoop);
768
  }
769
 
770
+ function endGame(){
771
+ gameActive = false;
772
+ alert('Match over!');
773
  }
774
 
775
+ function playerDeath(){
776
+ gameActive = false;
777
+ deathScreenEl.classList.remove('hidden');
778
  }
779
 
780
  document.getElementById('respawnBtn').addEventListener('click', ()=>{
781
+ deathScreenEl.classList.add('hidden');
782
+ landingScreenEl.classList.remove('hidden');
783
  });
784
 
785
+ // Biome selectors start the game
786
+ document.querySelectorAll('.biome-selector').forEach(el=>{
787
+ el.addEventListener('click', ()=>{
788
+ selectedBiome = el.getAttribute('data-biome');
789
+ startGame();
790
+ });
791
+ });
792
+
793
+ // allow E/Q from anywhere
794
  window.addEventListener('keydown', (e)=>{
795
+ const k = e.key.toLowerCase();
796
+ if (k === 'e' && gameActive) keys.e = true;
797
+ if (k === 'q' && gameActive) keys.q = true;
 
 
 
 
 
 
 
798
  });
799
 
800
+ // Initialize
801
  resizeCanvas();
802
  populateWorld();
803
  feather.replace();
 
804
  </script>
805
  </body>
806
  </html>