Codex CLI commited on
Commit
a646668
·
1 Parent(s): 9b606b6

feat(powerups): add infinite ammo powerup mechanics and UI integration

Browse files
Files changed (9) hide show
  1. index.html +28 -0
  2. src/combat.js +11 -3
  3. src/globals.js +10 -0
  4. src/hud.js +69 -3
  5. src/main.js +7 -0
  6. src/pickups.js +133 -0
  7. src/player.js +10 -1
  8. src/waves.js +4 -1
  9. src/weapon.js +27 -5
index.html CHANGED
@@ -181,6 +181,33 @@
181
  opacity: 0;
182
  }
183
  </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  </head>
185
  <body>
186
  <div id="hud">
@@ -211,6 +238,7 @@
211
 
212
  <!-- Health Bottom-Left -->
213
  <div id="ui-health" class="hud-card bl">
 
214
  <div id="health-label"><span class="hud-title">HEALTH</span><span id="health-text">100</span></div>
215
  <div id="health-bar">
216
  <div id="health-fill" style="width: 100%"></div>
 
181
  opacity: 0;
182
  }
183
  </style>
184
+ <style>
185
+ /* Powerup chips next to health */
186
+ .powerup-chips { display: flex; gap: 8px; margin-bottom: 8px; flex-wrap: wrap; }
187
+ .pu-chip {
188
+ display: inline-block;
189
+ padding: 2px 8px;
190
+ font-size: 12px;
191
+ letter-spacing: 1px;
192
+ border-radius: 999px;
193
+ border: 1px solid rgba(255,255,255,0.3);
194
+ color: #fff;
195
+ text-shadow: 0 1px 2px rgba(0,0,0,0.5);
196
+ box-shadow: 0 0 10px rgba(255,255,255,0.05);
197
+ pointer-events: none;
198
+ user-select: none;
199
+ position: relative;
200
+ overflow: hidden;
201
+ }
202
+ .pu-chip .pu-fill { position: absolute; left: 0; top: 0; bottom: 0; width: 0%; border-radius: 999px; filter: saturate(1.1); }
203
+ .pu-chip .pu-text { position: relative; z-index: 1; }
204
+ .pu-chip.blink { animation: puBlink 0.6s linear infinite; }
205
+ @keyframes puBlink {
206
+ 0% { opacity: 1; filter: brightness(1); }
207
+ 50% { opacity: 0.35; filter: brightness(1.4); }
208
+ 100% { opacity: 1; filter: brightness(1); }
209
+ }
210
+ </style>
211
  </head>
212
  <body>
213
  <div id="hud">
 
238
 
239
  <!-- Health Bottom-Left -->
240
  <div id="ui-health" class="hud-card bl">
241
+ <div id="powerup-chips" class="powerup-chips"></div>
242
  <div id="health-label"><span class="hud-title">HEALTH</span><span id="health-text">100</span></div>
243
  <div id="health-bar">
244
  <div id="health-fill" style="width: 100%"></div>
src/combat.js CHANGED
@@ -24,14 +24,17 @@ export function performShooting(delta) {
24
  if (G.input.shoot && G.shootCooldown <= 0 && G.state === 'playing') {
25
  if (G.weapon.reloading) return;
26
 
27
- if (G.weapon.ammo <= 0) {
 
28
  G.shootCooldown = 0.2;
29
  G.weapon.recoil += CFG.gun.recoilKick * 0.25;
30
  return;
31
  }
32
 
33
  G.shootCooldown = 1 / (CFG.gun.rof * (G.weapon.rofMult || 1));
34
- G.weapon.ammo--;
 
 
35
  G.weapon.recoil += CFG.gun.recoilKick;
36
  updateHUD();
37
 
@@ -182,7 +185,12 @@ export function performShooting(delta) {
182
  playGunshot();
183
  }
184
 
185
- if (!G.weapon.reloading && G.weapon.ammo === 0 && (G.weapon.reserve > 0 || G.weapon.reserve === Infinity)) {
 
 
 
 
 
186
  beginReload();
187
  }
188
  }
 
24
  if (G.input.shoot && G.shootCooldown <= 0 && G.state === 'playing') {
25
  if (G.weapon.reloading) return;
26
 
27
+ const infinite = G.weapon.infiniteAmmoTimer > 0;
28
+ if (!infinite && G.weapon.ammo <= 0) {
29
  G.shootCooldown = 0.2;
30
  G.weapon.recoil += CFG.gun.recoilKick * 0.25;
31
  return;
32
  }
33
 
34
  G.shootCooldown = 1 / (CFG.gun.rof * (G.weapon.rofMult || 1));
35
+ if (!infinite) {
36
+ G.weapon.ammo--;
37
+ }
38
  G.weapon.recoil += CFG.gun.recoilKick;
39
  updateHUD();
40
 
 
185
  playGunshot();
186
  }
187
 
188
+ if (
189
+ !G.weapon.reloading &&
190
+ G.weapon.infiniteAmmoTimer <= 0 &&
191
+ G.weapon.ammo === 0 &&
192
+ (G.weapon.reserve > 0 || G.weapon.reserve === Infinity)
193
+ ) {
194
  beginReload();
195
  }
196
  }
src/globals.js CHANGED
@@ -76,10 +76,20 @@ export const G = {
76
  // Dynamic fire-rate buff
77
  rofMult: 1,
78
  rofBuffTimer: 0,
 
 
 
 
 
 
79
  materials: [],
80
  glowT: 0
81
  },
82
 
 
 
 
 
83
  fx: { tracers: [], impacts: [], flashes: [], dusts: [], portals: [] },
84
  // Player grenades and preview helpers
85
  grenades: [],
 
76
  // Dynamic fire-rate buff
77
  rofMult: 1,
78
  rofBuffTimer: 0,
79
+ rofBuffTotal: 0,
80
+ // Infinite ammo buff
81
+ infiniteAmmoTimer: 0,
82
+ infiniteAmmoTotal: 0,
83
+ ammoBeforeInf: null,
84
+ reserveBeforeInf: null,
85
  materials: [],
86
  glowT: 0
87
  },
88
 
89
+ // Temporary movement speed buff (used by accelerator)
90
+ movementMult: 1,
91
+ movementBuffTimer: 0,
92
+
93
  fx: { tracers: [], impacts: [], flashes: [], dusts: [], portals: [] },
94
  // Player grenades and preview helpers
95
  grenades: [],
src/hud.js CHANGED
@@ -2,7 +2,7 @@ import { G } from './globals.js';
2
  import { CFG } from './config.js';
3
 
4
  // Cache HUD element refs and last values to minimize DOM churn
5
- const HUD = {
6
  waveEl: /** @type {HTMLElement|null} */(document.getElementById('wave')),
7
  scoreEl: /** @type {HTMLElement|null} */(document.getElementById('score')),
8
  enemiesEl: /** @type {HTMLElement|null} */(document.getElementById('enemies')),
@@ -10,6 +10,7 @@ const HUD = {
10
  grenadesEl: /** @type {HTMLElement|null} */(document.getElementById('grenades')),
11
  healthText: /** @type {HTMLElement|null} */(document.getElementById('health-text')),
12
  healthFill: /** @type {HTMLElement|null} */(document.getElementById('health-fill')),
 
13
  ch: {
14
  root: /** @type {HTMLElement|null} */(document.getElementById('crosshair')),
15
  left: /** @type {HTMLElement|null} */(document.getElementById('ch-left')),
@@ -34,6 +35,7 @@ const HUD = {
34
  enemies: -1,
35
  ammo: '',
36
  grenades: -1,
 
37
  }
38
  };
39
 
@@ -62,8 +64,13 @@ export function updateHUD() {
62
  HUD.last.enemies = G.waves.aliveCount;
63
  if (HUD.enemiesEl) HUD.enemiesEl.textContent = String(G.waves.aliveCount);
64
  }
65
- const reserveText = G.weapon.reserve === Infinity ? '∞' : String(G.weapon.reserve);
66
- const ammoText = `${G.weapon.ammo}/${reserveText}`;
 
 
 
 
 
67
  if (ammoText !== HUD.last.ammo) {
68
  HUD.last.ammo = ammoText;
69
  if (HUD.ammoEl) HUD.ammoEl.textContent = ammoText;
@@ -72,6 +79,65 @@ export function updateHUD() {
72
  HUD.last.grenades = G.grenadeCount;
73
  if (HUD.grenadesEl) HUD.grenadesEl.textContent = String(G.grenadeCount);
74
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  }
76
 
77
  export function showWaveBanner(text) {
 
2
  import { CFG } from './config.js';
3
 
4
  // Cache HUD element refs and last values to minimize DOM churn
5
+ const HUD = {
6
  waveEl: /** @type {HTMLElement|null} */(document.getElementById('wave')),
7
  scoreEl: /** @type {HTMLElement|null} */(document.getElementById('score')),
8
  enemiesEl: /** @type {HTMLElement|null} */(document.getElementById('enemies')),
 
10
  grenadesEl: /** @type {HTMLElement|null} */(document.getElementById('grenades')),
11
  healthText: /** @type {HTMLElement|null} */(document.getElementById('health-text')),
12
  healthFill: /** @type {HTMLElement|null} */(document.getElementById('health-fill')),
13
+ powerupsEl: /** @type {HTMLElement|null} */(document.getElementById('powerup-chips')),
14
  ch: {
15
  root: /** @type {HTMLElement|null} */(document.getElementById('crosshair')),
16
  left: /** @type {HTMLElement|null} */(document.getElementById('ch-left')),
 
35
  enemies: -1,
36
  ammo: '',
37
  grenades: -1,
38
+ powerupsKey: ''
39
  }
40
  };
41
 
 
64
  HUD.last.enemies = G.waves.aliveCount;
65
  if (HUD.enemiesEl) HUD.enemiesEl.textContent = String(G.waves.aliveCount);
66
  }
67
+ let ammoText;
68
+ if (G.weapon.infiniteAmmoTimer > 0) {
69
+ ammoText = '∞/∞';
70
+ } else {
71
+ const reserveText = G.weapon.reserve === Infinity ? '∞' : String(G.weapon.reserve);
72
+ ammoText = `${G.weapon.ammo}/${reserveText}`;
73
+ }
74
  if (ammoText !== HUD.last.ammo) {
75
  HUD.last.ammo = ammoText;
76
  if (HUD.ammoEl) HUD.ammoEl.textContent = ammoText;
 
79
  HUD.last.grenades = G.grenadeCount;
80
  if (HUD.grenadesEl) HUD.grenadesEl.textContent = String(G.grenadeCount);
81
  }
82
+
83
+ // Powerup chips next to health
84
+ const active = [];
85
+ if (G.weapon.rofBuffTimer > 0) active.push({ id: 'accelerator', name: 'ACCELERATE', color: 0xffd84d, time: G.weapon.rofBuffTimer });
86
+ if (G.weapon.infiniteAmmoTimer > 0) active.push({ id: 'infinite', name: 'INFINITE AMMO', color: 0x4b0082, time: G.weapon.infiniteAmmoTimer });
87
+ const key = active.map(a => a.id).join(',');
88
+ if (key !== HUD.last.powerupsKey) {
89
+ HUD.last.powerupsKey = key;
90
+ const el = HUD.powerupsEl;
91
+ if (el) {
92
+ // Clear and rebuild chips
93
+ el.innerHTML = '';
94
+ for (const p of active) {
95
+ const chip = document.createElement('div');
96
+ chip.className = 'pu-chip';
97
+ chip.dataset.id = p.id;
98
+ const r = (p.color >> 16) & 255;
99
+ const g = (p.color >> 8) & 255;
100
+ const b = p.color & 255;
101
+ chip.style.borderColor = `rgba(${r},${g},${b},0.75)`;
102
+ chip.style.boxShadow = `0 0 10px rgba(${r},${g},${b},0.35)`;
103
+ chip.style.backgroundColor = `rgba(${r},${g},${b},0.12)`;
104
+ const fill = document.createElement('div');
105
+ fill.className = 'pu-fill';
106
+ fill.style.backgroundColor = `rgba(${r},${g},${b},0.35)`;
107
+ fill.style.width = '100%';
108
+ const text = document.createElement('span');
109
+ text.className = 'pu-text';
110
+ text.textContent = p.name;
111
+ chip.appendChild(fill);
112
+ chip.appendChild(text);
113
+ el.appendChild(chip);
114
+ }
115
+ }
116
+ }
117
+ // Update blink state even if set didn't change
118
+ const el = HUD.powerupsEl;
119
+ if (el) {
120
+ for (const p of active) {
121
+ const chip = el.querySelector(`.pu-chip[data-id="${p.id}"]`);
122
+ if (chip) {
123
+ const shouldBlink = p.time != null && p.time <= 3;
124
+ if (shouldBlink) chip.classList.add('blink'); else chip.classList.remove('blink');
125
+ // Update progress fill width if total known
126
+ const total = (p.id === 'accelerator') ? (G.weapon.rofBuffTotal || 0) : (p.id === 'infinite' ? (G.weapon.infiniteAmmoTotal || 0) : 0);
127
+ const fill = chip.querySelector('.pu-fill');
128
+ if (fill && total > 0 && p.time != null) {
129
+ const t = Math.max(0, Math.min(1, p.time / total));
130
+ fill.style.width = (t * 100).toFixed(1) + '%';
131
+ }
132
+ }
133
+ }
134
+ // Also remove blink from any chips not active
135
+ const nodes = el.querySelectorAll('.pu-chip');
136
+ nodes.forEach(node => {
137
+ const id = node.dataset.id;
138
+ if (!active.find(a => a.id === id)) node.classList.remove('blink');
139
+ });
140
+ }
141
  }
142
 
143
  export function showWaveBanner(text) {
src/main.js CHANGED
@@ -191,6 +191,13 @@ function startGame() {
191
  G.weapon.appliedYaw = 0;
192
  G.weapon.rofMult = 1;
193
  G.weapon.rofBuffTimer = 0;
 
 
 
 
 
 
 
194
 
195
  const overlay = document.getElementById('overlay');
196
  if (overlay) overlay.classList.add('hidden');
 
191
  G.weapon.appliedYaw = 0;
192
  G.weapon.rofMult = 1;
193
  G.weapon.rofBuffTimer = 0;
194
+ G.weapon.rofBuffTotal = 0;
195
+ G.movementMult = 1;
196
+ G.movementBuffTimer = 0;
197
+ G.weapon.infiniteAmmoTimer = 0;
198
+ G.weapon.infiniteAmmoTotal = 0;
199
+ G.weapon.ammoBeforeInf = null;
200
+ G.weapon.reserveBeforeInf = null;
201
 
202
  const overlay = document.getElementById('overlay');
203
  if (overlay) overlay.classList.add('hidden');
src/pickups.js CHANGED
@@ -106,6 +106,66 @@ function makeAcceleratorMesh() {
106
  return g;
107
  }
108
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  // Spawn N accelerator powerups at random world locations
110
  // They float near ground and rotate, granting x2 ROF for 20s on pickup
111
  export function spawnAccelerators(count) {
@@ -166,6 +226,62 @@ export function spawnAccelerators(count) {
166
  }
167
  }
168
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  // Spawns N small glowing green health orbs around a position
170
  export function spawnHealthOrbs(center, count) {
171
  // Allow larger drops (e.g., golem 15–20); cap to keep it reasonable
@@ -341,6 +457,23 @@ export function updatePickups(delta) {
341
  // Apply/refresh ROF buff: x2 for 20s
342
  G.weapon.rofMult = 2;
343
  G.weapon.rofBuffTimer = 20;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
  // Audio cue for powerup pickup
345
  try { playPowerupPickup(); } catch {}
346
  }
 
106
  return g;
107
  }
108
 
109
+ // Infinite ammo (indigo bullet) shared resources
110
+ const INF_COLOR = 0x4b0082; // indigo
111
+
112
+ function makeInfiniteAmmoMesh() {
113
+ const g = new THREE.Group();
114
+
115
+ // Bullet body: cylinder + conical tip
116
+ const bodyMat = new THREE.MeshBasicMaterial({ color: INF_COLOR, fog: false });
117
+ const bodyGeo = new THREE.CylinderGeometry(0.10, 0.10, 0.52, 16, 1);
118
+ const body = new THREE.Mesh(bodyGeo, bodyMat);
119
+ body.position.set(0, 0.0, 0);
120
+ g.add(body);
121
+
122
+ const tipGeo = new THREE.ConeGeometry(0.10, 0.20, 16);
123
+ const tip = new THREE.Mesh(tipGeo, bodyMat);
124
+ tip.position.set(0, 0.36, 0);
125
+ g.add(tip);
126
+
127
+ // Base cap
128
+ const baseMat = new THREE.MeshBasicMaterial({ color: 0x221133, fog: false });
129
+ const baseGeo = new THREE.CylinderGeometry(0.11, 0.11, 0.06, 16, 1);
130
+ const base = new THREE.Mesh(baseGeo, baseMat);
131
+ base.position.set(0, -0.29, 0);
132
+ g.add(base);
133
+
134
+ // Glow sprites similar to accelerator
135
+ const innerMat = new THREE.SpriteMaterial({
136
+ map: GLOW_TEX,
137
+ color: INF_COLOR,
138
+ transparent: true,
139
+ opacity: 0.45,
140
+ depthWrite: false,
141
+ depthTest: true,
142
+ blending: THREE.NormalBlending,
143
+ fog: false
144
+ });
145
+ const outerMat = new THREE.SpriteMaterial({
146
+ map: GLOW_TEX,
147
+ color: INF_COLOR,
148
+ transparent: true,
149
+ opacity: 0.45,
150
+ depthWrite: false,
151
+ depthTest: true,
152
+ blending: THREE.AdditiveBlending,
153
+ fog: false
154
+ });
155
+ const inner = new THREE.Sprite(innerMat);
156
+ const outer = new THREE.Sprite(outerMat);
157
+ inner.scale.set(0.7, 0.7, 1);
158
+ outer.scale.set(1.7, 1.7, 1);
159
+ g.add(outer);
160
+ g.add(inner);
161
+
162
+ g.castShadow = false;
163
+ g.receiveShadow = false;
164
+ g.userData.glowInner = inner;
165
+ g.userData.glowOuter = outer;
166
+ return g;
167
+ }
168
+
169
  // Spawn N accelerator powerups at random world locations
170
  // They float near ground and rotate, granting x2 ROF for 20s on pickup
171
  export function spawnAccelerators(count) {
 
226
  }
227
  }
228
 
229
+ // Spawn N infinite-ammo powerups (indigo bullet) scattered around waves anchor
230
+ export function spawnInfiniteAmmo(count) {
231
+ const n = Math.max(0, Math.min(3, Math.floor(count)));
232
+ const half = CFG.forestSize / 2;
233
+ const margin = 12;
234
+
235
+ function sampleAroundAnchor() {
236
+ const a = G.waves?.spawnAnchor;
237
+ if (!a) return null;
238
+ const Rmin = 8;
239
+ const Rmax = 26;
240
+ const u = G.random();
241
+ const r = Math.sqrt(u * (Rmax * Rmax - Rmin * Rmin) + Rmin * Rmin);
242
+ const t = G.random() * Math.PI * 2;
243
+ let x = a.x + Math.cos(t) * r;
244
+ let z = a.z + Math.sin(t) * r;
245
+ x = Math.max(-half + margin, Math.min(half - margin, x));
246
+ z = Math.max(-half + margin, Math.min(half - margin, z));
247
+ return { x, z };
248
+ }
249
+
250
+ function sampleGlobal() {
251
+ const clear = (CFG.clearRadius || 12) + 4;
252
+ for (let tries = 0; tries < 10; tries++) {
253
+ const x = (G.random() * 2 - 1) * (half - margin);
254
+ const z = (G.random() * 2 - 1) * (half - margin);
255
+ if (Math.hypot(x, z) < clear) continue;
256
+ return { x, z };
257
+ }
258
+ return { x: (G.random() * 2 - 1) * (half - margin), z: (G.random() * 2 - 1) * (half - margin) };
259
+ }
260
+
261
+ for (let i = 0; i < n; i++) {
262
+ const pt = sampleAroundAnchor() || sampleGlobal();
263
+ const gy = getTerrainHeight(pt.x, pt.z);
264
+ const group = makeInfiniteAmmoMesh();
265
+ const baseY = gy + 0.60;
266
+ group.position.set(pt.x, baseY + 0.14, pt.z);
267
+ group.scale.setScalar(1.5); // match accelerator size
268
+ G.scene.add(group);
269
+
270
+ const p = {
271
+ type: 'infiniteAmmo',
272
+ mesh: group,
273
+ pos: group.position,
274
+ baseY,
275
+ bobT: G.random() * Math.PI * 2,
276
+ rotSpeed: 2.8 + G.random() * 1.2,
277
+ glowInner: group.userData.glowInner,
278
+ glowOuter: group.userData.glowOuter,
279
+ glowT: G.random() * Math.PI * 2
280
+ };
281
+ G.powerups.push(p);
282
+ }
283
+ }
284
+
285
  // Spawns N small glowing green health orbs around a position
286
  export function spawnHealthOrbs(center, count) {
287
  // Allow larger drops (e.g., golem 15–20); cap to keep it reasonable
 
457
  // Apply/refresh ROF buff: x2 for 20s
458
  G.weapon.rofMult = 2;
459
  G.weapon.rofBuffTimer = 20;
460
+ G.weapon.rofBuffTotal = 20;
461
+ // Movement speed buff: +50% for 20s
462
+ G.movementMult = 1.5;
463
+ G.movementBuffTimer = 20;
464
+ // Audio cue for powerup pickup
465
+ try { playPowerupPickup(); } catch {}
466
+ } else if (p.type === 'infiniteAmmo') {
467
+ // Apply/refresh infinite ammo for 12s
468
+ if (G.weapon.infiniteAmmoTimer <= 0) {
469
+ G.weapon.ammoBeforeInf = G.weapon.ammo;
470
+ G.weapon.reserveBeforeInf = G.weapon.reserve;
471
+ }
472
+ G.weapon.infiniteAmmoTimer = 12;
473
+ G.weapon.infiniteAmmoTotal = 12;
474
+ // Cancel any reload in progress
475
+ G.weapon.reloading = false;
476
+ G.weapon.reloadTimer = 0;
477
  // Audio cue for powerup pickup
478
  try { playPowerupPickup(); } catch {}
479
  }
src/player.js CHANGED
@@ -49,6 +49,15 @@ export function updatePlayer(delta) {
49
  const P = G.player;
50
  const M = CFG.player.move;
51
 
 
 
 
 
 
 
 
 
 
52
  // Forward/right in the horizontal plane
53
  G.camera.getWorldDirection(FWD);
54
  FWD.y = 0; FWD.normalize();
@@ -64,7 +73,7 @@ export function updatePlayer(delta) {
64
  if (wishLen > 0.0001) { wishX /= wishLen; wishZ /= wishLen; }
65
 
66
  // Desired speeds
67
- let baseSpeed = P.speed * (G.input.sprint ? CFG.player.sprintMult : 1);
68
  const crouchMult = (CFG.player.crouchMult || 1);
69
  // If not sliding, crouch reduces speed
70
  if (G.input.crouch && !P.sliding) baseSpeed *= crouchMult;
 
49
  const P = G.player;
50
  const M = CFG.player.move;
51
 
52
+ // Handle timed movement buff
53
+ if (G.movementBuffTimer > 0) {
54
+ G.movementBuffTimer -= delta;
55
+ if (G.movementBuffTimer <= 0) {
56
+ G.movementBuffTimer = 0;
57
+ G.movementMult = 1;
58
+ }
59
+ }
60
+
61
  // Forward/right in the horizontal plane
62
  G.camera.getWorldDirection(FWD);
63
  FWD.y = 0; FWD.normalize();
 
73
  if (wishLen > 0.0001) { wishX /= wishLen; wishZ /= wishLen; }
74
 
75
  // Desired speeds
76
+ let baseSpeed = P.speed * (G.movementMult || 1) * (G.input.sprint ? CFG.player.sprintMult : 1);
77
  const crouchMult = (CFG.player.crouchMult || 1);
78
  // If not sliding, crouch reduces speed
79
  if (G.input.crouch && !P.sliding) baseSpeed *= crouchMult;
src/waves.js CHANGED
@@ -4,7 +4,7 @@ import { G } from './globals.js';
4
  import { getTerrainHeight } from './world.js';
5
  import { showWaveBanner } from './hud.js';
6
  import { spawnEnemy } from './enemies.js';
7
- import { spawnAccelerators } from './pickups.js';
8
 
9
  export function startNextWave() {
10
  const waveCount = Math.min(
@@ -39,6 +39,9 @@ export function startNextWave() {
39
  // Spawn 0..2 accelerator powerups at random locations
40
  const accelCount = Math.floor(G.random() * 3); // 0,1,2
41
  if (accelCount > 0) spawnAccelerators(accelCount);
 
 
 
42
  }
43
 
44
  export function updateWaves(delta) {
 
4
  import { getTerrainHeight } from './world.js';
5
  import { showWaveBanner } from './hud.js';
6
  import { spawnEnemy } from './enemies.js';
7
+ import { spawnAccelerators, spawnInfiniteAmmo } from './pickups.js';
8
 
9
  export function startNextWave() {
10
  const waveCount = Math.min(
 
39
  // Spawn 0..2 accelerator powerups at random locations
40
  const accelCount = Math.floor(G.random() * 3); // 0,1,2
41
  if (accelCount > 0) spawnAccelerators(accelCount);
42
+ // Spawn at least 1 infinite ammo on wave 1 for visibility, then occasionally
43
+ const infCount = (G.waves.current === 1) ? 1 : Math.floor(G.random() * 2);
44
+ if (infCount > 0) spawnInfiniteAmmo(infCount);
45
  }
46
 
47
  export function updateWaves(delta) {
src/weapon.js CHANGED
@@ -139,6 +139,7 @@ export function setupWeapon() {
139
 
140
  export function beginReload() {
141
  if (G.weapon.reloading) return;
 
142
  if (G.weapon.ammo >= CFG.gun.magSize) return;
143
  G.weapon.reloading = true;
144
  G.weapon.reloadTimer = CFG.gun.reloadTime;
@@ -176,7 +177,7 @@ export function updateWeapon(delta) {
176
  G.weapon.spread += (target - G.weapon.spread) * k;
177
 
178
  let reloadTilt = 0;
179
- if (G.weapon.reloading) {
180
  G.weapon.reloadTimer -= delta;
181
  reloadTilt = 0.4 * Math.sin(Math.min(1, 1 - G.weapon.reloadTimer / CFG.gun.reloadTime) * Math.PI);
182
  if (G.weapon.reloadTimer <= 0) {
@@ -238,7 +239,7 @@ export function updateWeapon(delta) {
238
  G.weapon.appliedPitch = G.weapon.viewPitch;
239
  G.weapon.appliedYaw = G.weapon.viewYaw;
240
 
241
- // ----- Temporary fire-rate buff + weapon glow -----
242
  if (G.weapon.rofBuffTimer > 0) {
243
  G.weapon.rofBuffTimer -= delta;
244
  if (G.weapon.rofBuffTimer <= 0) {
@@ -247,8 +248,27 @@ export function updateWeapon(delta) {
247
  }
248
  }
249
 
250
- const active = G.weapon.rofBuffTimer > 0;
251
- // Pulse emissive when active (subtle)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  const mats = G.weapon.materials || [];
253
  if (active) {
254
  G.weapon.glowT += delta * 3.0;
@@ -256,7 +276,9 @@ export function updateWeapon(delta) {
256
  for (let i = 0; i < mats.length; i++) {
257
  const m = mats[i];
258
  if (!m || !m.isMaterial) continue;
259
- if (m.emissive) m.emissive.setHex(0xffd84d);
 
 
260
  if ('emissiveIntensity' in m) m.emissiveIntensity = 0.8 + pulse * 0.6;
261
  }
262
  } else {
 
139
 
140
  export function beginReload() {
141
  if (G.weapon.reloading) return;
142
+ if (G.weapon.infiniteAmmoTimer > 0) return;
143
  if (G.weapon.ammo >= CFG.gun.magSize) return;
144
  G.weapon.reloading = true;
145
  G.weapon.reloadTimer = CFG.gun.reloadTime;
 
177
  G.weapon.spread += (target - G.weapon.spread) * k;
178
 
179
  let reloadTilt = 0;
180
+ if (G.weapon.reloading && G.weapon.infiniteAmmoTimer <= 0) {
181
  G.weapon.reloadTimer -= delta;
182
  reloadTilt = 0.4 * Math.sin(Math.min(1, 1 - G.weapon.reloadTimer / CFG.gun.reloadTime) * Math.PI);
183
  if (G.weapon.reloadTimer <= 0) {
 
239
  G.weapon.appliedPitch = G.weapon.viewPitch;
240
  G.weapon.appliedYaw = G.weapon.viewYaw;
241
 
242
+ // ----- Temporary fire-rate buff -----
243
  if (G.weapon.rofBuffTimer > 0) {
244
  G.weapon.rofBuffTimer -= delta;
245
  if (G.weapon.rofBuffTimer <= 0) {
 
248
  }
249
  }
250
 
251
+ // ----- Infinite ammo buff timer and restore -----
252
+ if (G.weapon.infiniteAmmoTimer > 0) {
253
+ G.weapon.infiniteAmmoTimer -= delta;
254
+ if (G.weapon.infiniteAmmoTimer <= 0) {
255
+ G.weapon.infiniteAmmoTimer = 0;
256
+ // Restore original ammo/reserve values if saved
257
+ if (G.weapon.ammoBeforeInf != null) {
258
+ G.weapon.ammo = G.weapon.ammoBeforeInf;
259
+ G.weapon.ammoBeforeInf = null;
260
+ }
261
+ if (G.weapon.reserveBeforeInf != null) {
262
+ G.weapon.reserve = G.weapon.reserveBeforeInf;
263
+ G.weapon.reserveBeforeInf = null;
264
+ }
265
+ updateHUD();
266
+ }
267
+ }
268
+
269
+ // ----- Weapon glow while buffs are active -----
270
+ const active = (G.weapon.rofBuffTimer > 0) || (G.weapon.infiniteAmmoTimer > 0);
271
+ // Pulse emissive when active (subtle), color depends on buff
272
  const mats = G.weapon.materials || [];
273
  if (active) {
274
  G.weapon.glowT += delta * 3.0;
 
276
  for (let i = 0; i < mats.length; i++) {
277
  const m = mats[i];
278
  if (!m || !m.isMaterial) continue;
279
+ // Indigo for infinite ammo, yellow for accelerator
280
+ const color = (G.weapon.infiniteAmmoTimer > 0) ? 0x4b0082 : 0xffd84d;
281
+ if (m.emissive) m.emissive.setHex(color);
282
  if ('emissiveIntensity' in m) m.emissiveIntensity = 0.8 + pulse * 0.6;
283
  }
284
  } else {