holdspot-online / script.js
alienisoneofus's picture
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> &gt;
<span>${subcategoryNames[markerData.subcategory]}</span> &gt;
<span>Visibility: ${markerData.zDepth}</span> &gt;
<span>Likes: ${markerData.likes}</span> &gt;
<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)];
}