const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); const scoreElement = document.getElementById('score'); const healthElement = document.getElementById('health'); const startMessage = document.getElementById('startMessage'); //const weaponSelector = document.getElementById('weaponSelector'); const playButton = document.getElementById('playButton'); const quitButton = document.getElementById('quitButton'); const imageCache = new Map(); const bulletImageCache = new Map(); const enemyImageCache = new Map(); canvas.width = window.innerWidth; canvas.height = window.innerHeight; const MAP_WIDTH = 3000; const MAP_HEIGHT = 2000; let audioContext, analyser, source; let audioElement; let player, camera; let floatingTexts = []; let otherPlayers = {}; let serverEnemies = []; let serverBullets = []; let particles = []; let score = 0, health = 100; let gameStarted = false; let joystick = { active: false, startX: 0, startY: 0, endX: 0, endY: 0 }; let isDebugging = document.getElementById('debugCheckbox').checked; let socket; let currentSongDuration = 0; let songStartTime = 0; let backgroundCanvas, backgroundCtx; let uiButton, uiWidget, menuButton; let isWidgetVisible = false; let audioData = new Uint8Array(128); let currentUser = null; let minimapCanvas, minimapCtx, minimapState = 0; let playerLevel = 1; let playerExperience = 0; let playerMaxExperience = 100; let isPlayerPaused = false; let openInterfaces = { menu: false, levelUp: false }; let selectedClassId = null; const infoButton = document.getElementById('infoButton'); const infoWidget = document.getElementById('infoWidget'); let isInfoWidgetVisible = false; let playerSynthCoins = 0; let synthCoinsCounter; let currentCoinFloatingText = null; let shopButton; let shopWidget; let isShopVisible = false; let playerWeapons = []; let wasPausedBeforeHidden = false; let isAutoFireEnabled = false; let shootingJoystick = { active: false, startX: 0, startY: 0, endX: 0, endY: 0, outerRadius: 50, innerRadius: 0, maxInnerRadius: 40, shootThreshold: 0.5, growthRate: 200, // pixels per second }; let lastUpdateTime = Date.now(); let impactEffects = []; let currentSongName = ''; class NotificationSystem { constructor() { this.notificationContainer = null; this.createNotificationContainer(); } createNotificationContainer() { this.notificationContainer = document.createElement('div'); this.notificationContainer.id = 'notificationContainer'; document.body.appendChild(this.notificationContainer); } showNotification(message, notificationImage) { console.log('showNotification called with message:', message, 'and image:', notificationImage); console.log('Type of image:', typeof notificationImage); const notificationElement = document.createElement('div'); notificationElement.className = 'custom-notification draggable'; let content = `
`; if (notificationImage && typeof notificationImage === 'string') { console.log('Adding image to notification:', notificationImage); content += `Achievement`; } else { console.log('No valid image provided for notification'); } content += `

${message}

`; console.log('Notification content:', content); notificationElement.innerHTML = content; const closeButton = notificationElement.querySelector('.close-notification'); closeButton.addEventListener('click', () => this.closeNotification(notificationElement)); this.notificationContainer.appendChild(notificationElement); setTimeout(() => notificationElement.classList.add('show'), 10); makeDraggable(notificationElement); // Add auto-hide after 3 seconds setTimeout(() => this.closeNotification(notificationElement), 3000); } closeNotification(notificationElement) { notificationElement.classList.remove('show'); setTimeout(() => notificationElement.remove(), 300); } } const notificationSystem = new NotificationSystem(); function preloadImage(src) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(img); img.onerror = reject; img.src = src; }); } function handleAchievementUnlock(data) { console.log('Achievement unlocked (full data):', JSON.stringify(data)); if (!notificationSystem) { console.error('NotificationSystem not initialized'); return; } const message = `Achievement Unlocked: ${data.name}\n${data.description}`; const notificationImage = data.image; console.log('Showing notification:', message); console.log('Achievement image path:', notificationImage); console.log('Calling showNotification with message:', message, 'and image:', notificationImage); notificationSystem.showNotification(message, notificationImage); } function fetchUserAchievements() { const achievementsGrid = document.getElementById('achievementsGrid'); const achievementInfoBox = document.getElementById('achievementInfoBox'); if (!achievementsGrid) { console.error('Achievements grid not found'); return; } if (!currentUser) { displaySignInMessage(achievementsGrid, 'achievements'); if (achievementInfoBox) { achievementInfoBox.style.display = 'none'; } return; } if (achievementInfoBox) { achievementInfoBox.style.display = 'block'; } fetch('/get-user-achievements', { headers: { 'Authorization': getSessionToken() } }) .then(response => { if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }) .then(achievements => { if (Array.isArray(achievements) && achievements.length > 0) { displayAllAchievements(achievements); } else { displayNoAchievementsMessage(achievementsGrid); } }) .catch(error => { console.error('Error fetching user achievements:', error); displayErrorMessage(achievementsGrid, 'Failed to load achievements. Please try again later.'); }); } function displaySignInMessage(container, feature) { container.innerHTML = `

Please sign in to view your ${feature}.

`; } function displayNoAchievementsMessage(container) { container.innerHTML = `

You haven't unlocked any achievements yet.

Keep playing to earn achievements!

`; } function displayErrorMessage(container, message) { container.innerHTML = `

${message}

`; } function displayAllAchievements(achievements) { const achievementsGrid = document.getElementById('achievementsGrid'); if (!achievementsGrid) { console.error('Achievements grid not found'); return; } achievementsGrid.innerHTML = ''; achievements.forEach(achievement => { const achievementElement = document.createElement('div'); achievementElement.classList.add('achievement'); if (!achievement.unlocked) { achievementElement.classList.add('locked'); } achievementElement.innerHTML = ` ${achievement.name} ${!achievement.unlocked ? '
' : ''} `; achievementElement.addEventListener('click', () => showAchievementInfo(achievement)); achievementElement.addEventListener('mouseover', () => showAchievementInfo(achievement)); achievementsGrid.appendChild(achievementElement); }); // Show info for the first achievement by default if (achievements.length > 0) { showAchievementInfo(achievements[0]); } } function showAchievementInfo(achievement) { const image = document.getElementById('achievementImage'); const name = document.getElementById('achievementName'); const description = document.getElementById('achievementDescription'); const unlockedClasses = document.getElementById('unlockedClasses'); const unlockedSkins = document.getElementById('unlockedSkins'); image.src = achievement.image; name.textContent = achievement.name; if (achievement.unlocked) { name.classList.remove('locked'); description.textContent = achievement.description; } else { name.classList.add('locked'); description.textContent = `Unlock Requirements: ${achievement.unlock_requirements}`; } // Display unlockable items for both locked and unlocked achievements unlockedClasses.innerHTML = '
Classes Unlocked:
'; if (achievement.unlocked_classes && achievement.unlocked_classes.length > 0) { achievement.unlocked_classes.forEach(c => { unlockedClasses.innerHTML += `
${c.name}${c.name}
`; }); } else { unlockedClasses.innerHTML += '

No classes unlocked

'; } unlockedSkins.innerHTML = '
Skins Unlocked:
'; if (achievement.unlocked_skins && achievement.unlocked_skins.length > 0) { achievement.unlocked_skins.forEach(s => { unlockedSkins.innerHTML += `
${s.name}${s.name}
`; }); } else { unlockedSkins.innerHTML += '

No skins unlocked

'; } unlockedClasses.style.display = 'block'; unlockedSkins.style.display = 'block'; } function hideAchievementInfo() { const infoBox = document.getElementById('achievementInfoBox'); infoBox.classList.add('hidden'); } document.addEventListener('click', (event) => { const achievementsTab = document.getElementById('achievements'); if (achievementsTab && !achievementsTab.contains(event.target)) { // Optionally hide or reset achievement info here if needed } }); function createAchievementsTab() { const achievementsTab = document.createElement('div'); achievementsTab.id = 'achievements'; achievementsTab.classList.add('tab-content'); achievementsTab.innerHTML = `
Achievement

Unlocks:

`; return achievementsTab; } function initShop() { let shopButton = document.getElementById('shopButton'); if (!shopButton) { shopButton = document.createElement('button'); shopButton.id = 'shopButton'; shopButton.textContent = 'Shop'; shopButton.classList.add('game-button'); shopButton.style.display = 'none'; // Hide by default shopButton.addEventListener('click', toggleShop); document.body.appendChild(shopButton); } let shopWidget = document.getElementById('shopWidget'); if (!shopWidget) { shopWidget = document.createElement('div'); shopWidget.id = 'shopWidget'; shopWidget.classList.add('hidden'); shopWidget.innerHTML = `

`; document.body.appendChild(shopWidget); } shopWidget.querySelector('.close-button').addEventListener('click', hideShop); // Add event listener for the shop tab button const shopTabButton = shopWidget.querySelector('.tab-button[data-tab="shop"]'); shopTabButton.addEventListener('click', () => showShopTab('shop')); } function showShopTab(tabName) { const shopWidget = document.getElementById('shopWidget'); const tabContents = shopWidget.querySelectorAll('.tab-content'); const tabButtons = shopWidget.querySelectorAll('.tab-button'); tabContents.forEach(content => content.classList.remove('active')); tabButtons.forEach(button => button.classList.remove('active')); const selectedTab = shopWidget.querySelector(`#${tabName}`); const selectedButton = shopWidget.querySelector(`.tab-button[data-tab="${tabName}"]`); if (selectedTab && selectedButton) { selectedTab.classList.add('active'); selectedButton.classList.add('active'); } } function showShopButton() { const shopButton = document.getElementById('shopButton'); if (shopButton) { shopButton.style.display = 'block'; } else { console.error('Shop button not found in the DOM'); } } function toggleShop() { const shopWidget = document.getElementById('shopWidget'); if (shopWidget.classList.contains('visible')) { hideShop(); } else { showShop(); } } // Add this function to show the shop function showShop() { const shopWidget = document.getElementById('shopWidget'); shopWidget.classList.remove('hidden'); shopWidget.classList.add('visible'); // Force a refresh of the shop items fetchShopItems().then(() => { forceShowShopContent(); debugShopWidget(); }); pausePlayer(); } function forceShowShopContent() { const shopWidget = document.getElementById('shopWidget'); const shopContent = shopWidget.querySelector('#shop'); const shopTab = shopWidget.querySelector('.tab-button[data-tab="shop"]'); // Hide all tab contents const allTabContents = shopWidget.querySelectorAll('.tab-content'); allTabContents.forEach(content => content.style.display = 'none'); // Deactivate all tabs const allTabs = shopWidget.querySelectorAll('.tab-button'); allTabs.forEach(tab => tab.classList.remove('active')); // Show shop content and activate shop tab if (shopContent) { shopContent.style.display = 'block'; } if (shopTab) { shopTab.classList.add('active'); } } // Add this function to hide the shop function hideShop() { const shopWidget = document.getElementById('shopWidget'); shopWidget.classList.remove('visible'); shopWidget.classList.add('hidden'); unpausePlayer(); } function fetchShopItems() { if (!player || !player.playerClass || !player.playerClass._id) { console.error('Player or player class not initialized'); return Promise.resolve(); } return fetch('/get-shop-items', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ player_class: player.playerClass._id }), }) .then(response => response.json()) .then(weapons => { displayShopItems(weapons); }) .catch(error => { console.error('Error fetching shop items:', error); }); } // Add this function to display shop items function displayShopItems(weapons) { const weaponSelector = document.getElementById('weaponSelector'); if (!weaponSelector) { console.error('Weapon selector not found'); return; } weaponSelector.innerHTML = ''; // Clear existing content weapons.forEach(weapon => { const weaponOption = document.createElement('div'); weaponOption.classList.add('weapon-option'); weaponOption.innerHTML = `${weapon.name}`; weaponOption.addEventListener('click', () => selectWeapon(weapon, weaponOption)); weaponSelector.appendChild(weaponOption); }); if (weapons.length > 0) { selectWeapon(weapons[0], weaponSelector.firstChild); } else { debugLog('No weapons available in the shop'); } } function selectWeapon(weapon, selectedElement) { const weaponOptions = document.querySelectorAll('.weapon-option'); weaponOptions.forEach(option => option.classList.remove('selected')); selectedElement.classList.add('selected'); const weaponImage = document.getElementById('weaponImage'); const weaponName = document.getElementById('weaponName'); const weaponStats = document.getElementById('weaponStats'); const buyButton = document.getElementById('buyWeaponButton'); if (weaponImage && weaponName && weaponStats && buyButton) { weaponImage.innerHTML = `${weapon.name}`; weaponName.textContent = weapon.name; weaponStats.innerHTML = `

Damage: ${weapon.base_attributes.damage}

Fire Rate: ${weapon.base_attributes.fire_rate}

Bullet Speed: ${weapon.base_attributes.bullet_speed}

Bullet Size: ${weapon.base_attributes.bullet_size}

Price: ${weapon.price} Synth Coins

`; buyButton.onclick = () => buyWeapon(weapon); } else { console.error('One or more weapon info elements not found'); } } // Add this function to create a shop item element function createShopItemElement(weapon) { const itemElement = document.createElement('div'); itemElement.classList.add('shop-item'); itemElement.innerHTML = ` ${weapon.name}

${weapon.name}

Price: ${weapon.price} Synth Coins

`; const buyButton = itemElement.querySelector('.buy-button'); buyButton.addEventListener('click', () => buyWeapon(weapon)); return itemElement; } // Add this function to handle weapon purchase function buyWeapon(weapon) { fetch('/buy-weapon', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ player_id: socket.id, weapon_id: weapon._id, }), }) .then(response => response.json()) .then(data => { if (data.success) { debugLog('Weapon purchase successful:', data); player.synthCoins = data.updatedPlayerData.synth_coins; player.weapons = data.updatedPlayerData.weapons; player.loadWeaponImages(); player.weaponAngles = player.weapons.map(() => 0); player.lastShootTimes = player.weapons.map(() => 0); updateSynthCoinsDisplay(); notificationSystem.showNotification('Weapon purchased successfully!'); fetchShopItems(); // Refresh shop items } else { notificationSystem.showNotification(data.message); } }) .catch(error => { console.error('Error buying weapon:', error); notificationSystem.showNotification('An error occurred while purchasing the weapon: ' + error.message); }); } function loadAndCacheImage(src) { if (imageCache.has(src)) { return imageCache.get(src); } const img = new Image(); img.src = src; img.onerror = () => { console.error(`Failed to load image: ${src}`); imageCache.set(src, null); }; imageCache.set(src, img); return img; } function preloadBulletImage(src) { if (bulletImageCache.has(src)) { return bulletImageCache.get(src); } const img = new Image(); img.src = src; bulletImageCache.set(src, img); return img; } function preloadEnemyImages() { fetch('/get-enemy-images') .then(response => response.json()) .then(images => { images.forEach(imageSrc => { if (!enemyImageCache.has(imageSrc)) { const img = new Image(); img.src = imageSrc; enemyImageCache.set(imageSrc, img); } }); }) .catch(error => console.error('Error preloading enemy images:', error)); } class Camera { constructor(width, height) { this.x = 0; this.y = 0; this.width = width; this.height = height; } follow(player) { this.x = Math.floor(player.x - this.width / 2); this.y = Math.floor(player.y - this.height / 2); this.x = Math.max(0, Math.min(this.x, MAP_WIDTH - this.width)); this.y = Math.max(0, Math.min(this.y, MAP_HEIGHT - this.height)); } } // Add this new class for managing trails class Trail { constructor(maxLength = 10) { this.positions = []; this.maxLength = maxLength; } update(x, y) { this.positions.unshift({x, y}); if (this.positions.length > this.maxLength) { this.positions.pop(); } } draw(ctx, color) { if (!ctx) { console.error('Canvas context is undefined in Trail.draw'); return; } for (let i = 0; i < this.positions.length; i++) { const alpha = 1 - (i / this.positions.length); ctx.beginPath(); ctx.arc(this.positions[i].x, this.positions[i].y, 5, 0, Math.PI * 2); ctx.fillStyle = `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${alpha})`; ctx.fill(); ctx.closePath(); } } } class Skin { constructor(type, color, shape) { this.type = type; this.color = color; this.shape = shape; this.animationPhase = 0; } update() { this.animationPhase = (this.animationPhase + 0.1) % (2 * Math.PI); } draw(ctx, x, y, radius) { ctx.save(); ctx.translate(x, y); switch (this.shape) { case 'circle': this.drawCircle(ctx, radius); break; case 'triangle': this.drawTriangle(ctx, radius); break; case 'square': this.drawSquare(ctx, radius); break; default: this.drawCircle(ctx, radius); } ctx.restore(); } drawCircle(ctx, radius) { ctx.beginPath(); ctx.arc(0, 0, radius, 0, Math.PI * 2); ctx.fillStyle = this.color; ctx.fill(); ctx.closePath(); // Add pulsating effect const pulseRadius = radius * (1 + 0.1 * Math.sin(this.animationPhase)); ctx.beginPath(); ctx.arc(0, 0, pulseRadius, 0, Math.PI * 2); ctx.strokeStyle = this.color; ctx.lineWidth = 2; ctx.stroke(); ctx.closePath(); } drawTriangle(ctx, radius) { ctx.beginPath(); ctx.moveTo(0, -radius); ctx.lineTo(-radius * Math.sqrt(3) / 2, radius / 2); ctx.lineTo(radius * Math.sqrt(3) / 2, radius / 2); ctx.closePath(); ctx.fillStyle = this.color; ctx.fill(); // Add rotating effect ctx.save(); ctx.rotate(this.animationPhase); ctx.beginPath(); ctx.moveTo(0, -radius * 1.2); ctx.lineTo(-radius * Math.sqrt(3) / 2 * 1.2, radius / 2 * 1.2); ctx.lineTo(radius * Math.sqrt(3) / 2 * 1.2, radius / 2 * 1.2); ctx.closePath(); ctx.strokeStyle = this.color; ctx.lineWidth = 2; ctx.stroke(); ctx.restore(); } drawSquare(ctx, radius) { ctx.beginPath(); ctx.rect(-radius, -radius, radius * 2, radius * 2); ctx.fillStyle = this.color; ctx.fill(); ctx.closePath(); // Add spinning effect ctx.save(); ctx.rotate(this.animationPhase); ctx.beginPath(); ctx.rect(-radius * 1.2, -radius * 1.2, radius * 2.4, radius * 2.4); ctx.strokeStyle = this.color; ctx.lineWidth = 2; ctx.stroke(); ctx.restore(); } } // Update the Player class class Player { constructor(x, y, name, skinData, weapons, playerClass, ctx) { this.x = x; this.y = y; this.name = name; this.radius = 20; this.maxSpeed = playerClass?.base_attributes?.speed || 5; // Default to 5 if undefined this.velocityX = 0; this.velocityY = 0; this.keys = { up: false, down: false, left: false, right: false }; this.trail = new Trail(); this.setSkin(skinData); this.weapons = Array.isArray(weapons) ? weapons : (weapons ? [weapons] : []); this.lastShootTimes = this.weapons.map(() => 0); this.weaponImages = new Map(); this.currentWeaponIndex = 0; this.weaponAngle = 0; this.weaponAngles = this.weapons.map(() => 0); this.orbitRadius = 40; // Distance of weapons from player center this.playerClass = playerClass || {}; // Default to empty object if undefined this.playerClassImage = playerClass ? loadAndCacheImage(playerClass.image_source) : null; this.health = playerClass?.base_attributes?.health || 100; // Default to 100 if undefined this.damageMultiplier = playerClass?.base_attributes?.damage_multiplier || 1; // Default to 1 if undefined this.level = 1; this.lastShootTime = 0; this.ctx = ctx; this.experience = 0; this.maxExperience = 100; this.isPaused = false; this.synthCoins = 0; this.maxHealth = playerClass.base_attributes.health; this.currentHealth = this.maxHealth; this.healthRegen = playerClass.base_attributes.health_regen || 1; this.isShooting = false; this.mouseX = 0; this.mouseY = 0; this.autoFireEnabled = false; } loadWeaponImages() { this.weapons.forEach(weapon => { if (weapon && weapon.image_source) { this.loadWeaponImage(weapon.image_source); } }); } loadWeaponImage(imageSrc) { if (!this.weaponImages.has(imageSrc)) { const img = new Image(); img.src = imageSrc; img.onload = () => { this.weaponImages.set(imageSrc, img); }; img.onerror = () => { console.error(`Failed to load weapon image: ${imageSrc}`); }; } } setSkin(skinData) { this.skin = skinData; if (this.skin && this.skin.hat) { this.loadHatImage(); // Call this method to load the hat image } } getCurrentWeapon() { return this.weapons[this.currentWeaponIndex]; } upgradeWeapon(attribute, value) { const currentWeapon = this.getCurrentWeapon(); if (currentWeapon && currentWeapon.base_attributes) { if (attribute in currentWeapon.base_attributes) { currentWeapon.base_attributes[attribute] += value; debugLog(`Upgraded ${attribute} to ${currentWeapon.base_attributes[attribute]}`); } else { console.error(`Attribute ${attribute} not found in weapon attributes`); } } else { console.error('Current weapon or base attributes not found'); } } loadHatImage() { if (this.skin.hat && this.skin.hat.value) { this.hatImage = new Image(); this.hatImage.src = this.skin.hat.value; this.hatImage.onload = () => { debugLog(`Hat image loaded for player ${this.name}`); }; } } updateHealth() { // Health update logic will be handled by the server // This method is here for potential client-side predictions or interpolations } update() { if (this.keys.up) this.velocityY -= 0.5; if (this.keys.down) this.velocityY += 0.5; if (this.keys.left) this.velocityX -= 0.5; if (this.keys.right) this.velocityX += 0.5; this.velocityX *= 0.95; this.velocityY *= 0.95; const speed = Math.sqrt(this.velocityX * this.velocityX + this.velocityY * this.velocityY); if (speed > this.maxSpeed) { const ratio = this.maxSpeed / speed; this.velocityX *= ratio; this.velocityY *= ratio; } this.x += this.velocityX; this.y += this.velocityY; this.x = Math.max(this.radius, Math.min(MAP_WIDTH - this.radius, this.x)); this.y = Math.max(this.radius, Math.min(MAP_HEIGHT - this.radius, this.y)); this.trail.update(this.x, this.y); this.updateWeaponAngle(); this.updateHealth(); } updateWeaponAngle(nearestEnemy) { if (this.autoFireEnabled && nearestEnemy) { const targetAngle = Math.atan2(nearestEnemy.y - this.y, nearestEnemy.x - this.x); this.weaponAngles = this.weaponAngles.map((angle, index) => { const baseAngle = (index / this.weapons.length) * Math.PI * 2; return lerpAngle(angle, targetAngle + baseAngle, 0.1); }); } } findNearestEnemy() { return findNearestEnemy(); } draw() { if (!this.ctx) { console.error('Canvas context is undefined in Player.draw for player:', this.name); return; } this.trail.draw(this.ctx, [0, 255, 255]); // Apply color effect if any if (this.skin && this.skin.color && this.skin.color.effect === 'glow') { this.ctx.beginPath(); this.ctx.arc(this.x, this.y, this.radius + 5, 0, Math.PI * 2); this.ctx.fillStyle = `${this.skin.color.value}33`; this.ctx.fill(); this.ctx.closePath(); } // Draw player class image if (this.playerClassImage && this.playerClassImage.complete) { this.ctx.save(); this.ctx.translate(this.x, this.y); // Apply glow effect to the class image if present if (this.skin && this.skin.color && this.skin.color.effect === 'glow') { this.ctx.shadowColor = this.skin.color.value; this.ctx.shadowBlur = 10; } this.ctx.drawImage(this.playerClassImage, -this.radius, -this.radius, this.radius * 2, this.radius * 2); this.ctx.restore(); } // Draw weapon if (this.weaponImage && this.weaponImage.complete) { this.ctx.drawImage(this.weaponImage, this.x + this.radius, this.y - this.radius / 2, this.radius, this.radius); } // Draw hat if present this.drawHat(); // Set font for all text this.ctx.font = '12px Orbitron'; this.ctx.textAlign = 'center'; this.ctx.fillStyle = 'white'; // Draw player name this.ctx.fillText(this.name, this.x, this.y - this.radius - 5); // Draw player level this.ctx.fillText(`Lvl ${this.level}`, this.x, this.y - this.radius - 20); // Draw paused indicator if the player is paused if (this.isPaused) { this.ctx.font = '16px Orbitron'; this.ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'; this.ctx.fillText('PAUSED', this.x, this.y - this.radius - 30); } this.drawWeapons(); this.drawHealthBar(); } drawHealthBar() { const barWidth = this.radius * 2; const barHeight = 5; const x = this.x - this.radius; const y = this.y + this.radius + 5; // Draw background this.ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; this.ctx.fillRect(x, y, barWidth, barHeight); // Draw health const healthPercentage = this.currentHealth / this.maxHealth; this.ctx.fillStyle = `rgb(${255 - healthPercentage * 255}, ${healthPercentage * 255}, 0)`; this.ctx.fillRect(x, y, barWidth * healthPercentage, barHeight); } addWeapon(weapon) { this.weapons.push(weapon); this.weaponAngles.push(0); this.lastShootTimes.push(0); if (weapon && weapon.image_source) { this.loadWeaponImage(weapon.image_source); } } updateWeaponPositions() { const angleStep = (2 * Math.PI) / this.weapons.length; this.weaponAngles = this.weapons.map((_, index) => index * angleStep); } drawWeapons() { this.weapons.forEach((weapon, index) => { if (weapon && weapon.image_source) { const angle = this.weaponAngles[index]; const weaponX = this.x + Math.cos(angle) * this.orbitRadius; const weaponY = this.y + Math.sin(angle) * this.orbitRadius; const img = this.weaponImages.get(weapon.image_source); if (img && img.complete) { this.ctx.save(); this.ctx.translate(weaponX, weaponY); this.ctx.rotate(angle); this.ctx.drawImage(img, -this.radius / 2, -this.radius / 2, this.radius, this.radius); this.ctx.restore(); } } }); } setWeaponAngle(angle) { this.weaponAngle = angle; this.weaponAngles = this.weapons.map(() => angle); } drawHat() { if (this.hatImage && this.hatImage.complete) { const hatSize = this.radius * 1.5; const hatOffsetY = this.radius * 0.9; // Adjust this value to move the hat up or down this.ctx.drawImage( this.hatImage, this.x - hatSize / 2, this.y - this.radius - hatOffsetY, // Changed this line hatSize, hatSize ); } else if (this.skin && this.skin.hat) { debugLog(`Hat image not loaded for player ${this.name}`); } } updateWeaponAngleToMouse() { if (this.mouseX && this.mouseY) { const dx = this.mouseX - this.x; const dy = this.mouseY - this.y; const angle = Math.atan2(dy, dx); this.setWeaponAngle(angle); // Send weapon angle update to server socket.emit('player_update', { x: this.x, y: this.y, weapon_angles: this.weaponAngles, auto_fire_enabled: isAutoFireEnabled }); } } shoot(currentTime) { if (this.isPaused) return; const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); this.weapons.forEach((weapon, index) => { if (!weapon || !weapon.base_attributes) { console.error('Invalid weapon structure:', weapon); return; } const fireRate = weapon.base_attributes.fire_rate || 1; if (currentTime - this.lastShootTimes[index] > 1000 / fireRate) { let shootAngle; if (isAutoFireEnabled) { const nearestEnemy = findNearestEnemy(); shootAngle = nearestEnemy ? Math.atan2(nearestEnemy.y - this.y, nearestEnemy.x - this.x) : this.weaponAngle; } else { shootAngle = this.weaponAngle; } const weaponX = this.x + Math.cos(this.weaponAngles[index]) * this.orbitRadius; const weaponY = this.y + Math.sin(this.weaponAngles[index]) * this.orbitRadius; if ((isMobile && this.isShooting) || (!isMobile && (this.isShooting || isAutoFireEnabled))) { socket.emit('player_shoot', { angle: shootAngle, weaponX, weaponY, weaponIndex: index }); this.lastShootTimes[index] = currentTime; } } }); } } // Add this function to draw the UI function drawUI() { } // Add a new Enemy class class Enemy { constructor(data) { this.id = data.id; this.name = data.name; this.x = data.x; this.y = data.y; this.image = data.image; this.attributes = data.attributes; this.difficulty = data.difficulty; this.currentHealth = data.current_health; this.maxHealth = data.attributes.health; this.experience = data.experience; this.trail = new Trail(); this.loadImage(); } loadImage() { if (!enemyImageCache.has(this.image)) { const img = new Image(); img.src = this.image; enemyImageCache.set(this.image, img); } } update() { this.trail.update(this.x, this.y); } draw(ctx) { this.trail.draw(ctx, [255, 0, 0]); // Red trail for enemies const img = enemyImageCache.get(this.image); if (img && img.complete) { ctx.drawImage(img, this.x - 15, this.y - 15, 30, 30); // Assuming enemy size is 30x30 } else { // Fallback to a colored circle if image is not loaded ctx.beginPath(); ctx.arc(this.x, this.y, 15, 0, Math.PI * 2); ctx.fillStyle = 'red'; ctx.fill(); ctx.closePath(); } // Draw health bar const healthBarWidth = 30; const healthBarHeight = 4; const healthPercentage = this.currentHealth / this.maxHealth; ctx.fillStyle = 'red'; ctx.fillRect(this.x - healthBarWidth / 2, this.y - 20, healthBarWidth, healthBarHeight); ctx.fillStyle = 'green'; ctx.fillRect(this.x - healthBarWidth / 2, this.y - 20, healthBarWidth * healthPercentage, healthBarHeight); } } function init() { camera = new Camera(canvas.width, canvas.height); initAudio(); initDraggableElements(); const autoFireCheckbox = document.getElementById('autoFireCheckbox'); autoFireCheckbox.addEventListener('change', (e) => { isAutoFireEnabled = e.target.checked; if (isDebugging) { console.log('[DEBUG] Auto Fire ' + (isAutoFireEnabled ? 'enabled' : 'disabled')); } }); // Set up background canvas backgroundCanvas = document.createElement('canvas'); backgroundCanvas.width = canvas.width; backgroundCanvas.height = canvas.height; backgroundCtx = backgroundCanvas.getContext('2d'); backgroundCtx.fillStyle = 'rgb(18, 4, 88)'; backgroundCtx.fillRect(0, 0, backgroundCanvas.width, backgroundCanvas.height); uiButton = document.getElementById('uiButton'); uiWidget = document.getElementById('uiWidget'); menuButton = document.getElementById('menuButton'); if (uiButton) uiButton.addEventListener('click', toggleWidget); if (menuButton) menuButton.addEventListener('click', toggleWidget); uiButton.addEventListener('click', toggleWidget); menuButton.addEventListener('click', toggleWidget); minimapCanvas = document.getElementById('minimap'); minimapCtx = minimapCanvas.getContext('2d'); const toggleMinimapButton = document.getElementById('toggle-minimap'); toggleMinimapButton.addEventListener('click', toggleMinimap); const debugCheckbox = document.getElementById('debugCheckbox'); debugCheckbox.addEventListener('change', (e) => { isDebugging = e.target.checked; if (isDebugging) { console.log('[DEBUG] Debugging mode enabled'); } else { console.log('Debugging mode disabled'); } }); canvas.addEventListener('mousedown', (e) => { if (!isAutoFireEnabled && player) { player.isShooting = true; } }); canvas.addEventListener('mouseup', (e) => { if (!isAutoFireEnabled && player) { player.isShooting = false; } }); canvas.addEventListener('mousemove', (e) => { if (!isAutoFireEnabled && player) { updatePlayerAim(e.clientX, e.clientY); } }); const tabButtons = document.querySelectorAll('.tab-button'); tabButtons.forEach(button => { button.addEventListener('click', () => { const tabName = button.getAttribute('data-tab'); if (tabName === 'close') { hideWidget(); } else { showTab(tabName); } }); }); const closeButtons = document.querySelectorAll('.close-button'); closeButtons.forEach(button => { button.addEventListener('click', () => { hideClassSelector(); hideWidget(); hideInfoWidget(); }); }); // Focus // document.addEventListener('click', handleOutsideClick); const classSelectorButton = document.getElementById('classSelectorButton'); if (classSelectorButton) classSelectorButton.addEventListener('click', showClassSelector); classSelectorButton.addEventListener('click', showClassSelector); checkSession(); initShop(); // Add event listeners for auth buttons document.getElementById('signupButton').addEventListener('click', showSignupForm); document.getElementById('loginButton').addEventListener('click', showLoginForm); document.getElementById('logoutButton').addEventListener('click', logout); document.getElementById('submitSignup').addEventListener('click', signup); document.getElementById('submitLogin').addEventListener('click', login); // Add input validation listeners document.getElementById('signupUsername').addEventListener('input', validateSignupInput); document.getElementById('signupEmail').addEventListener('input', validateSignupInput); document.getElementById('signupPassword').addEventListener('input', validateSignupInput); infoButton.addEventListener('click', toggleInfoWidget); const classWidgetTabs = document.querySelectorAll('#classWidget .widget-tabs .tab-button'); classWidgetTabs.forEach(tab => { tab.addEventListener('click', function() { const tabName = this.getAttribute('data-tab'); showTab(tabName, 'classWidget'); }); }); const infoTabButtons = infoWidget.querySelectorAll('.tab-button'); infoTabButtons.forEach(button => { button.addEventListener('click', () => { const tabName = button.getAttribute('data-tab'); showInfoTab(tabName); }); }); infoWidget.querySelector('.close-button').addEventListener('click', hideInfoWidget); stopPropagationOnUIElements(); } function debugLog(...args) { if (isDebugging) { console.log('[DEBUG]', ...args); } } function updateSynthCoinsDisplay() { if (!gameStarted) return; const counter = document.getElementById('synthCoinsCounter'); if (!counter) { const newCounter = document.createElement('canvas'); newCounter.id = 'synthCoinsCounter'; document.body.appendChild(newCounter); } const counterCanvas = document.getElementById('synthCoinsCounter'); const counterCtx = counterCanvas.getContext('2d'); const text = `Synth Coins: ${player ? player.synthCoins : 0}`; counterCtx.font = '16px Orbitron'; const textWidth = counterCtx.measureText(text).width; counterCanvas.width = textWidth + 20; counterCanvas.height = 30; counterCtx.fillStyle = '#FFD700'; counterCtx.font = '16px Orbitron'; counterCtx.textAlign = 'left'; counterCtx.textBaseline = 'middle'; counterCtx.shadowColor = '#FFD700'; counterCtx.shadowBlur = 5; counterCtx.fillText(text, 10, counterCanvas.height / 2); counterCtx.shadowBlur = 0; } function showClassSelector() { const classWidget = document.getElementById('classWidget'); classWidget.classList.add('visible'); selectClassesTab(); fetchAndDisplayClasses(); } function selectClassesTab() { // Select the "Classes" tab const classesTab = document.querySelector('.widget-tabs .tab-button[data-tab="classes"]'); if (classesTab) { classesTab.classList.add('active'); // Add active class instead of simulating click } // Show the "Classes" content showTab('classes'); // Use the existing showTab function } function hideClassSelector() { const classWidget = document.getElementById('classWidget'); classWidget.classList.remove('visible'); } function fetchAndDisplayClasses() { fetch('/get-available-classes', { headers: { 'Authorization': getSessionToken() } }) .then(response => response.json()) .then(classes => { // Sort classes: unlocked first, then locked classes.sort((a, b) => { if (a.unlocked === b.unlocked) return 0; return a.unlocked ? -1 : 1; }); const classSelector = document.getElementById('classSelector'); classSelector.innerHTML = ''; classes.forEach(playerClass => { const classOption = document.createElement('div'); classOption.classList.add('class-option'); if (playerClass._id === selectedClassId) { classOption.classList.add('selected'); } classOption.innerHTML = `${playerClass.name}`; if (!playerClass.unlocked) { classOption.classList.add('locked'); const lockOverlay = document.createElement('div'); lockOverlay.classList.add('lock-overlay'); lockOverlay.innerHTML = ''; classOption.appendChild(lockOverlay); } classOption.addEventListener('click', () => selectClass(playerClass)); classOption.addEventListener('mouseover', () => displayClassInfo(playerClass)); classSelector.appendChild(classOption); }); // Display info for the first unlocked class by default const firstUnlockedClass = classes.find(c => c.unlocked); if (firstUnlockedClass) { displayClassInfo(firstUnlockedClass); } else if (classes.length > 0) { displayClassInfo(classes[0]); } }) .catch(error => console.error('Error loading classes:', error)); } function displayClassInfo(playerClass) { const classImage = document.getElementById('classImage'); const className = document.getElementById('className'); const classStats = document.getElementById('classStats'); classImage.innerHTML = `${playerClass.name}`; className.textContent = playerClass.name; classStats.innerHTML = `

Health: ${playerClass.base_attributes.health}

Speed: ${playerClass.base_attributes.speed}

Damage: ${playerClass.base_attributes.damage_multiplier}

`; // Remove the locked indicator if the class is unlocked if (playerClass.unlocked) { className.innerHTML = playerClass.name; } else { className.innerHTML = `${playerClass.name} (Locked)`; } } function handleLevelUp() { const upgradeOptions = getRandomUpgradeOptions(3); showLevelUpPopup(upgradeOptions); } function getRandomUpgradeOptions(count) { const allUpgrades = [ { type: 'bullet_speed', name: 'Bullet Speed', value: 1 }, { type: 'bullet_size', name: 'Bullet Size', value: 1 }, { type: 'damage', name: 'Damage', value: 2 }, { type: 'fire_rate', name: 'Fire Rate', value: 0.1 } ]; return shuffleArray(allUpgrades).slice(0, count); } function shuffleArray(array) { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } return array; } function pausePlayer() { if (!isPlayerPaused) { isPlayerPaused = true; player.isPaused = true; socket.emit('player_paused', { isPaused: true }); } } function unpausePlayer() { if (isPlayerPaused && !areInterfacesOpen()) { isPlayerPaused = false; player.isPaused = false; socket.emit('player_paused', { isPaused: false }); } } function showLevelUpPopup(options) { openInterfaces.levelUp = true; pausePlayer(); const popup = document.createElement('div'); popup.id = 'levelUpPopup'; popup.innerHTML = `

Level Up!

Choose an upgrade:

`; document.body.appendChild(popup); const upgradeOptions = document.getElementById('upgradeOptions'); options.forEach((option, index) => { const button = document.createElement('button'); button.classList.add('upgrade-button'); button.innerHTML = ` ${option.name} +${option.value} `; button.onclick = () => selectUpgrade(option, index); upgradeOptions.appendChild(button); }); // Add slot machine effect slotMachineEffect(options, upgradeOptions); } function slotMachineEffect(options, container) { const duration = 2000; // 2 seconds const interval = 100; // 0.1 second per change let elapsed = 0; const intervalId = setInterval(() => { elapsed += interval; if (elapsed < duration) { container.innerHTML = ''; getRandomUpgradeOptions(3).forEach(option => { const div = document.createElement('div'); div.classList.add('upgrade-option'); div.innerHTML = ` ${option.name} +${option.value} `; container.appendChild(div); }); } else { clearInterval(intervalId); container.innerHTML = ''; options.forEach((option, index) => { const button = document.createElement('button'); button.classList.add('upgrade-button'); button.innerHTML = ` ${option.name} +${option.value} `; button.onclick = () => selectUpgrade(option, index); container.appendChild(button); }); } }, interval); } function selectUpgrade(upgrade, index) { const popup = document.getElementById('levelUpPopup'); popup.classList.add('fade-out'); setTimeout(() => { popup.remove(); openInterfaces.levelUp = false; unpausePlayer(); }, 500); // Send both the upgrade details and the index socket.emit('upgrade_selected', { upgradeIndex: index, upgradeType: upgrade.type, upgradeValue: upgrade.value }); } function areInterfacesOpen() { return openInterfaces.menu || openInterfaces.levelUp || isShopVisible; // Add any other interface checks as needed } function drawExperienceBar() { if (!gameStarted) return; const expBar = document.getElementById('experienceBar'); if (!expBar) { const newExpBar = document.createElement('canvas'); newExpBar.id = 'experienceBar'; document.body.appendChild(newExpBar); } const expBarCanvas = document.getElementById('experienceBar'); const expBarCtx = expBarCanvas.getContext('2d'); const mapButton = document.getElementById('toggle-minimap'); const mapButtonRect = mapButton.getBoundingClientRect(); const shopButton = document.getElementById('shopButton'); const shopButtonRect = shopButton.getBoundingClientRect(); let barWidth = Math.min(window.innerWidth * 0.8, 400); const barHeight = Math.max(10, Math.min(window.innerHeight * 0.02, 20)); // Adjust width and position if on mobile if (window.innerWidth <= 600) { const availableWidth = window.innerWidth - shopButtonRect.width - mapButtonRect.width - 40; // 40px for padding barWidth = Math.min(availableWidth, barWidth); expBarCanvas.style.position = 'fixed'; expBarCanvas.style.left = `${shopButtonRect.right + 10}px`; // 10px padding from shop button expBarCanvas.style.bottom = '10px'; } else { expBarCanvas.style.position = 'fixed'; expBarCanvas.style.left = '50%'; expBarCanvas.style.bottom = '10px'; expBarCanvas.style.transform = 'translateX(-50%)'; } expBarCanvas.width = barWidth; expBarCanvas.height = barHeight; // Draw background expBarCtx.fillStyle = 'rgba(0, 0, 0, 0.5)'; expBarCtx.beginPath(); expBarCtx.roundRect(0, 0, barWidth, barHeight, barHeight / 2); expBarCtx.fill(); // Draw experience const fillWidth = (playerExperience / playerMaxExperience) * barWidth; expBarCtx.fillStyle = '#00ffff'; expBarCtx.beginPath(); expBarCtx.roundRect(0, 0, fillWidth, barHeight, barHeight / 2); expBarCtx.fill(); // Add glow effect expBarCtx.shadowColor = '#00ffff'; expBarCtx.shadowBlur = 10; expBarCtx.beginPath(); expBarCtx.roundRect(0, 0, fillWidth, barHeight, barHeight / 2); expBarCtx.stroke(); expBarCtx.shadowBlur = 0; // Draw text expBarCtx.fillStyle = 'white'; expBarCtx.font = `${Math.max(10, Math.min(barHeight * 0.8, 16))}px Orbitron`; expBarCtx.textAlign = 'center'; expBarCtx.textBaseline = 'middle'; // Adjust text based on available space const fullText = `Level ${playerLevel} - ${playerExperience}/${playerMaxExperience} XP`; const shortText = `Lvl ${playerLevel} - ${playerExperience}/${playerMaxExperience}`; const veryShortText = `Lvl ${playerLevel}`; let textToShow = fullText; if (expBarCtx.measureText(fullText).width > barWidth) { textToShow = shortText; if (expBarCtx.measureText(shortText).width > barWidth) { textToShow = veryShortText; } } expBarCtx.fillText(textToShow, barWidth / 2, barHeight / 2); } function checkSession() { const sessionToken = localStorage.getItem('sessionToken'); if (sessionToken) { // Verify session with server fetch('/verify-session', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ sessionToken }), }) .then(response => response.json()) .then(data => { if (data.valid) { currentUser = data.username; updateAuthUI(true); } else { localStorage.removeItem('sessionToken'); updateAuthUI(false); } }) .catch(error => { console.error('Error:', error); updateAuthUI(false); }); } else { updateAuthUI(false); } } function updateAuthUI(loggedIn) { const authButtons = document.getElementById('authButtons'); const logoutButton = document.getElementById('logoutButton'); const playButton = document.getElementById('playButton'); const welcomeMessage = document.getElementById('welcomeMessage'); const signupForm = document.getElementById('signupForm'); const loginForm = document.getElementById('loginForm'); if (loggedIn) { authButtons.style.display = 'none'; logoutButton.style.display = 'block'; // Changed from 'flex' to 'block' playButton.textContent = 'Join Game'; welcomeMessage.textContent = `Welcome to Resonance Rumble, ${currentUser}!`; signupForm.style.display = 'none'; loginForm.style.display = 'none'; } else { authButtons.style.display = 'flex'; logoutButton.style.display = 'none'; playButton.textContent = 'Join Game as Guest'; welcomeMessage.textContent = 'Welcome to Resonance Rumble'; // Optionally reset forms signupForm.style.display = 'none'; loginForm.style.display = 'none'; } } function showSignupForm() { document.getElementById('signupForm').style.display = 'flex'; document.getElementById('loginForm').style.display = 'none'; } function showLoginForm() { document.getElementById('loginForm').style.display = 'flex'; document.getElementById('signupForm').style.display = 'none'; } function validateSignupInput(event) { const input = event.target; const value = input.value; if (input.id === 'signupUsername') { // Username validation (e.g., at least 3 characters) input.classList.toggle('valid', value.length >= 3); input.classList.toggle('invalid', value.length < 3); } else if (input.id === 'signupEmail') { // Email validation const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; input.classList.toggle('valid', emailRegex.test(value)); input.classList.toggle('invalid', !emailRegex.test(value)); } else if (input.id === 'signupPassword') { // Password validation (e.g., at least 8 characters) input.classList.toggle('valid', value.length >= 8); input.classList.toggle('invalid', value.length < 8); } } function signup() { const username = document.getElementById('signupUsername').value; const email = document.getElementById('signupEmail').value; const password = document.getElementById('signupPassword').value; fetch('/signup', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ username, email, password }), }) .then(response => response.json()) .then(data => { if (data.success) { notificationSystem.showNotification('Signup successful! Please log in.'); showLoginForm(); } else { notificationSystem.showNotification(data.message); } }) .catch(error => { console.error('Error:', error); notificationSystem.showNotification('An error occurred during signup. Please try again.'); }); } function login() { const username = document.getElementById('loginUsername').value; const password = document.getElementById('loginPassword').value; fetch('/login', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ username, password }), }) .then(response => response.json()) .then(data => { if (data.success) { localStorage.setItem('sessionToken', data.sessionToken); currentUser = username; updateAuthUI(true); } else { notificationSystem.showNotification(data.message); } }) .catch(error => { console.error('Error:', error); notificationSystem.showNotification('An error occurred during login. Please try again.'); }); } function logout() { localStorage.removeItem('sessionToken'); currentUser = null; updateAuthUI(false); } function fetchUnlockedSkins() { if (!currentUser) { debugLog('No user logged in'); return; } debugLog('Fetching unlocked skins for user:', currentUser); fetch('/get-unlocked-skins', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ username: currentUser }), }) .then(response => { if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }) .then(unlockedSkins => { debugLog('Fetched unlocked skins:', unlockedSkins); displayUnlockedSkins(unlockedSkins); }) .catch(error => { console.error('Error fetching unlocked skins:', error); }); } function displayUnlockedSkins(unlockedSkins) { debugLog('Displaying unlocked skins:', unlockedSkins); const colorOptions = document.getElementById('colorOptions'); const hatOptions = document.getElementById('hatOptions'); if (!colorOptions || !hatOptions) { console.error('Skin customization elements not found in the DOM'); return; } debugLog('Color options element:', colorOptions); debugLog('Hat options element:', hatOptions); colorOptions.innerHTML = ''; hatOptions.innerHTML = ''; unlockedSkins.forEach(skin => { debugLog('Processing skin:', skin); const optionElement = document.createElement('div'); optionElement.classList.add('skin-option'); optionElement.setAttribute('data-skin-id', skin.id); optionElement.setAttribute('title', `${skin.name} (${skin.rarity})`); switch (skin.type) { case 'color': debugLog('Creating color option for:', skin.name); optionElement.style.backgroundColor = skin.value; if (skin.effect === 'glow') { optionElement.classList.add('glow-effect'); } colorOptions.appendChild(optionElement); break; case 'hat': debugLog('Creating hat option for:', skin.name); optionElement.style.backgroundImage = `url(${skin.value})`; hatOptions.appendChild(optionElement); break; } optionElement.addEventListener('click', () => selectSkin(skin.type, skin.id, optionElement)); }); debugLog('Color options after processing:', colorOptions.innerHTML); debugLog('Hat options after processing:', hatOptions.innerHTML); // Show a message if no skins are unlocked for a category if (colorOptions.children.length === 0) { colorOptions.innerHTML = '

No colors unlocked yet

'; } if (hatOptions.children.length === 0) { hatOptions.innerHTML = '

No hats unlocked yet

'; } } function toggleInfoWidget() { if (isInfoWidgetVisible) { hideInfoWidget(); } else { showInfoWidget(); } } function showInfoWidget() { infoWidget.classList.add('visible'); isInfoWidgetVisible = true; showInfoTab('socials'); // Open the Socials tab by default } function hideInfoWidget() { infoWidget.classList.remove('visible'); isInfoWidgetVisible = false; } function showInfoTab(tabName) { const tabContents = infoWidget.querySelectorAll('.tab-content'); tabContents.forEach(content => content.style.display = 'none'); const tabButtons = infoWidget.querySelectorAll('.tab-button'); tabButtons.forEach(button => button.classList.remove('active')); const selectedTab = document.getElementById(tabName); if (selectedTab) { selectedTab.style.display = 'block'; } const selectedButton = infoWidget.querySelector(`.tab-button[data-tab="${tabName}"]`); if (selectedButton) { selectedButton.classList.add('active'); } } function toggleWidget(event) { if (event) { event.stopPropagation(); } if (uiWidget.classList.contains('visible')) { hideWidget(); } else { showWidget(); } } // Add event listeners for close buttons document.querySelectorAll('.close-button').forEach(button => { button.addEventListener('click', () => { hideWidget(); }); }); function showWidget() { uiWidget.classList.add('visible'); openInterfaces.menu = true; // Add Achievements tab if it doesn't exist if (!document.getElementById('achievements')) { const achievementsTab = createAchievementsTab(); const widgetContent = uiWidget.querySelector('.widget-content'); if (widgetContent) { widgetContent.appendChild(achievementsTab); } else { console.error('Widget content container not found'); } // Add Achievements button to tab buttons const tabButtons = uiWidget.querySelector('.widget-tabs'); if (tabButtons) { const achievementsButton = document.createElement('button'); achievementsButton.classList.add('tab-button'); achievementsButton.setAttribute('data-tab', 'achievements'); achievementsButton.textContent = 'Achievements'; tabButtons.appendChild(achievementsButton); achievementsButton.addEventListener('click', () => showTab('achievements')); } else { console.error('Widget tabs container not found'); } } if (gameStarted && player) { pausePlayer(); showTab('settings'); // Open the Settings tab by default when in-game } else { showTab('customization'); // Open the Customization tab by default in the main menu } updateAccountTab(); } function updateAccountTab() { const accountContent = document.getElementById('accountContent'); if (currentUser) { fetch('/get-discord-info', { headers: { 'Authorization': getSessionToken() } }) .then(response => response.json()) .then(data => { if (data.linked) { accountContent.innerHTML = `

Welcome, ${currentUser}!

Your Discord account is linked:

Discord Avatar ${data.username}
`; document.getElementById('unlinkDiscordButton').addEventListener('click', unlinkDiscord); } else { accountContent.innerHTML = `

Welcome, ${currentUser}!

Link your Discord account:

`; document.getElementById('linkDiscordButton').addEventListener('click', linkDiscord); } }) .catch(error => { console.error('Error fetching Discord info:', error); accountContent.innerHTML = `

Welcome, ${currentUser}!

Error loading Discord information. Please try again later.

`; }); } else { displaySignInMessage(accountContent, 'account settings'); } } function getSessionToken() { return localStorage.getItem('sessionToken'); } function linkDiscord() { fetch('/initiate-discord-link', { method: 'POST', headers: { 'Authorization': getSessionToken(), 'Content-Type': 'application/json' } }) .then(response => response.json()) .then(data => { const width = 600; const height = 800; const left = (screen.width / 2) - (width / 2); const top = (screen.height / 2) - (height / 2); const popup = window.open(data.auth_url, 'discord-oauth', `width=${width},height=${height},left=${left},top=${top}`); window.addEventListener('message', function(event) { if (event.data === 'discord-linked') { popup.close(); updateAccountTab(); notificationSystem.showNotification('Discord account linked successfully!'); } }, false); }) .catch(error => { console.error('Error initiating Discord link:', error); notificationSystem.showNotification('An error occurred while linking Discord account'); }); } function unlinkDiscord() { fetch('/unlink-discord', { method: 'POST', headers: { 'Authorization': getSessionToken() } }) .then(response => response.json()) .then(data => { if (data.success) { notificationSystem.showNotification('Discord account unlinked successfully'); updateAccountTab(); } else { notificationSystem.showNotification('Failed to unlink Discord account'); } }) .catch(error => { console.error('Error unlinking Discord:', error); notificationSystem.showNotification('An error occurred while unlinking Discord account'); }); } function onCustomizationTabOpen() { const skinCustomization = document.getElementById('skinCustomization'); if (!skinCustomization) { console.error('skinCustomization element not found'); return; } // Show loading indicator immediately // skinCustomization.innerHTML = '
Loading skins...
'; // Load skin options asynchronously setTimeout(() => { loadSkinOptions(); }, 0); } function hideWidget() { uiWidget.classList.remove('visible'); openInterfaces.menu = false; if (gameStarted && player) { unpausePlayer(); } } // Focus // function handleOutsideClick(event) { // const uiWidget = document.getElementById('uiWidget'); // const classWidget = document.getElementById('classWidget'); // const infoWidget = document.getElementById('infoWidget'); // const menuButton = document.getElementById('menuButton'); // const classSelectorButton = document.getElementById('classSelectorButton'); // const infoButton = document.getElementById('infoButton'); // // Check and close UI Widget // if (openInterfaces.menu && !uiWidget.contains(event.target) && event.target !== menuButton) { // hideWidget(); // } // // Check and close Class Widget // if (classWidget.classList.contains('visible') && !classWidget.contains(event.target) && event.target !== classSelectorButton) { // hideClassSelector(); // } // // Check and close Info Widget // if (isInfoWidgetVisible && !infoWidget.contains(event.target) && event.target !== infoButton) { // hideInfoWidget(); // } // } function showTab(tabName) { debugLog('Showing tab:', tabName); // Hide all tab contents const tabContents = document.querySelectorAll('.tab-content'); tabContents.forEach(content => content.style.display = 'none'); // Deactivate all tab buttons const tabButtons = document.querySelectorAll('.tab-button'); tabButtons.forEach(button => button.classList.remove('active')); // Show the selected tab content const selectedTab = document.getElementById(tabName); if (selectedTab) { selectedTab.style.display = 'block'; } else { console.error('No tab found with name:', tabName); // If no valid tab is selected, default to the first tab const firstTab = tabContents[0]; if (firstTab) { firstTab.style.display = 'block'; tabName = firstTab.id; } } // Activate the selected tab button const selectedButton = document.querySelector(`.tab-button[data-tab="${tabName}"]`); if (selectedButton) { selectedButton.classList.add('active'); } if (tabName === 'customization') { onCustomizationTabOpen(); } else if (tabName === 'account') { updateAccountTab(); } else if (tabName === 'achievements') { fetchUserAchievements(); } } function initAudio() { audioContext = new (window.AudioContext || window.webkitAudioContext)(); analyser = audioContext.createAnalyser(); analyser.fftSize = 256; } function startGame() { socket = io({ reconnection: true, reconnectionDelay: 1000, reconnectionDelayMax: 5000, reconnectionAttempts: 5 }); preloadEnemyImages(); const playerName = currentUser || `Guest-${Math.floor(Math.random() * 10000)}`; socket.on('connect', () => { debugLog('Connected to server'); // Send join request with or without class_id socket.emit('join', { room: 'main_game_room', name: playerName, class_id: selectedClassId // This can be undefined if no class was selected }); }); socket.on('server_restart', (data) => { notificationSystem.showNotification(data.message); // Optionally, you could implement a reconnection attempt after a short delay setTimeout(() => { location.reload(); }, 5000); // Wait for 5 seconds before reloading the page }); socket.on('achievement_unlocked', (data) => { console.log('Received achievement_unlocked event:', data); handleAchievementUnlock(data); }); socket.on('reconnect', (attemptNumber) => { debugLog(`Reconnected to server after ${attemptNumber} attempts`); // Re-join the game room socket.emit('join', { room: 'main_game_room', name: playerName, class_id: selectedClassId // This can be undefined if no class was selected }); }); socket.on('disconnect', () => { debugLog('Disconnected from server'); // You might want to show a message to the user here hideShopButton(); }); socket.on('join_error', (data) => { notificationSystem.showNotification(data.message); // Disconnect the socket to prevent any further communication socket.disconnect(); // Reset game state gameStarted = false; document.getElementById('gameControls').style.display = 'flex'; document.getElementById('ui').style.display = 'none'; }); socket.on('reconnect_failed', () => { debugLog('Failed to reconnect to server'); // You might want to show a message to the user and/or reload the page }); socket.on('damage_dealt', (data) => { const damageText = Math.round(data.damage).toString(); showFloatingText(damageText, data.enemyX, data.enemyY, '#ff0000', false); // Red color for damage, not moving up }); socket.on('music_sync', (data) => { if (gameStarted) { const serverClientTimeDiff = Date.now() / 1000 - data.serverTime; currentSongDuration = data.songDuration || 180; // Use the provided duration or default to 3 minutes songStartTime = data.startTime + serverClientTimeDiff; const songPosition = (Date.now() / 1000 - data.startTime - serverClientTimeDiff) % currentSongDuration; setupAudio(data.song, songPosition); currentSongName = data.songName; updateSongDurationBar(); } }); socket.on('music_stop', () => { if (audioElement) { audioElement.pause(); } }); socket.on('game_alert', (data) => { if (gameStarted) { notificationSystem.showNotification(data.message); } }); socket.on('game_state', (data) => { debugLog('Received game state:', data); player = new Player( data.players[socket.id].x, data.players[socket.id].y, playerName, data.players[socket.id].skin, data.players[socket.id].weapons, data.players[socket.id].player_class, ctx ); player.level = data.players[socket.id].level || 1; player.experience = data.players[socket.id].experience || 0; player.maxExperience = data.players[socket.id].max_experience || 100; player.isPaused = data.players[socket.id].is_paused || false; player.synthCoins = data.players[socket.id].synth_coins || 0; player.currentHealth = data.players[socket.id].current_health; player.maxHealth = data.players[socket.id].max_health; updateSynthCoinsDisplay(); debugLog(`Initial player stats: Level ${player.level}, Exp ${player.experience}/${player.maxExperience}, Health ${player.currentHealth}/${player.maxHealth}`); player.weapons.forEach(weapon => { if (weapon && weapon.bullet_image_source) { preloadBulletImage(weapon.bullet_image_source); } else { console.error('Invalid weapon structure:', weapon); } }); loadPlayerSkin(playerName); otherPlayers = Object.entries(data.players).reduce((acc, [id, playerData]) => { if (id !== socket.id) { acc[id] = new Player( playerData.x, playerData.y, playerData.name, playerData.skin, playerData.weapon, playerData.player_class, ctx // Pass the canvas context here ); acc[id].level = playerData.level || 1; acc[id].isPaused = playerData.is_paused || false; loadPlayerSkin(playerData.name); } return acc; }, {}); // Preload bullet images for all weapons Object.values(data.players).forEach(playerData => { if (playerData.weapon && playerData.weapon.bullet_image_source) { preloadBulletImage(playerData.weapon.bullet_image_source); } }); serverEnemies = data.enemies.map(enemyData => new Enemy(enemyData)); serverBullets = data.bullets; gameStarted = true; document.getElementById('gameControls').style.display = 'none'; document.getElementById('ui').style.display = 'flex'; document.getElementById('minimap-container').style.display = 'block'; infoButton.style.display = 'none'; hideInfoWidget(); showShopButton(); // Show the shop button when the game starts socket.emit('request_music_sync'); }); socket.on('synth_coins_update', (data) => { synthCoins = data.coins.map(coin => new SynthCoin(coin.x, coin.y)); }); socket.on('new_synth_coin', (data) => { synthCoins.push(new SynthCoin(data.x, data.y)); }); socket.on('coin_collected', (data) => { debugLog('Coin collected event received:', data); updateCoinCollection(data); }); socket.on('collect_synth_coin', (data) => { debugLog('Collect synth coin event received:', data); updateCoinCollection(data); }); socket.on('player_update', (data) => { if (data.id !== socket.id) { if (!otherPlayers[data.id]) { otherPlayers[data.id] = new Player( data.player.x, data.player.y, data.player.name, data.player.skin, data.player.weapons, data.player.player_class, ctx ); } else { const updatedPlayer = otherPlayers[data.id]; Object.assign(updatedPlayer, data.player); updatedPlayer.setSkin(data.player.skin); updatedPlayer.weapons = data.player.weapons; updatedPlayer.loadWeaponImages(); updatedPlayer.weaponAngles = data.player.weapon_angles || updatedPlayer.weapons.map(() => 0); updatedPlayer.currentHealth = data.player.current_health; updatedPlayer.maxHealth = data.player.max_health; updatedPlayer.isPaused = data.player.is_paused; updatedPlayer.autoFireEnabled = data.player.auto_fire_enabled; } } else { // Update the current player's weapons if necessary player.weapons = data.player.weapons; player.loadWeaponImages(); player.weaponAngles = data.player.weapon_angles || player.weapons.map(() => 0); player.currentHealth = data.player.current_health; player.maxHealth = data.player.max_health; player.isPaused = data.player.is_paused; player.autoFireEnabled = data.player.auto_fire_enabled; } }); socket.on('upgrade_applied', (data) => { debugLog('Upgrade applied:', data); if (player) { player.upgradeWeapon(data.attribute, data.value); } else { console.error('Player object not found'); } }); socket.on('new_player', (data) => { debugLog('New player joined:', data); if (data.id !== socket.id) { otherPlayers[data.id] = new Player( data.player.x, data.player.y, data.player.name, data.player.skin, data.player.weapon, data.player.player_class, ctx ); loadPlayerSkin(data.player.name); } }); socket.on('player_shoot', (data) => { if (data.playerId in otherPlayers) { const player = otherPlayers[data.playerId]; for (let i = 0; i < player.weaponAttributes.count; i++) { const angle = data.angle + (Math.random() - 0.5) * 0.2; // Add some spread const bullet = { x: player.x, y: player.y, angle: angle, speed: player.weaponAttributes.speed, size: player.weaponAttributes.size, damage: player.weaponAttributes.damage, playerId: data.playerId }; serverBullets.push(bullet); } } }); socket.on('experience_update', (data) => { if (data.player_id === socket.id) { playerExperience = data.experience; playerMaxExperience = data.max_experience; playerLevel = data.level; player.level = data.level; debugLog(`Experience updated: ${playerExperience}/${playerMaxExperience}, Level: ${playerLevel}`); if (data.leveled_up) { debugLog('Level up!'); handleLevelUp(); } if (data.exp_gained) { showFloatingText(`+${data.exp_gained} XP`, player.x, player.y - 30, '#FFFFFF', true); // Gold color for XP, moving up } } else if (otherPlayers[data.player_id]) { otherPlayers[data.player_id].level = data.level; } }); socket.on('player_disconnected', (playerId) => { delete otherPlayers[playerId]; }); socket.on('game_update', (data) => { serverEnemies = data.enemies.map(enemyData => { const enemy = new Enemy(enemyData); enemy.update(); // Ensure the enemy's trail is initialized return enemy; }); serverBullets = data.bullets; // Update synth coins synthCoins = data.synth_coins.map(coinData => { const existingCoin = synthCoins.find(c => Math.hypot(c.x - coinData.x, c.y - coinData.y) < 0.1); if (existingCoin) { existingCoin.x = coinData.x; existingCoin.y = coinData.y; return existingCoin; } else { return new SynthCoin(coinData.x, coinData.y); } }); // Update player health if (data.players[socket.id]) { player.currentHealth = data.players[socket.id].current_health; player.maxHealth = data.players[socket.id].max_health; } // Update other players' health Object.entries(data.players).forEach(([id, playerData]) => { if (id !== socket.id && otherPlayers[id]) { otherPlayers[id].currentHealth = playerData.current_health; otherPlayers[id].maxHealth = playerData.max_health; } }); }); socket.on('bullet_impact', (data) => { impactEffects.push(new ImpactEffect(data.x, data.y, data.color, data.size, data.angle)); }); socket.on('new_bullet', (bulletData) => { serverBullets.push(bulletData); if (bulletData.bullet_image_source) { preloadBulletImage(bulletData.bullet_image_source); } }); socket.on('enemy_destroyed', (data) => { data.particles.forEach(particleData => { particles.push(new Particle( particleData.x, particleData.y, particleData.color, particleData.radius, particleData.velocityX, particleData.velocityY, particleData.ttl )); }); }); socket.on('player_died', (data) => { if (data.player_id === socket.id) { // This is the current player console.log('You died!'); // Implement any client-side death logic here (e.g., death animation, respawn timer) } else { // Another player died console.log(`Player ${data.player_id} died`); // Optionally implement logic for when other players die } }); socket.on('music_control', (data) => { debugLog('Received music control:', data); switch(data.action) { case 'start': case 'change': const serverClientTimeDiff = Date.now() / 1000 - data.serverTime; currentSongDuration = data.songDuration || 180; // Use the provided duration or default to 3 minutes songStartTime = data.startTime + serverClientTimeDiff; const songPosition = 0; // Start from the beginning for 'change' action setupAudio(data.song, songPosition); currentSongName = data.songName; updateSongDurationBar(); break; case 'stop': if (audioElement) { audioElement.pause(); } break; } }); socket.on('skin_update', (data) => { const { player_id, skin_data } = data; if (player_id === socket.id) { // Update the current player's skin player.setSkin(skin_data); } else if (otherPlayers[player_id]) { // Update other player's skin otherPlayers[player_id].setSkin(skin_data); } }); showSongDurationBar(); } function hideShopButton() { const shopButton = document.getElementById('shopButton'); if (shopButton) { shopButton.style.display = 'none'; } } function updateCoinCollection(data) { if (data.player_id === socket.id) { const coinsCollected = data.coin_count - player.synthCoins; player.synthCoins = data.coin_count; updateSynthCoinsDisplay(); debugLog('Updated player synth coins:', player.synthCoins); // Create or update floating text for coin collection if (currentCoinFloatingText) { currentCoinFloatingText.amount += coinsCollected; currentCoinFloatingText.text = `+$${currentCoinFloatingText.amount}`; currentCoinFloatingText.lifetime = 60; // Reset lifetime currentCoinFloatingText.y = player.y - 50; // Update position } else { currentCoinFloatingText = { text: `+$${coinsCollected}`, x: player.x, y: player.y - 50, color: '#FFD700', // Gold color lifetime: 60, velocity: -0.5, amount: coinsCollected }; floatingTexts.push(currentCoinFloatingText); } debugLog('Floating text created:', currentCoinFloatingText); } else if (otherPlayers[data.player_id]) { otherPlayers[data.player_id].synthCoins = data.coin_count; } // Remove the collected coin from the synthCoins array synthCoins = synthCoins.filter(coin => coin.x !== data.coin_x || coin.y !== data.coin_y); } function showFloatingText(text, x, y, color, moveUp = true) { const floatingText = { text: text, x: x, y: y, color: color, lifetime: 60, // Number of frames the text will be visible velocity: moveUp ? -1 : 0 // Upward velocity if moveUp is true, otherwise 0 }; floatingTexts.push(floatingText); } // Add an array to store floating texts // Add a function to load player skins function loadPlayerSkin(username) { fetch('/get-player-skin', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ username }), }) .then(response => { if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }) .then(skinData => { debugLog('Received skin data:', skinData); // Add this line for debugging if (player && player.name === username) { player.setSkin(skinData); } // Update other players' skins if necessary Object.values(otherPlayers).forEach(otherPlayer => { if (otherPlayer.name === username) { otherPlayer.setSkin(skinData); } }); }) .catch(error => { console.error('Error loading player skin:', error); // Set a default skin if there's an error const defaultSkin = { color: { name: 'Default Blue', value: '#00ffff', effect: null } }; if (player && player.name === username) { player.setSkin(defaultSkin); } Object.values(otherPlayers).forEach(otherPlayer => { if (otherPlayer.name === username) { otherPlayer.setSkin(defaultSkin); } }); }); } // Update the setupAudio function function setupAudio(audioSource, startTime = 0) { if (audioElement) { audioElement.pause(); } audioElement = new Audio(audioSource); audioElement.currentTime = startTime; audioElement.play().catch(e => console.error("Error playing audio:", e)); if (!audioContext) { audioContext = new (window.AudioContext || window.webkitAudioContext)(); } if (!analyser) { analyser = audioContext.createAnalyser(); analyser.fftSize = 256; audioData = new Uint8Array(analyser.frequencyBinCount); } if (!source) { source = audioContext.createMediaElementSource(audioElement); source.connect(analyser); analyser.connect(audioContext.destination); } } // Add this new function function toggleMinimap() { minimapState = (minimapState + 1) % 3; const minimap = document.getElementById('minimap'); switch (minimapState) { case 0: minimap.classList.remove('small', 'large'); break; case 1: minimap.classList.add('small'); minimap.classList.remove('large'); break; case 2: minimap.classList.remove('small'); minimap.classList.add('large'); break; } } // Add this new function function drawMinimap() { if (minimapState === 0) return; const minimap = document.getElementById('minimap'); const scale = minimapState === 1 ? 0.05 : 0.2; minimapCanvas.width = minimap.offsetWidth; minimapCanvas.height = minimap.offsetHeight; minimapCtx.fillStyle = 'rgba(18, 4, 88, 0.7)'; minimapCtx.fillRect(0, 0, minimapCanvas.width, minimapCanvas.height); // Draw player const playerX = (player.x / MAP_WIDTH) * minimapCanvas.width; const playerY = (player.y / MAP_HEIGHT) * minimapCanvas.height; minimapCtx.fillStyle = '#00ffff'; minimapCtx.beginPath(); minimapCtx.arc(playerX, playerY, 3, 0, Math.PI * 2); minimapCtx.fill(); // Draw player name on large minimap if (minimapState === 2) { drawPlayerName(minimapCtx, player.name, playerX, playerY, '#00ffff'); } // Draw other players minimapCtx.fillStyle = '#ff00ff'; Object.values(otherPlayers).forEach(otherPlayer => { const x = (otherPlayer.x / MAP_WIDTH) * minimapCanvas.width; const y = (otherPlayer.y / MAP_HEIGHT) * minimapCanvas.height; minimapCtx.beginPath(); minimapCtx.arc(x, y, 2, 0, Math.PI * 2); minimapCtx.fill(); // Draw other player names on large minimap if (minimapState === 2) { drawPlayerName(minimapCtx, otherPlayer.name, x, y, '#ff00ff'); } }); // Draw enemies minimapCtx.fillStyle = '#ff0000'; serverEnemies.forEach(enemy => { const x = (enemy.x / MAP_WIDTH) * minimapCanvas.width; const y = (enemy.y / MAP_HEIGHT) * minimapCanvas.height; minimapCtx.beginPath(); minimapCtx.arc(x, y, 2, 0, Math.PI * 2); minimapCtx.fill(); }); } // New helper function to draw player names function drawPlayerName(ctx, name, x, y, color) { ctx.font = '12px Orbitron'; ctx.fillStyle = color; ctx.textAlign = 'center'; ctx.textBaseline = 'bottom'; ctx.fillText(name, x, y - 5); } class SynthCoin { constructor(x, y) { this.x = x; this.y = y; this.radius = 10; this.angle = Math.random() * Math.PI * 2; this.glowAngle = 0; } update() { this.angle += 0.02; this.glowAngle += 0.1; } draw(ctx) { ctx.save(); ctx.translate(this.x, this.y); ctx.rotate(this.angle); // Draw coin base const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, this.radius); gradient.addColorStop(0, '#FFD700'); // Center: Gold gradient.addColorStop(0.8, '#DAA520'); // Edge: Goldenrod gradient.addColorStop(1, '#B8860B'); // Rim: Dark Goldenrod ctx.beginPath(); ctx.arc(0, 0, this.radius, 0, Math.PI * 2); ctx.fillStyle = gradient; ctx.fill(); // Draw coin edge ctx.beginPath(); ctx.arc(0, 0, this.radius, 0, Math.PI * 2); ctx.strokeStyle = '#B8860B'; ctx.lineWidth = 2; ctx.stroke(); // Draw "$" symbol ctx.fillStyle = '#B8860B'; ctx.font = `bold ${this.radius}px Arial`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('$', 0, 0); // Draw glow effect const glowSize = this.radius * 1.5; const glowGradient = ctx.createRadialGradient(0, 0, 0, 0, 0, glowSize); glowGradient.addColorStop(0, 'rgba(255, 215, 0, 0.5)'); glowGradient.addColorStop(1, 'rgba(255, 215, 0, 0)'); ctx.globalAlpha = 0.5 + Math.sin(this.glowAngle) * 0.2; // Pulsating glow ctx.beginPath(); ctx.arc(0, 0, glowSize, 0, Math.PI * 2); ctx.fillStyle = glowGradient; ctx.fill(); // Draw shine effect ctx.globalAlpha = 0.7; ctx.beginPath(); ctx.ellipse(-this.radius * 0.3, -this.radius * 0.3, this.radius * 0.2, this.radius * 0.4, Math.PI / 4, 0, Math.PI * 2); ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; ctx.fill(); ctx.restore(); } } let synthCoins = []; // Star class class Star { constructor() { this.reset(); } reset() { this.x = Math.random() * MAP_WIDTH; this.y = Math.random() * MAP_HEIGHT; this.size = Math.random() * 3 + 1; this.speed = Math.random() * 0.5 + 0.1; } update(audioData) { this.y += this.speed; if (this.y > MAP_HEIGHT) { this.reset(); this.y = 0; // Start from the top again } // Audio visualization effect const index = Math.floor(this.x / MAP_WIDTH * audioData.length); const audioIntensity = audioData[index] / 255; this.size = (Math.random() * 3 + 1) * (1 + audioIntensity); } draw(ctx) { ctx.fillStyle = `rgba(255, 255, 255, ${Math.random() * 0.5 + 0.5})`; ctx.beginPath(); ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); ctx.fill(); } } // Shooting Star class class ShootingStar { constructor() { this.reset(); } reset() { this.x = Math.random() * MAP_WIDTH; this.y = 0; this.length = Math.random() * 80 + 20; this.speed = Math.random() * 10 + 5; this.angle = Math.PI / 4 + (Math.random() * Math.PI / 4); this.active = true; this.tailPoints = []; } update(audioData) { this.x += Math.cos(this.angle) * this.speed; this.y += Math.sin(this.angle) * this.speed; // Audio visualization effect const index = Math.floor(this.x / MAP_WIDTH * audioData.length); const audioIntensity = audioData[index] / 255; this.speed = (Math.random() * 10 + 5) * (1 + audioIntensity); this.tailPoints.unshift({x: this.x, y: this.y}); if (this.tailPoints.length > 20) { this.tailPoints.pop(); } if (this.x > MAP_WIDTH || this.y > MAP_HEIGHT) { this.reset(); } } draw(ctx) { ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(this.x, this.y); for (let i = 0; i < this.tailPoints.length; i++) { const point = this.tailPoints[i]; ctx.lineTo(point.x, point.y); ctx.globalAlpha = 1 - (i / this.tailPoints.length); } ctx.stroke(); ctx.globalAlpha = 1; } } class NeonCloud { constructor(x, y, size) { this.x = x; this.y = y; this.size = size; this.baseColor = this.getRandomNeonColor(); this.offset = Math.random() * Math.PI * 2; } getRandomNeonColor() { const neonColors = [ '#ff00ff', // Magenta '#00ffff', // Cyan '#ff00a0', // Hot Pink '#00ff7f', // Spring Green '#ff6b6b', // Light Red '#4deeea', // Bright Turquoise ]; return neonColors[Math.floor(Math.random() * neonColors.length)]; } update(audioData) { const index = Math.floor(this.x / MAP_WIDTH * audioData.length); const audioIntensity = audioData[index] / 255; this.size = this.size * (1 + audioIntensity * 0.2); } draw(ctx) { ctx.save(); ctx.translate(this.x, this.y); const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, this.size); gradient.addColorStop(0, this.baseColor + '77'); // Semi-transparent gradient.addColorStop(1, this.baseColor + '00'); // Fully transparent ctx.beginPath(); ctx.arc(0, 0, this.size, 0, Math.PI * 2); ctx.fillStyle = gradient; ctx.globalAlpha = 0.7 + Math.sin(Date.now() / 1000 + this.offset) * 0.3; // Pulsating effect ctx.fill(); ctx.restore(); } } // Background effects manager class BackgroundEffects { constructor() { this.stars = Array(200).fill().map(() => new Star()); this.shootingStars = Array(5).fill().map(() => new ShootingStar()); this.neonClouds = Array(10).fill().map(() => new NeonCloud( Math.random() * MAP_WIDTH, Math.random() * MAP_HEIGHT, Math.random() * 100 + 50 )); } update(audioData) { this.stars.forEach(star => star.update(audioData)); this.shootingStars.forEach(star => { if (star.active) { star.update(audioData); } else { star.reset(); } }); this.neonClouds.forEach(cloud => cloud.update(audioData)); } draw(ctx) { // Draw neon clouds first, so they appear behind other elements this.neonClouds.forEach(cloud => cloud.draw(ctx)); this.stars.forEach(star => star.draw(ctx)); this.shootingStars.forEach(star => star.draw(ctx)); } } class AlertSystem { constructor() { this.alerts = []; this.alertDuration = 3000; // Duration in milliseconds } // addnotificationSystem.showNotification(message) { // const alert = { // message: message, // createdAt: Date.now(), // opacity: 1 // }; // this.alerts.push(alert); // } update() { const currentTime = Date.now(); this.alerts = this.alerts.filter(alert => { const elapsedTime = currentTime - alert.createdAt; if (elapsedTime > this.alertDuration) { return false; } if (elapsedTime > this.alertDuration / 2) { alert.opacity = 1 - ((elapsedTime - this.alertDuration / 2) / (this.alertDuration / 2)); } return true; }); } draw(ctx) { const alertHeight = 30; const padding = 10; let yOffset = padding; ctx.save(); ctx.font = '16px Orbitron'; ctx.textAlign = 'center'; this.alerts.forEach(alert => { ctx.fillStyle = `rgba(255, 255, 255, ${alert.opacity})`; ctx.fillText(alert.message, canvas.width / 2, yOffset + alertHeight / 2); yOffset += alertHeight + padding; }); ctx.restore(); } } // Create an instance of the AlertSystem const alertSystem = new AlertSystem(); // Create an instance of BackgroundEffects const backgroundEffects = new BackgroundEffects(); // Modify the update function function update() { // Clear main canvas ctx.clearRect(0, 0, canvas.width, canvas.height); if (!gameStarted || !player) { requestAnimationFrame(update); return; } const currentTime = Date.now(); const deltaTime = (currentTime - lastUpdateTime) / 1000; // Convert to seconds lastUpdateTime = currentTime; if (gameStarted && player) { if (!areInterfacesOpen()) { if (isAutoFireEnabled) { const nearestEnemy = findNearestEnemy(); player.updateWeaponAngle(nearestEnemy); player.shoot(currentTime, nearestEnemy); } else { // Handle manual aiming and shooting player.updateWeaponAngleToMouse(); if (player.isShooting) { player.shoot(currentTime); } } } socket.emit('player_update', { x: player.x, y: player.y, weapon_angles: player.weaponAngles, auto_fire_enabled: isAutoFireEnabled }); updateAndDrawShootingJoystick(deltaTime); } if (gameStarted) { alertSystem.update(); } if (gameStarted && player && !player.isPaused) { const allPlayers = { ...otherPlayers, [socket.id]: player }; synthCoins.forEach(coin => { coin.update(allPlayers); }); } impactEffects = impactEffects.filter(effect => { effect.update(); return effect.lifetime > 0; }); // Update Synth Coins synthCoins.forEach(coin => coin.update()); // Check for coin collection only if the player is not paused if (!player.isPaused) { synthCoins = synthCoins.filter(coin => { const distance = Math.hypot(player.x - coin.x, player.y - coin.y); if (distance <= player.radius + coin.radius) { // Coin collected socket.emit('collect_synth_coin', { coin_x: coin.x, coin_y: coin.y }); return false; } return true; }); } player.update(); camera.follow(player); serverEnemies.forEach(enemy => enemy.update()); ctx.save(); ctx.translate(-camera.x, -camera.y); // Draw the map boundaries ctx.strokeStyle = 'white'; ctx.strokeRect(0, 0, MAP_WIDTH, MAP_HEIGHT); // Update audio data if (analyser) { analyser.getByteFrequencyData(audioData); } // Update and draw background effects backgroundEffects.update(audioData); ctx.save(); ctx.globalCompositeOperation = 'lighter'; backgroundEffects.draw(ctx); ctx.globalCompositeOperation = 'source-over'; ctx.restore(); // Draw game objects drawGameObjects(); updateSynthCoinsDisplay(); ctx.restore(); // Draw UI elements (like joystick) after restoring the context if (joystick.active) { drawJoystick(); } drawMinimap(); drawExperienceBar(); // If the widget is visible, draw a semi-transparent overlay if (isWidgetVisible) { ctx.fillStyle = 'rgba(18, 4, 88, 0.5)'; ctx.fillRect(0, 0, canvas.width, canvas.height); } socket.emit('player_update', { x: player.x, y: player.y, auto_fire_enabled: isAutoFireEnabled // Send auto-fire preference }); if (gameStarted) { alertSystem.draw(ctx); } if (gameStarted) { updateSongDurationBar(); } requestAnimationFrame(update); debugLog('Player position:', player.x, player.y); debugLog('Number of enemies:', serverEnemies.length); } function stopPropagationOnUIElements() { const uiElements = document.querySelectorAll('#uiWidget, #classWidget, #infoWidget, #shopWidget, #mobileControls'); uiElements.forEach(element => { element.addEventListener('mousedown', (e) => e.stopPropagation()); element.addEventListener('touchstart', (e) => e.stopPropagation()); }); } function lerpAngle(a, b, t) { const diff = b - a; const adjusted = ((diff + Math.PI) % (2 * Math.PI)) - Math.PI; return a + adjusted * t; } function findNearestEnemy() { let nearestEnemy = null; let nearestDistance = Infinity; serverEnemies.forEach(enemy => { const distance = Math.hypot(enemy.x - player.x, enemy.y - player.y); if (distance < nearestDistance) { nearestDistance = distance; nearestEnemy = enemy; } }); return nearestEnemy; } // New function to draw game objects function drawGameObjects() { if (!ctx) { console.error('Canvas context is undefined in drawGameObjects'); return; } // Draw server-managed enemies serverEnemies.forEach(enemy => { if (isVisible(enemy.x, enemy.y)) { enemy.draw(ctx); } }); // Draw server-managed bullets serverBullets.forEach(bullet => { if (isVisible(bullet.x, bullet.y)) { const bulletImage = bulletImageCache.get(bullet.bullet_image_source); if (bulletImage && bulletImage.complete) { ctx.save(); ctx.translate(bullet.x, bullet.y); ctx.rotate(bullet.angle); ctx.drawImage(bulletImage, -bullet.size/2, -bullet.size/2, bullet.size, bullet.size); ctx.restore(); } else { // Fallback to drawing a circle if the image is not loaded ctx.beginPath(); ctx.arc(bullet.x, bullet.y, bullet.size, 0, Math.PI * 2); ctx.fillStyle = bullet.color || '#ffff00'; // Use the bullet's color or default to yellow ctx.fill(); ctx.closePath(); } } }); impactEffects.forEach(effect => { if (isVisible(effect.x, effect.y)) { effect.draw(ctx); } }); // Draw other players Object.values(otherPlayers).forEach(otherPlayer => { if (isVisible(otherPlayer.x, otherPlayer.y)) { otherPlayer.drawWeapons(); otherPlayer.draw(); } }); // Draw Synth Coins synthCoins.forEach(coin => { if (isVisible(coin.x, coin.y)) { coin.draw(ctx); } }); // Draw the player if (player) { player.drawWeapons(); player.draw(); } // Update and draw particles particles.forEach((particle, index) => { particle.update(); if (isVisible(particle.x, particle.y)) { particle.draw(); } if (particle.ttl <= 0) { particles.splice(index, 1); } }); // Update and draw floating texts floatingTexts = floatingTexts.filter((text, index) => { text.y += text.velocity; text.lifetime--; if (text.lifetime <= 0) { if (text === currentCoinFloatingText) { currentCoinFloatingText = null; } return false; } if (isVisible(text.x, text.y)) { ctx.font = '16px Orbitron'; ctx.fillStyle = text.color; ctx.textAlign = 'center'; let drawX = text.x; let drawY = text.y; // For damage text (not moving up), add a slight random offset if (text.velocity === 0) { drawX += (Math.random() - 0.5) * 2; // Random X offset drawY += (Math.random() - 0.5) * 2; // Random Y offset } ctx.fillText(text.text, drawX, drawY); } return true; }); } function selectClass(playerClass) { if (!playerClass.unlocked) { const unlockRequirements = playerClass.unlock_requirements_text || "Unlock requirements not available"; notificationSystem.showNotification(`This class is locked. ${unlockRequirements}`); return; } selectedClassId = playerClass._id; const classSelector = document.getElementById('classSelector'); const classOptions = classSelector.querySelectorAll('.class-option'); classOptions.forEach(option => option.classList.remove('selected')); const selectedOption = Array.from(classOptions).find(option => option.querySelector('img').alt === playerClass.name); if (selectedOption) { selectedOption.classList.add('selected'); } if (currentUser) { // Only send selection to server if user is logged in fetch('/select-class', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': getSessionToken() }, body: JSON.stringify({ class_id: selectedClassId }), }) .then(response => { if (!response.ok) { return response.json().then(data => { throw new Error(data.error || 'Failed to select class'); }); } return response.json(); }) .then(data => { if (data.success) { debugLog('Class selected successfully'); notificationSystem.showNotification(`${playerClass.name} class selected successfully!`); } else { throw new Error(data.error || 'Failed to select class'); } }) .catch(error => { console.error('Error selecting class:', error.message); notificationSystem.showNotification(`Failed to select class: ${error.message}`); }); } } function debugShopWidget() { debugLog('Debugging Shop Widget:'); const shopWidget = document.getElementById('shopWidget'); debugLog('Shop Widget:', shopWidget); debugLog('Shop Widget Classes:', shopWidget.classList); const weaponSelector = document.getElementById('weaponSelector'); debugLog('Weapon Selector:', weaponSelector); if (weaponSelector) { debugLog('Weapon Selector Children:', weaponSelector.children); debugLog('Weapon Selector Display:', getComputedStyle(weaponSelector).display); } const shopTab = document.querySelector('#shopWidget .tab-button[data-tab="shop"]'); debugLog('Shop Tab:', shopTab); if (shopTab) { debugLog('Shop Tab Classes:', shopTab.classList); } const shopContent = document.querySelector('#shopWidget #shop'); debugLog('Shop Content:', shopContent); if (shopContent) { debugLog('Shop Content Display:', getComputedStyle(shopContent).display); } debugLog('Player:', player); if (player && player.playerClass) { debugLog('Player Class ID:', player.playerClass._id); } } function showUnlockRequirements(playerClass) { const requirementsPopup = document.createElement('div'); requirementsPopup.classList.add('requirements-popup'); requirementsPopup.innerHTML = `

${playerClass.name} Unlock Requirements

`; document.body.appendChild(requirementsPopup); document.getElementById('closeRequirements').addEventListener('click', () => requirementsPopup.remove()); } function isVisible(x, y) { return x >= camera.x - 50 && x <= camera.x + camera.width + 50 && y >= camera.y - 50 && y <= camera.y + camera.height + 50; } function drawJoystick() { const joystickX = joystick.startX; const joystickY = joystick.startY; ctx.beginPath(); ctx.arc(joystickX, joystickY, 40, 0, Math.PI * 2); ctx.fillStyle = 'rgba(255, 255, 255, 0.2)'; ctx.fill(); ctx.closePath(); ctx.beginPath(); ctx.arc(joystick.endX, joystick.endY, 20, 0, Math.PI * 2); ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; ctx.fill(); ctx.closePath(); } function handleKeyDown(e) { if (!gameStarted) return; switch(e.key) { case 'ArrowUp': case 'w': case 'W': player.keys.up = true; break; case 'ArrowDown': case 's': case 'S': player.keys.down = true; break; case 'ArrowLeft': case 'a': case 'A': player.keys.left = true; break; case 'ArrowRight': case 'd': case 'D': player.keys.right = true; break; } } function handleKeyUp(e) { if (!gameStarted) return; switch(e.key) { case 'ArrowUp': case 'w': case 'W': player.keys.up = false; break; case 'ArrowDown': case 's': case 'S': player.keys.down = false; break; case 'ArrowLeft': case 'a': case 'A': player.keys.left = false; break; case 'ArrowRight': case 'd': case 'D': player.keys.right = false; break; } } let moveTouch = null; let aimTouch = null; function handleTouchStart(e) { if (!gameStarted || !player) return; // Check if the touch is on a UI element if (e.target.closest('#uiWidget, #classWidget, #infoWidget, #shopWidget, #mobileControls')) { return; } e.preventDefault(); const touches = e.changedTouches; for (let i = 0; i < touches.length; i++) { const touch = touches[i]; if (touch.clientX < window.innerWidth / 2) { // Left half of the screen - movement moveTouch = touch; joystick.active = true; joystick.startX = joystick.endX = touch.clientX; joystick.startY = joystick.endY = touch.clientY; } else { // Right half of the screen - aiming and shooting aimTouch = touch; shootingJoystick.active = true; shootingJoystick.startX = shootingJoystick.currentX = touch.clientX; shootingJoystick.startY = shootingJoystick.currentY = touch.clientY; shootingJoystick.innerRadius = 0; shootingJoystick.aimAngle = 0; } } } function handleTouchMove(e) { e.preventDefault(); if (!gameStarted || !player) return; const touches = e.changedTouches; for (let i = 0; i < touches.length; i++) { const touch = touches[i]; if (touch.identifier === moveTouch?.identifier) { // Movement joystick.endX = touch.clientX; joystick.endY = touch.clientY; const dx = joystick.endX - joystick.startX; const dy = joystick.endY - joystick.startY; const distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0) { const maxJoystickDistance = 40; const scale = Math.min(1, distance / maxJoystickDistance); player.velocityX = (dx / distance) * player.maxSpeed * scale; player.velocityY = (dy / distance) * player.maxSpeed * scale; } } else if (touch.identifier === aimTouch?.identifier) { // Aiming and shooting shootingJoystick.currentX = touch.clientX; shootingJoystick.currentY = touch.clientY; updatePlayerAim(touch.clientX, touch.clientY); } } } function handleTouchEnd(e) { e.preventDefault(); if (!gameStarted || !player) return; const touches = e.changedTouches; for (let i = 0; i < touches.length; i++) { const touch = touches[i]; if (touch.identifier === moveTouch?.identifier) { joystick.active = false; player.velocityX = 0; player.velocityY = 0; moveTouch = null; } else if (touch.identifier === aimTouch?.identifier) { shootingJoystick.active = false; aimTouch = null; player.isShooting = false; } } } function updateAndDrawShootingJoystick(deltaTime) { if (!shootingJoystick.active) { shootingJoystick.innerRadius = 0; return; } // Grow the inner circle shootingJoystick.innerRadius += shootingJoystick.growthRate * deltaTime; shootingJoystick.innerRadius = Math.min(shootingJoystick.innerRadius, shootingJoystick.maxInnerRadius); // Check if we should start shooting if (shootingJoystick.innerRadius >= shootingJoystick.maxInnerRadius * shootingJoystick.shootThreshold) { player.isShooting = true; } // Calculate the position of the inner circle const dx = shootingJoystick.currentX - shootingJoystick.startX; const dy = shootingJoystick.currentY - shootingJoystick.startY; const distance = Math.sqrt(dx * dx + dy * dy); const angle = Math.atan2(dy, dx); const cappedDistance = Math.min(distance, shootingJoystick.outerRadius); const innerX = shootingJoystick.startX + Math.cos(angle) * cappedDistance; const innerY = shootingJoystick.startY + Math.sin(angle) * cappedDistance; // Draw the outer circle (base of the joystick) ctx.beginPath(); ctx.arc(shootingJoystick.startX, shootingJoystick.startY, shootingJoystick.outerRadius, 0, Math.PI * 2); ctx.fillStyle = 'rgba(255, 0, 0, 0.2)'; ctx.fill(); ctx.closePath(); // Draw the inner circle (movable part of the joystick) ctx.beginPath(); ctx.arc(innerX, innerY, shootingJoystick.innerRadius, 0, Math.PI * 2); ctx.fillStyle = 'rgba(255, 0, 0, 0.5)'; ctx.fill(); ctx.closePath(); // Draw the aiming line ctx.beginPath(); ctx.moveTo(shootingJoystick.startX, shootingJoystick.startY); ctx.lineTo(innerX, innerY); ctx.strokeStyle = 'rgba(255, 0, 0, 0.8)'; ctx.lineWidth = 2; ctx.stroke(); } function updatePlayerAim(clientX, clientY) { if (!isAutoFireEnabled && player) { const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); if (isMobile && shootingJoystick.active) { // Mobile touch aiming const dx = shootingJoystick.currentX - shootingJoystick.startX; const dy = shootingJoystick.currentY - shootingJoystick.startY; shootingJoystick.aimAngle = Math.atan2(dy, dx); player.setWeaponAngle(shootingJoystick.aimAngle); } else { // Mouse aiming (for PC) const rect = canvas.getBoundingClientRect(); const mouseX = clientX - rect.left + camera.x; const mouseY = clientY - rect.top + camera.y; player.mouseX = mouseX; player.mouseY = mouseY; const dx = mouseX - player.x; const dy = mouseY - player.y; const angle = Math.atan2(dy, dx); player.setWeaponAngle(angle); } } } window.addEventListener('keydown', handleKeyDown); window.addEventListener('keyup', handleKeyUp); canvas.addEventListener('touchstart', handleTouchStart); canvas.addEventListener('touchmove', handleTouchMove); canvas.addEventListener('touchend', handleTouchEnd); document.addEventListener('visibilitychange', function() { if (!gameStarted || !player) return; // Only proceed if the game is running if (document.hidden) { wasPausedBeforeHidden = isPlayerPaused; if (!isPlayerPaused) { pausePlayer(); socket.emit('player_paused', { isPaused: true }); } } else { if (!wasPausedBeforeHidden && !areInterfacesOpen()) { unpausePlayer(); socket.emit('player_paused', { isPaused: false }); } } }); // Modify the play button event listener playButton.addEventListener('click', () => { if (socket && socket.connected) { // If socket is already connected, just try to join socket.emit('join', { room: 'main_game_room', name: currentUser || `Guest-${Math.floor(Math.random() * 10000)}`, weapon: currentWeapon }); } else { startGame(); } update(); }); quitButton.addEventListener('click', () => { window.location.reload(); }); // Update the window resize event handler window.addEventListener('resize', () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight; backgroundCanvas.width = canvas.width; backgroundCanvas.height = canvas.height; backgroundCtx.fillStyle = 'rgb(18, 4, 88)'; backgroundCtx.fillRect(0, 0, backgroundCanvas.width, backgroundCanvas.height); if (camera) { camera.width = canvas.width; camera.height = canvas.height; } if (gameStarted) { drawExperienceBar(); updateSynthCoinsDisplay(); } }); window.addEventListener('load', () => { const shopButton = document.getElementById('shopButton'); if (shopButton) { shopButton.addEventListener('click', toggleShop); } }); document.addEventListener('DOMContentLoaded', initShop); class Particle { constructor(x, y, color, radius, velocityX, velocityY, ttl) { this.x = x; this.y = y; this.color = color; this.radius = radius; this.velocityX = velocityX; this.velocityY = velocityY; this.ttl = ttl; } update() { this.x += this.velocityX; this.y += this.velocityY; this.ttl--; this.radius *= 0.98; } draw() { ctx.beginPath(); ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); ctx.fillStyle = this.color; ctx.fill(); ctx.closePath(); } } class ImpactEffect { constructor(x, y, color, size, angle) { this.x = x; this.y = y; this.color = color; this.size = size; this.angle = angle; this.lifetime = 20; // Number of frames the effect will last this.particles = []; this.generateParticles(); } generateParticles() { const particleCount = 10; for (let i = 0; i < particleCount; i++) { const speed = Math.random() * 2 + 1; const angle = this.angle + (Math.random() - 0.5) * Math.PI / 2; // Spread particles in a cone this.particles.push({ x: this.x, y: this.y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, size: Math.random() * this.size / 2, color: this.color }); } } update() { this.lifetime--; this.particles.forEach(particle => { particle.x += particle.vx; particle.y += particle.vy; particle.size *= 0.9; // Shrink particles over time }); } draw(ctx) { this.particles.forEach(particle => { ctx.beginPath(); ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2); ctx.fillStyle = particle.color; ctx.fill(); }); } } function updateSongDurationBar() { const now = Date.now() / 1000; const elapsed = now - songStartTime; const remaining = Math.max(0, currentSongDuration - elapsed); const progress = Math.min(1, elapsed / currentSongDuration); const fillElement = document.getElementById('songDurationFill'); const timerElement = document.getElementById('songTimer'); const songNameElement = document.querySelector('#songName .scrolling-text'); fillElement.style.width = `${(1 - progress) * 100}%`; timerElement.textContent = formatTime(remaining); songNameElement.textContent = `Now playing: ${currentSongName}`; } function formatTime(seconds) { const minutes = Math.floor(seconds / 60); const remainingSeconds = Math.floor(seconds % 60); return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; } function showSongDurationBar() { document.getElementById('songDurationBar').style.display = 'block'; } function hideSongDurationBar() { document.getElementById('songDurationBar').style.display = 'none'; } document.addEventListener('DOMContentLoaded', () => { const burgerButton = document.getElementById('burgerButton'); const menuButtons = document.getElementById('menuButtons'); burgerButton.addEventListener('click', () => { menuButtons.classList.toggle('show'); }); // Close the menu when clicking outside document.addEventListener('click', (event) => { if (!burgerButton.contains(event.target) && !menuButtons.contains(event.target)) { menuButtons.classList.remove('show'); } }); }); init();