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 {
|