Spaces:
Running
Running
You are a master programmer in HTML, CSS and JS and very good at logic implementation. You can find solution in very complicated situation, implement flawless methods or technique and create very complex, sturdy and meaningful piece of code. You also create very beautiful designs, top eye candy class for many CSS knowledgeable experts, with entire components occupying absolute minimum space possible while remaining usable.
3c189ce
verified
| // 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 = ` | |
| <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> | |
| <div class="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg"> | |
| <h3 class="font-bold mb-2">Basic Metrics</h3> | |
| <p>Markers Created: ${userData.markersCreated}</p> | |
| <p>Archetype Markers: ${userData.archetypeMarkersCreated}</p> | |
| <p>Likes Given: ${userData.likesGiven}</p> | |
| <p>Favorites Made: ${userData.favoritesMade}</p> | |
| <p>Interactions: ${userData.interactions}</p> | |
| </div> | |
| <div class="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg"> | |
| <h3 class="font-bold mb-2">Engagement</h3> | |
| <p>Share Clicked: ${userData.shareClicked}</p> | |
| <p>Links Clicked: ${userData.linksClicked}</p> | |
| <p>Emails Clicked: ${userData.emailsClicked}</p> | |
| <p>Archetype: ${userData.archetype || 'None'}</p> | |
| <p>Engagement: ${userData.engagement}</p> | |
| </div> | |
| <div class="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg"> | |
| <h3 class="font-bold mb-2">Tokens</h3> | |
| <p>Liquidity: ${userData.liquidityTokensGained}/${userData.liquidityTokensSpent}</p> | |
| <p>Value: ${userData.valueTokensGained}/${userData.valueTokensSpent}</p> | |
| <p>Engagement: ${userData.engagementTokensGained}/${userData.engagementTokensSpent}</p> | |
| </div> | |
| <div class="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg"> | |
| <h3 class="font-bold mb-2">Battles</h3> | |
| <p>Won: ${userData.battlesWon}</p> | |
| <p>Lost: ${userData.battlesLost}</p> | |
| <p>Visibility Changed: ${userData.battleVisibilityChanged}</p> | |
| <p>Win Ratio: ${userData.battleWinRatio.toFixed(2)}%</p> | |
| </div> | |
| </div> | |
| `; | |
| // 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 = ` | |
| <div class="mb-4"> | |
| <p>${activeCount} abilities active, ${inactiveCount} inactive</p> | |
| </div> | |
| <div class="abilities-grid"> | |
| `; | |
| // 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 += ` | |
| <div class="ability-item ${isActive ? 'active' : isAvailable ? '' : 'inactive'}" | |
| title="${ability.name}: ${ability.effect}"> | |
| <i class="fas ${ability.icon} ability-icon"></i> | |
| <span class="text-xs">${ability.name}</span> | |
| </div> | |
| `; | |
| }); | |
| gridHTML += '</div>'; | |
| 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 = ` | |
| <div class="mb-4"> | |
| <p>Current Exchange Rate: 1 token = ${exchangeRate} points</p> | |
| <p>Total Markers on Map: ${totalMarkers}</p> | |
| <p>Next exchange available in: ${formatTime(cooldownRemaining)}</p> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <select id="from-token" class="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700"> | |
| <option value="liquidity">Liquidity Tokens</option> | |
| <option value="value">Value Tokens</option> | |
| <option value="engagement">Engagement Tokens</option> | |
| </select> | |
| <i class="fas fa-exchange-alt"></i> | |
| <select id="to-token" class="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700"> | |
| <option value="liquidity">Liquidity Tokens</option> | |
| <option value="value">Value Tokens</option> | |
| <option value="engagement">Engagement Tokens</option> | |
| </select> | |
| <input type="number" id="exchange-amount" min="1" placeholder="Amount" | |
| class="w-24 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700"> | |
| <button id="exchange-btn" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition ${canExchange ? '' : 'opacity-50 cursor-not-allowed'}" | |
| ${canExchange ? '' : 'disabled'}> | |
| Exchange | |
| </button> | |
| </div> | |
| `; | |
| // 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 += ` | |
| <div class="shop-item"> | |
| <div class="shop-item-details"> | |
| <h3>${item.name}</h3> | |
| <p class="shop-item-meta">${item.description}</p> | |
| <p class="shop-item-meta">Effect: ${item.effect}</p> | |
| ${item.value ? `<p class="shop-item-meta">Value: ${item.value}</p>` : ''} | |
| ${item.time ? `<p class="shop-item-meta">Duration: ${Math.floor(item.time/1000)}s</p>` : ''} | |
| </div> | |
| <div class="shop-item-price"> | |
| <span class="font-bold">${item.price} ${item.type.charAt(0).toUpperCase() + item.type.slice(1)} Tokens</span> | |
| <button class="mt-2 px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm transition buy-btn" | |
| data-item-id="${item.id}" data-item-type="${tabName}"> | |
| Buy | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| }); | |
| 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 = ` | |
| <div class="mb-4"> | |
| <p>Total Markers: ${totalMarkers}</p> | |
| <p>Your Archetype Markers: ${archetypeMarkers}</p> | |
| <p>Battle Ratio: ${battleRatio}%</p> | |
| <p>Current Win Chance: ${winChance}%</p> | |
| </div> | |
| <div class="text-center"> | |
| <button id="start-battle-btn-inner" class="px-6 py-3 bg-red-600 hover:bg-red-700 text-white rounded-lg transition ${canBattle ? '' : 'opacity-50 cursor-not-allowed'}" | |
| ${canBattle ? '' : 'disabled'}> | |
| Start Battle | |
| </button> | |
| <p class="mt-2">Cooldown: <span id="battle-cooldown-inner">${formatTime(cooldownRemaining)}</span></p> | |
| </div> | |
| `; | |
| // 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 = ` | |
| <div class="text-center mb-4"> | |
| <i class="fas fa-${isWin ? 'trophy text-yellow-500' : 'skull text-red-500'} text-4xl mb-2"></i> | |
| <h3 class="text-xl font-bold">${isWin ? 'Victory!' : 'Defeat!'}</h3> | |
| <p>You battled against:</p> | |
| <p class="font-bold">${targetMarker.title}</p> | |
| <p>Visibility changed by: ${visibilityChange > 0 ? '+' : ''}${visibilityChange}</p> | |
| </div> | |
| <button id="close-battle-btn" class="w-full py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg transition"> | |
| Close | |
| </button> | |
| `; | |
| // 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: `<i class="fas ${markerData.subcategory}" style="color: ${iconColors[markerData.subcategory] || '#000'};"></i>`, | |
| 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: `<i class="fas ${marker.subcategory}" style="color: ${iconColors[marker.subcategory] || '#000'}; filter: brightness(1.5);"></i>`, | |
| 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: `<i class="fas ${marker.subcategory}" style="color: ${iconColors[marker.subcategory] || '#000'}; opacity: 0.3;"></i>`, | |
| iconSize: [48, 48], | |
| iconAnchor: [24, 24] | |
| }); | |
| marker.leafletMarker.setIcon(icon); | |
| } else { | |
| // Normal icon | |
| const icon = L.divIcon({ | |
| className: 'marker-icon', | |
| html: `<i class="fas ${marker.subcategory}" style="color: ${iconColors[marker.subcategory] || '#000'};"></i>`, | |
| 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 = ` | |
| <span>${markerData.category}</span> > | |
| <span>${subcategoryNames[markerData.subcategory]}</span> > | |
| <span>Visibility: ${markerData.zDepth}</span> > | |
| <span>Likes: ${markerData.likes}</span> > | |
| <span>Favorites: ${markerData.favorites}</span> | |
| `; | |
| 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 = '<i class="fas fa-link"></i>'; | |
| 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 = '<i class="fas fa-envelope"></i>'; | |
| 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 = '<i class="fas fa-star"></i>'; | |
| 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 = '<i class="fas fa-heart"></i>'; | |
| 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 = '<i class="fas fa-share"></i>'; | |
| 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)]; | |
| } |