3898 lines
125 KiB
JavaScript

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, image) {
console.log('showNotification called with message:', message, 'and image:', image);
const notificationElement = document.createElement('div');
notificationElement.className = 'custom-notification draggable';
let content = `<div class="notification-content">`;
if (image && typeof image === 'string') {
console.log('Adding image to notification:', image);
content += `<img src="${image}" alt="Achievement" class="notification-image" onerror="console.error('Failed to load image:', this.src);">`;
} else {
console.log('No valid image provided for notification');
}
content += `
<p>${message}</p>
<button class="close-notification">Close</button>
</div>`;
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}`;
console.log('Showing notification:', message);
console.log('Achievement image path:', data.image);
console.log('Calling showNotification with message:', message, 'and image:', data.image);
notificationSystem.showNotification(message, data.image);
}
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 = `
<div class="sign-in-message">
<p>Please sign in to view your ${feature}.</p>
</div>
`;
}
function displayNoAchievementsMessage(container) {
container.innerHTML = `
<div class="no-achievements-message">
<p>You haven't unlocked any achievements yet.</p>
<p>Keep playing to earn achievements!</p>
</div>
`;
}
function displayErrorMessage(container, message) {
container.innerHTML = `
<div class="error-message">
<p>${message}</p>
</div>
`;
}
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 = `
<img src="${achievement.image}" alt="${achievement.name}">
${!achievement.unlocked ? '<div class="lock-overlay"><i class="fas fa-lock"></i></div>' : ''}
`;
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 = '<h5>Classes Unlocked:</h5>';
if (achievement.unlocked_classes && achievement.unlocked_classes.length > 0) {
achievement.unlocked_classes.forEach(c => {
unlockedClasses.innerHTML += `<div class="unlock-item"><img src="${c.image}" alt="${c.name}"><span>${c.name}</span></div>`;
});
} else {
unlockedClasses.innerHTML += '<p>No classes unlocked</p>';
}
unlockedSkins.innerHTML = '<h5>Skins Unlocked:</h5>';
if (achievement.unlocked_skins && achievement.unlocked_skins.length > 0) {
achievement.unlocked_skins.forEach(s => {
unlockedSkins.innerHTML += `<div class="unlock-item"><img src="${s.image}" alt="${s.name}"><span>${s.name}</span></div>`;
});
} else {
unlockedSkins.innerHTML += '<p>No skins unlocked</p>';
}
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 = `
<div class="achievements-container">
<div id="achievementsGrid" class="achievements-grid"></div>
<div id="achievementInfoBox" class="achievement-info-box">
<img id="achievementImage" src="" alt="Achievement">
<h3 id="achievementName"></h3>
<p id="achievementDescription"></p>
<div id="achievementUnlocks">
<h4>Unlocks:</h4>
<div id="unlockedClasses"></div>
<div id="unlockedSkins"></div>
</div>
</div>
</div>
`;
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 = `
<div class="widget-tabs">
<button class="tab-button active" data-tab="shop">Shop</button>
</div>
<div class="widget-content">
<div id="shop" class="tab-content active">
<div id="weaponInfoBox">
<div id="weaponImage"></div>
<div id="weaponDetails">
<h3 id="weaponName"></h3>
<div id="weaponStats"></div>
<button id="buyWeaponButton" class="buy-button">Buy</button>
</div>
</div>
<div id="weaponSelector"></div>
</div>
</div>
<button class="close-button">Close</button>
`;
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 = `<img src="${weapon.image_source}" alt="${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 = `<img src="${weapon.image_source}" alt="${weapon.name}">`;
weaponName.textContent = weapon.name;
weaponStats.innerHTML = `
<p>Damage: ${weapon.base_attributes.damage}</p>
<p>Fire Rate: ${weapon.base_attributes.fire_rate}</p>
<p>Bullet Speed: ${weapon.base_attributes.bullet_speed}</p>
<p>Bullet Size: ${weapon.base_attributes.bullet_size}</p>
<p>Price: ${weapon.price} Synth Coins</p>
`;
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 = `
<img src="${weapon.image_source}" alt="${weapon.name}">
<h3>${weapon.name}</h3>
<p>Price: ${weapon.price} Synth Coins</p>
<button class="buy-button" data-weapon-id="${weapon._id}">Buy</button>
`;
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 = `<img src="${playerClass.image_source}" alt="${playerClass.name}">`;
if (!playerClass.unlocked) {
classOption.classList.add('locked');
const lockOverlay = document.createElement('div');
lockOverlay.classList.add('lock-overlay');
lockOverlay.innerHTML = '<i class="fas fa-lock"></i>';
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 = `<img src="${playerClass.image_source}" alt="${playerClass.name}">`;
className.textContent = playerClass.name;
classStats.innerHTML = `
<p>Health: ${playerClass.base_attributes.health}</p>
<p>Speed: ${playerClass.base_attributes.speed}</p>
<p>Damage: ${playerClass.base_attributes.damage_multiplier}</p>
`;
// Remove the locked indicator if the class is unlocked
if (playerClass.unlocked) {
className.innerHTML = playerClass.name;
} else {
className.innerHTML = `${playerClass.name} <span class="locked-indicator">(Locked)</span>`;
}
}
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 = `
<h2>Level Up!</h2>
<p>Choose an upgrade:</p>
<div id="upgradeOptions"></div>
`;
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 = `
<span class="upgrade-name">${option.name}</span>
<span class="upgrade-value">+${option.value}</span>
`;
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 = `
<span class="upgrade-name">${option.name}</span>
<span class="upgrade-value">+${option.value}</span>
`;
container.appendChild(div);
});
} else {
clearInterval(intervalId);
container.innerHTML = '';
options.forEach((option, index) => {
const button = document.createElement('button');
button.classList.add('upgrade-button');
button.innerHTML = `
<span class="upgrade-name">${option.name}</span>
<span class="upgrade-value">+${option.value}</span>
`;
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 = '<p>No colors unlocked yet</p>';
}
if (hatOptions.children.length === 0) {
hatOptions.innerHTML = '<p>No hats unlocked yet</p>';
}
}
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 = `
<h3>Welcome, ${currentUser}!</h3>
<p>Your Discord account is linked:</p>
<div class="discord-info">
<img src="https://cdn.discordapp.com/avatars/${data.discord_id}/${data.avatar}.png"
alt="Discord Avatar"
class="discord-avatar">
<span class="discord-username">${data.username}</span>
<button id="unlinkDiscordButton" class="small-button">Unlink Discord</button>
</div>
`;
document.getElementById('unlinkDiscordButton').addEventListener('click', unlinkDiscord);
} else {
accountContent.innerHTML = `
<h3>Welcome, ${currentUser}!</h3>
<p>Link your Discord account:</p>
<button id="linkDiscordButton" class="button">Link Discord</button>
`;
document.getElementById('linkDiscordButton').addEventListener('click', linkDiscord);
}
})
.catch(error => {
console.error('Error fetching Discord info:', error);
accountContent.innerHTML = `
<h3>Welcome, ${currentUser}!</h3>
<p>Error loading Discord information. Please try again later.</p>
`;
});
} 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 = '<div class="loading-spinner">Loading skins...</div>';
// 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) {
alertSystem.addnotificationSystem.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 = `
<h3>${playerClass.name} Unlock Requirements</h3>
<ul>
${Object.entries(playerClass.unlock_requirements).map(([key, value]) => `<li>${key}: ${value}</li>`).join('')}
</ul>
<button id="closeRequirements">Close</button>
`;
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();