3898 lines
125 KiB
JavaScript
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();
|