// EL PURO - SilkNet OS Core Engine (Authoritative Server Synchronization) const INITIAL_GAME_STATE = { cash: 25.0, cleanBonds: 0, heat: 0.0, totalEarned: 0.0, activeTab: 'lab', drugs: {}, skills: {}, achievements: {}, synthesis: {}, lastTick: Date.now() }; // Initialize drugs state DRUG_CATALOG.forEach(d => { INITIAL_GAME_STATE.drugs[d.id] = { unlocked: d.unlockedDefault, raw: 0.0, product: 0.0, suppliers: 0, workers: 0, dealers: 0 }; }); SKILL_TREE.forEach(s => { INITIAL_GAME_STATE.skills[s.id] = false; }); ACHIEVEMENTS.forEach(a => { INITIAL_GAME_STATE.achievements[a.id] = false; }); SYNTHESIS_RECIPES.forEach(r => { INITIAL_GAME_STATE.synthesis[r.id] = 0; }); let G = JSON.parse(JSON.stringify(INITIAL_GAME_STATE)); let cashRateTracker = 0.0; let pendingActions = []; // Actions queued between 6-second authoritative server ticks let isSyncing = false; // --- NUMBER FORMATTING HELPER (Max 1 decimal, graceful fallback) --- function fmtNum(val) { if (val === undefined || val === null || isNaN(val) || !isFinite(val)) return '0'; const rounded = Math.round(Number(val) * 10) / 10; // If it's an integer, show with .0 for uniform currency or clean integer for items return rounded.toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 }); } function fmtInt(val) { if (val === undefined || val === null || isNaN(val) || !isFinite(val)) return '0'; return Math.floor(Math.max(0, Number(val))).toLocaleString(); } // --- DOM CACHE --- const dom = { cash: document.getElementById('hud-cash'), rate: document.getElementById('hud-rate'), bonds: document.getElementById('hud-bonds'), multiplier: document.getElementById('hud-multiplier'), heatPct: document.getElementById('heat-pct'), heatBar: document.getElementById('heat-bar-fill'), viewport: document.getElementById('main-viewport'), clock: document.getElementById('system-clock'), userBadge: document.getElementById('user-badge'), authModal: document.getElementById('auth-modal'), raidModal: document.getElementById('raid-modal'), raidBribeCost: document.getElementById('raid-bribe-cost'), toastContainer: document.getElementById('toast-container'), syncTimer: document.getElementById('sync-timer-text'), syncBar: document.getElementById('sync-bar-fill'), quickStash: document.getElementById('quick-stash-items') }; // --- MULTIPLIERS & BONUSES --- function getProductionMultiplier() { let m = 1.0; m += G.cleanBonds * 0.03; if (G.skills['chem_1']) m += 0.15; if (G.achievements['millionaire']) m += 0.20; return m; } function getSellPriceMultiplier() { let m = 1.0; m += G.cleanBonds * 0.04; if (G.skills['king_2']) m += 0.25; if (G.achievements['first_hit']) m += 0.05; if (G.achievements['cartel_tier']) m += 0.15; return m; } function getRawCostMultiplier() { let m = 1.0; if (G.skills['chem_2']) m *= 0.85; return m; } function getDealerSpeedMultiplier() { let m = 1.0; if (G.skills['king_1']) m += 0.20; if (G.skills['king_3']) m += 0.50; return m; } // Cost scaling formulas function getSupplierCost(drugId) { const config = DRUG_CATALOG.find(d => d.id === drugId); const count = (G.drugs[drugId] && G.drugs[drugId].suppliers) || 0; return Math.floor((config.supplierCost || 75) * Math.pow(1.15, count)); } function getWorkerCost(drugId) { const config = DRUG_CATALOG.find(d => d.id === drugId); const count = G.drugs[drugId].workers; return Math.floor(config.workerCost * Math.pow(1.15, count)); } function getDealerCost(drugId) { const config = DRUG_CATALOG.find(d => d.id === drugId); const count = G.drugs[drugId].dealers; return Math.floor(config.dealerCost * Math.pow(1.15, count)); } function getUnlockCost(drugId) { return DRUG_CATALOG.find(d => d.id === drugId).unlockCost; } // --- NOTIFICATIONS (TOASTS) --- function showToast(msg, isDanger = false) { const toast = document.createElement('div'); toast.className = `toast ${isDanger ? 'danger' : ''}`; toast.textContent = msg; dom.toastContainer.appendChild(toast); setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => toast.remove(), 400); }, 2800); } // --- TAB SWITCHING --- function setTab(tabId) { G.activeTab = tabId; document.querySelectorAll('.nav-tab').forEach(b => { b.classList.toggle('active', b.dataset.tab === tabId); }); renderViewport(); } // --- RENDER VIEWPORT --- function renderViewport() { if (G.activeTab === 'lab') { renderLabView(); } else if (G.activeTab === 'streets') { renderStreetsView(); } else if (G.activeTab === 'tree') { renderSkillTreeView(); } else if (G.activeTab === 'offshore') { renderOffshoreView(); } } // 1. LAB VIEW function renderLabView() { let html = `
ACTIVE LABORATORIES SYNTHESIS LEVEL: ${G.cleanBonds}
`; DRUG_CATALOG.forEach(d => { const data = G.drugs[d.id] || { raw: 0, product: 0, suppliers: 0, workers: 0, dealers: 0, unlocked: false }; const rawCost = Math.floor(d.rawCost * getRawCostMultiplier()); const supplierCost = getSupplierCost(d.id); const workerCost = getWorkerCost(d.id); if (!data.unlocked) { const unlockCost = getUnlockCost(d.id); html += `
${d.name}
CLASSIFIED FORMULA
`; return; } const suppliers = data.suppliers || 0; const supplyRate = d.supplyRate || 1.0; const autoSupplySpeed = suppliers * supplyRate * getProductionMultiplier(); const autoProdSpeed = data.workers * d.produceRate * getProductionMultiplier(); html += `
${d.name}
${d.supplierName}s: ${suppliers} (+${fmtNum(autoSupplySpeed)} r/s) • ${d.workerName}s: ${data.workers} (${fmtNum(autoProdSpeed)}g/s)
Auto-Refines
1 Raw ➔ 1g
RAW SUPPLY (${d.rawName.toUpperCase()}) ${fmtNum(data.raw)}
READY PRODUCT (${d.productName.toUpperCase()}) ${fmtNum(data.product)}g
`; }); dom.viewport.innerHTML = html; } // 2. STREETS VIEW (Dealers & Blends) function renderStreetsView() { let html = `
STREET DISTRIBUTION & DEALER NETWORK
`; DRUG_CATALOG.filter(d => G.drugs[d.id] && G.drugs[d.id].unlocked).forEach(d => { const data = G.drugs[d.id]; const dealerCost = getDealerCost(d.id); const unitPrice = Math.floor(d.sellPrice * getSellPriceMultiplier()); html += `
${d.name}
${d.dealerName}s: ${data.dealers}
$${unitPrice.toLocaleString()}/g
In Stash ${fmtNum(data.product)}g
Deal Speed ${fmtNum(data.dealers * d.sellRate * getDealerSpeedMultiplier())}/s
`; }); // RECIPE SYNTHESIS SECTION html += `
DESIGNER COCKTAILS & BLENDS
`; SYNTHESIS_RECIPES.forEach(r => { const canCook = Object.entries(r.ingredients).every(([drugId, reqAmount]) => { return G.drugs[drugId] && G.drugs[drugId].product >= reqAmount; }); const ingrLabels = Object.entries(r.ingredients).map(([id, amt]) => { const has = G.drugs[id] ? G.drugs[id].product : 0; const drugDef = DRUG_CATALOG.find(d => d.id === id); return `${amt}g ${drugDef.name} (${fmtNum(has)}g)`; }).join(' + '); html += `
${r.name}
${r.description}
Recipe: ${ingrLabels}
`; }); dom.viewport.innerHTML = html; } // 3. SKILL TREE (SPIDERWEB) function renderSkillTreeView() { let html = `
UNDERWORLD TALENT SPIDERWEB
`; const branches = [ { id: 'chemist', name: 'Path of the Chemist', icon: 'game-icons:chemical-drop', color: '#00e5ff' }, { id: 'kingpin', name: 'Path of the Kingpin', icon: 'game-icons:crown', color: '#eab308' }, { id: 'smuggler', name: 'Path of the Smuggler', icon: 'game-icons:stealth-bomber', color: '#a855f7' } ]; branches.forEach(b => { html += `
${b.name.toUpperCase()}
`; const skillsInBranch = SKILL_TREE.filter(s => s.branch === b.id); skillsInBranch.forEach(s => { const isUnlocked = G.skills[s.id]; const prereqsMet = s.prereqs.every(req => G.skills[req]); html += `
${s.name} ${isUnlocked ? '✓' : ''}
${s.desc}
${isUnlocked ? 'ACQUIRED' : '$' + s.cost.toLocaleString()}
${!isUnlocked ? ` ` : ''}
`; }); }); dom.viewport.innerHTML = html; } // 4. OFFSHORE (PRESTIGE & MONEY LAUNDERING) function renderOffshoreView() { const bondsToEarn = Math.floor(Math.sqrt(Math.max(0, G.totalEarned) / 25000)); const nextBonds = Math.max(0, bondsToEarn - G.cleanBonds); let html = `
SWISS ACCOUNTS & MONEY LAUNDERING
Launder Dirty Syndicate Cash

Reset dirty cash, lab equipment, and street dealers in exchange for permanent Clean Offshore Bonds. Each Bond permanently boosts production (+3%) and sale values (+4%).

CLEAN BONDS UPON LAUNDERING:
+${nextBonds} Bonds
Career Volume: $${fmtNum(G.totalEarned)}
GLOBAL SYNDICATE ACHIEVEMENTS
`; ACHIEVEMENTS.forEach(a => { const has = G.achievements[a.id]; html += `
${has ? '🏆 ' : '🔒 '}${a.name}
${a.desc}
${a.reward}
`; }); dom.viewport.innerHTML = html; } // --- OPTIMISTIC CLIENT ACTIONS (Queued for Server Authority) --- function queueAction(action) { pendingActions.push(action); } function buyRaw(drugId) { const d = DRUG_CATALOG.find(x => x.id === drugId); const cost = Math.floor(d.rawCost * getRawCostMultiplier()); if (G.cash >= cost) { // Optimistic local UI update G.cash -= cost; G.drugs[drugId].raw += 1; queueAction({ type: 'buy_raw', drug_id: drugId, amount: 1 }); updateLiveInventory(); } } function manufacture(drugId) { const data = G.drugs[drugId]; if (data.raw >= 1) { data.raw -= 1; let yieldCount = 1; if (G.skills['chem_3'] && Math.random() < 0.10) { yieldCount = 3; showToast('99.1% Purity Proc! 3x Yield!', false); } data.product += yieldCount; addHeat(0.5); queueAction({ type: 'manufacture', drug_id: drugId, amount: 1 }); updateLiveInventory(); } } function sellManual(drugId, amount = 1) { const d = DRUG_CATALOG.find(x => x.id === drugId); const data = G.drugs[drugId]; const actual = Math.min(amount, Math.floor(data.product)); if (actual > 0) { data.product -= actual; const profit = actual * Math.floor(d.sellPrice * getSellPriceMultiplier()); G.cash += profit; G.totalEarned += profit; addHeat(actual * 0.8); queueAction({ type: 'sell_manual', drug_id: drugId, amount: actual }); checkAchievements(); updateLiveInventory(); } } function hireSupplier(drugId) { const cost = getSupplierCost(drugId); if (G.cash >= cost) { G.cash -= cost; if (!G.drugs[drugId].suppliers) G.drugs[drugId].suppliers = 0; G.drugs[drugId].suppliers += 1; queueAction({ type: 'hire_supplier', drug_id: drugId }); checkAchievements(); renderViewport(); updateLiveInventory(); } } function hireWorker(drugId) { const cost = getWorkerCost(drugId); if (G.cash >= cost) { G.cash -= cost; G.drugs[drugId].workers += 1; queueAction({ type: 'hire_worker', drug_id: drugId }); checkAchievements(); renderViewport(); updateLiveInventory(); } } function hireDealer(drugId) { const cost = getDealerCost(drugId); if (G.cash >= cost) { G.cash -= cost; G.drugs[drugId].dealers += 1; queueAction({ type: 'hire_dealer', drug_id: drugId }); renderViewport(); updateLiveInventory(); } } function unlockDrug(drugId) { const cost = getUnlockCost(drugId); if (G.cash >= cost && !G.drugs[drugId].unlocked) { G.cash -= cost; G.drugs[drugId].unlocked = true; showToast(`Unlocked formula for ${DRUG_CATALOG.find(d => d.id === drugId).name}!`); queueAction({ type: 'unlock_drug', drug_id: drugId }); checkAchievements(); renderViewport(); updateLiveInventory(); } } function blendRecipe(recipeId) { const r = SYNTHESIS_RECIPES.find(x => x.id === recipeId); const canCook = Object.entries(r.ingredients).every(([drugId, amt]) => G.drugs[drugId].product >= amt); if (canCook) { Object.entries(r.ingredients).forEach(([drugId, amt]) => { G.drugs[drugId].product -= amt; }); G.cash += r.sellPrice; G.totalEarned += r.sellPrice; addHeat(5); queueAction({ type: 'blend_recipe', recipe_id: recipeId }); showToast(`Synthesized & Sold ${r.name} for +$${r.sellPrice.toLocaleString()}!`); renderViewport(); updateLiveInventory(); } } function buySkill(skillId) { const s = SKILL_TREE.find(x => x.id === skillId); if (G.cash >= s.cost && !G.skills[skillId]) { G.cash -= s.cost; G.skills[skillId] = true; queueAction({ type: 'buy_skill', skill_id: skillId }); showToast(`Skill Activated: ${s.name}`); renderViewport(); updateLiveInventory(); } } function launderMoney() { const bondsToEarn = Math.floor(Math.sqrt(Math.max(0, G.totalEarned) / 25000)); const gain = Math.max(0, bondsToEarn - G.cleanBonds); if (gain < 1) return; if (confirm(`Launder all assets for +${gain} Clean Offshore Bonds? Your syndicate operations will reset.`)) { queueAction({ type: 'launder_prestige' }); G.cleanBonds += gain; G.cash = 50.0; G.heat = 0.0; DRUG_CATALOG.forEach(d => { G.drugs[d.id] = { unlocked: d.unlockedDefault, raw: 0.0, product: 0.0, suppliers: 0, workers: 0, dealers: 0 }; }); checkAchievements(); showToast(`Laundered! You now have ${G.cleanBonds} Clean Bonds.`); renderViewport(); updateLiveInventory(); } } // --- POLICE HEAT & RAIDS --- function addHeat(amount) { let riseMod = G.skills['smug_1'] ? 0.65 : 1.0; G.heat = Math.min(100.0, G.heat + (amount * riseMod)); } function checkRaidEvent() { if (G.heat >= 85 && Math.random() < 0.25) { triggerRaidModal(); } } function triggerRaidModal() { const bribeCost = Math.max(100, Math.floor(G.cash * 0.15 * (G.skills['smug_2'] ? 0.5 : 1.0))); dom.raidBribeCost.textContent = bribeCost.toLocaleString(); dom.raidModal.classList.add('active'); document.getElementById('btn-raid-bribe').onclick = () => { if (G.cash >= bribeCost) { G.cash -= bribeCost; G.heat = 0.0; queueAction({ type: 'bribe_police' }); dom.raidModal.classList.remove('active'); showToast('Sheriff bribed. Heat cleared.'); updateLiveInventory(); } else { showToast('Insufficient cash for bribe!', true); } }; document.getElementById('btn-raid-stash').onclick = () => { G.heat = 20.0; queueAction({ type: 'flush_stash' }); dom.raidModal.classList.remove('active'); showToast('Stash flushed & secured. Heat diminished.'); updateLiveInventory(); }; document.getElementById('btn-raid-shootout').onclick = () => { dom.raidModal.classList.remove('active'); if (Math.random() < 0.5) { const reward = Math.floor(G.cash * 0.5); G.cash += reward; G.heat = 0.0; showToast(`Broke through blockade! Seized +$${reward.toLocaleString()}!`); } else { DRUG_CATALOG.forEach(d => { G.drugs[d.id].product = Math.floor(G.drugs[d.id].product * 0.7); }); G.heat = 30.0; showToast('Agents seized 30% of stored product!', true); } updateLiveInventory(); }; } // --- ACHIEVEMENTS CHECK --- function checkAchievements() { if (G.totalEarned > 0 && !G.achievements['first_hit']) { G.achievements['first_hit'] = true; showToast('Achievement: First Batch!'); } if (G.drugs['weed'] && G.drugs['weed'].workers >= 10 && !G.achievements['grower_op']) { G.achievements['grower_op'] = true; showToast('Achievement: Green Thumb!'); } if (G.drugs['meth'] && G.drugs['meth'].unlocked && !G.achievements['blue_sky']) { G.achievements['blue_sky'] = true; showToast('Achievement: Say My Name!'); } if (G.drugs['cocaine'] && G.drugs['cocaine'].unlocked && !G.achievements['cartel_tier']) { G.achievements['cartel_tier'] = true; showToast('Achievement: El Patrón!'); } if (G.cash >= 1000000 && !G.achievements['millionaire']) { G.achievements['millionaire'] = true; showToast('Achievement: Seven Figures!'); } if (G.cleanBonds >= 1 && !G.achievements['laundered']) { G.achievements['laundered'] = true; showToast('Achievement: Clean Hands!'); } } // --- LIGHTWEIGHT LOCAL TICK (Smooth 60FPS UI feedback between server syncs) --- function clientTick(deltaSec) { let earnedThisTick = 0; const prodMulti = getProductionMultiplier(); const sellMulti = getSellPriceMultiplier(); const dealerSpeed = getDealerSpeedMultiplier(); DRUG_CATALOG.forEach(d => { const data = G.drugs[d.id]; if (!data || !data.unlocked) return; // Auto Supply const suppliers = data.suppliers || 0; if (suppliers > 0) { const supplyRate = d.supplyRate || 1.0; const toSupply = suppliers * supplyRate * prodMulti * deltaSec; data.raw = data.raw + toSupply; } // Auto Production if (data.workers > 0 && data.raw > 0) { const prodPerSec = data.workers * d.produceRate * prodMulti; const toMake = Math.min(data.raw, prodPerSec * deltaSec); data.raw = Math.max(0, data.raw - toMake); data.product = data.product + toMake; } // Auto Sales if (data.dealers > 0 && data.product > 0) { const sellPerSec = data.dealers * d.sellRate * dealerSpeed; const toSell = Math.min(data.product, sellPerSec * deltaSec); data.product = Math.max(0, data.product - toSell); const revenue = toSell * Math.floor(d.sellPrice * sellMulti); G.cash += revenue; G.totalEarned += revenue; earnedThisTick += revenue; } }); if (G.heat > 0) { G.heat = Math.max(0.0, G.heat - (0.5 * deltaSec)); } if (deltaSec > 0) { cashRateTracker = cashRateTracker * 0.85 + (earnedThisTick / deltaSec) * 0.15; } // Direct, surgical DOM text updates every tick (no lag, no re-rendering entire card HTML) updateLiveInventory(); updateSyncCountdown(); } const TICK_INTERVAL_MS = 6000; let lastSyncTimestamp = Date.now(); function updateSyncCountdown() { const elapsed = Date.now() - lastSyncTimestamp; const remainingMs = Math.max(0, TICK_INTERVAL_MS - elapsed); const remainingSec = (remainingMs / 1000).toFixed(1); const pct = Math.min(100, Math.max(0, (remainingMs / TICK_INTERVAL_MS) * 100)); if (dom.syncTimer) { dom.syncTimer.textContent = `${remainingSec}s`; } if (dom.syncBar) { dom.syncBar.style.width = `${pct}%`; } } function updateLiveInventory() { dom.cash.textContent = fmtNum(G.cash); dom.rate.textContent = fmtNum(cashRateTracker); dom.bonds.textContent = fmtInt(G.cleanBonds); dom.multiplier.textContent = fmtInt(G.cleanBonds * 4); dom.heatPct.textContent = `${Math.round(G.heat)}%`; dom.heatBar.style.width = `${Math.min(100, Math.round(G.heat))}%`; const rawCostMulti = getRawCostMultiplier(); const prodMulti = getProductionMultiplier(); let stashChipsHtml = ''; // Surgically update raw, product, buttons, and stash chips DRUG_CATALOG.forEach(d => { const drugData = G.drugs[d.id]; if (!drugData) return; // Populate quick stash overview for owned/unlocked products if (drugData.unlocked || drugData.product > 0 || drugData.raw > 0) { stashChipsHtml += `
${d.name.split(' ')[0]}: ${fmtNum(drugData.product)}g
`; } // Live text update for raw & product const rawEl = document.getElementById(`val-raw-${d.id}`); if (rawEl) rawEl.textContent = fmtNum(drugData.raw); const prodEl = document.getElementById(`val-prod-${d.id}`); if (prodEl) prodEl.textContent = `${fmtNum(drugData.product)}g`; const streetProdEl = document.getElementById(`val-prod-street-${d.id}`); if (streetProdEl) streetProdEl.textContent = `${fmtNum(drugData.product)}g`; const supplierCountEl = document.getElementById(`val-supplier-count-${d.id}`); if (supplierCountEl) supplierCountEl.textContent = `${d.supplierName}s: ${drugData.suppliers || 0}`; const autoSupplyEl = document.getElementById(`val-auto-supply-${d.id}`); if (autoSupplyEl) { const currentSupplySpeed = (drugData.suppliers || 0) * (d.supplyRate || 1.0) * prodMulti; autoSupplyEl.textContent = `(+${fmtNum(currentSupplySpeed)} r/s)`; autoSupplyEl.style.color = (drugData.suppliers || 0) > 0 ? '#38bdf8' : '#8b949e'; } const workerCountEl = document.getElementById(`val-worker-count-${d.id}`); if (workerCountEl) workerCountEl.textContent = `${d.workerName}s: ${drugData.workers}`; const autoProdEl = document.getElementById(`val-auto-prod-${d.id}`); if (autoProdEl) { const currentSpeed = drugData.workers * d.produceRate * prodMulti; autoProdEl.textContent = `(${fmtNum(currentSpeed)}g/s)`; autoProdEl.style.color = (drugData.workers > 0 && drugData.raw > 0) ? '#4ade80' : '#8b949e'; } // Live button state updates (enable/disable as resources change) const rawCost = Math.floor(d.rawCost * rawCostMulti); const supplierCost = getSupplierCost(d.id); const workerCost = getWorkerCost(d.id); const dealerCost = getDealerCost(d.id); const btnBuyRaw = document.getElementById(`btn-buy-raw-${d.id}`); if (btnBuyRaw) btnBuyRaw.disabled = (G.cash < rawCost); const btnHireSupplier = document.getElementById(`btn-hire-supplier-${d.id}`); if (btnHireSupplier) btnHireSupplier.disabled = (G.cash < supplierCost); const btnMake = document.getElementById(`btn-make-${d.id}`); if (btnMake) btnMake.disabled = (drugData.raw < 1); const btnSell = document.getElementById(`btn-sell-${d.id}`); if (btnSell) btnSell.disabled = (drugData.product < 1); const btnHireWorker = document.getElementById(`btn-hire-worker-${d.id}`); if (btnHireWorker) btnHireWorker.disabled = (G.cash < workerCost); const btnHireDealer = document.getElementById(`btn-hire-dealer-${d.id}`); if (btnHireDealer) btnHireDealer.disabled = (G.cash < dealerCost); const btnSell10x = document.getElementById(`btn-sell-10x-${d.id}`); if (btnSell10x) btnSell10x.disabled = (drugData.product < 10); const btnUnlock = document.getElementById(`btn-unlock-${d.id}`); if (btnUnlock) btnUnlock.disabled = (G.cash < getUnlockCost(d.id)); }); if (dom.quickStash) { dom.quickStash.innerHTML = stashChipsHtml || 'No active stash'; } } // --- AUTHORITATIVE SERVER TICK (Every 6 seconds) --- async function authoritativeServerTick() { if (isSyncing) return; isSyncing = true; lastSyncTimestamp = Date.now(); // Grab queued actions and reset local queue const actionsToSend = [...pendingActions]; pendingActions = []; if (API.getToken()) { const serverState = await API.syncTick(actionsToSend); if (serverState) { // Reconcile authoritative server state (protects against browser devtools tampering) G = { ...INITIAL_GAME_STATE, ...serverState, activeTab: G.activeTab // Preserve UI view tab }; checkAchievements(); updateLiveInventory(); } } else { // Guest mode / offline fallback: persist locally localStorage.setItem('el_puro_local_save', JSON.stringify(G)); } isSyncing = false; } // Game loop with requestAnimationFrame let lastFrameTime = performance.now(); function gameLoop(now) { const delta = (now - lastFrameTime) / 1000; lastFrameTime = now; if (delta > 0 && delta < 5) { clientTick(delta); } requestAnimationFrame(gameLoop); } // Server tick: exactly every 6 seconds to prevent flooding server setInterval(authoritativeServerTick, TICK_INTERVAL_MS); // Clock updates setInterval(() => { const d = new Date(); dom.clock.textContent = d.toTimeString().substring(0, 5); }, 1000); // Periodic raid check (every 15 seconds) setInterval(checkRaidEvent, 15000); // --- INITIALIZATION & EVENTS --- document.addEventListener('DOMContentLoaded', async () => { const username = API.getUsername(); if (username) { dom.userBadge.textContent = `[${username}]`; } // Load initial authoritative save from server if logged in let loaded = false; if (API.getToken()) { const remote = await API.loadRemoteSave(); if (remote) { G = { ...INITIAL_GAME_STATE, ...remote, activeTab: 'lab' }; loaded = true; } } if (!loaded) { const local = localStorage.getItem('el_puro_local_save'); if (local) { try { G = { ...INITIAL_GAME_STATE, ...JSON.parse(local), activeTab: 'lab' }; } catch (e) { console.warn('Corrupt local save, using initial state'); } } } // Setup navigation tabs document.querySelectorAll('.nav-tab').forEach(b => { b.addEventListener('click', () => setTab(b.dataset.tab)); }); // Auth Modal handling dom.userBadge.addEventListener('click', () => dom.authModal.classList.add('active')); document.getElementById('btn-close-auth').addEventListener('click', () => dom.authModal.classList.remove('active')); document.getElementById('btn-login-submit').addEventListener('click', async () => { const u = document.getElementById('auth-username').value.trim(); const p = document.getElementById('auth-password').value; if (!u || !p) return showToast('Enter username and password', true); try { const res = await API.login(u, p); dom.userBadge.textContent = `[${res.username}]`; dom.authModal.classList.remove('active'); showToast(`Connected: Welcome ${res.username}`); // Fetch server authoritative state const remote = await API.loadRemoteSave(); if (remote) { G = { ...INITIAL_GAME_STATE, ...remote, activeTab: G.activeTab }; renderViewport(); } } catch (e) { showToast(e.message, true); } }); document.getElementById('btn-register-submit').addEventListener('click', async () => { const u = document.getElementById('auth-username').value.trim(); const p = document.getElementById('auth-password').value; const hp = document.getElementById('auth-website-hp').value; if (!u || !p) return showToast('Enter username and password', true); try { const res = await API.register(u, p, hp); dom.userBadge.textContent = `[${res.username}]`; dom.authModal.classList.remove('active'); showToast(`Syndicate registered: ${res.username}`); // Fetch clean server initialized state const remote = await API.loadRemoteSave(); if (remote) { G = { ...INITIAL_GAME_STATE, ...remote, activeTab: G.activeTab }; renderViewport(); } } catch (e) { showToast(e.message, true); } }); renderViewport(); updateLiveInventory(); requestAnimationFrame(gameLoop); });