Guest User

first fruit of 2.5 pro labor

a guest
Mar 31st, 2025
25
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 73.15 KB | None | 0 0
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>Enhanced Endless Top-Down Shooter</title>
  5. <meta charset="utf-8"/>
  6. <script src="https://cdnjs.cloudflare.com/ajax/libs/noisejs/2.1.0/noisejs.min.js"></script>
  7. <style>
  8. body {
  9. margin: 0;
  10. overflow: hidden;
  11. background-color: #111;
  12. display: flex;
  13. flex-direction: column; /* Stack vertically */
  14. justify-content: center;
  15. align-items: center;
  16. height: 100vh;
  17. font-family: sans-serif;
  18. color: white;
  19. }
  20.  
  21. canvas {
  22. border: 1px solid white;
  23. background-color: #1a1a1a;
  24. }
  25.  
  26. #ui {
  27. position: absolute;
  28. top: 10px;
  29. left: 10px;
  30. background: rgba(0, 0, 0, 0.6);
  31. padding: 10px;
  32. border-radius: 5px;
  33. font-size: 0.9em;
  34. }
  35.  
  36. #ui div {
  37. margin-bottom: 3px;
  38. }
  39.  
  40. #level-up-options {
  41. position: absolute;
  42. bottom: 20px;
  43. left: 50%;
  44. transform: translateX(-50%);
  45. display: none; /* Initially hidden */
  46. gap: 15px;
  47. z-index: 10;
  48. }
  49.  
  50. .level-up-button {
  51. padding: 15px 25px;
  52. background-color: #4CAF50;
  53. color: white;
  54. border: none;
  55. border-radius: 8px;
  56. cursor: pointer;
  57. font-size: 1em;
  58. transition: background-color 0.3s;
  59. }
  60.  
  61. .level-up-button:hover {
  62. background-color: #45a049;
  63. }
  64.  
  65. #color-selection {
  66. margin-bottom: 15px;
  67. display: block; /* Initially visible */
  68. }
  69.  
  70. #color-selection button {
  71. padding: 8px 15px;
  72. margin: 0 5px;
  73. cursor: pointer;
  74. border: 1px solid white;
  75. background: none;
  76. color: white;
  77. border-radius: 4px;
  78. }
  79.  
  80. #color-selection button.selected {
  81. background-color: white;
  82. color: black;
  83. }
  84.  
  85. #game-container {
  86. position: relative; /* Needed for absolute positioning of UI */
  87. }
  88.  
  89. /* Style for power-up timer display */
  90. .power-up-timer {
  91. color: yellow;
  92. font-weight: bold;
  93. }
  94. </style>
  95. </head>
  96. <body>
  97.  
  98. <div id="color-selection">
  99. <span>Choose Player Color:</span>
  100. <button data-color="blue" class="selected">Blue</button>
  101. <button data-color="cyan">Cyan</button>
  102. <button data-color="#ff8800">Orange</button>
  103. <button data-color="magenta">Magenta</button>
  104. </div>
  105.  
  106. <div id="game-container">
  107. <div id="ui">
  108. <div>Level: <span id="level">1</span></div>
  109. <div>XP: <span id="xp">0</span> / <span id="xp-needed">100</span></div>
  110. <div>Health: <span id="health">100</span> / <span id="max-health">100</span></div>
  111. <div>Wave: <span id="wave">1</span></div>
  112. <div>Enemies Left: <span id="enemies-left">0</span></div>
  113. <div>Weapon: <span id="current-weapon">Pistol</span></div>
  114. <div>Ammo: <span id="ammo">--</span></div>
  115. <div id="power-up-status"></div>
  116. </div>
  117.  
  118. <div id="level-up-options"></div>
  119.  
  120. <canvas id="gameCanvas"></canvas>
  121. </div>
  122.  
  123. <script>
  124. // --- DOM Elements ---
  125. const canvas = document.getElementById('gameCanvas');
  126. const ctx = canvas.getContext('2d');
  127. const uiLevel = document.getElementById('level');
  128. const uiXp = document.getElementById('xp');
  129. const uiXpNeeded = document.getElementById('xp-needed');
  130. const uiHealth = document.getElementById('health');
  131. const uiMaxHealth = document.getElementById('max-health');
  132. const uiWave = document.getElementById('wave');
  133. const uiEnemiesLeft = document.getElementById('enemies-left');
  134. const uiCurrentWeapon = document.getElementById('current-weapon');
  135. const uiAmmo = document.getElementById('ammo');
  136. const uiPowerUpStatus = document.getElementById('power-up-status');
  137. const levelUpOptionsDiv = document.getElementById('level-up-options');
  138. const colorSelectionDiv = document.getElementById('color-selection');
  139. const gameContainerDiv = document.getElementById('game-container');
  140.  
  141. // --- Game Settings ---
  142. let PLAYER_COLOR = 'blue'; // Default, will be set by selection
  143. const PLAYER_SIZE = 15;
  144. const PLAYER_BASE_SPEED = 250; // Pixels per second
  145. const ENEMY_START_SIZE = 20;
  146. const ENEMY_START_HEALTH = 50;
  147. const ENEMY_START_SPEED = 100;
  148. const ENEMY_BASE_DAMAGE = 10;
  149. const PROJECTILE_BASE_SPEED = 500;
  150. const PROJECTILE_BASE_SIZE = 5;
  151. const PROJECTILE_COLOR = 'yellow';
  152. const XP_ORB_SIZE = 5;
  153. const XP_ORB_COLOR = 'lime';
  154. const AMMO_PICKUP_SIZE = 6;
  155. const AMMO_PICKUP_COLOR = 'lightblue';
  156. const POWERUP_SIZE = 10;
  157. const POWERUP_SPAWN_CHANCE = 0.05; // Chance per enemy death
  158. const XP_PER_KILL = 25;
  159. const BASE_XP_TO_LEVEL = 100;
  160. const XP_LEVEL_MULTIPLIER = 1.2;
  161. const HEALTH_BAR_HEIGHT = 5;
  162. const HEALTH_BAR_WIDTH_FACTOR = 1.5;
  163. const PARTICLE_LIFETIME = 0.5; // seconds
  164. const MAX_PARTICLES = 300; // Increased max particles
  165. const GRID_SIZE = 50; // For background grid
  166. const BACKGROUND_NOISE_SCALE = 250; // Controls 'zoom' level of background noise
  167. const CAMERA_SHAKE_DECAY = 0.85;
  168. const AMMO_DROP_CHANCE = 0.3; // 30% chance for an enemy to drop ammo
  169.  
  170. // --- Game State ---
  171. let player;
  172. let enemies = [];
  173. let projectiles = [];
  174. let xpOrbs = [];
  175. let ammoPickups = [];
  176. let powerUps = [];
  177. let particles = [];
  178. let keys = {};
  179. let mouse = {x: 0, y: 0, down: false};
  180. let camera = {x: 0, y: 0, shakeIntensity: 0, shakeDuration: 0};
  181. let currentWave = 1;
  182. let enemiesToSpawn = 0;
  183. let enemiesAlive = 0;
  184. let spawningDone = false;
  185. let lastTime = 0;
  186. let timeToNextSpawn = 0;
  187. let gameTime = 0;
  188. let choosingLevelUp = false;
  189. let isGameOver = false;
  190. let noise; // For procedural background
  191. let audioCtx; // For Web Audio API
  192.  
  193. // --- Audio Placeholders ---
  194. function initAudio() {
  195. try {
  196. audioCtx = new (window.AudioContext || window.webkitAudioContext)();
  197. console.log("AudioContext initialized.");
  198. } catch (e) {
  199. console.error("Web Audio API is not supported in this browser", e);
  200. }
  201. }
  202.  
  203. function playSound(type) {
  204. if (!audioCtx) return; // Do nothing if audio is not available
  205.  
  206. console.log("Play sound:", type); // Placeholder log
  207.  
  208. // === Basic Sound Example (Simple Beep) ===
  209. if (type === 'shoot_pistol') {
  210. const oscillator = audioCtx.createOscillator();
  211. const gainNode = audioCtx.createGain();
  212.  
  213. oscillator.connect(gainNode);
  214. gainNode.connect(audioCtx.destination);
  215.  
  216. oscillator.type = 'triangle'; // triangle, sine, square, sawtooth
  217. oscillator.frequency.setValueAtTime(880, audioCtx.currentTime); // A5 note
  218. gainNode.gain.setValueAtTime(0.3, audioCtx.currentTime); // Start volume
  219.  
  220. // Quick fade out
  221. gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.1);
  222.  
  223. oscillator.start(audioCtx.currentTime);
  224. oscillator.stop(audioCtx.currentTime + 0.1);
  225. }
  226. // --- Add more sound types here ---
  227. // e.g., 'hit_player', 'hit_enemy', 'enemy_death', 'level_up', 'pickup_xp', 'pickup_ammo', 'pickup_powerup', 'melee_swing', 'shoot_shotgun' etc.
  228. // Implement using OscillatorNode, GainNode (for envelopes), BiquadFilterNode (for tone shaping), BufferSourceNode (for noise/samples)
  229. }
  230.  
  231.  
  232. // --- Utility Functions ---
  233. function getRandomColor() {
  234. const letters = '0123456789ABCDEF';
  235.  
  236. let color = '#';
  237.  
  238. for (let i = 0; i < 6; i++) {
  239.  
  240. color += letters[Math.floor(Math.random() * 16)];
  241.  
  242. }
  243.  
  244. return color;
  245. }
  246.  
  247. function distance(x1, y1, x2, y2) {
  248.  
  249. return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
  250.  
  251. }
  252.  
  253.  
  254. function normalize(x, y) {
  255.  
  256. const len = Math.sqrt(x * x + y * y);
  257.  
  258. if (len === 0) return {x: 0, y: 0};
  259.  
  260. return {x: x / len, y: y / len};
  261.  
  262. }
  263.  
  264. function worldToScreen(x, y) {
  265. // Apply camera shake
  266. const screenX = x - camera.x + canvas.width / 2 + (Math.random() - 0.5) * camera.shakeIntensity;
  267. const screenY = y - camera.y + canvas.height / 2 + (Math.random() - 0.5) * camera.shakeIntensity;
  268. return {x: screenX, y: screenY};
  269. }
  270.  
  271. function screenToWorld(x, y) {
  272.  
  273. return {
  274.  
  275. x: x + camera.x - canvas.width / 2,
  276.  
  277. y: y + camera.y - canvas.height / 2
  278.  
  279. };
  280.  
  281. }
  282.  
  283. function applyCameraShake(intensity, duration) {
  284. camera.shakeIntensity = Math.max(camera.shakeIntensity, intensity);
  285. camera.shakeDuration = Math.max(camera.shakeDuration, duration);
  286. }
  287.  
  288. function randomInRange(min, max) {
  289. return Math.random() * (max - min) + min;
  290. }
  291.  
  292. // --- Classes ---
  293.  
  294. class Entity { /* ... (keep existing, including drawHealthBar, takeDamage) ... */
  295. constructor(x, y, size, color, health) {
  296. this.x = x;
  297. this.y = y;
  298. this.size = size;
  299. this.color = color;
  300. this.maxHealth = health;
  301. this.health = health;
  302. this.vx = 0; // velocity x
  303. this.vy = 0; // velocity y
  304. }
  305.  
  306. draw(ctx) {
  307. // Abstract method - should be implemented by subclasses
  308. }
  309.  
  310. update(dt) {
  311. this.x += this.vx * dt;
  312. this.y += this.vy * dt;
  313. }
  314.  
  315. drawHealthBar(ctx) {
  316. if (this.health < this.maxHealth && this.health > 0) { // Don't draw if dead
  317. const screenPos = worldToScreen(this.x, this.y);
  318. const barWidth = this.size * HEALTH_BAR_WIDTH_FACTOR;
  319. const barX = screenPos.x - barWidth / 2;
  320. const barY = screenPos.y - this.size - 8; // Position ABOVE the entity
  321.  
  322. // Background of health bar
  323. ctx.fillStyle = 'grey';
  324. ctx.fillRect(barX, barY, barWidth, HEALTH_BAR_HEIGHT);
  325.  
  326. // Current health
  327. const healthPercentage = this.health / this.maxHealth;
  328. const healthColor = healthPercentage > 0.5 ? 'green' : (healthPercentage > 0.2 ? 'orange' : 'red');
  329. ctx.fillStyle = healthColor;
  330. ctx.fillRect(barX, barY, barWidth * healthPercentage, HEALTH_BAR_HEIGHT);
  331. }
  332. }
  333.  
  334. takeDamage(amount) {
  335. if (this.health <= 0) return false; // Already dead
  336.  
  337. this.health -= amount;
  338. createParticles(this.x, this.y, 5, 'red', 50); // Generic hit particle
  339.  
  340. if (this.health <= 0) {
  341. this.health = 0;
  342. return true; // Died
  343. }
  344. return false; // Survived
  345. }
  346. }
  347.  
  348. class Player extends Entity {
  349. constructor(x, y, color) {
  350. super(x, y, PLAYER_SIZE, color, 100);
  351. this.baseSpeed = PLAYER_BASE_SPEED;
  352. this.speed = this.baseSpeed;
  353. this.xp = 0;
  354. this.level = 1;
  355. this.xpToNextLevel = BASE_XP_TO_LEVEL;
  356.  
  357. // --- Weapon Definitions ---
  358. this.weapons = [ // Use an array for easy switching by index
  359. { // 0: Bat (Melee)
  360. name: "Bat", type: 'melee', damage: 25, rateOfFire: 2, // swings per second
  361. range: 40, swingArc: Math.PI / 2, // 90 degree arc
  362. lastSwingTime: 0, color: '#aaa', knockback: 150
  363. },
  364. { // 1: Knife (Melee) - Faster, less damage/range
  365. name: "Knife", type: 'melee', damage: 15, rateOfFire: 4,
  366. range: 30, swingArc: Math.PI / 3, // 60 degree arc
  367. lastSwingTime: 0, color: '#ccc', knockback: 50
  368. },
  369. { // 2: Pistol (Ranged)
  370. name: "Pistol", type: 'ranged', damage: 18, rateOfFire: 5,
  371. projectileSpeed: PROJECTILE_BASE_SPEED, projectileSize: PROJECTILE_BASE_SIZE,
  372. ammoType: 'pistol', ammoPerShot: 1, maxAmmo: 100,
  373. lastShotTime: 0, color: PROJECTILE_COLOR, recoil: 50,
  374. accuracy: 0.05, projectileCount: 1, spreadAngle: 0
  375. },
  376. { // 3: SMG (Ranged) - High fire rate, less damage, less accurate
  377. name: "SMG", type: 'ranged', damage: 12, rateOfFire: 12,
  378. projectileSpeed: PROJECTILE_BASE_SPEED * 0.9, projectileSize: PROJECTILE_BASE_SIZE * 0.8,
  379. ammoType: 'smg', ammoPerShot: 1, maxAmmo: 200,
  380. lastShotTime: 0, color: 'orange', recoil: 70,
  381. accuracy: 0.15, projectileCount: 1, spreadAngle: 0
  382. },
  383. { // 4: Shotgun (Ranged) - Multi-projectile, spread
  384. name: "Shotgun", type: 'ranged', damage: 10, rateOfFire: 1.5, // Damage per pellet
  385. projectileSpeed: PROJECTILE_BASE_SPEED * 0.8, projectileSize: PROJECTILE_BASE_SIZE * 0.7,
  386. ammoType: 'shotgun', ammoPerShot: 1, maxAmmo: 50, // Uses 1 shell, fires multiple pellets
  387. lastShotTime: 0, color: 'red', recoil: 250,
  388. accuracy: 0.1, projectileCount: 6, spreadAngle: 0.5 // Radians (approx 30 deg)
  389. }
  390. // Add more: Flamethrower (continuous particles?), Grenade Launcher, etc.
  391. ];
  392. this.currentWeaponIndex = 2; // Start with Pistol
  393. this.currentWeapon = this.weapons[this.currentWeaponIndex];
  394.  
  395. // --- Ammo ---
  396. this.ammo = {
  397. pistol: 50,
  398. smg: 0,
  399. shotgun: 10,
  400. // Add other ammo types if needed
  401. };
  402.  
  403. // --- Power-up Effects ---
  404. this.powerUps = {
  405. invincibility: 0, // duration remaining
  406. doubleDamage: 0,
  407. speedBoost: 0,
  408. rapidFire: 0, // Multiplier for fire rate
  409. xpMultiplier: 0, // Multiplier for XP gain
  410. magnetRadius: 0 // Increased XP orb magnet radius
  411. };
  412. this.baseMagnetRadius = XP_ORB_MAGNET_RADIUS; // Need to define this constant
  413.  
  414. this.aimAngle = 0; // For drawing aim line/weapon
  415. }
  416.  
  417. draw(ctx) {
  418. const screenPos = worldToScreen(this.x, this.y);
  419. let drawColor = this.color;
  420.  
  421. // Visual feedback for invincibility
  422. if (this.powerUps.invincibility > 0) {
  423. // Pulsating alpha or different color
  424. ctx.globalAlpha = 0.5 + Math.sin(gameTime * 10) * 0.2;
  425. ctx.fillStyle = 'white'; // Aura color
  426. ctx.beginPath();
  427. ctx.arc(screenPos.x, screenPos.y, this.size + 5, 0, Math.PI * 2);
  428. ctx.fill();
  429. ctx.globalAlpha = 1.0;
  430. }
  431.  
  432. ctx.fillStyle = drawColor;
  433. ctx.beginPath();
  434. ctx.arc(screenPos.x, screenPos.y, this.size, 0, Math.PI * 2);
  435. ctx.fill();
  436.  
  437. // Draw aiming direction / weapon representation
  438. const angle = this.aimAngle;
  439. ctx.strokeStyle = 'white';
  440. ctx.lineWidth = 3;
  441. ctx.beginPath();
  442. ctx.moveTo(screenPos.x, screenPos.y);
  443. const weaponLength = this.size + 8; // Simple line representing weapon barrel/swing
  444. ctx.lineTo(screenPos.x + Math.cos(angle) * weaponLength, screenPos.y + Math.sin(angle) * weaponLength);
  445. ctx.stroke();
  446. ctx.lineWidth = 1; // Reset
  447.  
  448. this.drawHealthBar(ctx);
  449. }
  450.  
  451. update(dt) {
  452. if (isGameOver) return;
  453.  
  454. // Update aim angle
  455. this.aimAngle = Math.atan2(mouse.y - canvas.height / 2, mouse.x - canvas.width / 2);
  456.  
  457. // Update Power-up timers and effects
  458. this.updatePowerUps(dt);
  459.  
  460. // Movement
  461. this.vx = 0;
  462. this.vy = 0;
  463. const currentSpeed = this.speed * (this.powerUps.speedBoost > 0 ? 1.5 : 1); // Apply speed boost
  464. if (keys['w'] || keys['ArrowUp']) this.vy = -currentSpeed;
  465. if (keys['s'] || keys['ArrowDown']) this.vy = currentSpeed;
  466. if (keys['a'] || keys['ArrowLeft']) this.vx = -currentSpeed;
  467. if (keys['d'] || keys['ArrowRight']) this.vx = currentSpeed;
  468.  
  469. if (this.vx !== 0 && this.vy !== 0) {
  470. const factor = 1 / Math.sqrt(2);
  471. this.vx *= factor;
  472. this.vy *= factor;
  473. }
  474.  
  475. super.update(dt); // Apply velocity
  476.  
  477. // Apply friction to recoil impulse (simple decay)
  478. // This part might need tuning or a different approach
  479. this.vx *= 0.9; // Gradually reduce recoil velocity
  480. this.vy *= 0.9;
  481.  
  482. // Weapon Switching (Number keys 1 through 0)
  483. for (let i = 1; i <= 9; i++) {
  484. if (keys[i.toString()] && this.weapons.length >= i) {
  485. this.switchWeapon(i - 1);
  486. keys[i.toString()] = false; // Consume the key press
  487. }
  488. }
  489. if (keys['0'] && this.weapons.length >= 10) { // Handle '0' key for 10th weapon
  490. this.switchWeapon(9);
  491. keys['0'] = false;
  492. }
  493.  
  494.  
  495. // Shooting / Attacking
  496. if (mouse.down) {
  497. this.tryAttack();
  498. }
  499. }
  500.  
  501. switchWeapon(index) {
  502. if (index >= 0 && index < this.weapons.length && index !== this.currentWeaponIndex) {
  503. this.currentWeaponIndex = index;
  504. this.currentWeapon = this.weapons[index];
  505. console.log(`Switched to weapon: ${this.currentWeapon.name}`);
  506. updateUI(); // Update UI for weapon/ammo
  507. playSound('weapon_switch'); // Placeholder sound
  508. }
  509. }
  510.  
  511. tryAttack() {
  512. const weapon = this.currentWeapon;
  513. const fireRateMultiplier = (this.powerUps.rapidFire > 0) ? 1.5 : 1;
  514. const cooldown = 1 / (weapon.rateOfFire * fireRateMultiplier);
  515. const lastAttackTime = weapon.type === 'melee' ? weapon.lastSwingTime : weapon.lastShotTime;
  516.  
  517. if ((gameTime - lastAttackTime) > cooldown) {
  518. if (weapon.type === 'melee') {
  519. this.performMeleeAttack(weapon);
  520. weapon.lastSwingTime = gameTime;
  521. } else if (weapon.type === 'ranged') {
  522. // Check Ammo
  523. const ammoType = weapon.ammoType;
  524. if (ammoType && this.ammo[ammoType] >= weapon.ammoPerShot) {
  525. this.ammo[ammoType] -= weapon.ammoPerShot;
  526. this.shootRanged(weapon);
  527. weapon.lastShotTime = gameTime;
  528. updateUI(); // Update ammo display
  529. } else if (!ammoType) { // Weapon doesn't use ammo
  530. this.shootRanged(weapon);
  531. weapon.lastShotTime = gameTime;
  532. } else {
  533. // Out of ammo sound?
  534. playSound('out_of_ammo');
  535. }
  536. }
  537. }
  538. }
  539.  
  540. performMeleeAttack(weapon) {
  541. console.log(`Swinging ${weapon.name}`);
  542. playSound('melee_swing'); // Placeholder
  543.  
  544. const attackAngle = this.aimAngle;
  545. const halfArc = weapon.swingArc / 2;
  546. let enemiesHit = 0;
  547.  
  548. enemies.forEach(enemy => {
  549. const dist = distance(this.x, this.y, enemy.x, enemy.y);
  550. if (dist < weapon.range + enemy.size / 2) { // Check range
  551. const angleToEnemy = Math.atan2(enemy.y - this.y, enemy.x - this.x);
  552. let angleDiff = attackAngle - angleToEnemy;
  553.  
  554. // Normalize angle difference to be between -PI and PI
  555. while (angleDiff > Math.PI) angleDiff -= Math.PI * 2;
  556. while (angleDiff < -Math.PI) angleDiff += Math.PI * 2;
  557.  
  558. if (Math.abs(angleDiff) < halfArc) { // Check if within swing arc
  559. const damageMultiplier = (this.powerUps.doubleDamage > 0) ? 2 : 1;
  560. const died = enemy.takeDamage(weapon.damage * damageMultiplier);
  561. enemiesHit++;
  562.  
  563. // Apply knockback
  564. const pushDir = normalize(enemy.x - this.x, enemy.y - this.y);
  565. enemy.vx += pushDir.x * weapon.knockback;
  566. enemy.vy += pushDir.y * weapon.knockback;
  567.  
  568. if (died) {
  569. enemy.onDeath();
  570. // Remove enemy (handled in main loop cleanup or dedicated function)
  571. }
  572. }
  573. }
  574. });
  575.  
  576. if (enemiesHit > 0) {
  577. playSound('melee_hit'); // Placeholder
  578. }
  579.  
  580. // Maybe add a visual swing arc particle effect
  581. createParticles(this.x, this.y, 5, weapon.color, 100, attackAngle, weapon.swingArc);
  582. }
  583.  
  584. shootRanged(weapon) {
  585. const damageMultiplier = (this.powerUps.doubleDamage > 0) ? 2 : 1;
  586. const baseAngle = this.aimAngle;
  587.  
  588. // Muzzle flash particles
  589. const muzzleX = this.x + Math.cos(baseAngle) * (this.size + 5);
  590. const muzzleY = this.y + Math.sin(baseAngle) * (this.size + 5);
  591. createParticles(muzzleX, muzzleY, 10, 'orange', 150, baseAngle, Math.PI / 6);
  592. playSound(`shoot_${weapon.name.toLowerCase()}`.replace(' ', '_')); // e.g., playSound('shoot_pistol')
  593.  
  594. for (let i = 0; i < weapon.projectileCount; i++) {
  595. // Accuracy: Random deviation from the aim angle
  596. const accuracyOffset = (Math.random() - 0.5) * weapon.accuracy;
  597.  
  598. // Spread: Angle offset for multi-projectile weapons
  599. let spreadOffset = 0;
  600. if (weapon.projectileCount > 1) {
  601. // Distribute projectiles evenly within the spreadAngle
  602. spreadOffset = (i / (weapon.projectileCount - 1) - 0.5) * weapon.spreadAngle;
  603. }
  604.  
  605. const currentAngle = baseAngle + accuracyOffset + spreadOffset;
  606. const dx = Math.cos(currentAngle);
  607. const dy = Math.sin(currentAngle);
  608.  
  609. // Start projectile slightly outside the player
  610. const projX = this.x + dx * (this.size + weapon.projectileSize);
  611. const projY = this.y + dy * (this.size + weapon.projectileSize);
  612.  
  613. projectiles.push(new Projectile(
  614. projX, projY,
  615. dx * weapon.projectileSpeed, dy * weapon.projectileSpeed,
  616. weapon.damage * damageMultiplier,
  617. weapon.projectileSize, weapon.color, this
  618. ));
  619. }
  620.  
  621. // Apply Recoil
  622. const recoilStrength = weapon.recoil || 0;
  623. this.vx -= Math.cos(baseAngle) * recoilStrength * 0.1; // Apply a fraction of recoil as impulse
  624. this.vy -= Math.sin(baseAngle) * recoilStrength * 0.1;
  625.  
  626. // Apply Camera Shake
  627. applyCameraShake(recoilStrength / 20, 0.1); // Shake proportional to recoil
  628. }
  629.  
  630. addXp(amount) {
  631. if (choosingLevelUp) return;
  632.  
  633. const xpMultiplier = (this.powerUps.xpMultiplier > 0) ? 1.5 : 1;
  634. this.xp += amount * xpMultiplier;
  635.  
  636. if (this.xp >= this.xpToNextLevel) {
  637. this.levelUp();
  638. }
  639. updateUI();
  640. }
  641.  
  642. levelUp() {
  643. this.level++;
  644. this.xp -= this.xpToNextLevel; // Carry over excess XP
  645. this.xpToNextLevel = Math.floor(BASE_XP_TO_LEVEL * (XP_LEVEL_MULTIPLIER ** (this.level - 1))); // Recalculate based on base and level
  646. this.health = this.maxHealth; // Heal on level up
  647. console.log(`Level Up! Reached level ${this.level}`);
  648. playSound('level_up');
  649. createParticles(this.x, this.y, 50, 'gold', 150); // Level up particle burst
  650. updateUI();
  651. presentLevelUpOptions();
  652. }
  653.  
  654. updatePowerUps(dt) {
  655. let statusChanged = false;
  656. for (const type in this.powerUps) {
  657. if (this.powerUps[type] > 0) {
  658. this.powerUps[type] -= dt;
  659. if (this.powerUps[type] <= 0) {
  660. this.powerUps[type] = 0;
  661. console.log(`Power-up expired: ${type}`);
  662. statusChanged = true;
  663. // Reset effects specific to expiration if needed
  664. if (type === 'speedBoost') this.speed = this.baseSpeed;
  665. if (type === 'magnetRadius') XP_ORB_MAGNET_RADIUS = this.baseMagnetRadius; // Reset magnet radius
  666. }
  667. }
  668. }
  669. // Apply continuous effects
  670. this.speed = this.baseSpeed * (this.powerUps.speedBoost > 0 ? 1.5 : 1);
  671. // Apply magnet radius boost
  672. XP_ORB_MAGNET_RADIUS = this.baseMagnetRadius * (this.powerUps.magnetRadius > 0 ? 2.0 : 1); // Double radius example
  673.  
  674.  
  675. if (statusChanged) {
  676. updateUI(); // Update the UI power-up display
  677. }
  678. }
  679.  
  680. activatePowerUp(type, duration) {
  681. console.log(`Activated power-up: ${type} for ${duration}s`);
  682. this.powerUps[type] = (this.powerUps[type] || 0) + duration; // Add duration, stacking
  683.  
  684. // Immediate effect application if necessary
  685. if (type === 'speedBoost') this.speed = this.baseSpeed * 1.5;
  686. if (type === 'magnetRadius') XP_ORB_MAGNET_RADIUS = this.baseMagnetRadius * 2.0;
  687.  
  688. playSound('pickup_powerup');
  689. updateUI();
  690. }
  691.  
  692. takeDamage(amount) {
  693. if (this.powerUps.invincibility > 0 || isGameOver) {
  694. // Flash effect even if invincible?
  695. createParticles(this.x, this.y, 3, 'grey', 50);
  696. return false; // Invincible or game over
  697. }
  698.  
  699. const died = super.takeDamage(amount);
  700. playSound('hit_player'); // Placeholder
  701. applyCameraShake(10, 0.15); // Shake on taking damage
  702. updateUI(); // Update health display
  703.  
  704. if (died && !isGameOver) {
  705. gameOver();
  706. return true;
  707. }
  708. return died;
  709. }
  710.  
  711. }
  712.  
  713. // --- Enemy Base Class and Variants ---
  714. class Enemy extends Entity {
  715. constructor(x, y, wave, options = {}) {
  716. const sizeMultiplier = options.sizeMult || (1 + (wave - 1) * 0.05);
  717. const healthMultiplier = options.healthMult || (1 + (wave - 1) * 0.1);
  718. const speedMultiplier = options.speedMult || (1 + (wave - 1) * 0.02);
  719.  
  720. const size = (options.baseSize || ENEMY_START_SIZE) * sizeMultiplier;
  721. const health = (options.baseHealth || ENEMY_START_HEALTH) * healthMultiplier;
  722. const speed = (options.baseSpeed || ENEMY_START_SPEED) * speedMultiplier;
  723. const color = options.color || `hsl(${randomInRange(0, 50)}, 70%, 50%)`; // Red/Orange hues
  724.  
  725. super(x, y, size, color, health);
  726. this.speed = speed;
  727. this.baseDamage = options.baseDamage || ENEMY_BASE_DAMAGE;
  728. this.damage = this.baseDamage * (1 + (wave - 1) * 0.08); // Damage scales
  729. this.xpValue = (options.xpValue || XP_PER_KILL) * (1 + (wave - 1) * 0.05);
  730. this.enemyType = options.type || 'standard'; // For identification
  731. }
  732.  
  733. draw(ctx) { // Standard square enemy draw
  734. const screenPos = worldToScreen(this.x, this.y);
  735. ctx.fillStyle = this.color;
  736. ctx.fillRect(screenPos.x - this.size / 2, screenPos.y - this.size / 2, this.size, this.size);
  737. this.drawHealthBar(ctx);
  738. }
  739.  
  740. update(dt, target) { // Standard chase behavior
  741. // Guard clause (keep the improved one)
  742. if (!target || typeof target.x === 'undefined' || typeof target.y === 'undefined' || this.health <= 0) {
  743. this.vx = 0;
  744. this.vy = 0;
  745. return;
  746. }
  747.  
  748. // --- Add Logging ---
  749. const dx = target.x - this.x;
  750. const dy = target.y - this.y;
  751. // console.log(`Enemy update: dx=${dx}, dy=${dy}`); // Optional: Log inputs to normalize
  752.  
  753. const moveDir = normalize(dx, dy);
  754.  
  755. // --- Log the result of normalize AND add a defensive check ---
  756. // console.log('Result of normalize:', moveDir); // LOG THE RESULT
  757. if (!moveDir || typeof moveDir.x === 'undefined') {
  758. console.error(`CRITICAL: normalize returned invalid value! moveDir:`, moveDir, ` Inputs: dx=${dx}, dy=${dy}`);
  759. // Provide a default value to prevent crash, although this indicates a deeper issue
  760. this.vx = 0;
  761. this.vy = 0;
  762. // Optionally uncomment the line below to pause execution if debugger is open
  763. // debugger;
  764. } else {
  765. // This block should now only execute if moveDir is a valid object
  766. this.vx = moveDir.x * this.speed; // Line 718 (or around there)
  767. this.vy = moveDir.y * this.speed;
  768. }
  769. // --- End Logging & Check ---
  770.  
  771.  
  772. super.update(dt); // Apply velocity changes
  773. }
  774.  
  775. onDeath() {
  776. if (!player) return; // Avoid errors if player is gone
  777.  
  778. // Drop XP orb
  779. xpOrbs.push(new XpOrb(this.x, this.y, this.xpValue));
  780.  
  781. // Chance to drop Ammo
  782. if (Math.random() < AMMO_DROP_CHANCE) {
  783. // Drop ammo type based on player's needs or random? Let's do random for now.
  784. const ammoTypes = ['pistol', 'smg', 'shotgun'];
  785. const typeToDrop = ammoTypes[Math.floor(Math.random() * ammoTypes.length)];
  786. const amount = typeToDrop === 'shotgun' ? randomInRange(1, 3) : randomInRange(5, 15); // Adjust amounts
  787. ammoPickups.push(new AmmoPickup(this.x + randomInRange(-5, 5), this.y + randomInRange(-5, 5), typeToDrop, Math.floor(amount)));
  788. }
  789.  
  790. // Chance to drop PowerUp
  791. if (Math.random() < POWERUP_SPAWN_CHANCE) {
  792. powerUps.push(PowerUp.createRandom(this.x, this.y));
  793. }
  794.  
  795. // Death particles
  796. createParticles(this.x, this.y, 20, this.color, 100, null, Math.PI * 2, {friction: 0.96}); // More explosive particles
  797. playSound('enemy_death'); // Placeholder
  798. }
  799. }
  800.  
  801. class FastEnemy extends Enemy {
  802. constructor(x, y, wave) {
  803. super(x, y, wave, {
  804. type: 'fast',
  805. baseSpeed: ENEMY_START_SPEED * 1.5,
  806. baseHealth: ENEMY_START_HEALTH * 0.7,
  807. baseSize: ENEMY_START_SIZE * 0.8,
  808. color: `hsl(${randomInRange(200, 240)}, 80%, 60%)`, // Blue/Cyan hues
  809. xpValue: XP_PER_KILL * 0.8
  810. });
  811. }
  812.  
  813. // Override draw for different shape (e.g., triangle)
  814. draw(ctx) {
  815. const screenPos = worldToScreen(this.x, this.y);
  816. const s = this.size;
  817. ctx.fillStyle = this.color;
  818. ctx.beginPath();
  819. // Simple triangle pointing towards movement direction (or player)
  820. const angle = Math.atan2(this.vy, this.vx); // Angle of velocity
  821. ctx.moveTo(screenPos.x + Math.cos(angle) * s * 0.8, screenPos.y + Math.sin(angle) * s * 0.8);
  822. ctx.lineTo(screenPos.x + Math.cos(angle + Math.PI * 0.8) * s * 0.6, screenPos.y + Math.sin(angle + Math.PI * 0.8) * s * 0.6);
  823. ctx.lineTo(screenPos.x + Math.cos(angle - Math.PI * 0.8) * s * 0.6, screenPos.y + Math.sin(angle - Math.PI * 0.8) * s * 0.6);
  824. ctx.closePath();
  825. ctx.fill();
  826. this.drawHealthBar(ctx);
  827. }
  828. }
  829.  
  830. class TankEnemy extends Enemy {
  831. constructor(x, y, wave) {
  832. super(x, y, wave, {
  833. type: 'tank',
  834. baseSpeed: ENEMY_START_SPEED * 0.6,
  835. baseHealth: ENEMY_START_HEALTH * 2.5,
  836. baseSize: ENEMY_START_SIZE * 1.3,
  837. color: `hsl(${randomInRange(30, 60)}, 60%, 40%)`, // Dark Orange/Brown hues
  838. xpValue: XP_PER_KILL * 1.5,
  839. baseDamage: ENEMY_BASE_DAMAGE * 1.2
  840. });
  841. }
  842.  
  843. // Override draw for different shape (e.g., hexagon)
  844. draw(ctx) {
  845. const screenPos = worldToScreen(this.x, this.y);
  846. const s = this.size / 2;
  847. ctx.fillStyle = this.color;
  848. ctx.beginPath();
  849. for (let i = 0; i < 6; i++) {
  850. ctx.lineTo(screenPos.x + s * Math.cos(Math.PI / 3 * i), screenPos.y + s * Math.sin(Math.PI / 3 * i));
  851. }
  852. ctx.closePath();
  853. ctx.fill();
  854. this.drawHealthBar(ctx);
  855. }
  856. }
  857.  
  858. // --- Add RangedEnemy, ZigZagEnemy etc. later ---
  859.  
  860. class Projectile { /* ... (keep existing, maybe add owner check later) ... */
  861. constructor(x, y, vx, vy, damage, size, color, owner) {
  862. this.x = x;
  863. this.y = y;
  864. this.vx = vx;
  865. this.vy = vy;
  866. this.damage = damage;
  867. this.size = size;
  868. this.color = color;
  869. this.owner = owner; // Could be Player or an Enemy type
  870. this.life = 3; // Time to live in seconds
  871. }
  872.  
  873. draw(ctx) {
  874. const screenPos = worldToScreen(this.x, this.y);
  875. ctx.fillStyle = this.color;
  876. ctx.beginPath();
  877. ctx.arc(screenPos.x, screenPos.y, this.size, 0, Math.PI * 2);
  878. ctx.fill();
  879. }
  880.  
  881. update(dt) {
  882. this.x += this.vx * dt;
  883. this.y += this.vy * dt;
  884. this.life -= dt;
  885. }
  886. }
  887.  
  888. // --- Pickups ---
  889. let XP_ORB_MAGNET_RADIUS = 100; // Can be modified by power-ups
  890.  
  891. class XpOrb {
  892. constructor(x, y, value) { /* ... (keep most of existing) ... */
  893. this.x = x;
  894. this.y = y;
  895. this.value = value;
  896. this.size = XP_ORB_SIZE;
  897. this.color = XP_ORB_COLOR;
  898. this.magnetSpeed = 350; // Speed towards player when close
  899. // Magnet radius is now a global variable potentially modified by powerups
  900. }
  901.  
  902. draw(ctx) { /* ... (keep existing pulsating draw) ... */
  903. const screenPos = worldToScreen(this.x, this.y);
  904. ctx.fillStyle = this.color;
  905. ctx.beginPath();
  906. // Pulsating effect
  907. ctx.arc(screenPos.x, screenPos.y, this.size + Math.sin(gameTime * 6) * 1.5, 0, Math.PI * 2);
  908. ctx.fill();
  909. }
  910.  
  911. update(dt, target) {
  912. if (!target) return;
  913. // Move towards player if close enough
  914. const magnetRadius = XP_ORB_MAGNET_RADIUS; // Use the potentially modified radius
  915. const dist = distance(this.x, this.y, target.x, target.y);
  916. if (dist < magnetRadius && dist > 1) { // Avoid division by zero if right on top
  917. const moveDir = normalize(target.x - this.x, target.y - this.y);
  918. // Move faster when closer, up to magnetSpeed
  919. const speedFactor = Math.min(1, (magnetRadius - dist) / magnetRadius) * 1.5; // Non-linear attraction
  920. this.x += moveDir.x * this.magnetSpeed * speedFactor * dt;
  921. this.y += moveDir.y * this.magnetSpeed * speedFactor * dt;
  922. }
  923. }
  924. }
  925.  
  926. class AmmoPickup {
  927. constructor(x, y, ammoType, amount) {
  928. this.x = x;
  929. this.y = y;
  930. this.ammoType = ammoType;
  931. this.amount = amount;
  932. this.size = AMMO_PICKUP_SIZE;
  933. this.color = AMMO_PICKUP_COLOR; // Maybe different color per type?
  934. this.life = 20; // Disappears after some time
  935. }
  936.  
  937. draw(ctx) {
  938. const screenPos = worldToScreen(this.x, this.y);
  939. ctx.fillStyle = this.color;
  940. ctx.fillRect(screenPos.x - this.size / 2, screenPos.y - this.size / 2, this.size, this.size); // Simple square
  941. // Could draw text ('P', 'SG', 'SMG') on it
  942. ctx.fillStyle = 'black';
  943. ctx.font = 'bold 8px sans-serif';
  944. ctx.textAlign = 'center';
  945. ctx.textBaseline = 'middle';
  946. let ammoInitial = this.ammoType.substring(0, 1).toUpperCase();
  947. if (this.ammoType === 'shotgun') ammoInitial = 'SG';
  948. if (this.ammoType === 'smg') ammoInitial = 'SM';
  949. ctx.fillText(ammoInitial, screenPos.x, screenPos.y + 1);
  950. }
  951.  
  952. update(dt) {
  953. this.life -= dt;
  954. // Simple bobbing motion
  955. this.y += Math.sin(gameTime * 3 + this.x) * 0.2; // Offset phase by x position
  956. }
  957. }
  958.  
  959. class PowerUp {
  960. constructor(x, y, type, duration, color, symbol) {
  961. this.x = x;
  962. this.y = y;
  963. this.type = type; // e.g., 'invincibility', 'doubleDamage'
  964. this.duration = duration;
  965. this.size = POWERUP_SIZE;
  966. this.color = color;
  967. this.symbol = symbol; // Text symbol like 'S', '2x'
  968. this.life = 30; // Disappears after some time
  969. }
  970.  
  971. draw(ctx) {
  972. const screenPos = worldToScreen(this.x, this.y);
  973. // Draw background shape (e.g., star or circle)
  974. ctx.fillStyle = this.color;
  975. ctx.beginPath();
  976. // Simple star shape
  977. const outerRadius = this.size * 1.2;
  978. const innerRadius = this.size * 0.6;
  979. ctx.moveTo(screenPos.x, screenPos.y - outerRadius);
  980. for (let i = 0; i < 5; i++) {
  981. ctx.lineTo(screenPos.x + Math.cos((18 + i * 72) * Math.PI / 180) * outerRadius, screenPos.y - Math.sin((18 + i * 72) * Math.PI / 180) * outerRadius);
  982. ctx.lineTo(screenPos.x + Math.cos((54 + i * 72) * Math.PI / 180) * innerRadius, screenPos.y - Math.sin((54 + i * 72) * Math.PI / 180) * innerRadius);
  983. }
  984. ctx.closePath();
  985. ctx.fill();
  986.  
  987. // Draw symbol
  988. ctx.fillStyle = 'white';
  989. ctx.font = 'bold 10px sans-serif';
  990. ctx.textAlign = 'center';
  991. ctx.textBaseline = 'middle';
  992. ctx.fillText(this.symbol, screenPos.x, screenPos.y + 1);
  993. }
  994.  
  995. update(dt) {
  996. this.life -= dt;
  997. // Gentle rotation
  998. // this.angle = (this.angle || 0) + dt * 1; // Needs angle property and drawing rotation
  999. // Bobbing motion
  1000. this.y += Math.sin(gameTime * 2.5 + this.x) * 0.3;
  1001. }
  1002.  
  1003. static createRandom(x, y) {
  1004. const types = [
  1005. {type: 'invincibility', duration: 5, color: 'white', symbol: '★'},
  1006. {type: 'doubleDamage', duration: 10, color: 'red', symbol: '2x'},
  1007. {type: 'speedBoost', duration: 10, color: 'lightblue', symbol: '>>'},
  1008. {type: 'rapidFire', duration: 8, color: 'yellow', symbol: 'RF'},
  1009. {type: 'xpMultiplier', duration: 15, color: 'lime', symbol: 'XP'},
  1010. {type: 'magnetRadius', duration: 20, color: 'violet', symbol: 'M'}
  1011. ];
  1012. const choice = types[Math.floor(Math.random() * types.length)];
  1013. return new PowerUp(x, y, choice.type, choice.duration, choice.color, choice.symbol);
  1014. }
  1015. }
  1016.  
  1017.  
  1018. class Particle { /* ... (keep existing, but maybe add friction/gravity options later) ... */
  1019. constructor(x, y, vx, vy, size, color, life, options = {}) {
  1020. this.x = x;
  1021. this.y = y;
  1022. this.vx = vx;
  1023. this.vy = vy;
  1024. this.size = size;
  1025. this.color = color;
  1026. this.life = life;
  1027. this.initialLife = life;
  1028. this.friction = options.friction || 0.98; // Default friction
  1029. this.gravity = options.gravity || 0; // Default no gravity
  1030. }
  1031.  
  1032. draw(ctx) {
  1033. const screenPos = worldToScreen(this.x, this.y);
  1034. const lifeRatio = Math.max(0, this.life / this.initialLife);
  1035. ctx.globalAlpha = lifeRatio; // Fade out
  1036. ctx.fillStyle = this.color;
  1037. ctx.beginPath();
  1038. // Shrink based on lifeRatio - make shrinking optional or configurable?
  1039. ctx.arc(screenPos.x, screenPos.y, this.size * lifeRatio, 0, Math.PI * 2);
  1040. ctx.fill();
  1041. ctx.globalAlpha = 1.0; // Reset alpha
  1042. }
  1043.  
  1044. update(dt) {
  1045. this.x += this.vx * dt;
  1046. this.y += this.vy * dt;
  1047. this.vy += this.gravity * dt; // Apply gravity
  1048. this.vx *= this.friction; // Apply friction
  1049. this.vy *= this.friction;
  1050. this.life -= dt;
  1051. }
  1052. }
  1053.  
  1054. // --- Game Logic ---
  1055. function init() {
  1056. console.log("Initializing game...");
  1057. isGameOver = false;
  1058. gameContainerDiv.style.display = 'block'; // Show game area
  1059. colorSelectionDiv.style.display = 'none'; // Hide color selection
  1060.  
  1061. // Get selected player color
  1062. const selectedButton = colorSelectionDiv.querySelector('button.selected');
  1063. PLAYER_COLOR = selectedButton ? selectedButton.dataset.color : 'blue';
  1064.  
  1065. canvas.width = window.innerWidth * 0.9;
  1066. canvas.height = window.innerHeight * 0.9;
  1067.  
  1068. if (typeof Noise === 'undefined') {
  1069. console.warn("Noise library not found. Background will be simple grid.");
  1070. noise = null;
  1071. } else {
  1072. noise = new Noise(Math.random()); // Initialize noise generator
  1073. console.log("Noise library initialized for background.");
  1074. }
  1075.  
  1076. initAudio(); // Initialize Web Audio API context
  1077.  
  1078. player = new Player(0, 0, PLAYER_COLOR);
  1079. camera.x = player.x;
  1080. camera.y = player.y;
  1081. camera.shakeIntensity = 0;
  1082. camera.shakeDuration = 0;
  1083.  
  1084. // Reset game state arrays
  1085. enemies = [];
  1086. projectiles = [];
  1087. xpOrbs = [];
  1088. ammoPickups = [];
  1089. powerUps = [];
  1090. particles = [];
  1091.  
  1092. keys = {};
  1093. mouse = {x: 0, y: 0, down: false};
  1094. currentWave = 1;
  1095. gameTime = 0;
  1096. choosingLevelUp = false;
  1097. levelUpOptionsDiv.style.display = 'none';
  1098.  
  1099. // Remove old listeners before adding new ones (important for restart)
  1100. window.removeEventListener('keydown', handleKeyDown);
  1101. window.removeEventListener('keyup', handleKeyUp);
  1102. window.removeEventListener('mousedown', handleMouseDown);
  1103. window.removeEventListener('mouseup', handleMouseUp);
  1104. window.removeEventListener('mousemove', handleMouseMove);
  1105. window.removeEventListener('resize', handleResize);
  1106.  
  1107. // Add Event Listeners
  1108. window.addEventListener('keydown', handleKeyDown);
  1109. window.addEventListener('keyup', handleKeyUp);
  1110. window.addEventListener('mousedown', handleMouseDown);
  1111. window.addEventListener('mouseup', handleMouseUp);
  1112. window.addEventListener('mousemove', handleMouseMove);
  1113. window.addEventListener('resize', handleResize);
  1114.  
  1115.  
  1116. startWave();
  1117. lastTime = performance.now();
  1118. updateUI(); // Initial UI update
  1119. requestAnimationFrame(gameLoop);
  1120. console.log("Game initialized and loop started.");
  1121. }
  1122.  
  1123. // --- Event Handlers --- (Separate functions for easier removal)
  1124. function handleKeyDown(e) {
  1125. keys[e.key.toLowerCase()] = true;
  1126. }
  1127.  
  1128. function handleKeyUp(e) {
  1129. keys[e.key.toLowerCase()] = false;
  1130. }
  1131.  
  1132. function handleMouseDown(e) {
  1133. if (e.button === 0) mouse.down = true;
  1134. }
  1135.  
  1136. function handleMouseUp(e) {
  1137. if (e.button === 0) mouse.down = false;
  1138. }
  1139.  
  1140. function handleMouseMove(e) {
  1141. const rect = canvas.getBoundingClientRect();
  1142. mouse.x = e.clientX - rect.left;
  1143. mouse.y = e.clientY - rect.top;
  1144. }
  1145.  
  1146. function handleResize() {
  1147. canvas.width = window.innerWidth * 0.9;
  1148. canvas.height = window.innerHeight * 0.9;
  1149. // Optional: Adjust camera or UI if needed on resize
  1150. }
  1151.  
  1152. function setupColorSelection() {
  1153. colorSelectionDiv.querySelectorAll('button').forEach(button => {
  1154. button.style.backgroundColor = button.dataset.color; // Show color on button
  1155. button.addEventListener('click', () => {
  1156. colorSelectionDiv.querySelector('button.selected')?.classList.remove('selected');
  1157. button.classList.add('selected');
  1158. });
  1159. });
  1160. // Add a start button
  1161. const startButton = document.createElement('button');
  1162. startButton.textContent = 'Start Game';
  1163. startButton.style.marginTop = '10px';
  1164. startButton.style.padding = '10px 20px';
  1165. startButton.onclick = init; // Call init when start is clicked
  1166. colorSelectionDiv.appendChild(startButton);
  1167. }
  1168.  
  1169.  
  1170. function startWave() {
  1171. enemiesToSpawn = 5 + Math.floor(currentWave * 1.8); // Slightly more enemies per wave
  1172. enemiesAlive = 0; // Reset count of active enemies
  1173. spawningDone = false;
  1174. timeToNextSpawn = 1.0; // Longer initial delay for wave start
  1175. console.log(`Starting Wave ${currentWave} with ${enemiesToSpawn} enemies.`);
  1176. // Maybe wave start announcement/effect?
  1177. updateUI();
  1178. }
  1179.  
  1180. function spawnEnemy() {
  1181. if (enemiesToSpawn <= 0) {
  1182. spawningDone = true;
  1183. return;
  1184. }
  1185.  
  1186. // Spawn enemies off-screen relative to the camera's view
  1187. const spawnDist = Math.max(canvas.width, canvas.height) * 0.7; // Spawn closer than before?
  1188. const angle = Math.random() * Math.PI * 2;
  1189. const spawnX = player.x + Math.cos(angle) * spawnDist;
  1190. const spawnY = player.y + Math.sin(angle) * spawnDist;
  1191.  
  1192. // --- Choose Enemy Type ---
  1193. let enemyTypeChoice = Math.random();
  1194. let newEnemy;
  1195. if (currentWave > 3 && enemyTypeChoice < 0.25) { // Introduce tanks after wave 3
  1196. newEnemy = new TankEnemy(spawnX, spawnY, currentWave);
  1197. } else if (currentWave > 1 && enemyTypeChoice < 0.6) { // Introduce fast enemies after wave 1
  1198. newEnemy = new FastEnemy(spawnX, spawnY, currentWave);
  1199. } else {
  1200. newEnemy = new Enemy(spawnX, spawnY, currentWave); // Standard enemy
  1201. }
  1202. enemies.push(newEnemy);
  1203.  
  1204. enemiesToSpawn--;
  1205. enemiesAlive++; // Increment count of enemies currently alive
  1206. updateUI();
  1207.  
  1208. // Time until next spawn decreases with wave number, but has a minimum
  1209. timeToNextSpawn = Math.max(0.08, 1.0 - currentWave * 0.04);
  1210. }
  1211.  
  1212. function update(dt) {
  1213. if (isGameOver || !player) return; // Stop updates if game over or player missing
  1214.  
  1215. gameTime += dt;
  1216.  
  1217. // Update Camera Shake
  1218. if (camera.shakeDuration > 0) {
  1219. camera.shakeDuration -= dt;
  1220. camera.shakeIntensity *= CAMERA_SHAKE_DECAY; // Dampen shake
  1221. if (camera.shakeDuration <= 0) {
  1222. camera.shakeIntensity = 0;
  1223. camera.shakeDuration = 0;
  1224. }
  1225. }
  1226.  
  1227.  
  1228. // Don't update game logic while choosing level up? (Current setup doesn't pause)
  1229. // if (choosingLevelUp) return;
  1230.  
  1231. player.update(dt);
  1232.  
  1233. // Update Camera to follow player smoothly
  1234. const cameraLag = 0.08; // Smaller value = tighter follow
  1235. camera.x += (player.x - camera.x) * cameraLag;
  1236. camera.y += (player.y - camera.y) * cameraLag;
  1237.  
  1238.  
  1239. // Spawning
  1240. if (!spawningDone) {
  1241. timeToNextSpawn -= dt;
  1242. if (timeToNextSpawn <= 0) {
  1243. spawnEnemy();
  1244. }
  1245. }
  1246.  
  1247. // Update Enemies
  1248. enemies.forEach(enemy => enemy.update(dt, player));
  1249.  
  1250. // Update Projectiles
  1251. projectiles.forEach(p => p.update(dt));
  1252.  
  1253. // Update XP Orbs
  1254. xpOrbs.forEach(orb => orb.update(dt, player));
  1255.  
  1256. // Update Ammo Pickups
  1257. ammoPickups.forEach(ap => ap.update(dt));
  1258.  
  1259. // Update PowerUps
  1260. powerUps.forEach(pu => pu.update(dt));
  1261.  
  1262. // Update Particles
  1263. particles.forEach(p => p.update(dt));
  1264.  
  1265.  
  1266. // --- Cleanup Dead/Expired Entities ---
  1267. projectiles = projectiles.filter(p => p.life > 0);
  1268. enemies = enemies.filter(e => e.health > 0); // Filter out dead enemies AFTER processing hits/death logic
  1269. xpOrbs = xpOrbs.filter(orb => distance(player.x, player.y, orb.x, orb.y) > 1); // Keep orbs not collected yet
  1270. ammoPickups = ammoPickups.filter(ap => ap.life > 0 && distance(player.x, player.y, ap.x, ap.y) > player.size); // Keep if alive and not collected
  1271. powerUps = powerUps.filter(pu => pu.life > 0 && distance(player.x, player.y, pu.x, pu.y) > player.size); // Keep if alive and not collected
  1272. particles = particles.filter(p => p.life > 0);
  1273.  
  1274. // Recalculate enemiesAlive count after filtering
  1275. enemiesAlive = enemies.length;
  1276.  
  1277.  
  1278. // Collisions
  1279. checkCollisions();
  1280.  
  1281. // Check for next wave (only if player exists)
  1282. if (player && spawningDone && enemiesAlive <= 0) {
  1283. currentWave++;
  1284. startWave();
  1285. // Maybe give a small health/ammo bonus between waves?
  1286. player.health = Math.min(player.maxHealth, player.health + player.maxHealth * 0.1); // Heal 10%
  1287. updateUI();
  1288. }
  1289. }
  1290.  
  1291. function checkCollisions() {
  1292. if (!player) return;
  1293.  
  1294. // Projectile vs Enemy
  1295. for (let i = projectiles.length - 1; i >= 0; i--) {
  1296. const p = projectiles[i];
  1297. let projectileRemoved = false;
  1298. for (let j = enemies.length - 1; j >= 0; j--) {
  1299. if (projectileRemoved) break; // Projectile already hit something
  1300.  
  1301. const e = enemies[j];
  1302. if (e.health <= 0) continue; // Skip dead enemies
  1303.  
  1304. // More accurate circle-shape collision check
  1305. const distSq = (p.x - e.x) ** 2 + (p.y - e.y) ** 2;
  1306. const radiiSumSq = (p.size + e.size / 2) ** 2; // Approximate enemy radius
  1307.  
  1308. if (distSq < radiiSumSq) {
  1309. const died = e.takeDamage(p.damage);
  1310. playSound('hit_enemy'); // Placeholder
  1311.  
  1312. // Remove projectile (unless it pierces - add logic later)
  1313. projectiles.splice(i, 1);
  1314. projectileRemoved = true;
  1315.  
  1316. if (died) {
  1317. e.onDeath(); // Handle drops, particles etc.
  1318. // Enemy removal is handled in the main update loop's filter
  1319. updateUI(); // Update enemies left count
  1320. }
  1321. // break; // Projectile hits only one enemy (usually)
  1322. }
  1323. }
  1324. }
  1325.  
  1326. // Player vs Enemy
  1327. enemies.forEach(e => {
  1328. if (e.health <= 0 || !player) return; // Skip dead enemies or if player gone
  1329.  
  1330. const distBetween = distance(player.x, player.y, e.x, e.y);
  1331. const collisionThreshold = player.size + e.size / 2;
  1332.  
  1333. if (distBetween < collisionThreshold) {
  1334. // Apply damage over time based on overlap? Or just per-touch?
  1335. // Let's stick to damage-per-frame-of-contact for now
  1336. player.takeDamage(e.damage * (1 / 60)); // Damage scaled by typical frame time
  1337.  
  1338. // Push apart based on overlap
  1339. const overlap = collisionThreshold - distBetween;
  1340. if (distBetween > 0.1) { // Avoid division by zero if perfectly overlapped
  1341. const moveDir = normalize(player.x - e.x, player.y - e.y);
  1342. const pushForce = overlap * 0.5; // Push strength
  1343. player.x += moveDir.x * pushForce;
  1344. player.y += moveDir.y * pushForce;
  1345. e.x -= moveDir.x * pushForce;
  1346. e.y -= moveDir.y * pushForce;
  1347. } else { // If perfectly overlapped, push in a random direction slightly
  1348. const randAngle = Math.random() * Math.PI * 2;
  1349. player.x += Math.cos(randAngle) * 0.5;
  1350. player.y += Math.sin(randAngle) * 0.5;
  1351. }
  1352. }
  1353. });
  1354.  
  1355.  
  1356. // Player vs XP Orb
  1357. for (let i = xpOrbs.length - 1; i >= 0; i--) {
  1358. const orb = xpOrbs[i];
  1359. if (distance(player.x, player.y, orb.x, orb.y) < player.size + orb.size) {
  1360. player.addXp(orb.value);
  1361. xpOrbs.splice(i, 1); // Remove orb
  1362. playSound('pickup_xp'); // Placeholder
  1363. createParticles(player.x, player.y, 3, XP_ORB_COLOR, 30, null, Math.PI * 2, {life: 0.2}); // Small pickup effect
  1364. }
  1365. }
  1366.  
  1367. // Player vs Ammo Pickup
  1368. for (let i = ammoPickups.length - 1; i >= 0; i--) {
  1369. const pickup = ammoPickups[i];
  1370. if (distance(player.x, player.y, pickup.x, pickup.y) < player.size + pickup.size) {
  1371. const weapon = player.weapons.find(w => w.ammoType === pickup.ammoType);
  1372. if (weapon) {
  1373. const currentAmmo = player.ammo[pickup.ammoType] || 0;
  1374. const maxAmmo = weapon.maxAmmo;
  1375. player.ammo[pickup.ammoType] = Math.min(maxAmmo, currentAmmo + pickup.amount);
  1376. ammoPickups.splice(i, 1);
  1377. console.log(`Picked up ${pickup.amount} ${pickup.ammoType} ammo.`);
  1378. playSound('pickup_ammo'); // Placeholder
  1379. updateUI();
  1380. createParticles(player.x, player.y, 5, AMMO_PICKUP_COLOR, 40, null, Math.PI * 2, {life: 0.3});
  1381. } else {
  1382. // Maybe allow picking up even if no weapon uses it yet?
  1383. // console.log(`No weapon uses ammo type: ${pickup.ammoType}`);
  1384. ammoPickups.splice(i, 1); // Remove anyway to avoid clutter
  1385. }
  1386. }
  1387. }
  1388.  
  1389. // Player vs PowerUp
  1390. for (let i = powerUps.length - 1; i >= 0; i--) {
  1391. const pu = powerUps[i];
  1392. if (distance(player.x, player.y, pu.x, pu.y) < player.size + pu.size) {
  1393. player.activatePowerUp(pu.type, pu.duration);
  1394. powerUps.splice(i, 1);
  1395. // Powerup pickup particle effect
  1396. createParticles(player.x, player.y, 20, pu.color, 150, null, Math.PI * 2, {life: 0.6});
  1397. }
  1398. }
  1399. }
  1400.  
  1401.  
  1402. function render() {
  1403. // Clear canvas
  1404. // Use fillRect for background color instead of relying on body style
  1405. ctx.fillStyle = '#1a1a1a'; // Canvas background color
  1406. ctx.fillRect(0, 0, canvas.width, canvas.height);
  1407.  
  1408. // Draw background (Noise or Grid)
  1409. drawBackground();
  1410.  
  1411. // --- Draw Game Objects --- (Order matters for layering)
  1412.  
  1413. // Draw Pickups first (lowest layer)
  1414. xpOrbs.forEach(orb => orb.draw(ctx));
  1415. ammoPickups.forEach(ap => ap.draw(ctx));
  1416. powerUps.forEach(pu => pu.draw(ctx));
  1417.  
  1418. // Draw Enemies
  1419. enemies.forEach(enemy => enemy.draw(ctx));
  1420.  
  1421. // Draw Player
  1422. if (player) {
  1423. player.draw(ctx);
  1424. }
  1425.  
  1426. // Draw Projectiles
  1427. projectiles.forEach(p => p.draw(ctx));
  1428.  
  1429. // Draw Particles (Highest layer)
  1430. particles.forEach(p => p.draw(ctx));
  1431.  
  1432. }
  1433.  
  1434. function drawBackground() {
  1435. if (noise) {
  1436. // Procedural Noise Background
  1437. const TILE_RENDER_SIZE = GRID_SIZE; // Size to render each noise 'pixel'
  1438. ctx.globalAlpha = 0.4; // Make background less prominent
  1439.  
  1440. // Calculate visible world bounds based on camera and canvas size
  1441. const viewWidth = canvas.width * 1.1; // Render slightly larger than view
  1442. const viewHeight = canvas.height * 1.1;
  1443. const startWorldX = camera.x - viewWidth / 2;
  1444. const startWorldY = camera.y - viewHeight / 2;
  1445. const endWorldX = startWorldX + viewWidth;
  1446. const endWorldY = startWorldY + viewHeight;
  1447.  
  1448. // Determine tile range to draw
  1449. const startTileX = Math.floor(startWorldX / TILE_RENDER_SIZE);
  1450. const startTileY = Math.floor(startWorldY / TILE_RENDER_SIZE);
  1451. const endTileX = Math.ceil(endWorldX / TILE_RENDER_SIZE);
  1452. const endTileY = Math.ceil(endWorldY / TILE_RENDER_SIZE);
  1453.  
  1454. // Color variation based on wave (simple example: shift hue)
  1455. const baseHue = 180 + (currentWave * 5) % 180; // Shifts from cyan towards green/yellow/red over waves
  1456.  
  1457. for (let ty = startTileY; ty < endTileY; ty++) {
  1458. for (let tx = startTileX; tx < endTileX; tx++) {
  1459. const worldX = tx * TILE_RENDER_SIZE;
  1460. const worldY = ty * TILE_RENDER_SIZE;
  1461.  
  1462. // Get noise value for this tile's world position
  1463. // Divide by noise scale to control the 'zoom' level of the noise pattern
  1464. const noiseValue = noise.simplex2(worldX / BACKGROUND_NOISE_SCALE, worldY / BACKGROUND_NOISE_SCALE); // Value between -1 and 1
  1465.  
  1466. // Map noise value to color (e.g., grayscale or color variations)
  1467. // Example: Map to lightness/saturation variations of the base hue
  1468. const lightness = Math.floor(5 + (noiseValue + 1) / 2 * 20); // Map -1..1 to 5..25
  1469. const saturation = Math.floor(30 + (noiseValue + 1) / 2 * 30); // Map -1..1 to 30..60
  1470.  
  1471. ctx.fillStyle = `hsl(${baseHue}, ${saturation}%, ${lightness}%)`;
  1472.  
  1473. // Convert tile world coords to screen coords for drawing
  1474. const screenPos = worldToScreen(worldX, worldY);
  1475. ctx.fillRect(screenPos.x - TILE_RENDER_SIZE / 2, screenPos.y - TILE_RENDER_SIZE / 2, TILE_RENDER_SIZE, TILE_RENDER_SIZE); // Draw tile centered? No, top-left
  1476. // Correction: Calculate top-left screen pos directly
  1477. const screenTileX = (worldX - camera.x + canvas.width / 2);
  1478. const screenTileY = (worldY - camera.y + canvas.height / 2);
  1479. ctx.fillRect(screenTileX, screenTileY, TILE_RENDER_SIZE, TILE_RENDER_SIZE);
  1480.  
  1481. }
  1482. }
  1483. ctx.globalAlpha = 1.0; // Reset alpha
  1484.  
  1485. } else {
  1486. // Fallback Grid Background
  1487. ctx.strokeStyle = '#444';
  1488. ctx.lineWidth = 1;
  1489. const startWorldX = camera.x - canvas.width / 2;
  1490. const startWorldY = camera.y - canvas.height / 2;
  1491. const offsetX = startWorldX % GRID_SIZE;
  1492. const offsetY = startWorldY % GRID_SIZE;
  1493. const startScreenX = -offsetX;
  1494. const startScreenY = -offsetY;
  1495.  
  1496. for (let x = startScreenX; x < canvas.width; x += GRID_SIZE) {
  1497. ctx.beginPath();
  1498. ctx.moveTo(x, 0);
  1499. ctx.lineTo(x, canvas.height);
  1500. ctx.stroke();
  1501. }
  1502. for (let y = startScreenY; y < canvas.height; y += GRID_SIZE) {
  1503. ctx.beginPath();
  1504. ctx.moveTo(0, y);
  1505. ctx.lineTo(canvas.width, y);
  1506. ctx.stroke();
  1507. }
  1508. }
  1509. }
  1510.  
  1511.  
  1512. function createParticles(x, y, count, color, speed, baseAngle = null, spread = Math.PI * 2, options = {}) {
  1513. if (particles.length > MAX_PARTICLES - count) {
  1514. // Prune oldest particles if limit reached - shift based on count needed
  1515. particles.splice(0, particles.length + count - MAX_PARTICLES);
  1516. }
  1517. for (let i = 0; i < count; i++) {
  1518. const angle = (baseAngle !== null ? baseAngle : Math.random() * Math.PI * 2) + (Math.random() - 0.5) * spread;
  1519. const currentSpeed = speed * (0.5 + Math.random()); // Vary speed more (0.5x to 1.5x)
  1520. const vx = Math.cos(angle) * currentSpeed;
  1521. const vy = Math.sin(angle) * currentSpeed;
  1522. const size = options.size || (Math.random() * 2 + 1);
  1523. const life = (options.life || PARTICLE_LIFETIME) * (0.7 + Math.random() * 0.6); // Vary life more
  1524.  
  1525. // Allow color variations slightly
  1526. let particleColor = color;
  1527. if (options.colorVariance) {
  1528. // Example: Adjust lightness slightly
  1529. // This requires parsing hsl, modifying, and converting back, or using a library.
  1530. // Simple approach: Assume color is hex, slightly change value (crude)
  1531. // For HSL colors, easier: `hsl(${hue}, ${sat}%, ${light + random(-5,5)}%)`
  1532. }
  1533.  
  1534. particles.push(new Particle(x, y, vx, vy, size, particleColor, life, options)); // Pass options
  1535. }
  1536. }
  1537.  
  1538.  
  1539. function updateUI() {
  1540. if (!player) return; // Don't update if player doesn't exist
  1541.  
  1542. uiLevel.textContent = player.level;
  1543. uiXp.textContent = Math.floor(player.xp);
  1544. uiXpNeeded.textContent = player.xpToNextLevel;
  1545. uiHealth.textContent = Math.ceil(player.health);
  1546. uiMaxHealth.textContent = player.maxHealth;
  1547. uiWave.textContent = currentWave;
  1548. uiEnemiesLeft.textContent = enemiesAlive + enemiesToSpawn; // Total remaining for the wave
  1549.  
  1550. // Weapon and Ammo
  1551. uiCurrentWeapon.textContent = player.currentWeapon.name;
  1552. const ammoType = player.currentWeapon.ammoType;
  1553. if (ammoType) {
  1554. const currentAmmo = player.ammo[ammoType] || 0;
  1555. uiAmmo.textContent = `${currentAmmo} / ${player.currentWeapon.maxAmmo}`;
  1556. } else if (player.currentWeapon.type === 'melee') {
  1557. uiAmmo.textContent = `Swing: ${player.currentWeapon.rateOfFire}/s`;
  1558. } else {
  1559. uiAmmo.textContent = "Inf"; // Infinite ammo or N/A
  1560. }
  1561.  
  1562. // Power-up Status
  1563. let powerUpHtml = '';
  1564. for (const type in player.powerUps) {
  1565. if (player.powerUps[type] > 0) {
  1566. powerUpHtml += `<div class="power-up-timer">${type}: ${player.powerUps[type].toFixed(1)}s</div>`;
  1567. }
  1568. }
  1569. uiPowerUpStatus.innerHTML = powerUpHtml;
  1570. }
  1571.  
  1572.  
  1573. function presentLevelUpOptions() {
  1574. if (!player) return;
  1575. choosingLevelUp = true;
  1576. levelUpOptionsDiv.innerHTML = ''; // Clear previous options
  1577. levelUpOptionsDiv.style.display = 'flex';
  1578.  
  1579. const options = generateUpgradeOptions(3); // Get 3 options
  1580.  
  1581. options.forEach(option => {
  1582. const button = document.createElement('button');
  1583. button.classList.add('level-up-button');
  1584. button.textContent = option.description;
  1585. button.onclick = () => {
  1586. applyUpgrade(option);
  1587. levelUpOptionsDiv.style.display = 'none';
  1588. choosingLevelUp = false;
  1589. createParticles(player.x, player.y, 30, 'gold', 100); // Particle burst on choice
  1590. playSound('upgrade_select'); // Placeholder
  1591. };
  1592. levelUpOptionsDiv.appendChild(button);
  1593. });
  1594. }
  1595.  
  1596. function generateUpgradeOptions(count) {
  1597. if (!player) return [];
  1598.  
  1599. const possibleUpgrades = [];
  1600.  
  1601. // --- Player Stat Upgrades ---
  1602. possibleUpgrades.push({type: 'player_stat', stat: 'maxHealth', value: 20, description: 'Max Health +20'});
  1603. possibleUpgrades.push({type: 'player_stat', stat: 'speed', value: 15, description: 'Move Speed +15'});
  1604. possibleUpgrades.push({type: 'player_stat', stat: 'magnet', value: 25, description: 'XP Magnet Radius +25%'}); // Value as percentage increase
  1605.  
  1606. // --- Weapon Specific Upgrades ---
  1607. player.weapons.forEach((weapon, index) => {
  1608. if (weapon.type === 'ranged') {
  1609. possibleUpgrades.push({
  1610. type: 'weapon_stat',
  1611. index: index,
  1612. stat: 'damage',
  1613. value: Math.ceil(weapon.damage * 0.15),
  1614. description: `${weapon.name} Damage +${Math.ceil(weapon.damage * 0.15)}`
  1615. }); // Scale increase by base damage
  1616. possibleUpgrades.push({
  1617. type: 'weapon_stat',
  1618. index: index,
  1619. stat: 'rateOfFire',
  1620. value: weapon.rateOfFire * 0.1,
  1621. description: `${weapon.name} Fire Rate +10%`
  1622. }); // Percentage based increase
  1623. if (weapon.maxAmmo) {
  1624. possibleUpgrades.push({
  1625. type: 'weapon_stat',
  1626. index: index,
  1627. stat: 'maxAmmo',
  1628. value: Math.ceil(weapon.maxAmmo * 0.2),
  1629. description: `${weapon.name} Max Ammo +20%`
  1630. });
  1631. }
  1632. if (weapon.projectileCount > 1) { // Shotgun spread reduce?
  1633. // possibleUpgrades.push({ type: 'weapon_stat', index: index, stat: 'spreadAngle', value: -0.05, description: `${weapon.name} Spread -0.05rad` });
  1634. }
  1635. if (weapon.recoil > 0) {
  1636. possibleUpgrades.push({
  1637. type: 'weapon_stat',
  1638. index: index,
  1639. stat: 'recoil',
  1640. value: -weapon.recoil * 0.15,
  1641. description: `${weapon.name} Recoil -15%`
  1642. });
  1643. }
  1644. // Could add projectile speed, accuracy etc.
  1645. } else if (weapon.type === 'melee') {
  1646. possibleUpgrades.push({
  1647. type: 'weapon_stat',
  1648. index: index,
  1649. stat: 'damage',
  1650. value: Math.ceil(weapon.damage * 0.2),
  1651. description: `${weapon.name} Damage +${Math.ceil(weapon.damage * 0.2)}`
  1652. });
  1653. possibleUpgrades.push({
  1654. type: 'weapon_stat',
  1655. index: index,
  1656. stat: 'rateOfFire',
  1657. value: weapon.rateOfFire * 0.15,
  1658. description: `${weapon.name} Swing Speed +15%`
  1659. });
  1660. possibleUpgrades.push({
  1661. type: 'weapon_stat',
  1662. index: index,
  1663. stat: 'range',
  1664. value: 5,
  1665. description: `${weapon.name} Range +5`
  1666. });
  1667. // Could add swing arc?
  1668. }
  1669. });
  1670.  
  1671. // Add finding a new weapon? (More complex logic needed)
  1672.  
  1673. // Shuffle and pick 'count' unique options
  1674. const shuffled = possibleUpgrades.sort(() => 0.5 - Math.random());
  1675.  
  1676. // Ensure variety? (e.g., avoid offering 3 upgrades for the same weapon) - simple slice for now
  1677. return shuffled.slice(0, count);
  1678. }
  1679.  
  1680. function applyUpgrade(option) {
  1681. if (!player) return;
  1682. console.log("Applying upgrade:", option.description);
  1683.  
  1684. if (option.type === 'weapon_stat') {
  1685. const weapon = player.weapons[option.index];
  1686. if (weapon) {
  1687. weapon[option.stat] += option.value;
  1688. // Ensure some stats don't go below reasonable minimums
  1689. if (option.stat === 'rateOfFire') weapon[option.stat] = Math.max(0.1, weapon[option.stat]);
  1690. if (option.stat === 'spreadAngle') weapon[option.stat] = Math.max(0, weapon[option.stat]);
  1691. if (option.stat === 'recoil') weapon[option.stat] = Math.max(0, weapon[option.stat]);
  1692.  
  1693. console.log(`New ${weapon.name} ${option.stat}: ${weapon[option.stat].toFixed(2)}`);
  1694. // If current weapon was upgraded, update UI immediately
  1695. if (player.weapons[player.currentWeaponIndex] === weapon) {
  1696. updateUI();
  1697. }
  1698. } else {
  1699. console.warn(`Tried to upgrade non-existent weapon at index: ${option.index}`);
  1700. }
  1701. } else if (option.type === 'player_stat') {
  1702. if (option.stat === 'maxHealth') {
  1703. player.maxHealth += option.value;
  1704. player.health += option.value; // Also heal by the amount increased
  1705. } else if (option.stat === 'speed') {
  1706. player.baseSpeed += option.value; // Upgrade the base speed
  1707. player.speed = player.baseSpeed * (player.powerUps.speedBoost > 0 ? 1.5 : 1); // Reapply boost if active
  1708. } else if (option.stat === 'magnet') {
  1709. player.baseMagnetRadius *= (1 + option.value / 100); // Increase base radius by percentage
  1710. XP_ORB_MAGNET_RADIUS = player.baseMagnetRadius * (player.powerUps.magnetRadius > 0 ? 2.0 : 1); // Reapply boost
  1711. }
  1712. console.log(`New player ${option.stat}: ${player[option.stat] !== undefined ? player[option.stat] : player.baseMagnetRadius}`);
  1713. }
  1714. updateUI(); // Update UI for player stats or potentially weapon stats if current
  1715. }
  1716.  
  1717. function gameOver() {
  1718. if (isGameOver) return; // Prevent multiple calls
  1719. isGameOver = true;
  1720. console.log("Game Over!");
  1721. playSound('game_over'); // Placeholder
  1722.  
  1723. // More dramatic effect? Stop sounds, screen fade?
  1724. applyCameraShake(30, 0.5); // Final shake
  1725.  
  1726. // Use timeout to allow effects to play before alert/reset
  1727. setTimeout(() => {
  1728. alert(`Game Over! You reached Wave ${currentWave} and Level ${player.level}.`);
  1729.  
  1730. // Clean reset
  1731. player = null; // Stop rendering/updating player specifically
  1732. enemies = [];
  1733. projectiles = [];
  1734. xpOrbs = [];
  1735. ammoPickups = [];
  1736. powerUps = [];
  1737. particles = [];
  1738. keys = {};
  1739. mouse = {x: 0, y: 0, down: false};
  1740. choosingLevelUp = false;
  1741. levelUpOptionsDiv.style.display = 'none';
  1742. gameContainerDiv.style.display = 'none'; // Hide game area
  1743. colorSelectionDiv.style.display = 'block'; // Show color selection again
  1744.  
  1745. // Remove listeners to prevent duplicates on restart
  1746. window.removeEventListener('keydown', handleKeyDown);
  1747. window.removeEventListener('keyup', handleKeyUp);
  1748. window.removeEventListener('mousedown', handleMouseDown);
  1749. window.removeEventListener('mouseup', handleMouseUp);
  1750. window.removeEventListener('mousemove', handleMouseMove);
  1751. window.removeEventListener('resize', handleResize);
  1752.  
  1753. // Don't automatically restart, wait for player to click start again
  1754. console.log("Game state reset. Ready for new game.");
  1755.  
  1756. }, 500); // Delay before alert/reset
  1757. }
  1758.  
  1759.  
  1760. let animationFrameId = null; // To potentially cancel the loop
  1761. function gameLoop(currentTime) {
  1762. if (isGameOver && player === null) { // Ensure loop stops cleanly after reset
  1763. console.log("Game loop stopped.");
  1764. cancelAnimationFrame(animationFrameId);
  1765. return;
  1766. }
  1767.  
  1768. const dt = Math.min(0.05, (currentTime - lastTime) / 1000); // Delta time in seconds, with frame cap
  1769. lastTime = currentTime;
  1770.  
  1771. if (!choosingLevelUp) { // Only update game state if not in level up screen (optional pause)
  1772. update(dt);
  1773. } else {
  1774. // Optionally update *some* things while paused, like particles or UI animations
  1775. gameTime += dt; // Keep game time running for animations?
  1776. particles.forEach(p => p.update(dt));
  1777. particles = particles.filter(p => p.life > 0);
  1778. // Update Camera Shake even during level up pause
  1779. if (camera.shakeDuration > 0) {
  1780. camera.shakeDuration -= dt;
  1781. camera.shakeIntensity *= CAMERA_SHAKE_DECAY;
  1782. if (camera.shakeDuration <= 0) {
  1783. camera.shakeIntensity = 0;
  1784. camera.shakeDuration = 0;
  1785. }
  1786. }
  1787.  
  1788. }
  1789.  
  1790. render();
  1791. updateUI(); // Update UI constantly, even during level up
  1792.  
  1793. animationFrameId = requestAnimationFrame(gameLoop);
  1794. }
  1795.  
  1796. // --- Start Game ---
  1797. // Set up the color selection UI first, which then calls init()
  1798. window.onload = setupColorSelection;
  1799.  
  1800.  
  1801. </script>
  1802.  
  1803. </body>
  1804. </html>
Advertisement
Add Comment
Please, Sign In to add comment