// Firebase Configuration const firebaseConfig = { apiKey: "AIzaSyB5NffQxssjZKEAUyp8Wyt2Y3rJmJOVfx4", authDomain: "holdspot-977d3.firebaseapp.com", projectId: "holdspot-977d3", storageBucket: "holdspot-977d3.firebasestorage.app", messagingSenderId: "969993737434", appId: "1:969993737434:web:17f43caab0bf46966701e3" }; // Initialize Firebase firebase.initializeApp(firebaseConfig); const db = firebase.firestore(); // Icon Colors const iconColors = { 'fa-tag': '#6d6875', 'fa-shopping-cart': '#1e3a8a', 'fa-right-left': '#5a189a', 'fa-gift': '#d00000', 'fa-file-contract': '#007200', 'fa-plane': '#023e8a', 'fa-bed': '#6d4c41', 'fa-map-marker-alt': '#e63946', 'fa-users': '#4a1e6b', 'fa-road': '#386641', 'fa-briefcase': '#003049', 'fa-user-tie': '#283618', 'fa-person-digging': '#2d6a4f', 'fa-heart': '#9d0208', 'fa-lightbulb': '#ff8500', 'fa-palette': '#9d4edd', 'fa-film': '#560bad', 'fa-hiking': '#004b23', 'fa-glass-cheers': '#ff6d00', 'fa-star': '#ffb700', 'fa-exclamation-triangle': '#dc2f02', 'fa-eye': '#480ca8', 'fa-clipboard-list': '#495057', 'fa-compass': '#774936', 'fa-bolt': '#ffd000' }; // Subcategory Names const subcategoryNames = { 'fa-tag': 'Sell', 'fa-shopping-cart': 'Buy', 'fa-right-left': 'Exchange', 'fa-gift': 'Gifts', 'fa-file-contract': 'Lease', 'fa-plane': 'Travel', 'fa-bed': 'Accommodation', 'fa-map-marker-alt': 'Places', 'fa-users': 'Communities', 'fa-road': 'Trips', 'fa-briefcase': 'Jobs', 'fa-user-tie': 'Professional', 'fa-concierge-bell': 'Services', 'fa-heart': 'Dating', 'fa-lightbulb': 'Ideas', 'fa-palette': 'Arts', 'fa-film': 'Entertainment', 'fa-hiking': 'Recreational', 'fa-glass-cheers': 'Gatherings', 'fa-star': 'Experiences', 'fa-exclamation-triangle': 'Alerts', 'fa-eye': 'Monitor', 'fa-clipboard-list': 'Reports', 'fa-compass': 'Orientation', 'fa-bolt': 'Actions' }; // Icon Categories const iconCategories = { 'Transactions': [ 'fa-tag', 'fa-shopping-cart', 'fa-right-left', 'fa-gift', 'fa-file-contract' ], 'Movement': [ 'fa-plane', 'fa-bed', 'fa-map-marker-alt', 'fa-users', 'fa-road' ], 'Connections': [ 'fa-briefcase', 'fa-user-tie', 'fa-concierge-bell', 'fa-heart', 'fa-lightbulb' ], 'Leisure': [ 'fa-palette', 'fa-film', 'fa-hiking', 'fa-glass-cheers', 'fa-star' ], 'Oversight': [ 'fa-exclamation-triangle', 'fa-eye', 'fa-clipboard-list', 'fa-compass', 'fa-bolt' ] }; // Archetype Mapping const archetype = { 'Transactions': 'MERCHANT', 'Movement': 'EXPLORER', 'Connections': 'DIPLOMAT', 'Leisure': 'ARTISAN', 'Oversight': 'SENTINEL' }; // Abilities Data const abilitiesData = [ { id: 'foundation', name: 'First Foundation', icon: 'fa-home', requirement: '2 markers created', effect: '+1 random token when create markers', prerequisite: [], row: 1, col: 1 }, { id: 'territoryClaim', name: 'Territory Claim', icon: 'fa-flag', requirement: 'Subtract/add 10 Visibility points in battles', effect: 'Winning/losing in battles subtract/add +1 Visibility', prerequisite: ['foundation'], row: 2, col: 1 }, { id: 'branchingPaths', name: 'Branching Paths', icon: 'fa-code-branch', requirement: 'Create 5 markers in the same category', effect: '+1 Visibility for new markers in Archetype', prerequisite: ['foundation'], row: 2, col: 2 }, { id: 'hybridization', name: 'Wild Hybridization', icon: 'fa-dna', requirement: 'Gain 25 points in any metric', effect: '+1 Visibility for new markers', prerequisite: ['foundation'], row: 2, col: 3 }, { id: 'headhunter', name: 'Lethal Headhunter', icon: 'fa-crosshairs', requirement: 'Win 25 battles', effect: 'Battle cooldown reduced by 10%', prerequisite: ['territoryClaim', 'branchingPaths'], row: 3, col: 1 }, { id: 'mediator', name: 'Lost Mediator', icon: 'fa-balance-scale', requirement: 'Lost 25 battles', effect: 'Win chances in battle increased by 5%', prerequisite: ['territoryClaim', 'branchingPaths'], row: 3, col: 2 }, { id: 'grandmaster', name: 'Gory Grandmaster', icon: 'fa-crown', requirement: 'Battle 10 Archetype markers', effect: 'Gain +1 random token when battle against Archetype markers', prerequisite: ['branchingPaths', 'hybridization'], row: 3, col: 3 }, { id: 'manipulator', name: 'Tricky Manipulator', icon: 'fa-chess', requirement: 'Create 10 Archetype markers', effect: 'Gain +1 random token when create Archetype markers', prerequisite: ['branchingPaths', 'hybridization'], row: 3, col: 4 }, { id: 'networker', name: 'Logical Networker', icon: 'fa-network-wired', requirement: 'Gain 25 in Link/Mail metric', effect: 'Gain +1 random token when click first time on Link or Mail markers', prerequisite: ['hybridization'], row: 3, col: 5 }, { id: 'matchmaker', name: 'Lovely Matchmaker', icon: 'fa-heart', requirement: 'Gain 25 in Likes/Fav metric', effect: 'Gain +1 random token when first time click on Like or Fav markers', prerequisite: ['hybridization'], row: 3, col: 6 }, { id: 'tactician', name: 'Brutal Tactician', icon: 'fa-brain', requirement: 'Lost 50 battles', effect: 'Gain +5 random tokens on each battle lost', prerequisite: ['headhunter', 'mediator'], row: 4, col: 1 }, { id: 'battleExpert', name: 'Battle Expert', icon: 'fa-shield-alt', requirement: 'Win 50 battles', effect: 'Win chances in battle increased by 10%', prerequisite: ['headhunter', 'mediator'], row: 4, col: 2 }, { id: 'pathfinder', name: 'Smart Pathfinder', icon: 'fa-compass', requirement: 'Subtract/add +10 Visibility points in battles', effect: 'Winning/losing in battles subtract/add +5 more Visibility', prerequisite: ['mediator', 'grandmaster'], row: 4, col: 3 }, { id: 'overlord', name: 'Absolute Overlord', icon: 'fa-skull', requirement: 'Gain 50 in Share metric', effect: 'Gain +5 random tokens when first time click on Share for Archetype markers', prerequisite: ['grandmaster', 'manipulator'], row: 4, col: 4 }, { id: 'emissary', name: 'Gloomy Emissary', icon: 'fa-dove', requirement: 'Lose against 50 non Archetype markers', effect: 'Gain +10 random tokens on each battle lost against non Archetype markers', prerequisite: ['manipulator', 'networker'], row: 4, col: 5 }, { id: 'kingpin', name: 'Rude Kingpin', icon: 'fa-user-tie', requirement: 'Create 100 non Archetype markers', effect: 'Gain +10 random token when create markers that are not Archetype', prerequisite: ['networker', 'matchmaker'], row: 4, col: 6 }, { id: 'hybridWarrior', name: 'Hybrid Warrior', icon: 'fa-fist-raised', requirement: 'Win against 50 non Archetype markers', effect: 'Gain +10 random tokens on each battle victory against non Archetype markers', prerequisite: ['matchmaker'], row: 4, col: 7 }, { id: 'virtuoso', name: 'Imperious Virtuoso', icon: 'fa-star', requirement: 'Own over 500 points in Visibility', effect: 'Markers gain +1 Visibility when first time click on Like/Fav/Link/Mail or Share', prerequisite: ['matchmaker'], row: 4, col: 8 }, { id: 'arbitrator', name: 'Arcane Arbitrator', icon: 'fa-gavel', requirement: 'Spend 250 tokens on shop items', effect: 'Shop items that influence battles have 25% increased effect duration', prerequisite: ['tactician', 'battleExpert'], row: 5, col: 1 }, { id: 'legend', name: 'Undying Legend', icon: 'fa-trophy', requirement: 'Win 100 battles', effect: 'Gain +10 random tokens on each battle victory', prerequisite: ['battleExpert', 'pathfinder'], row: 5, col: 2 }, { id: 'quartermaster', name: 'Tyrannical Quartermaster', icon: 'fa-box', requirement: 'Lose 100 battles', effect: 'Gain +10 random tokens on each battle lost', prerequisite: ['pathfinder', 'overlord'], row: 5, col: 3 }, { id: 'commander', name: 'Sovereign Commander', icon: 'fa-chess-king', requirement: 'Win/Lose battle ratio is above 50', effect: 'Win chances in battles increased by 25%', prerequisite: ['overlord', 'emissary'], row: 5, col: 4 }, { id: 'titan', name: 'Primordial Titan', icon: 'fa-mountain', requirement: 'Gain 250 points in Interaction metric', effect: 'Gain +10 random token and add +5 Visibility when first time click on markers', prerequisite: ['emissary', 'kingpin'], row: 5, col: 5 }, { id: 'dominator', name: 'Omniscient Dominator', icon: 'fa-crown', requirement: 'Subtract/add 100 Visibility points in battles', effect: 'Winning/losing in battles subtract/add +10 more Visibility', prerequisite: ['kingpin', 'hybridWarrior'], row: 5, col: 6 }, { id: 'visionary', name: 'Remote Visionary', icon: 'fa-eye', requirement: 'Gain 500 points in any metric', effect: 'Gain +10 random token when first time click on Like/Fav/Link/Mail or Share', prerequisite: ['hybridWarrior', 'virtuoso'], row: 5, col: 7 }, { id: 'expert', name: 'Trustfull Expert', icon: 'fa-award', requirement: 'Create 50 Archetype markers', effect: '+10 random token when create Archetype markers', prerequisite: ['virtuoso'], row: 5, col: 8 }, { id: 'pioneer', name: 'Spiritual Pioneer', icon: 'fa-flag', requirement: 'Create 100 markers', effect: '+10 random token when create markers', prerequisite: ['virtuoso'], row: 5, col: 9 }, { id: 'mastermind', name: 'Intimate Mastermind', icon: 'fa-brain', requirement: 'Buy 50 items in shop', effect: 'Shop items are now 25% cheaper', prerequisite: [], row: 5, col: 10 } ]; // Shop Items const shopItems = { potions: [ { id: 'potion1', name: 'Victory Elixir', value: 25, price: 250, time: 9000, type: 'liquidity', effect: '+25% win chance', description: 'Increases battle win chances temporarily' }, { id: 'potion2', name: 'Archetype Brew', value: 25, price: 250, time: 9000, type: 'value', effect: '+25% win chance against Archetype markers', description: 'Increases chance to battle Archetype markers' }, { id: 'potion3', name: 'Battle Serum', value: 5, price: 250, time: 9000, type: 'engagement', effect: 'x5 Visibility on battles', description: 'Improve battle gains' }, { id: 'potion4', name: 'Holy Water', value: 50, price: 1500, time: 18000, type: 'value', effect: 'Each win in battles give additional 50 random tokens', description: 'Gain tokens in battles' } ], utilities: [ { id: 'util1', name: 'Visibility Boost', price: 500, type: 'liquidity', effect: '+100 Visibility to owned marker', description: 'Increase marker visibility' }, { id: 'util2', name: 'Marker Relocator', price: 500, type: 'value', effect: 'Move owned marker to new location', description: 'Relocate your markers' }, { id: 'util3', name: 'Marker Eraser', price: 500, type: 'engagement', effect: 'Delete owned marker', description: 'Remove your markers' }, { id: 'util4', name: 'Visibility Crusher', price: 1000, type: 'value', effect: '-100 Visibility to any marker', description: 'Decrease any marker visibility' }, { id: 'util5', name: 'Exchange Master', price: 1000, type: 'value', effect: 'Reset exchange cooldown', description: 'Reset exchange cooldown' } ], boosters: [ { id: 'boost1', name: 'Token Amplifier', value: 3, price: 1000, time: 6000, type: 'value', effect: 'Token gain tripled', description: 'Boost token gains' }, { id: 'boost2', name: 'Abilities Surge', value: 3, price: 1000, time: 6000, type: 'engagement', effect: 'Abilities effects tripled', description: 'Boost abilities gains' }, { id: 'boost3', name: 'Experience Multiplier', value: 2, price: 1000, time: 6000, type: 'liquidity', effect: 'Metric gain ', description: 'Double metric progression' }, { id: 'boost4', name: 'Total Conquerer', value: 20, price: 2000, time: 6000, type: 'value', effect: 'Visibility from all sources increased', description: 'Visibility killer' } ] }; // Global Variables let map; let markers = {}; let currentUser = null; let userData = { markersCreated: 0, archetypeMarkersCreated: 0, likesGiven: 0, favoritesMade: 0, interactions: 0, shareClicked: 0, linksClicked: 0, emailsClicked: 0, archetype: '', liquidityTokensGained: 0, liquidityTokensSpent: 0, valueTokensGained: 0, valueTokensSpent: 0, engagementTokensGained: 0, engagementTokensSpent: 0, battlesWon: 0, battlesLost: 0, battleVisibilityChanged: 0, battleWinRatio: 0, engagement: 0, lastInteraction: Date.now(), abilities: [], activeItems: [], lastExchange: 0, lastBattle: 0 }; let selectedMarker = null; let addingMarker = false; let filteredMarkers = []; let utilityMode = null; let utilityTarget = null; // Initialize App document.addEventListener('DOMContentLoaded', async () => { initMap(); await initUser(); setupEventListeners(); loadUserData(); subscribeToMarkers(); // Initial token display updateTokenDisplay(); }); // Initialize Map function initMap() { map = L.map('map', { zoomControl: false, attributionControl: false, cursor: 'default', doubleClickZoom: false, minZoom: 2, maxZoom: 10, maxBounds: [[-60, -120], [60, 120]], maxBoundsViscosity: 1, center: [20, 0], zoom: 2 }); L.tileLayer('https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png', { attribution: '© OpenStreetMap, © CartoDB', subdomains: 'abcd', minZoom: 2, maxZoom: 10 }).addTo(map); map.on('click', handleMapClick); } // Initialize User async function initUser() { // Check if user exists in localStorage let userId = localStorage.getItem('userId'); if (!userId) { // Generate new UUID userId = generateUUID(); localStorage.setItem('userId', userId); } currentUser = { id: userId, liquidityTokens: 0, valueTokens: 0, engagementTokens: 0 }; // Check if user exists in Firestore const userDoc = await db.collection('users').doc(userId).get(); if (!userDoc.exists) { // Create new user document await db.collection('users').doc(userId).set({ userID: userId, liquidityTokens: 0, valueTokens: 0, engagementTokens: 0 }); } else { // Load user data const data = userDoc.data(); currentUser.liquidityTokens = data.liquidityTokens || 0; currentUser.valueTokens = data.valueTokens || 0; currentUser.engagementTokens = data.engagementTokens || 0; } } // Generate UUID function generateUUID() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0; const v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } // Setup Event Listeners function setupEventListeners() { // Theme Toggle document.getElementById('theme-toggle').addEventListener('click', toggleTheme); // Search Bar document.getElementById('search-bar').addEventListener('input', handleSearch); // Action Buttons document.getElementById('dashboard-btn').addEventListener('click', () => openModal('dashboard')); document.getElementById('battle-btn').addEventListener('click', () => openModal('battle')); document.getElementById('shop-btn').addEventListener('click', () => openModal('shop')); document.getElementById('add-marker-btn').addEventListener('click', startAddMarker); // Modal Overlay document.getElementById('modal-overlay').addEventListener('click', closeModal); // Add Marker Form document.getElementById('add-marker-form').addEventListener('submit', handleAddMarker); // Dashboard Tabs document.querySelectorAll('.tab-button').forEach(button => { button.addEventListener('click', () => switchTab('dashboard', button.dataset.tab)); }); // Analytics Tabs document.querySelectorAll('.analytics-tab-button').forEach(button => { button.addEventListener('click', () => switchAnalyticsTab(button.dataset.analyticsTab)); }); // Shop Tabs document.querySelectorAll('.shop-tab-button').forEach(button => { button.addEventListener('click', () => switchTab('shop', button.dataset.shopTab)); }); // Battle Button document.getElementById('start-battle-btn').addEventListener('click', startBattle); // Category Selection for Add Marker document.getElementById('marker-category').addEventListener('change', populateSubcategories); // Populate initial subcategories populateSubcategories(); } // Toggle Theme function toggleTheme() { document.documentElement.classList.toggle('dark'); localStorage.setItem('theme', document.documentElement.classList.contains('dark') ? 'dark' : 'light'); } // Handle Search function handleSearch(e) { const query = e.target.value.toLowerCase(); filteredMarkers = Object.values(markers).filter(marker => marker.title.toLowerCase().includes(query) || marker.body.toLowerCase().includes(query) || marker.subcategory.toLowerCase().includes(query) ); // Update map markers updateMapMarkers(); } // Open Modal function openModal(modalType) { document.body.classList.add('modal-active'); document.getElementById('modal-overlay').classList.remove('hidden'); document.getElementById(`${modalType}-modal`).classList.remove('hidden'); // Load modal content switch(modalType) { case 'dashboard': loadDashboardContent(); break; case 'shop': loadShopContent(); break; case 'battle': loadBattleContent(); break; } } // Close Modal function closeModal() { document.body.classList.remove('modal-active'); document.getElementById('modal-overlay').classList.add('hidden'); document.querySelectorAll('[id$="-modal"]').forEach(modal => { modal.classList.add('hidden'); }); // Reset utility mode if active if (utilityMode) { utilityMode = null; utilityTarget = null; showNotification('Utility mode cancelled', 'info'); updateMapMarkers(); } } // Switch Tab function switchTab(modalType, tabName) { // Hide all tabs document.querySelectorAll(`#${modalType}-modal .tab-content`).forEach(tab => { tab.classList.add('hidden'); }); // Remove active class from all buttons document.querySelectorAll(`#${modalType}-modal .tab-button`).forEach(button => { button.classList.remove('active'); }); // Show selected tab document.getElementById(`${tabName}-tab`).classList.remove('hidden'); // Add active class to selected button document.querySelector(`#${modalType}-modal .tab-button[data-tab="${tabName}"]`).classList.add('active'); } // Switch Analytics Tab function switchAnalyticsTab(tabName) { // Hide all charts document.querySelectorAll('.analytics-chart-container').forEach(container => { container.classList.add('hidden'); }); // Remove active class from all buttons document.querySelectorAll('.analytics-tab-button').forEach(button => { button.classList.remove('active'); }); // Show selected chart document.getElementById(`${tabName}-chart-container`).classList.remove('hidden'); // Add active class to selected button document.querySelector(`.analytics-tab-button[data-analytics-tab="${tabName}"]`).classList.add('active'); // Load chart data loadChartData(tabName); } // Handle Map Click function handleMapClick(e) { if (addingMarker) { // Place marker openModal('add-marker'); addingMarker = false; document.getElementById('map').style.cursor = 'default'; } } // Start Add Marker Process function startAddMarker() { addingMarker = true; document.getElementById('map').style.cursor = 'crosshair'; showNotification('Click on the map to place your marker', 'info'); } // Populate Subcategories function populateSubcategories() { const category = document.getElementById('marker-category').value; const subcategorySelect = document.getElementById('marker-subcategory'); // Clear existing options subcategorySelect.innerHTML = ''; if (category && iconCategories[category]) { iconCategories[category].forEach(icon => { const option = document.createElement('option'); option.value = icon; option.textContent = subcategoryNames[icon]; subcategorySelect.appendChild(option); }); } } // Handle Add Marker Form Submission async function handleAddMarker(e) { e.preventDefault(); // Get form values const title = document.getElementById('marker-title-input').value; const body = document.getElementById('marker-body-input').value; const category = document.getElementById('marker-category').value; const subcategory = document.getElementById('marker-subcategory').value; const email = document.getElementById('marker-email').value; const link = document.getElementById('marker-link').value; // Validate inputs if (!title || !body || !category || !subcategory) { showNotification('Please fill in all required fields', 'error'); return; } // Get map center for marker position const center = map.getCenter(); // Calculate initial visibility based on abilities let visibility = 100; if (userData.abilities.includes('branchingPaths') && userData.archetype === archetype[category]) { visibility += 1; } if (userData.abilities.includes('hybridization')) { visibility += 1; } // Create marker object const markerData = { title, body, category, subcategory, email: email || null, link: link || null, lat: center.lat, lng: center.lng, likes: 0, favorites: 0, shares: 0, userID: currentUser.id, zDepth: visibility // Using zDepth as visibility }; try { // Save to Firestore const docRef = await db.collection('markers').add(markerData); // Update user metrics userData.markersCreated++; if (userData.archetype === archetype[category]) { userData.archetypeMarkersCreated++; } // Award tokens based on abilities let tokensAwarded = false; if (userData.abilities.includes('foundation')) { awardRandomToken(); tokensAwarded = true; } if (userData.abilities.includes('manipulator') && userData.archetype === archetype[category]) { awardRandomToken(); tokensAwarded = true; } if (userData.abilities.includes('pioneer')) { awardRandomToken(); tokensAwarded = true; } // Save user data saveUserData(); // Update token display updateTokenDisplay(); // Show success notification showNotification( `Marker created successfully! ${tokensAwarded ? 'Tokens awarded.' : ''}`, 'success' ); // Close modal and reset form closeModal(); document.getElementById('add-marker-form').reset(); } catch (error) { console.error('Error creating marker:', error); showNotification('Failed to create marker. Please try again.', 'error'); } } // Award Random Token function awardRandomToken() { const tokenTypes = ['liquidity', 'value', 'engagement']; const tokenType = tokenTypes[Math.floor(Math.random() * tokenTypes.length)]; switch(tokenType) { case 'liquidity': currentUser.liquidityTokens++; userData.liquidityTokensGained++; break; case 'value': currentUser.valueTokens++; userData.valueTokensGained++; break; case 'engagement': currentUser.engagementTokens++; userData.engagementTokensGained++; break; } // Update Firestore db.collection('users').doc(currentUser.id).update({ [`userData.${tokenType}Tokens`]: currentUser[`${tokenType}Tokens`] }); } // Load Dashboard Content function loadDashboardContent() { // Metrics Tab const metricsTab = document.getElementById('metrics-tab'); metricsTab.innerHTML = `

Basic Metrics

Markers Created: ${userData.markersCreated}

Archetype Markers: ${userData.archetypeMarkersCreated}

Likes Given: ${userData.likesGiven}

Favorites Made: ${userData.favoritesMade}

Interactions: ${userData.interactions}

Engagement

Share Clicked: ${userData.shareClicked}

Links Clicked: ${userData.linksClicked}

Emails Clicked: ${userData.emailsClicked}

Archetype: ${userData.archetype || 'None'}

Engagement: ${userData.engagement}

Tokens

Liquidity: ${userData.liquidityTokensGained}/${userData.liquidityTokensSpent}

Value: ${userData.valueTokensGained}/${userData.valueTokensSpent}

Engagement: ${userData.engagementTokensGained}/${userData.engagementTokensSpent}

Battles

Won: ${userData.battlesWon}

Lost: ${userData.battlesLost}

Visibility Changed: ${userData.battleVisibilityChanged}

Win Ratio: ${userData.battleWinRatio.toFixed(2)}%

`; // Analytics Tab - will be loaded when tab is switched // Abilities Tab loadAbilitiesContent(); // Exchange Tab loadExchangeContent(); } // Load Abilities Content function loadAbilitiesContent() { const abilitiesTab = document.getElementById('abilities-tab'); // Count active abilities const activeCount = userData.abilities.length; const inactiveCount = abilitiesData.length - activeCount; // Create grid let gridHTML = `

${activeCount} abilities active, ${inactiveCount} inactive

`; // Sort abilities by row and column const sortedAbilities = [...abilitiesData].sort((a, b) => { if (a.row !== b.row) return a.row - b.row; return a.col - b.col; }); // Add abilities to grid sortedAbilities.forEach(ability => { const isActive = userData.abilities.includes(ability.id); const isAvailable = checkAbilityPrerequisites(ability.prerequisite); gridHTML += `
${ability.name}
`; }); gridHTML += '
'; abilitiesTab.innerHTML = gridHTML; } // Check Ability Prerequisites function checkAbilityPrerequisites(prerequisites) { return prerequisites.every(req => userData.abilities.includes(req)); } // Load Exchange Content function loadExchangeContent() { const exchangeTab = document.getElementById('exchange-tab'); // Calculate exchange rate (1:100/total markers) const totalMarkers = Object.keys(markers).length; const exchangeRate = totalMarkers > 0 ? (100 / totalMarkers).toFixed(2) : 0; // Check cooldown const now = Date.now(); const cooldownRemaining = Math.max(0, 86400000 - (now - userData.lastExchange)); const canExchange = cooldownRemaining <= 0; exchangeTab.innerHTML = `

Current Exchange Rate: 1 token = ${exchangeRate} points

Total Markers on Map: ${totalMarkers}

Next exchange available in: ${formatTime(cooldownRemaining)}

`; // Add event listener to exchange button document.getElementById('exchange-btn').addEventListener('click', handleExchange); } // Format Time function formatTime(ms) { const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); if (hours > 0) { return `${hours}h ${minutes % 60}m`; } if (minutes > 0) { return `${minutes}m ${seconds % 60}s`; } return `${seconds}s`; } // Handle Exchange function handleExchange() { const fromToken = document.getElementById('from-token').value; const toToken = document.getElementById('to-token').value; const amount = parseInt(document.getElementById('exchange-amount').value); // Validate inputs if (isNaN(amount) || amount <= 0) { showNotification('Please enter a valid amount', 'error'); return; } // Check if user has enough tokens if (currentUser[`${fromToken}Tokens`] < amount) { showNotification('Insufficient tokens', 'error'); return; } // Calculate exchange rate const totalMarkers = Object.keys(markers).length; const exchangeRate = totalMarkers > 0 ? (100 / totalMarkers) : 0; const receivedAmount = Math.floor(amount * exchangeRate); // Perform exchange currentUser[`${fromToken}Tokens`] -= amount; currentUser[`${toToken}Tokens`] += receivedAmount; // Update user data userData[`${fromToken}TokensSpent`] += amount; userData[`${toToken}TokensGained`] += receivedAmount; userData.lastExchange = Date.now(); // Update Firestore db.collection('users').doc(currentUser.id).update({ [`${fromToken}Tokens`]: currentUser[`${fromToken}Tokens`], [`${toToken}Tokens`]: currentUser[`${toToken}Tokens`] }); // Save user data saveUserData(); // Update token display updateTokenDisplay(); // Show success notification showNotification(`Exchanged ${amount} ${fromToken} tokens for ${receivedAmount} ${toToken} tokens`, 'success'); // Reload exchange content loadExchangeContent(); } // Load Shop Content function loadShopContent() { // Potions Tab loadShopTab('potions'); // Utilities Tab loadShopTab('utilities'); // Boosters Tab loadShopTab('boosters'); } // Load Shop Tab function loadShopTab(tabName) { const tabElement = document.getElementById(`${tabName}-tab`); const items = shopItems[tabName]; let html = ''; items.forEach(item => { html += `

${item.name}

${item.description}

Effect: ${item.effect}

${item.value ? `

Value: ${item.value}

` : ''} ${item.time ? `

Duration: ${Math.floor(item.time/1000)}s

` : ''}
${item.price} ${item.type.charAt(0).toUpperCase() + item.type.slice(1)} Tokens
`; }); tabElement.innerHTML = html; // Add event listeners to buy buttons document.querySelectorAll('.buy-btn').forEach(button => { button.addEventListener('click', (e) => { const itemId = e.target.dataset.itemId; const itemType = e.target.dataset.itemType; buyItem(itemId, itemType); }); }); } // Buy Item function buyItem(itemId, itemType) { const item = [...shopItems.potions, ...shopItems.utilities, ...shopItems.boosters] .find(i => i.id === itemId); // Check if user has enough tokens if (currentUser[`${item.type}Tokens`] < item.price) { showNotification('Insufficient tokens', 'error'); return; } // Deduct tokens currentUser[`${item.type}Tokens`] -= item.price; userData[`${item.type}TokensSpent`] += item.price; // Update Firestore db.collection('users').doc(currentUser.id).update({ [`${item.type}Tokens`]: currentUser[`${item.type}Tokens`] }); // Save user data saveUserData(); // Update token display updateTokenDisplay(); // Handle item-specific actions switch(itemType) { case 'potions': case 'boosters': // Add to active items with expiration userData.activeItems.push({ id: itemId, type: itemType, expiresAt: Date.now() + item.time }); showNotification(`${item.name} activated!`, 'success'); break; case 'utilities': // Enter utility mode utilityMode = itemId; utilityTarget = null; closeModal(); showNotification(`Select a marker to apply ${item.name}`, 'info'); updateMapMarkers(); break; } } // Load Battle Content function loadBattleContent() { const battleInfo = document.getElementById('battle-info'); // Calculate battle stats const totalMarkers = Object.keys(markers).length; const archetypeMarkers = Object.values(markers).filter(m => archetype[m.category] === userData.archetype ).length; // Calculate battle ratio const battleRatio = totalMarkers > 0 ? ((userData.battlesWon + userData.battlesLost) / totalMarkers * 100).toFixed(1) : 0; // Calculate win chance (base 50% with modifiers) let winChance = 50; // Apply potion modifiers const victoryElixir = userData.activeItems.find(item => item.id === 'potion1'); if (victoryElixir) { winChance += 25; } // Apply ability modifiers if (userData.abilities.includes('mediator')) { winChance += 5; } if (userData.abilities.includes('battleExpert')) { winChance += 10; } if (userData.abilities.includes('commander')) { winChance += 25; } // Clamp win chance between 5% and 95% winChance = Math.max(5, Math.min(95, winChance)); // Check cooldown const now = Date.now(); const cooldownRemaining = Math.max(0, 300000 - (now - userData.lastBattle)); const canBattle = cooldownRemaining <= 0; battleInfo.innerHTML = `

Total Markers: ${totalMarkers}

Your Archetype Markers: ${archetypeMarkers}

Battle Ratio: ${battleRatio}%

Current Win Chance: ${winChance}%

Cooldown: ${formatTime(cooldownRemaining)}

`; // Update cooldown timer if (!canBattle) { const cooldownInterval = setInterval(() => { const remaining = Math.max(0, 300000 - (Date.now() - userData.lastBattle)); document.getElementById('battle-cooldown-inner').textContent = formatTime(remaining); if (remaining <= 0) { clearInterval(cooldownInterval); document.getElementById('start-battle-btn-inner').disabled = false; document.getElementById('start-battle-btn-inner').classList.remove('opacity-50', 'cursor-not-allowed'); } }, 1000); } } // Start Battle async function startBattle() { // Check cooldown const now = Date.now(); if (now - userData.lastBattle < 300000) { showNotification('Battle cooldown not ready yet', 'error'); return; } // Get all markers const markerList = Object.values(markers); if (markerList.length === 0) { showNotification('No markers available to battle', 'error'); return; } // Select random marker const targetMarker = markerList[Math.floor(Math.random() * markerList.length)]; // Determine win/loss (random with modifiers) let winChance = 50; // Apply potion modifiers const victoryElixir = userData.activeItems.find(item => item.id === 'potion1'); if (victoryElixir) { winChance += 25; } // Apply ability modifiers if (userData.abilities.includes('mediator')) { winChance += 5; } if (userData.abilities.includes('battleExpert')) { winChance += 10; } if (userData.abilities.includes('commander')) { winChance += 25; } // Clamp win chance winChance = Math.max(5, Math.min(95, winChance)); // Determine result const isWin = Math.random() * 100 < winChance; // Update battle stats if (isWin) { userData.battlesWon++; } else { userData.battlesLost++; } // Update visibility let visibilityChange = 0; if (isWin) { visibilityChange = -10; if (userData.abilities.includes('territoryClaim')) { visibilityChange -= 1; } if (userData.abilities.includes('pathfinder')) { visibilityChange -= 5; } if (userData.abilities.includes('dominator')) { visibilityChange -= 10; } } else { visibilityChange = 10; if (userData.abilities.includes('territoryClaim')) { visibilityChange += 1; } if (userData.abilities.includes('pathfinder')) { visibilityChange += 5; } if (userData.abilities.includes('dominator')) { visibilityChange += 10; } } userData.battleVisibilityChanged += Math.abs(visibilityChange); // Update marker visibility try { await db.collection('markers').doc(targetMarker.id).update({ zDepth: Math.max(0, targetMarker.zDepth + visibilityChange) }); } catch (error) { console.error('Error updating marker visibility:', error); } // Award tokens for wins/losses if (isWin) { // Check for Holy Water effect const holyWater = userData.activeItems.find(item => item.id === 'potion4'); if (holyWater) { for (let i = 0; i < 50; i++) { awardRandomToken(); } } // Check for abilities that give tokens on win if (userData.abilities.includes('legend')) { for (let i = 0; i < 10; i++) { awardRandomToken(); } } // Check for Hybrid Warrior ability if (userData.abilities.includes('hybridWarrior') && archetype[targetMarker.category] !== userData.archetype) { for (let i = 0; i < 10; i++) { awardRandomToken(); } } } else { // Check for abilities that give tokens on loss if (userData.abilities.includes('tactician')) { for (let i = 0; i < 5; i++) { awardRandomToken(); } } if (userData.abilities.includes('quartermaster')) { for (let i = 0; i < 10; i++) { awardRandomToken(); } } // Check for Emissary ability if (userData.abilities.includes('emissary') && archetype[targetMarker.category] !== userData.archetype) { for (let i = 0; i < 10; i++) { awardRandomToken(); } } } // Update last battle time userData.lastBattle = now; // Save user data saveUserData(); // Update token display updateTokenDisplay(); // Show battle result const battleInfo = document.getElementById('battle-info'); battleInfo.innerHTML = `

${isWin ? 'Victory!' : 'Defeat!'}

You battled against:

${targetMarker.title}

Visibility changed by: ${visibilityChange > 0 ? '+' : ''}${visibilityChange}

`; // Add event listener to close button document.getElementById('close-battle-btn').addEventListener('click', closeModal); // Reload battle content after delay setTimeout(() => { if (document.getElementById('battle-modal').classList.contains('hidden') === false) { loadBattleContent(); } }, 5000); } // Load Chart Data function loadChartData(chartType) { // This would typically load data from localStorage or Firestore // For now, we'll create sample data switch(chartType) { case 'journey': loadJourneyChart(); break; case 'performance': loadPerformanceChart(); break; case 'battlegrounds': loadBattlegroundsChart(); break; } } // Load Journey Chart function loadJourneyChart() { const ctx = document.getElementById('journey-chart').getContext('2d'); // Sample data - in a real app this would come from user history const labels = Array.from({length: 14}, (_, i) => `Day ${i+1}`); const archetypeData = Array.from({length: 14}, () => Object.keys(archetype)[Math.floor(Math.random() * Object.keys(archetype).length)] ); const battlesWon = Array.from({length: 14}, () => Math.floor(Math.random() * 10)); const battlesLost = Array.from({length: 14}, () => Math.floor(Math.random() * 10)); const valueTokens = Array.from({length: 14}, () => Math.floor(Math.random() * 50)); new Chart(ctx, { type: 'bar', data: { labels: labels, datasets: [ { label: 'Battles Won', data: battlesWon, backgroundColor: 'rgba(75, 192, 192, 0.2)', borderColor: 'rgba(75, 192, 192, 1)', borderWidth: 1 }, { label: 'Battles Lost', data: battlesLost, backgroundColor: 'rgba(255, 99, 132, 0.2)', borderColor: 'rgba(255, 99, 132, 1)', borderWidth: 1 }, { label: 'Value Tokens Gained', data: valueTokens, type: 'line', borderColor: 'rgba(153, 102, 255, 1)', backgroundColor: 'rgba(153, 102, 255, 0.2)', borderDash: [5, 5], yAxisID: 'y1' } ] }, options: { responsive: true, scales: { y: { beginAtZero: true, title: { display: true, text: 'Battles' } }, y1: { position: 'right', beginAtZero: true, title: { display: true, text: 'Value Tokens' }, grid: { drawOnChartArea: false } } } } }); } // Load Performance Chart function loadPerformanceChart() { const ctx = document.getElementById('performance-chart').getContext('2d'); // Sample data const data = Array.from({length: 20}, () => ({ x: Math.floor(Math.random() * 100), y: Math.floor(Math.random() * 100), r: Math.floor(Math.random() * 30) + 5, archetype: Object.keys(archetype)[Math.floor(Math.random() * Object.keys(archetype).length)] })); new Chart(ctx, { type: 'bubble', data: { datasets: [{ label: 'Daily Performance', data: data, backgroundColor: 'rgba(54, 162, 235, 0.5)' }] }, options: { responsive: true, scales: { x: { title: { display: true, text: 'Engagement' } }, y: { title: { display: true, text: 'Battle Visibility Changed' } } } } }); } // Load Battlegrounds Chart function loadBattlegroundsChart() { const ctx = document.getElementById('battlegrounds-chart').getContext('2d'); // Sample data const data = Array.from({length: 15}, () => ({ x: Math.random() * 100, y: Math.random() * 2, archetype: Object.keys(archetype)[Math.floor(Math.random() * Object.keys(archetype).length)] })); new Chart(ctx, { type: 'scatter', data: { datasets: [{ label: 'Specialization vs. Battle Success', data: data, backgroundColor: 'rgba(255, 99, 132, 0.5)' }] }, options: { responsive: true, scales: { x: { title: { display: true, text: 'Archetype Markers Created (%)' } }, y: { title: { display: true, text: 'Battle Win/Loss Ratio' } } } } }); } // Subscribe to Markers function subscribeToMarkers() { db.collection('markers').onSnapshot(snapshot => { snapshot.docChanges().forEach(change => { const doc = change.doc; const data = {...doc.data(), id: doc.id}; if (change.type === 'added') { addMarkerToMap(data); } else if (change.type === 'modified') { updateMarkerOnMap(data); } else if (change.type === 'removed') { removeMarkerFromMap(doc.id); } }); // Update filtered markers if (document.getElementById('search-bar').value) { handleSearch({target: {value: document.getElementById('search-bar').value}}); } else { filteredMarkers = Object.values(markers); } }); } // Add Marker to Map function addMarkerToMap(markerData) { // Create icon const icon = L.divIcon({ className: 'marker-icon', html: ``, iconSize: [48, 48], iconAnchor: [24, 24] }); // Create marker const marker = L.marker([markerData.lat, markerData.lng], {icon: icon}); // Add click event marker.on('click', () => openMarkerDetails(markerData)); // Add to map marker.addTo(map); // Store reference markers[markerData.id] = { ...markerData, leafletMarker: marker }; } // Update Marker on Map function updateMarkerOnMap(markerData) { // Remove old marker if exists if (markers[markerData.id]) { map.removeLayer(markers[markerData.id].leafletMarker); } // Add updated marker addMarkerToMap(markerData); // If this was the selected marker, update details if (selectedMarker && selectedMarker.id === markerData.id) { selectedMarker = markerData; if (!document.getElementById('marker-modal').classList.contains('hidden')) { openMarkerDetails(markerData); } } } // Remove Marker from Map function removeMarkerFromMap(markerId) { if (markers[markerId]) { map.removeLayer(markers[markerId].leafletMarker); delete markers[markerId]; } } // Update Map Markers (for filtering) function updateMapMarkers() { // Clear all markers Object.values(markers).forEach(marker => { map.removeLayer(marker.leafletMarker); }); // Add filtered markers (filteredMarkers.length > 0 ? filteredMarkers : Object.values(markers)).forEach(marker => { // Check if in utility mode and owned by user if (utilityMode && marker.userID === currentUser.id) { // Highlight owned markers const icon = L.divIcon({ className: 'marker-icon', html: ``, iconSize: [48, 48], iconAnchor: [24, 24] }); marker.leafletMarker.setIcon(icon); } else if (utilityMode) { // Dim non-owned markers const icon = L.divIcon({ className: 'marker-icon', html: ``, iconSize: [48, 48], iconAnchor: [24, 24] }); marker.leafletMarker.setIcon(icon); } else { // Normal icon const icon = L.divIcon({ className: 'marker-icon', html: ``, iconSize: [48, 48], iconAnchor: [24, 24] }); marker.leafletMarker.setIcon(icon); } marker.leafletMarker.addTo(map); // Add click handler if not in utility mode if (!utilityMode) { marker.leafletMarker.off('click').on('click', () => openMarkerDetails(marker)); } else if (marker.userID === currentUser.id) { // Add utility click handler marker.leafletMarker.off('click').on('click', () => handleUtilityMarkerClick(marker)); } }); } // Open Marker Details function openMarkerDetails(markerData) { selectedMarker = markerData; // Update modal content document.getElementById('marker-title').textContent = markerData.title; // Create breadcrumbs const breadcrumbs = document.getElementById('marker-breadcrumbs'); breadcrumbs.innerHTML = ` ${markerData.category} > ${subcategoryNames[markerData.subcategory]} > Visibility: ${markerData.zDepth} > Likes: ${markerData.likes} > Favorites: ${markerData.favorites} `; document.getElementById('marker-body').textContent = markerData.body; // Create action buttons const actions = document.getElementById('marker-actions'); actions.innerHTML = ''; // Only show actions that exist if (markerData.link) { const linkBtn = document.createElement('button'); linkBtn.className = 'p-2 rounded-full bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600'; linkBtn.innerHTML = ''; linkBtn.addEventListener('click', () => { window.open(markerData.link, '_blank'); userData.linksClicked++; userData.interactions++; saveUserData(); updateTokenDisplay(); }); actions.appendChild(linkBtn); } if (markerData.email) { const emailBtn = document.createElement('button'); emailBtn.className = 'p-2 rounded-full bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600'; emailBtn.innerHTML = ''; emailBtn.addEventListener('click', () => { window.location.href = `mailto:${markerData.email}`; userData.emailsClicked++; userData.interactions++; saveUserData(); updateTokenDisplay(); }); actions.appendChild(emailBtn); } const favoriteBtn = document.createElement('button'); favoriteBtn.className = 'p-2 rounded-full bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600'; favoriteBtn.innerHTML = ''; favoriteBtn.addEventListener('click', async () => { try { await db.collection('markers').doc(markerData.id).update({ favorites: markerData.favorites + 1 }); userData.favoritesMade++; userData.interactions++; saveUserData(); updateTokenDisplay(); showNotification('Added to favorites!', 'success'); } catch (error) { console.error('Error updating favorites:', error); showNotification('Failed to add to favorites', 'error'); } }); actions.appendChild(favoriteBtn); const likeBtn = document.createElement('button'); likeBtn.className = 'p-2 rounded-full bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600'; likeBtn.innerHTML = ''; likeBtn.addEventListener('click', async () => { try { await db.collection('markers').doc(markerData.id).update({ likes: markerData.likes + 1 }); userData.likesGiven++; userData.interactions++; saveUserData(); updateTokenDisplay(); showNotification('Liked!', 'success'); } catch (error) { console.error('Error updating likes:', error); showNotification('Failed to like', 'error'); } }); actions.appendChild(likeBtn); const shareBtn = document.createElement('button'); shareBtn.className = 'p-2 rounded-full bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600'; shareBtn.innerHTML = ''; shareBtn.addEventListener('click', () => { const url = `${window.location.origin}?marker=${markerData.id}`; navigator.clipboard.writeText(url); userData.shareClicked++; userData.interactions++; saveUserData(); updateTokenDisplay(); showNotification('Link copied to clipboard!', 'success'); }); actions.appendChild(shareBtn); // Open modal openModal('marker'); } // Handle Utility Marker Click async function handleUtilityMarkerClick(marker) { if (!utilityMode || marker.userID !== currentUser.id) return; try { switch(utilityMode) { case 'util1': // Visibility Boost await db.collection('markers').doc(marker.id).update({ zDepth: marker.zDepth + 100 }); showNotification('Visibility boosted by 100!', 'success'); break; case 'util2': // Marker Relocator if (!utilityTarget) { utilityTarget = marker; showNotification('Now click where you want to move the marker', 'info'); return; } else { // Move marker to new location await db.collection('markers').doc(marker.id).update({ lat: utilityTarget.lat, lng: utilityTarget.lng }); showNotification('Marker relocated!', 'success'); } break; case 'util3': // Marker Eraser await db.collection('markers').doc(marker.id).delete(); showNotification('Marker deleted!', 'success'); break; case 'util4': // Visibility Crusher // This utility affects any marker, not just owned ones // We handle this differently in the marker click handler break; } } catch (error) { console.error('Error applying utility:', error); showNotification('Failed to apply utility', 'error'); } // Reset utility mode utilityMode = null; utilityTarget = null; updateMapMarkers(); } // Show Notification function showNotification(message, type) { const container = document.getElementById('notification-container'); const notification = document.createElement('div'); notification.className = `notification ${type}`; notification.textContent = message; container.appendChild(notification); // Remove after delay setTimeout(() => { notification.remove(); }, type === 'error' ? 5000 : 3000); } // Update Token Display function updateTokenDisplay() { document.getElementById('liquidity-tokens').textContent = currentUser.liquidityTokens; document.getElementById('value-tokens').textContent = currentUser.valueTokens; document.getElementById('engagement-tokens').textContent = currentUser.engagementTokens; } // Load User Data function loadUserData() { const savedData = localStorage.getItem('userData'); if (savedData) { userData = JSON.parse(savedData); } // Determine archetype determineArchetype(); } // Save User Data function saveUserData() { // Calculate derived metrics userData.battleWinRatio = userData.battlesWon + userData.battlesLost > 0 ? (userData.battlesWon / (userData.battlesWon + userData.battlesLost)) * 100 : 0; // Calculate engagement (interactions per 24h) const now = Date.now(); const oneDay = 24 * 60 * 60 * 1000; if (now - userData.lastInteraction > oneDay) { userData.engagement = userData.interactions; userData.lastInteraction = now; userData.interactions = 0; } localStorage.setItem('userData', JSON.stringify(userData)); } // Determine Archetype function determineArchetype() { // In a real implementation, this would analyze user's markers // For now, we'll randomly assign an archetype const archetypes = Object.values(archetype); userData.archetype = archetypes[Math.floor(Math.random() * archetypes.length)]; }