Spaces:
Running
Running
Codex CLI
commited on
Commit
·
a646668
1
Parent(s):
9b606b6
feat(powerups): add infinite ammo powerup mechanics and UI integration
Browse files- index.html +28 -0
- src/combat.js +11 -3
- src/globals.js +10 -0
- src/hud.js +69 -3
- src/main.js +7 -0
- src/pickups.js +133 -0
- src/player.js +10 -1
- src/waves.js +4 -1
- 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 |
-
|
|
|
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 |
-
|
|
|
|
|
35 |
G.weapon.recoil += CFG.gun.recoilKick;
|
36 |
updateHUD();
|
37 |
|
@@ -182,7 +185,12 @@ export function performShooting(delta) {
|
|
182 |
playGunshot();
|
183 |
}
|
184 |
|
185 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
66 |
-
|
|
|
|
|
|
|
|
|
|
|
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
|
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 |
-
|
251 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
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 {
|