bbc123321 commited on
Commit
65bb0ca
·
verified ·
1 Parent(s): 076379c

Manual changes saved

Browse files
Files changed (1) hide show
  1. index.html +103 -86
index.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>BattleZone Royale - Enemies Gather First</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <script src="https://unpkg.com/feather-icons"></script>
9
  <style>
@@ -76,7 +76,7 @@
76
  <div class="flex space-x-4 items-center">
77
  <div style="background:#111827;padding:.5rem .75rem;border-radius:.5rem;display:flex;align-items:center;gap:.5rem;"><i data-feather="map-pin"></i><span id="currentBiome">-</span></div>
78
  <div style="background:#111827;padding:.5rem .75rem;border-radius:.5rem;display:flex;align-items:center;gap:.5rem;"><i data-feather="clock"></i><span id="gameTimer">5:00</span></div>
79
- <div style="background:#111827;padding:.5rem .75rem;border-radius:.5rem;display:flex;align-items:center;gap:.5rem;"><i data-feather="users"></i><span id="playerCount">1/20</span></div>
80
  </div>
81
  </header>
82
 
@@ -141,7 +141,7 @@
141
  const goHomeBtn = document.getElementById('goHomeBtn');
142
  const continueBtn = document.getElementById('continueBtn');
143
 
144
- // World (bigger)
145
  const WORLD = { width: 6000, height: 4000 };
146
  let camera = { x:0, y:0 };
147
 
@@ -153,13 +153,13 @@
153
  }
154
  window.addEventListener('resize', resizeCanvas);
155
 
156
- // Player (pickaxe separate from 5 inventory slots)
157
  const player = {
158
  id:'player', x: WORLD.width/2, y: WORLD.height/2, radius:16, angle:0, speed:220,
159
  health:100, armor:0, kills:0, materials:0,
160
- inventory: [null,null,null,null,null], // 5 slots
161
  selectedSlot:0,
162
- equippedIndex: -1, // -1 = pickaxe equipped, >=0 = inventory slot equipped
163
  lastShot:0, lastMelee:0
164
  };
165
 
@@ -222,9 +222,9 @@
222
  // Entities
223
  const bullets = [];
224
  const chests = [];
225
- const objects = []; // harvestables & walls
226
  const enemies = [];
227
- const pickups = []; // visible ground pickups
228
 
229
  function rand(min,max){ return Math.random()*(max-min)+min; }
230
  function randInt(min,max){ return Math.floor(Math.random()*(max-min))+min; }
@@ -239,7 +239,6 @@
239
  return 'ruins';
240
  }
241
 
242
- // Weapon factory
243
  function makeWeaponProto(w){
244
  return { name:w.name, dmg:w.dmg, rate:w.rate, color:w.color, magSize:w.magSize || 12, startReserve:w.startReserve || (w.magSize*2 || 24) };
245
  }
@@ -247,7 +246,7 @@
247
  function generateLootForBiome(b){
248
  const roll = Math.random();
249
  if (roll < 0.35) return { type:'medkit', amount:1 };
250
- if (roll < 0.7) return { type:'materials', amount: randInt(5,20) };
251
  const weapons = [
252
  { name:'Pistol', dmg:12, rate:320, color:'#ffd86b', magSize:12, startReserve:24 },
253
  { name:'SMG', dmg:6, rate:120, color:'#8ef0ff', magSize:30, startReserve:90 },
@@ -257,13 +256,16 @@
257
  return { type:'weapon', weapon: weapons[randInt(0, weapons.length)] };
258
  }
259
 
260
- // Populate world
261
  function populateWorld(){
262
  chests.length = 0; objects.length = 0; enemies.length = 0; pickups.length = 0;
263
  for (let i=0;i<260;i++){
264
  const x = rand(150, WORLD.width-150);
265
  const y = rand(150, WORLD.height-150);
266
- chests.push({ x,y, opened:false, loot: generateLootForBiome(biomeAt(x,y)) });
 
 
 
267
  }
268
  for (let i=0;i<700;i++){
269
  const t = Math.random();
@@ -274,14 +276,14 @@
274
  const hp = type==='wood'?40 : (type==='stone'?80:160);
275
  objects.push({ x,y, type, hp, maxHp:hp, dead:false });
276
  }
277
- for (let i=0;i<19;i++){
 
278
  const ex = rand(300, WORLD.width-300);
279
  const ey = rand(300, WORLD.height-300);
280
  enemies.push({
281
- id:'e'+i, x:ex, y:ey, radius:14, angle:0, speed:110+rand(-20,20),
282
  health: 80 + randInt(0,40), lastMelee:0, meleeRate:800 + randInt(-200,200),
283
  roamTimer: rand(0,3),
284
- // enemy state machine and inventory
285
  inventory: [null,null,null,null,null],
286
  selectedSlot: 0,
287
  equippedIndex: -1,
@@ -289,15 +291,16 @@
289
  lastShot: 0,
290
  reloadingUntil: 0,
291
  lastAttackedTime: 0,
292
- state: 'gather', // 'gather' -> 'combat' -> 'defend' ...
293
- gatherTimeLeft: rand(8,16), // seconds to try gather before switching
294
- target: null // {type:'chest'|'object'|'pickup'|'player', ref:...}
 
295
  });
296
  }
297
  updatePlayerCount();
298
  }
299
 
300
- // HUD init
301
  function initHUD(){
302
  hudHealth.classList.remove('hidden');
303
  hudGearWrap.classList.remove('hidden');
@@ -345,7 +348,7 @@
345
  feather.replace();
346
  }
347
 
348
- // Camera helpers
349
  function cameraUpdate(){
350
  if (!canvas.width || !canvas.height) return;
351
  camera.x = player.x - canvas.width/2;
@@ -355,7 +358,7 @@
355
  }
356
  function worldToScreen(wx,wy){ return { x: Math.round(wx - camera.x), y: Math.round(wy - camera.y) }; }
357
 
358
- // Shooting, reload, melee (player)
359
  function shootBullet(originX, originY, targetX, targetY, weaponObj, shooterId){
360
  if (!weaponObj || (typeof weaponObj.ammoInMag === 'number' && weaponObj.ammoInMag <= 0)) return false;
361
  const speed = 1100;
@@ -407,7 +410,7 @@
407
  }
408
  }
409
 
410
- // Interact (player)
411
  function interactNearby(){
412
  const sel = player.selectedSlot;
413
  const selItem = player.inventory[sel];
@@ -423,13 +426,12 @@
423
  if (chest.opened) continue;
424
  const d = Math.hypot(chest.x - player.x, chest.y - player.y);
425
  if (d < range){
426
- // open and spawn pickups (player)
427
  chest.opened = true;
428
  const loot = chest.loot;
429
  const px = chest.x + rand(-20,20), py = chest.y + rand(-20,20);
430
  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) });
431
  else if (loot.type === 'medkit') pickups.push({ x:px, y:py, type:'medkit', amount: loot.amount || 1 });
432
- else if (loot.type === 'materials') pickups.push({ x:px, y:py, type:'materials', amount: loot.amount || 5 });
433
  if (Math.random() < 0.25){
434
  pickups.push({ x:px+8, y:py+8, type:'ammo', forWeapon: null, amount: randInt(6,30) });
435
  }
@@ -478,7 +480,7 @@
478
  }
479
  }
480
 
481
- // Enemy helpers: equip best weapon, reload, collect pickups
482
  function enemyEquipBestWeapon(e){
483
  let bestIdx = -1;
484
  let bestScore = -Infinity;
@@ -536,7 +538,6 @@
536
  }
537
  }
538
 
539
- // Build Q
540
  function tryBuild(){
541
  if (player.materials < 10) return;
542
  player.materials -= 10;
@@ -554,7 +555,7 @@
554
  return true;
555
  }
556
 
557
- // LOS helper + blocker finder
558
  function hasLineOfSight(x1,y1,x2,y2){
559
  const vx = x2 - x1, vy = y2 - y1;
560
  const vlen2 = vx*vx + vy*vy;
@@ -591,14 +592,13 @@
591
  return closest;
592
  }
593
 
594
- // Bullets update: mark enemies as attacked when hit
595
  function bulletsUpdate(dt){
596
  for (let i=bullets.length-1;i>=0;i--){
597
  const b = bullets[i];
598
  b.x += b.vx * dt; b.y += b.vy * dt;
599
  b.traveled += Math.hypot(b.vx*dt, b.vy*dt);
600
  b.life -= dt;
601
- // hit player
602
  if (b.dmg > 0 && b.shooter !== 'player'){
603
  if (Math.hypot(player.x - b.x, player.y - b.y) < 16){
604
  player.health -= b.dmg;
@@ -606,7 +606,6 @@
606
  bullets.splice(i,1); continue;
607
  }
608
  }
609
- // hit enemies
610
  if (b.dmg > 0){
611
  for (const e of enemies){
612
  if (e.health <= 0) continue;
@@ -620,7 +619,6 @@
620
  }
621
  if (!bullets[i]) continue;
622
  }
623
- // collision with objects & chests
624
  if (!b.tracer && b.dmg > 0){
625
  for (const obj of objects){
626
  if (obj.dead) continue;
@@ -638,7 +636,7 @@
638
  const px = chest.x + rand(-20,20), py = chest.y + rand(-20,20);
639
  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) });
640
  else if (loot.type === 'medkit') pickups.push({ x:px, y:py, type:'medkit', amount: loot.amount || 1 });
641
- else if (loot.type === 'materials') pickups.push({ x:px, y:py, type:'materials', amount: loot.amount || 5 });
642
  bullets.splice(i,1); break;
643
  }
644
  }
@@ -648,7 +646,7 @@
648
  }
649
  }
650
 
651
- // Find nearest unopened chest or nearest harvestable object
652
  function findNearestChest(e, maxDist = 1200){
653
  let best = null; let bd = Infinity;
654
  for (const c of chests){ if (c.opened) continue; const d = Math.hypot(c.x - e.x, c.y - e.y); if (d < bd && d <= maxDist){ bd = d; best = c; } }
@@ -665,56 +663,56 @@
665
  return best;
666
  }
667
 
668
- // Enemies: improved state machine - gather then combat
669
  function updateEnemies(dt, now){
670
  for (const e of enemies){
671
  if (e.health <= 0) continue;
672
 
673
- // If recently attacked by player, switch to combat immediately
674
  if (now - e.lastAttackedTime < 4000){
675
  e.state = 'combat';
676
  }
677
 
678
- // Decrease gather timer if still gathering
679
  if (e.state === 'gather'){
680
  e.gatherTimeLeft -= dt;
681
  }
682
 
683
- // If underheal and has medkit, heal immediately (even while gathering)
684
- if (e.health < 60){
685
- let used = false;
 
686
  for (let s=0;s<5;s++){
687
  const it = e.inventory[s];
688
- if (it && it.type === 'medkit'){
689
- it.amount -= 1;
690
- e.health = Math.min(120, e.health + 50);
691
- if (it.amount <= 0) e.inventory[s] = null;
692
- used = true;
693
- break;
694
- }
 
 
 
 
695
  }
696
- if (used) continue; // use time to heal this tick
697
  }
698
 
699
- // Determine transition from 'gather' to 'combat'
700
  const hasUsefulWeapon = e.inventory.some(it => it && it.type === 'weapon' && (it.ammoInMag > 0 || it.ammoReserve > 0));
701
  if ((e.gatherTimeLeft <= 0 || e.materials >= 20 || hasUsefulWeapon) && e.state === 'gather'){
702
  e.state = 'combat';
703
  }
704
 
705
- // State behavior:
706
  if (e.state === 'gather'){
707
- // prefer pickups, then chests, then harvest objects
708
  let targetPickup = findNearestPickup(e, 240);
709
  if (targetPickup){
710
- // move to pickup
711
  const angle = Math.atan2(targetPickup.y - e.y, targetPickup.x - e.x);
712
  e.angle = angle;
713
  e.x += Math.cos(e.angle) * e.speed * dt * 0.9;
714
  e.y += Math.sin(e.angle) * e.speed * dt * 0.9;
715
  if (Math.hypot(targetPickup.x - e.x, targetPickup.y - e.y) < 18){
716
  enemyPickupCollect(e, targetPickup);
717
- // remove pickup from global array
718
  const idx = pickups.indexOf(targetPickup);
719
  if (idx >= 0) pickups.splice(idx,1);
720
  }
@@ -729,7 +727,6 @@
729
  e.x += Math.cos(e.angle) * e.speed * dt * 0.9;
730
  e.y += Math.sin(e.angle) * e.speed * dt * 0.9;
731
  } else {
732
- // open chest and directly give loot to enemy (avoid leaving pickups, faster)
733
  chestTarget.opened = true;
734
  const loot = chestTarget.loot;
735
  if (loot.type === 'weapon'){
@@ -737,13 +734,12 @@
737
  } else if (loot.type === 'medkit'){
738
  enemyPickupCollect(e, { type:'medkit', amount: loot.amount || 1 });
739
  } else if (loot.type === 'materials'){
740
- enemyPickupCollect(e, { type:'materials', amount: loot.amount || 5 });
741
  }
742
  }
743
  continue;
744
  }
745
 
746
- // no chest nearby -> try harvest nearest wood/stone
747
  let objTarget = findNearestHarvestable(e, 700);
748
  if (objTarget){
749
  const d = Math.hypot(objTarget.x - e.x, objTarget.y - e.y);
@@ -752,13 +748,11 @@
752
  e.x += Math.cos(e.angle) * e.speed * dt * 0.8;
753
  e.y += Math.sin(e.angle) * e.speed * dt * 0.8;
754
  } else {
755
- // melee hit slice: reduce object hp over time
756
- objTarget.hp -= 40 * dt; // faster than player to let enemies gather
757
  if (objTarget.hp <= 0 && !objTarget.dead){
758
  objTarget.dead = true;
759
  const matGain = objTarget.type === 'wood' ? 3 : 6;
760
  e.materials += matGain;
761
- // small chance to spawn a weapon or medkit when breaking
762
  if (Math.random() < 0.08){
763
  const lootRoll = Math.random();
764
  if (lootRoll < 0.4) enemyPickupCollect(e, { type:'medkit', amount:1 });
@@ -770,20 +764,15 @@
770
  continue;
771
  }
772
 
773
- // nothing to gather -> roam slowly
774
  e.x += Math.cos(e.angle) * e.speed * dt * 0.25;
775
  e.y += Math.sin(e.angle) * e.speed * dt * 0.25;
776
  if (Math.random() < 0.01) e.angle += (Math.random()-0.5)*2;
777
  continue;
778
  } // end gather
779
 
780
- // Combat or defend state
781
- // ensure equipped weapon selected if present
782
- if (e.equippedIndex === -1){
783
- enemyEquipBestWeapon(e);
784
- }
785
-
786
- // auto-reload if needed
787
  if (e.equippedIndex >= 0){
788
  const eq = e.inventory[e.equippedIndex];
789
  if (eq && eq.type === 'weapon' && eq.ammoInMag <= 0 && eq.ammoReserve > 0){
@@ -791,7 +780,7 @@
791
  }
792
  }
793
 
794
- // pick nearest target (player preferred)
795
  let target = player;
796
  let bestDist = Math.hypot(player.x - e.x, player.y - e.y);
797
  for (const other of enemies){
@@ -800,13 +789,11 @@
800
  if (d < bestDist && Math.random() < 0.6){ bestDist = d; target = other; }
801
  }
802
 
803
- // If target is player & player is very close while enemy has low HP: try to heal/build/retreat rather than attack
804
  const distToPlayer = Math.hypot(player.x - e.x, player.y - e.y);
805
  if (distToPlayer < 160 && e.health < 35 && e.materials >= 10 && Math.random() < 0.5){
806
  enemyTryBuild(e);
807
  }
808
 
809
- // If target blocked by structure, try to break it first
810
  const blocked = !hasLineOfSight(e.x, e.y, target.x, target.y);
811
  if (blocked){
812
  const blocker = findBlockingObject(e.x, e.y, target.x, target.y);
@@ -816,7 +803,6 @@
816
  blocker.hp -= 18 * dt * 2;
817
  if (blocker.hp <= 0){ blocker.dead = true; e.materials += (blocker.type === 'wood' ? 3 : 6); }
818
  } else {
819
- // shoot at blocker if possible
820
  if (e.equippedIndex >= 0){
821
  const eq = e.inventory[e.equippedIndex];
822
  if (eq && eq.type === 'weapon' && eq.ammoInMag > 0 && now - e.lastShot > (eq.weapon.rate || 300)){
@@ -835,7 +821,7 @@
835
  }
836
  }
837
 
838
- // Attack logic: melee if close, otherwise ranged if has weapon and LOS
839
  if (distToPlayer < 36 && now - e.lastMelee > e.meleeRate){
840
  e.lastMelee = now;
841
  const dmg = 10 + randInt(0,8);
@@ -846,7 +832,6 @@
846
  const eq = e.inventory[e.equippedIndex];
847
  if (eq && eq.type === 'weapon' && now - (e.lastShot||0) > (eq.weapon.rate || 300)){
848
  if (eq.ammoInMag <= 0){
849
- // reload if ammoReserve available
850
  if (eq.ammoReserve > 0) reloadItem(eq);
851
  } else {
852
  if (hasLineOfSight(e.x, e.y, target.x, target.y)){
@@ -856,20 +841,17 @@
856
  shootBullet(e.x + Math.cos(angle)*12, e.y + Math.sin(angle)*12, target.x + (Math.random()-0.5)*6, target.y + (Math.random()-0.5)*6, eq, e.id);
857
  e.lastAttackedTime = now;
858
  } else {
859
- // move to get LOS
860
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
861
  e.x += Math.cos(e.angle) * e.speed * dt * 0.6;
862
  e.y += Math.sin(e.angle) * e.speed * dt * 0.6;
863
  }
864
  }
865
  } else {
866
- // no weapon -> move toward target to melee
867
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
868
  e.x += Math.cos(e.angle) * e.speed * dt * 0.9;
869
  e.y += Math.sin(e.angle) * e.speed * dt * 0.9;
870
  }
871
  } else {
872
- // unarmed: move in and melee
873
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
874
  e.x += Math.cos(e.angle) * e.speed * dt * 0.9;
875
  e.y += Math.sin(e.angle) * e.speed * dt * 0.9;
@@ -877,7 +859,7 @@
877
  }
878
  }
879
 
880
- // Storm (unchanged)
881
  const storm = { maxRadius: 2400, radius:2400, centerX: WORLD.width/2, centerY: WORLD.height/2, damagePerSecond:1, closingSpeed: 0.6, active:false };
882
  let stormDamageAccumulator = 0;
883
  function playerInStorm(){ return Math.hypot(player.x - storm.centerX, player.y - storm.centerY) > storm.radius; }
@@ -905,10 +887,9 @@
905
  }
906
  }
907
 
908
- // Utility
909
  function updatePlayerCount(){
910
  const aliveEnemies = enemies.filter(e => e.health > 0).length;
911
- document.getElementById('playerCount').textContent = `${1 + aliveEnemies}/20`;
912
  if (gameActive && aliveEnemies === 0 && player.health > 0){
913
  gameActive = false;
914
  victoryScreen.classList.remove('hidden');
@@ -916,7 +897,7 @@
916
  }
917
  }
918
 
919
- // Drawing (unchanged)
920
  function drawWorld(){
921
  const TILE = 600;
922
  const cols = Math.ceil(WORLD.width / TILE);
@@ -1009,18 +990,55 @@
1009
  const s = worldToScreen(e.x,e.y);
1010
  ctx.save();
1011
  ctx.translate(s.x,s.y); ctx.rotate(e.angle);
 
 
1012
  ctx.fillStyle='rgba(0,0,0,0.18)'; ctx.beginPath(); ctx.ellipse(0,12,14,6,0,0,Math.PI*2); ctx.fill();
1013
  ctx.fillStyle='#ff6b6b'; ctx.beginPath(); ctx.moveTo(12,0); ctx.lineTo(-10,-8); ctx.lineTo(-10,8); ctx.closePath(); ctx.fill();
 
 
1014
  ctx.fillStyle='rgba(0,0,0,0.6)'; ctx.fillRect(-18,-22,36,6);
1015
  const hpPct = Math.max(0, Math.min(1, e.health/120));
1016
  ctx.fillStyle='#ff6b6b'; ctx.fillRect(-18,-22,36*hpPct,6);
1017
- if (e.equippedIndex >= 0 && e.inventory[e.equippedIndex] && e.inventory[e.equippedIndex].type === 'weapon') {
1018
- ctx.fillStyle = e.inventory[e.equippedIndex].weapon.color || '#fff';
1019
- ctx.fillRect(-10,12,8,6);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1020
  }
1021
- // draw state marker small
 
1022
  ctx.fillStyle = e.state === 'gather' ? 'rgba(0,200,200,0.9)' : (e.state === 'combat' ? 'rgba(255,80,80,0.95)' : 'rgba(255,200,80,0.9)');
1023
  ctx.beginPath(); ctx.arc(18, -18, 5, 0, Math.PI*2); ctx.fill();
 
1024
  ctx.restore();
1025
  }
1026
  }
@@ -1085,7 +1103,7 @@
1085
 
1086
  function drawCrosshair(){ }
1087
 
1088
- // Main loop
1089
  let lastTime = 0;
1090
  function gameLoop(ts){
1091
  if (!gameActive) return;
@@ -1110,7 +1128,6 @@
1110
 
1111
  player.angle = Math.atan2(mouse.worldY - player.y, mouse.worldX - player.x);
1112
 
1113
- // determine active weapon for player
1114
  let activeWeaponItem = null;
1115
  if (player.equippedIndex >= 0) activeWeaponItem = player.inventory[player.equippedIndex];
1116
  else {
@@ -1143,7 +1160,7 @@
1143
  updateEnemies(dt, performance.now());
1144
  bulletsUpdate(dt);
1145
 
1146
- // pickups auto-collect for player
1147
  for (let i=pickups.length-1;i>=0;i--){
1148
  const p = pickups[i];
1149
  if (Math.hypot(p.x - player.x, p.y - player.y) < 18){ pickupCollect(p); pickups.splice(i,1); updateHUD(); }
 
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>BattleZone Royale - Enemies Gather First (Improved)</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <script src="https://unpkg.com/feather-icons"></script>
9
  <style>
 
76
  <div class="flex space-x-4 items-center">
77
  <div style="background:#111827;padding:.5rem .75rem;border-radius:.5rem;display:flex;align-items:center;gap:.5rem;"><i data-feather="map-pin"></i><span id="currentBiome">-</span></div>
78
  <div style="background:#111827;padding:.5rem .75rem;border-radius:.5rem;display:flex;align-items:center;gap:.5rem;"><i data-feather="clock"></i><span id="gameTimer">5:00</span></div>
79
+ <div style="background:#111827;padding:.5rem .75rem;border-radius:.5rem;display:flex;align-items:center;gap:.5rem;"><i data-feather="users"></i><span id="playerCount">1/50</span></div>
80
  </div>
81
  </header>
82
 
 
141
  const goHomeBtn = document.getElementById('goHomeBtn');
142
  const continueBtn = document.getElementById('continueBtn');
143
 
144
+ // World
145
  const WORLD = { width: 6000, height: 4000 };
146
  let camera = { x:0, y:0 };
147
 
 
153
  }
154
  window.addEventListener('resize', resizeCanvas);
155
 
156
+ // Player
157
  const player = {
158
  id:'player', x: WORLD.width/2, y: WORLD.height/2, radius:16, angle:0, speed:220,
159
  health:100, armor:0, kills:0, materials:0,
160
+ inventory: [null,null,null,null,null],
161
  selectedSlot:0,
162
+ equippedIndex: -1,
163
  lastShot:0, lastMelee:0
164
  };
165
 
 
222
  // Entities
223
  const bullets = [];
224
  const chests = [];
225
+ const objects = [];
226
  const enemies = [];
227
+ const pickups = [];
228
 
229
  function rand(min,max){ return Math.random()*(max-min)+min; }
230
  function randInt(min,max){ return Math.floor(Math.random()*(max-min))+min; }
 
239
  return 'ruins';
240
  }
241
 
 
242
  function makeWeaponProto(w){
243
  return { name:w.name, dmg:w.dmg, rate:w.rate, color:w.color, magSize:w.magSize || 12, startReserve:w.startReserve || (w.magSize*2 || 24) };
244
  }
 
246
  function generateLootForBiome(b){
247
  const roll = Math.random();
248
  if (roll < 0.35) return { type:'medkit', amount:1 };
249
+ if (roll < 0.7) return { type:'materials', amount: 10 }; // always 10 per request
250
  const weapons = [
251
  { name:'Pistol', dmg:12, rate:320, color:'#ffd86b', magSize:12, startReserve:24 },
252
  { name:'SMG', dmg:6, rate:120, color:'#8ef0ff', magSize:30, startReserve:90 },
 
256
  return { type:'weapon', weapon: weapons[randInt(0, weapons.length)] };
257
  }
258
 
259
+ // Populate world - spawn 49 enemies
260
  function populateWorld(){
261
  chests.length = 0; objects.length = 0; enemies.length = 0; pickups.length = 0;
262
  for (let i=0;i<260;i++){
263
  const x = rand(150, WORLD.width-150);
264
  const y = rand(150, WORLD.height-150);
265
+ const loot = generateLootForBiome(biomeAt(x,y));
266
+ // ensure materials amount=10 (per request) if type materials
267
+ if (loot.type === 'materials') loot.amount = 10;
268
+ chests.push({ x,y, opened:false, loot });
269
  }
270
  for (let i=0;i<700;i++){
271
  const t = Math.random();
 
276
  const hp = type==='wood'?40 : (type==='stone'?80:160);
277
  objects.push({ x,y, type, hp, maxHp:hp, dead:false });
278
  }
279
+ // spawn 49 enemies
280
+ for (let i=0;i<49;i++){
281
  const ex = rand(300, WORLD.width-300);
282
  const ey = rand(300, WORLD.height-300);
283
  enemies.push({
284
+ id:'e'+i, x:ex, y:ey, radius:14, angle:rand(0,Math.PI*2), speed:110+rand(-20,20),
285
  health: 80 + randInt(0,40), lastMelee:0, meleeRate:800 + randInt(-200,200),
286
  roamTimer: rand(0,3),
 
287
  inventory: [null,null,null,null,null],
288
  selectedSlot: 0,
289
  equippedIndex: -1,
 
291
  lastShot: 0,
292
  reloadingUntil: 0,
293
  lastAttackedTime: 0,
294
+ state: 'gather',
295
+ gatherTimeLeft: rand(8,16),
296
+ target: null,
297
+ nextHealTime: 0 // only heal using medkit and with cooldown
298
  });
299
  }
300
  updatePlayerCount();
301
  }
302
 
303
+ // HUD
304
  function initHUD(){
305
  hudHealth.classList.remove('hidden');
306
  hudGearWrap.classList.remove('hidden');
 
348
  feather.replace();
349
  }
350
 
351
+ // Camera
352
  function cameraUpdate(){
353
  if (!canvas.width || !canvas.height) return;
354
  camera.x = player.x - canvas.width/2;
 
358
  }
359
  function worldToScreen(wx,wy){ return { x: Math.round(wx - camera.x), y: Math.round(wy - camera.y) }; }
360
 
361
+ // Combat utilities
362
  function shootBullet(originX, originY, targetX, targetY, weaponObj, shooterId){
363
  if (!weaponObj || (typeof weaponObj.ammoInMag === 'number' && weaponObj.ammoInMag <= 0)) return false;
364
  const speed = 1100;
 
410
  }
411
  }
412
 
413
+ // Player interact
414
  function interactNearby(){
415
  const sel = player.selectedSlot;
416
  const selItem = player.inventory[sel];
 
426
  if (chest.opened) continue;
427
  const d = Math.hypot(chest.x - player.x, chest.y - player.y);
428
  if (d < range){
 
429
  chest.opened = true;
430
  const loot = chest.loot;
431
  const px = chest.x + rand(-20,20), py = chest.y + rand(-20,20);
432
  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) });
433
  else if (loot.type === 'medkit') pickups.push({ x:px, y:py, type:'medkit', amount: loot.amount || 1 });
434
+ else if (loot.type === 'materials') pickups.push({ x:px, y:py, type:'materials', amount: loot.amount || 10 });
435
  if (Math.random() < 0.25){
436
  pickups.push({ x:px+8, y:py+8, type:'ammo', forWeapon: null, amount: randInt(6,30) });
437
  }
 
480
  }
481
  }
482
 
483
+ // Enemy helpers
484
  function enemyEquipBestWeapon(e){
485
  let bestIdx = -1;
486
  let bestScore = -Infinity;
 
538
  }
539
  }
540
 
 
541
  function tryBuild(){
542
  if (player.materials < 10) return;
543
  player.materials -= 10;
 
555
  return true;
556
  }
557
 
558
+ // LOS helpers
559
  function hasLineOfSight(x1,y1,x2,y2){
560
  const vx = x2 - x1, vy = y2 - y1;
561
  const vlen2 = vx*vx + vy*vy;
 
592
  return closest;
593
  }
594
 
595
+ // Bullets update
596
  function bulletsUpdate(dt){
597
  for (let i=bullets.length-1;i>=0;i--){
598
  const b = bullets[i];
599
  b.x += b.vx * dt; b.y += b.vy * dt;
600
  b.traveled += Math.hypot(b.vx*dt, b.vy*dt);
601
  b.life -= dt;
 
602
  if (b.dmg > 0 && b.shooter !== 'player'){
603
  if (Math.hypot(player.x - b.x, player.y - b.y) < 16){
604
  player.health -= b.dmg;
 
606
  bullets.splice(i,1); continue;
607
  }
608
  }
 
609
  if (b.dmg > 0){
610
  for (const e of enemies){
611
  if (e.health <= 0) continue;
 
619
  }
620
  if (!bullets[i]) continue;
621
  }
 
622
  if (!b.tracer && b.dmg > 0){
623
  for (const obj of objects){
624
  if (obj.dead) continue;
 
636
  const px = chest.x + rand(-20,20), py = chest.y + rand(-20,20);
637
  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) });
638
  else if (loot.type === 'medkit') pickups.push({ x:px, y:py, type:'medkit', amount: loot.amount || 1 });
639
+ else if (loot.type === 'materials') pickups.push({ x:px, y:py, type:'materials', amount: loot.amount || 10 });
640
  bullets.splice(i,1); break;
641
  }
642
  }
 
646
  }
647
  }
648
 
649
+ // Nearest helpers
650
  function findNearestChest(e, maxDist = 1200){
651
  let best = null; let bd = Infinity;
652
  for (const c of chests){ if (c.opened) continue; const d = Math.hypot(c.x - e.x, c.y - e.y); if (d < bd && d <= maxDist){ bd = d; best = c; } }
 
663
  return best;
664
  }
665
 
666
+ // Enemy AI: gather-first state machine, heal only using medkits, visible weapon drawing logic supported in drawEnemies
667
  function updateEnemies(dt, now){
668
  for (const e of enemies){
669
  if (e.health <= 0) continue;
670
 
671
+ // If recently attacked, go combat
672
  if (now - e.lastAttackedTime < 4000){
673
  e.state = 'combat';
674
  }
675
 
 
676
  if (e.state === 'gather'){
677
  e.gatherTimeLeft -= dt;
678
  }
679
 
680
+ // Only heal using medkit: check cooldown and presence in inventory
681
+ if (e.health < 60 && now >= (e.nextHealTime || 0)){
682
+ // find medkit in inventory
683
+ let medIdx = -1;
684
  for (let s=0;s<5;s++){
685
  const it = e.inventory[s];
686
+ if (it && it.type === 'medkit' && it.amount > 0){ medIdx = s; break; }
687
+ }
688
+ if (medIdx !== -1){
689
+ // use medkit (consume one), set cooldown so it's not instant recurring
690
+ const kit = e.inventory[medIdx];
691
+ kit.amount -= 1;
692
+ e.health = Math.min(120, e.health + 50);
693
+ if (kit.amount <= 0) e.inventory[medIdx] = null;
694
+ e.nextHealTime = now + 6000; // 6s cooldown before next possible medkit use
695
+ // do not switch state to combat just because healing occurred
696
+ continue;
697
  }
 
698
  }
699
 
700
+ // transition gather -> combat if conditions met
701
  const hasUsefulWeapon = e.inventory.some(it => it && it.type === 'weapon' && (it.ammoInMag > 0 || it.ammoReserve > 0));
702
  if ((e.gatherTimeLeft <= 0 || e.materials >= 20 || hasUsefulWeapon) && e.state === 'gather'){
703
  e.state = 'combat';
704
  }
705
 
 
706
  if (e.state === 'gather'){
707
+ // prioritize nearby pickups, then chests, then harvest objects
708
  let targetPickup = findNearestPickup(e, 240);
709
  if (targetPickup){
 
710
  const angle = Math.atan2(targetPickup.y - e.y, targetPickup.x - e.x);
711
  e.angle = angle;
712
  e.x += Math.cos(e.angle) * e.speed * dt * 0.9;
713
  e.y += Math.sin(e.angle) * e.speed * dt * 0.9;
714
  if (Math.hypot(targetPickup.x - e.x, targetPickup.y - e.y) < 18){
715
  enemyPickupCollect(e, targetPickup);
 
716
  const idx = pickups.indexOf(targetPickup);
717
  if (idx >= 0) pickups.splice(idx,1);
718
  }
 
727
  e.x += Math.cos(e.angle) * e.speed * dt * 0.9;
728
  e.y += Math.sin(e.angle) * e.speed * dt * 0.9;
729
  } else {
 
730
  chestTarget.opened = true;
731
  const loot = chestTarget.loot;
732
  if (loot.type === 'weapon'){
 
734
  } else if (loot.type === 'medkit'){
735
  enemyPickupCollect(e, { type:'medkit', amount: loot.amount || 1 });
736
  } else if (loot.type === 'materials'){
737
+ enemyPickupCollect(e, { type:'materials', amount: loot.amount || 10 });
738
  }
739
  }
740
  continue;
741
  }
742
 
 
743
  let objTarget = findNearestHarvestable(e, 700);
744
  if (objTarget){
745
  const d = Math.hypot(objTarget.x - e.x, objTarget.y - e.y);
 
748
  e.x += Math.cos(e.angle) * e.speed * dt * 0.8;
749
  e.y += Math.sin(e.angle) * e.speed * dt * 0.8;
750
  } else {
751
+ objTarget.hp -= 40 * dt;
 
752
  if (objTarget.hp <= 0 && !objTarget.dead){
753
  objTarget.dead = true;
754
  const matGain = objTarget.type === 'wood' ? 3 : 6;
755
  e.materials += matGain;
 
756
  if (Math.random() < 0.08){
757
  const lootRoll = Math.random();
758
  if (lootRoll < 0.4) enemyPickupCollect(e, { type:'medkit', amount:1 });
 
764
  continue;
765
  }
766
 
767
+ // roam
768
  e.x += Math.cos(e.angle) * e.speed * dt * 0.25;
769
  e.y += Math.sin(e.angle) * e.speed * dt * 0.25;
770
  if (Math.random() < 0.01) e.angle += (Math.random()-0.5)*2;
771
  continue;
772
  } // end gather
773
 
774
+ // Combat behavior
775
+ if (e.equippedIndex === -1) enemyEquipBestWeapon(e);
 
 
 
 
 
776
  if (e.equippedIndex >= 0){
777
  const eq = e.inventory[e.equippedIndex];
778
  if (eq && eq.type === 'weapon' && eq.ammoInMag <= 0 && eq.ammoReserve > 0){
 
780
  }
781
  }
782
 
783
+ // choose target (player preferred)
784
  let target = player;
785
  let bestDist = Math.hypot(player.x - e.x, player.y - e.y);
786
  for (const other of enemies){
 
789
  if (d < bestDist && Math.random() < 0.6){ bestDist = d; target = other; }
790
  }
791
 
 
792
  const distToPlayer = Math.hypot(player.x - e.x, player.y - e.y);
793
  if (distToPlayer < 160 && e.health < 35 && e.materials >= 10 && Math.random() < 0.5){
794
  enemyTryBuild(e);
795
  }
796
 
 
797
  const blocked = !hasLineOfSight(e.x, e.y, target.x, target.y);
798
  if (blocked){
799
  const blocker = findBlockingObject(e.x, e.y, target.x, target.y);
 
803
  blocker.hp -= 18 * dt * 2;
804
  if (blocker.hp <= 0){ blocker.dead = true; e.materials += (blocker.type === 'wood' ? 3 : 6); }
805
  } else {
 
806
  if (e.equippedIndex >= 0){
807
  const eq = e.inventory[e.equippedIndex];
808
  if (eq && eq.type === 'weapon' && eq.ammoInMag > 0 && now - e.lastShot > (eq.weapon.rate || 300)){
 
821
  }
822
  }
823
 
824
+ // Attack
825
  if (distToPlayer < 36 && now - e.lastMelee > e.meleeRate){
826
  e.lastMelee = now;
827
  const dmg = 10 + randInt(0,8);
 
832
  const eq = e.inventory[e.equippedIndex];
833
  if (eq && eq.type === 'weapon' && now - (e.lastShot||0) > (eq.weapon.rate || 300)){
834
  if (eq.ammoInMag <= 0){
 
835
  if (eq.ammoReserve > 0) reloadItem(eq);
836
  } else {
837
  if (hasLineOfSight(e.x, e.y, target.x, target.y)){
 
841
  shootBullet(e.x + Math.cos(angle)*12, e.y + Math.sin(angle)*12, target.x + (Math.random()-0.5)*6, target.y + (Math.random()-0.5)*6, eq, e.id);
842
  e.lastAttackedTime = now;
843
  } else {
 
844
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
845
  e.x += Math.cos(e.angle) * e.speed * dt * 0.6;
846
  e.y += Math.sin(e.angle) * e.speed * dt * 0.6;
847
  }
848
  }
849
  } else {
 
850
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
851
  e.x += Math.cos(e.angle) * e.speed * dt * 0.9;
852
  e.y += Math.sin(e.angle) * e.speed * dt * 0.9;
853
  }
854
  } else {
 
855
  e.angle = Math.atan2(target.y - e.y, target.x - e.x);
856
  e.x += Math.cos(e.angle) * e.speed * dt * 0.9;
857
  e.y += Math.sin(e.angle) * e.speed * dt * 0.9;
 
859
  }
860
  }
861
 
862
+ // Storm
863
  const storm = { maxRadius: 2400, radius:2400, centerX: WORLD.width/2, centerY: WORLD.height/2, damagePerSecond:1, closingSpeed: 0.6, active:false };
864
  let stormDamageAccumulator = 0;
865
  function playerInStorm(){ return Math.hypot(player.x - storm.centerX, player.y - storm.centerY) > storm.radius; }
 
887
  }
888
  }
889
 
 
890
  function updatePlayerCount(){
891
  const aliveEnemies = enemies.filter(e => e.health > 0).length;
892
+ document.getElementById('playerCount').textContent = `${1 + aliveEnemies}/50`;
893
  if (gameActive && aliveEnemies === 0 && player.health > 0){
894
  gameActive = false;
895
  victoryScreen.classList.remove('hidden');
 
897
  }
898
  }
899
 
900
+ // Drawing
901
  function drawWorld(){
902
  const TILE = 600;
903
  const cols = Math.ceil(WORLD.width / TILE);
 
990
  const s = worldToScreen(e.x,e.y);
991
  ctx.save();
992
  ctx.translate(s.x,s.y); ctx.rotate(e.angle);
993
+
994
+ // shadow & body
995
  ctx.fillStyle='rgba(0,0,0,0.18)'; ctx.beginPath(); ctx.ellipse(0,12,14,6,0,0,Math.PI*2); ctx.fill();
996
  ctx.fillStyle='#ff6b6b'; ctx.beginPath(); ctx.moveTo(12,0); ctx.lineTo(-10,-8); ctx.lineTo(-10,8); ctx.closePath(); ctx.fill();
997
+
998
+ // HP bar
999
  ctx.fillStyle='rgba(0,0,0,0.6)'; ctx.fillRect(-18,-22,36,6);
1000
  const hpPct = Math.max(0, Math.min(1, e.health/120));
1001
  ctx.fillStyle='#ff6b6b'; ctx.fillRect(-18,-22,36*hpPct,6);
1002
+
1003
+ // If equipped with weapon, draw it in hand visibly
1004
+ if (e.equippedIndex >= 0 && e.inventory[e.equippedIndex] && e.inventory[e.equippedIndex].type === 'weapon'){
1005
+ const we = e.inventory[e.equippedIndex];
1006
+ const color = we.weapon.color || '#ddd';
1007
+ ctx.save();
1008
+ // position weapon in right-hand area
1009
+ ctx.translate(12, 2);
1010
+ ctx.rotate(-0.08);
1011
+ // body of weapon
1012
+ ctx.fillStyle = color;
1013
+ ctx.fillRect(0, -4, 24, 8);
1014
+ // muzzle detail
1015
+ ctx.fillStyle = '#222';
1016
+ ctx.fillRect(18, -2, 6, 4);
1017
+ // small magazine indicator
1018
+ const magPct = Math.max(0, we.ammoInMag / we.weapon.magSize);
1019
+ ctx.fillStyle = 'rgba(0,0,0,0.35)';
1020
+ ctx.fillRect(4, 6, 18, 4);
1021
+ ctx.fillStyle = '#0f0';
1022
+ ctx.fillRect(4, 6, 18 * magPct, 4);
1023
+ ctx.restore();
1024
+ } else {
1025
+ // draw pickaxe if unarmed (small icon)
1026
+ if (e.equippedIndex === -1){
1027
+ ctx.save();
1028
+ ctx.translate(12, 6);
1029
+ ctx.rotate(-0.22);
1030
+ ctx.fillStyle = '#8b6b4a';
1031
+ ctx.fillRect(-2,0,4,10);
1032
+ ctx.fillStyle = '#cfcfcf';
1033
+ ctx.fillRect(-6,-4,10,4);
1034
+ ctx.restore();
1035
+ }
1036
  }
1037
+
1038
+ // draw state marker
1039
  ctx.fillStyle = e.state === 'gather' ? 'rgba(0,200,200,0.9)' : (e.state === 'combat' ? 'rgba(255,80,80,0.95)' : 'rgba(255,200,80,0.9)');
1040
  ctx.beginPath(); ctx.arc(18, -18, 5, 0, Math.PI*2); ctx.fill();
1041
+
1042
  ctx.restore();
1043
  }
1044
  }
 
1103
 
1104
  function drawCrosshair(){ }
1105
 
1106
+ // Game loop
1107
  let lastTime = 0;
1108
  function gameLoop(ts){
1109
  if (!gameActive) return;
 
1128
 
1129
  player.angle = Math.atan2(mouse.worldY - player.y, mouse.worldX - player.x);
1130
 
 
1131
  let activeWeaponItem = null;
1132
  if (player.equippedIndex >= 0) activeWeaponItem = player.inventory[player.equippedIndex];
1133
  else {
 
1160
  updateEnemies(dt, performance.now());
1161
  bulletsUpdate(dt);
1162
 
1163
+ // player pickup auto-collect
1164
  for (let i=pickups.length-1;i>=0;i--){
1165
  const p = pickups[i];
1166
  if (Math.hypot(p.x - player.x, p.y - player.y) < 18){ pickupCollect(p); pickups.splice(i,1); updateHUD(); }